Quick Overview

Find the earliest bad entry in a monotonic release history, returning -1 when every version is good. This compact coding prompt tests boundary-safe binary search, empty and all-good inputs, first-element failures, loop invariants, and logarithmic performance on histories with up to a million versions.

Find the First Bad Version in a Monotonic Release History

Company: Qualcomm

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Technical Screen

# Find the First Bad Version in a Monotonic Release History You are given a release history as a boolean array `bad`. Versions use zero-based indices. Once a bad version appears, every later version is also bad, so the array has the form `False, ..., False, True, ..., True`. Return the index of the first bad version. Return `-1` if every version is good. ## Function Signature ```python def first_bad_version(bad: list[bool]) -> int: ... ``` ## Constraints - `0 <= len(bad) <= 1_000_000` - `bad` is monotonic: no `False` value appears after a `True` value. - The implementation should remain efficient when the history is very large. ## Examples ```text Input: bad = [False, False, True, True, True] Output: 2 ``` ```text Input: bad = [False, False] Output: -1 ``` ```text Input: bad = [True] Output: 0 ```

Overview: Find the earliest bad entry in a monotonic release history, returning -1 when every version is good. This compact coding prompt tests boundary-safe binary search, empty and all-good inputs, first-element failures, loop invariants, and logarithmic performance on histories with up to a million versions.

A deployment system keeps a log of every release it has ever shipped, ordered oldest first and indexed from zero. Each entry records whether that release was broken. Once a release breaks, every release after it inherits the same defect, so the log is **monotonic**: no working release ever appears after a broken one. The log therefore always has the shape `False, False, ..., True, True, ..., True` -- a (possibly empty) block of working releases followed by a (possibly empty) block of broken ones. You are given the log as a list of booleans `bad`, where `bad[i]` is `True` exactly when release `i` is broken. Return the zero-based index of the **earliest broken release**. If no release in the log is broken, return `-1`. The log can be very large, so a solution that inspects only a small number of entries is preferred over one that reads every entry. ## Output semantics The answer is a single integer and is always unique. Monotonicity guarantees there is at most one index that is broken and whose predecessor (if any) works, so no tie-breaking rule is needed. Return `-1`, and only `-1`, when the log contains no broken release. An empty log contains no broken release and therefore also returns `-1`. ## Examples **Example 1** ```text Input: bad = [False, False, True, True, True] Output: 2 ``` Releases 0 and 1 work. Release 2 is the first broken one, and releases 3 and 4 inherit the same defect. **Example 2** ```text Input: bad = [False, False] Output: -1 ``` Every release in the log works, so there is no earliest broken release and the sentinel `-1` is returned. **Example 3** ```text Input: bad = [True] Output: 0 ``` The very first release is already broken.

Constraints

  • 0 <= len(bad) <= 1_000_000
  • Every element of bad is a boolean: True or False.
  • bad is monotonic: no False appears at any index after a True, so bad always has the form False*, True*.
  • The empty log is permitted by the lower bound 0 <= len(bad); it contains no broken release and returns -1.
  • The return value is either the sentinel -1 or an index in [0, len(bad) - 1], so it always lies in [-1, 999_999]. Every value and every intermediate index therefore fits comfortably in a 32-bit signed integer -- int is sufficient in Java and C++ -- and nothing comes anywhere near 2^53.

Examples

Input: ([],)

Expected Output: -1

Input: ([False],)

Expected Output: -1

Hints

  1. Monotonicity means the log is already sorted: every False sits before every True. That is exactly the precondition a search that halves its range needs.
  2. Instead of asking "which entry is broken?", ask "is the earliest broken release at or before index i?". That predicate is False on a prefix of the log and True on the rest, so you can decide which half to discard from a single lookup.
  3. Carry the best (smallest) broken index you have seen so far. Landing on a broken release means you can record it and keep searching strictly to its left; landing on a working one means everything to the left works too. If you finish without ever recording an index, the answer is the sentinel.

Loading coding console...

Show the approach

Approach

The defect is inherited, which makes the log sorted: every False precedes every True. So the predicate "release i is broken" is monotonic in i -- once it turns True it never turns back -- and finding the earliest broken release is the classic problem of finding the leftmost True in a sorted boolean array. That is a binary search.

The reference keeps a half-open-free formulation that is easy to get right: lo and hi bracket the still-unknown range, and answer holds the smallest broken index found so far, initialised to the sentinel -1.

On each step it probes the midpoint:

  • If bad[mid] is True, release mid is broken, so the earliest broken release is mid or something strictly to its left. Record mid as the current best answer and move hi to mid - 1.
  • If bad[mid] is False, release mid works, and by monotonicity so does every release before it. Nothing at or left of mid can be the answer, so move lo to mid + 1.

The loop maintains the invariant that the true answer is either already stored in answer or lies inside [lo, hi]. Each iteration halves the width of that range, so the loop runs at most floor(log2(n)) + 1 times and terminates when the range is empty.

Two edge cases fall out of the initialisation rather than needing special-case branches. An empty log makes hi = -1, so the loop body never executes and the untouched sentinel -1 is returned. A log with no broken release never takes the True branch, so again -1 survives. Conversely an all-broken log drives hi down to -1 while pinning answer at 0.

The midpoint is computed as lo + (hi - lo) / 2 in Java and C++ rather than (lo + hi) / 2. With n <= 10^6 the naive form cannot actually overflow here, but the subtraction form is the habit worth keeping. The C++ version takes std::vector<bool>, which is the bit-packed specialisation: bad[mid] yields a proxy object rather than a real bool&, so it is read by value and never bound to a reference.

Only the O(log n) probes are counted as the algorithm's work. Materialising the input list is inherently O(n) in every language, so no wall-clock measurement can separate a binary search from a linear scan on a fixture that must be deserialised first -- the complexity claim is about lookups into bad, which is what the "inspect only a small number of entries" requirement asks for.

Time complexity:
O(log n)
Space complexity:
O(1)