Hello again. In the previous lesson, you chose a half-open search interval for a first-true search and gave it a contract: return the first index where a monotone predicate becomes true, or if it never does.
This lesson makes that contract durable during the loop. A loop invariant is the precise statement that remains true before the first iteration and after every iteration. For binary search, it is the difference between a template you remember and an algorithm you can safely adapt under pressure.
By the end, you will be able to state the invariant for a half-open first-true search over integer indices, interpret each part, and use it to inspect a trace for correctness.
The object we must not lose: the boundary
Let be a monotone predicate over indices through :
Define the answer, the boundary , as:
Thus is always in :
- when all elements are true.
- when all elements are false.
- Otherwise, is the first valid index satisfying .
For a sorted vector a and target x, a standard example is:
Then is the position returned by std::lower_bound.
Binary search does not need to know directly. It needs to preserve the fact that remains among the positions still possible.
The half-open first-true invariant
For the half-open formulation, initialize:
int lo = 0;
int hi = n;
and continue while:
while (lo < hi)
The full loop invariant is:
Read this as a three-part claim.
| Part | Meaning |
|---|---|
| The bounds form a valid half-open subinterval of the index domain. | |
Every index before lo is false | The first true cannot be to the left of lo. |
Every index from hi onward is true | The first true cannot be to the right of hi. |
The unresolved indices are those in:
But the boundary itself is a position, not necessarily an element index. Therefore the invariant says the answer satisfies:
This is the compact “candidate containment” form of the invariant.
First-true invariant:
lonever passes the first true position, andhinever falls before it.
The more detailed false-prefix and true-suffix version is usually better when proving updates. The compact version is excellent for quickly checking whether an implementation has discarded the answer.
Why empty regions make the invariant work
The initial state is:
At that moment:
- The false region is , which is empty.
- The true region is , also empty.
- The possible boundary interval is , so every legal answer is still possible.
Statements about every element in an empty interval are automatically true. This is called vacuous truth, and it is doing useful engineering work here: no special branch is required for all-true, all-false, or empty inputs.
For an empty vector:
The loop does not run. The invariant already implies:
which is exactly the correct insertion position.
Binary Search - A Different Perspective | Python Algorithms
Watch “Binary Search – A Different Perspective” by mCoding for a concise visual account of answer containment and interval shrinkage. The presenter searches for a first false rather than a first true, but the invariant structure is the same with the truth labels reversed.
In the implementation segment, watch the bound updates. Focus on the claim that low and high are limits on the answer position, not merely array indices being inspected. Then watch the convergence logic, which states the compact invariant: the answer stays between the two bounds while their gap shrinks.
A concrete invariant trace
Consider this predicate:
The boundary is:
Use the usual midpoint:
The trace below emphasizes what has been proved, rather than merely recording variables.
| State | lo | hi | Known false indices | Known true indices | Boundary still possible at |
|---|---|---|---|---|---|
| Initially | 0 | 5 | none | none | through |
After testing mid = 2, false | 3 | 5 | none | through | |
After testing mid = 4, true | 3 | 4 | through | ||
After testing mid = 3, true | 3 | 3 | only |
At the final state:
Since the invariant says , it forces:
Notice what never appears in this reasoning: “we happened to save the best answer seen so far.” The bounds themselves encode all the information needed to recover the answer.

The midpoint is evidence, not just a location
At the start of every loop iteration, the guard gives:
With the half-open midpoint formula,
we have:
So mid is always a valid array index. In particular, although hi may equal , the predicate is never evaluated at .
Now consider what each predicate result proves.
When is false
Monotonicity says every index at or before mid is false:
Therefore the first true position cannot be at mid or to its left. The lower bound can move to:
The false-prefix portion of the invariant becomes larger, while hi stays unchanged. Crucially, remains in the allowed interval.
When is true
Monotonicity says every index from mid onward is true:
The midpoint might itself be the first true index, so it cannot be discarded. The upper bound becomes:
The true-suffix portion of the invariant becomes larger, and the boundary remains possible because it may equal the new hi.
These updates will be derived formally in the next lesson. For now, recognize their invariant-level meaning:
| Observation | New fact established | Bound that moves |
|---|---|---|
| is false | All positions through mid are false | lo = mid + 1 |
| is true | All positions from mid are true | hi = mid |
This is why hi = mid is correct in a half-open first-true loop, while lo = mid would be wrong in the false case: it would retain an index already proved false and can fail to make progress.
Lecture 6 Binary Search - CMU School of Computer Science
Read this portion of Carnegie Mellon University’s lecture notes to see how loop invariants are introduced as statements about the interval under consideration. Its code is for exact-match search rather than first-true search, so use it to study the proof discipline and half-open bounds, not as a boundary-search template.
In Section 2, “Implementing Binary Search” (pp. 4–5), begin where the notes introduce lo and hi as interval bounds. Read the invariant setup. Focus on the basic range invariant 0 \leq lo \leq hi \leq n, the reason for using [lo,hi), and the assertion that the midpoint lies within that interval.
Two equivalent ways to state the invariant
When writing a proof or explaining your solution in an interview, choose one of these forms, then be ready to translate between them.
1. Classification form
This form states exactly what the algorithm has learned about the domain. It is best for deriving and checking updates.
2. Boundary-containment form
Let be the first true position, with if no true index exists. Then:
This form states that the answer remains bracketed. It is concise and makes the termination argument almost immediate.
The two forms are equivalent because is monotone. For example, if all indices before lo are false, then cannot be less than lo. If all actual indices from hi onward are true, then cannot exceed hi.
A strong explanation often states both:
All indices before
loare known false and all indices fromhionward are known true; equivalently, the first true boundary remains in the closed position interval .
Be precise about the subtle contrast:
- The unresolved element interval is half-open: .
- The possible boundary positions form a closed interval: .
That one-character difference explains many apparent off-by-one mysteries.
Diagnosing invariant mistakes
A loop can look plausible while maintaining an invariant too weak or simply wrong.
Mistake 1: “The answer is inside the unresolved array interval”
One might say:
This fails when hi itself is the correct boundary position. For example, with all false values:
the correct answer is . Initially hi = 3, but .
The correct statement is:
The answer is a position, so it may equal the exclusive endpoint of the unresolved element interval.
Mistake 2: evaluating the predicate at hi
Because hi can equal , this is invalid:
if (P(hi)) {
// ...
}
The invariant guarantees only that indices in are true when they exist. It does not define .
Mistake 3: treating “known true” as “the answer”
If is true, mid is an answer candidate, but it is not necessarily the first true index. The invariant only lets you conclude:
That is why the search continues to the left rather than returning immediately.
Mistake 4: omitting the bounds condition
The semantic claims about false and true regions are not enough by themselves for safe implementation. You also need:
It proves that mid is in range whenever the loop runs and that the arithmetic has a well-defined interval interpretation.
A proof-oriented code annotation
Here is the template annotated with the invariant you should be able to state from memory:
template <class Pred>
int first_true(int n, Pred P) {
int lo = 0;
int hi = n;
// Invariant:
// 1. 0 <= lo <= hi <= n
// 2. P(i) is false for every i in [0, lo)
// 3. P(i) is true for every i in [hi, n)
// Therefore, the first true boundary b satisfies lo <= b <= hi.
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (P(mid)) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo;
}
At termination, the loop guard is false:
Together with the range part of the invariant, , this yields:
And since the boundary-containment invariant says:
we conclude:
That is the central correctness story in one chain of reasoning. A later lesson will separate it into the standard proof obligations: initialization, preservation, termination, and partial correctness.
Conclusion
A half-open first-true binary search maintains more than an interval of indices. It maintains a classification of the entire domain:
- is proved false.
- is proved true.
- The first-true boundary remains in .
- The range condition keeps all midpoint accesses safe.
The key conceptual distinction is that contains unresolved elements, whereas contains possible boundary positions. Once lo and hi meet, the invariant forces that shared position to be the answer.
Next, you will derive the midpoint updates formally and show that each one preserves this invariant rather than merely seeming intuitive.
Can't find a good explanation? Sign up and we'll make it for you
Sign up