Create your own
Lesson illustration

Invariant-Preserving Updates for First-True Search

Hello again. Last time, you established the half-open first-true contract: for a monotone predicate , return the first position where is true, returning when every array element is false. You also established the invariant that everything before lo is known false, everything from hi onward is known true, and the boundary remains bracketed by .

Now we make the loop body inevitable. Rather than memorizing hi = mid and lo = mid + 1, you will derive each assignment from the invariant and from exactly what one predicate evaluation proves. This is the transferable skill behind reliable binary-search variants.


The update rule follows the meaning of the bounds

Recall the first-true invariant for an integer domain :

The active element range is . The first-true boundary , however, is a position, so it is bracketed by:

While lo < hi, choose:

int mid = lo + (hi - lo) / 2;

Because mid lies in the active interval,

so mid is always a valid index. The only remaining question is: once we evaluate P(mid), which portion of the array can be classified with certainty?


Binary Search - Algorithms for Competitive Programming

Read the “Search on arbitrary predicate” section of CP-Algorithms. It presents the same first-true idea using a different, sentinel-based contract: a left boundary known false and a right boundary known true.

In “Search on arbitrary predicate,” begin with the explanation that binary search can partition an array using a monotone Boolean function. Read the transition description, then continue through the preservation argument and code. Notice that the article uses l = -1 and r = n as sentinels, so its false update is l = m rather than our lo = mid + 1. Do not copy its arithmetic into the half-open template blindly; focus on the shared reasoning: a false midpoint belongs with the known-false side, and a true midpoint belongs with the known-true side.

The difference between these two formulations is worth making explicit:

FormulationMeaning of left-side variable after a false midpoint
Sentinel bracketIt stores the index of a known-false element, so use l = mid.
Half-open first trueIt stores the first index not already known false, so use lo = mid + 1.

The evidence is identical. The variable contracts are different.


Case 1: the midpoint is true

Suppose:

Since is monotone, every later index is also true:

This adds mid and everything to its right to the known-true region.

Crucially, mid might be the first true index. Therefore, discarding it would be unsound. The boundary can equal mid:

So the new upper bound must retain mid as a possible boundary position:

hi = mid;

Formal invariant preservation

Let the updated variables be:

We verify each invariant clause.

1. Bounds remain valid. Since the loop is running, , and the midpoint satisfies . Therefore:

2. The known-false prefix remains correct. It is unchanged:

Every index there was already known false.

3. The known-true suffix expands correctly. The new true suffix is:

At mid, the predicate evaluation directly gave true. At every index after mid, monotonicity gives true. Hence:

The invariant survives.

A first-true search after midpoint index 3 evaluates true: index 3 remains a candidate boundary, so the right boundary is set to 3 while positions to its right are discarded from the unresolved range.

A useful way to say this in an interview is:

A true midpoint proves the boundary is at or left of mid; because mid itself may be the first true position, I set hi = mid.


Case 2: the midpoint is false

Now suppose:

Monotonicity tells us that no earlier index can be true. If some had , monotonicity would force to be true too, contradicting the observation.

Thus:

The entire range through mid is now known false. Since a first-true boundary cannot occur at a false index, the new lower bound should begin immediately after mid:

lo = mid + 1;

Formal invariant preservation

Let:

Again, verify the invariant mechanically.

1. Bounds remain valid. We know . Since bounds are integers:

Therefore:

2. The known-false prefix expands correctly. The new false prefix is:

Every index in this interval is at most mid, and has therefore been proved false.

3. The known-true suffix remains correct. It is unchanged:

Those indices were already known true under the old invariant.

So the update preserves the invariant.

The corresponding interview-quality explanation is:

A false midpoint proves that the boundary is strictly right of mid. I discard mid along with the entire left half, setting lo = mid + 1.

The distinction between “at or left of” and “strictly right of” is exactly where the asymmetric +1 comes from.


One trace, interpreted as accumulated proof

Consider this monotone predicate:

The answer is .

SteplohimidResultWhat has now been proved
Initial state07No indices classified yet
1073trueIndices through are true; set hi = 3
2031falseIndices and are false; set lo = 2
3232falseIndex is also false; set lo = 3
Final state33The boundary must be position

At every row, the implementation does not “guess” which half to search. It makes a stronger statement:

  • A false result enlarges the proved-false prefix.
  • A true result enlarges the proved-true suffix.
  • The unresolved range contains only positions that could still be the boundary.

When lo and hi meet, no uncertainty remains.


Why nearby-looking updates fail

The correct updates are not arbitrary stylistic choices. Some alternatives violate the invariant immediately; others preserve enough information to look plausible but fail to make progress.

ObservationIncorrect updateWhat goes wrong
P(mid) is truelo = mid + 1This places a known-true mid inside the claimed false prefix. It can also discard the actual answer when .
P(mid) is truehi = mid - 1This can discard mid, even though it may be the first true index.
P(mid) is falsehi = midThe new true suffix would include a midpoint known to be false.
P(mid) is falselo = midIt leaves mid, already known false, in the unresolved range. With hi = lo + 1, mid equals lo, so the loop can repeat forever.
P(mid) is truehi = mid + 1It retains an extra index and can fail to shrink a one-element active range.

The key lesson is slightly deeper than “remember the plus one.” An update needs both properties:

  1. Preservation: it must not contradict the false-prefix and true-suffix classifications.
  2. Progress: it must shrink the unresolved interval whenever the loop runs.

For the half-open contract, the strongest safe eliminations are precisely:

if (P(mid)) {
    hi = mid;
} else {
    lo = mid + 1;
}

The complete first-true loop

Here is the template whose body you have now derived rather than memorized:

template <class Pred>
int first_true(int n, Pred P) {
    int lo = 0;
    int hi = n;

    while (lo < hi) {
        int mid = lo + (hi - lo) / 2;

        if (P(mid)) {
            // P(mid) is true, so every index in [mid, n) is true.
            // mid could be the first true position.
            hi = mid;
        } else {
            // P(mid) is false, so every index in [0, mid + 1) is false.
            lo = mid + 1;
        }
    }

    return lo;
}

For a sorted vector a, this becomes lower-bound behavior:

int pos = first_true(static_cast<int>(a.size()),
                     [&](int i) { return a[i] >= target; });

The function returns the first position where inserting target would preserve sorted order. If target is absent, pos is still meaningful: it is the insertion position. If target is larger than every element, the predicate is all false and the function returns a.size().


Deliberate implementation practice

Spend about 10–15 minutes turning the template into a habit without treating it as a magic incantation.

  1. Implement first_true from a blank editor, including the invariant as a comment.
  2. Test it on Boolean vectors represented by a lambda over a vector<bool> or vector<int>.
  3. For each case, record lo, hi, mid, and the predicate result until termination.

Use at least these boundary patterns:

Predicate valuesExpected first-true position
empty domain0
1
0
3
0
1
2
3

While tracing, do not merely check the final answer. At each iteration, verify the invariant:

  • every index before lo is false;
  • every index from hi onward is true;
  • the known boundary remains between lo and hi.

That check catches flawed updates before a test case happens to expose them.


Conclusion

The first-true updates follow directly from the classification invariant:

  • If is true, mid may be the first true position, so retain it with hi = mid.
  • If is false, the boundary lies strictly after it, so discard it with lo = mid + 1.
  • The updates are asymmetric because a true midpoint must remain a candidate, while a false midpoint cannot be one.
  • Correct binary search needs both invariant preservation and strict interval shrinkage.

Next, you will derive a last-true search from this same boundary reasoning, rather than learning a disconnected second template.

Can't find a good explanation? Sign up and we'll make it for you

Sign up