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:
| Formulation | Meaning of left-side variable after a false midpoint |
|---|---|
| Sentinel bracket | It stores the index of a known-false element, so use l = mid. |
| Half-open first true | It 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 useful way to say this in an interview is:
A true midpoint proves the boundary is at or left of
mid; becausemiditself may be the first true position, I sethi = 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 discardmidalong with the entire left half, settinglo = 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 .
| Step | lo | hi | mid | Result | What has now been proved |
|---|---|---|---|---|---|
| Initial state | 0 | 7 | — | — | No indices classified yet |
| 1 | 0 | 7 | 3 | true | Indices through are true; set hi = 3 |
| 2 | 0 | 3 | 1 | false | Indices and are false; set lo = 2 |
| 3 | 2 | 3 | 2 | false | Index is also false; set lo = 3 |
| Final state | 3 | 3 | — | — | The 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.
| Observation | Incorrect update | What goes wrong |
|---|---|---|
P(mid) is true | lo = mid + 1 | This places a known-true mid inside the claimed false prefix. It can also discard the actual answer when . |
P(mid) is true | hi = mid - 1 | This can discard mid, even though it may be the first true index. |
P(mid) is false | hi = mid | The new true suffix would include a midpoint known to be false. |
P(mid) is false | lo = mid | It 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 true | hi = mid + 1 | It 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:
- Preservation: it must not contradict the false-prefix and true-suffix classifications.
- 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.
- Implement
first_truefrom a blank editor, including the invariant as a comment. - Test it on Boolean vectors represented by a lambda over a
vector<bool>orvector<int>. - For each case, record
lo,hi,mid, and the predicate result until termination.
Use at least these boundary patterns:
| Predicate values | Expected first-true position |
|---|---|
| empty domain | 0 |
| 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
lois false; - every index from
hionward is true; - the known boundary remains between
loandhi.
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,
midmay be the first true position, so retain it withhi = 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