Hello again. In the previous lesson, you established the prerequisite for binary search: a predicate must be monotone over a clearly ordered domain. For a first-true search, its values have the form false values followed by true values.
Now comes the implementation decision that causes many off-by-one bugs: what do your bounds mean? An interval such as or is not merely notation. It is a contract governing which candidates remain unresolved, what happens on empty input, and whether a “not found” result can be represented safely.
By the end of this lesson, you should be able to select inclusive or half-open bounds deliberately, state the result contract they support, and derive the matching loop guard and updates without mixing conventions.
Start with the boundary contract, not the loop
Let be monotone over valid array indices through :
The cleanest first-true contract is not “return an index containing true.” That wording fails when every value is false. Instead, define a boundary position :
This contract always has an answer:
- If some index is true, is the first true index.
- If all values are false, .
- If , .
So the output is an insertion-style position in , not necessarily a valid element index in .
For a sorted vector and target x, this is exactly the contract behind a lower bound:
The returned boundary is the first position at which x could be inserted while preserving sorted order.
That distinction resolves several edge cases before any code exists. In particular, returning is not an error for a boundary search. It means “there is no true element among the array indices.”
Binary Search - Algorithms for Competitive Programming
Read the “Implementation” section from Algorithms for Competitive Programming for a compact treatment of sentinels and half-open bounds. It is especially useful for seeing why positions just outside an array can be part of a mathematical contract without ever being accessed.
In the “Implementation” section, start with the sentinel boundary setup. Continue through the code and the paragraph immediately after it, stopping before the “Search on arbitrary predicate” heading. Focus on the distinction between assigning a bound the value -1 or n and actually evaluating an array element at that location.
Half-open bounds: make the empty range natural
The most useful first-true convention is a half-open unresolved interval:
Initialize it as:
Here, hi is exclusive: it may equal , but P(hi) is never evaluated.
A precise interpretation during the search is:
- Every index strictly before
lois known false. - Every index at or after
hiis known true, if it is an actual array index. - The boundary position lies between
loandhi, inclusive.
The remaining unclassified element indices are exactly those in . Meanwhile, the boundary itself is a gap between indices, so it may equal either endpoint.
The matching updates
Choose
When lo < hi, this guarantees:
Thus mid is always a valid array index.
Now inspect .
If is true, mid may be the first true index. You must not discard it as a possible boundary. Set:
This removes mid from the unresolved element interval, but retains it as the right endpoint of the possible boundary range.
If is false, neither mid nor anything left of it can be the first true index. Set:
The resulting first-true search is:
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)) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo; // First true index, or n if none exists.
}
For now, focus on the contract rather than memorizing the template. A later lesson will formalize its invariant and prove why these updates preserve it.
Trace: a normal transition
Suppose:
Then , and the first true boundary is .
| Iteration | lo | hi | mid | Action | |
|---|---|---|---|---|---|
| Start | 0 | 5 | — | — | unresolved indices are |
| 1 | 0 | 5 | 2 | false | lo = 3 |
| 2 | 3 | 5 | 4 | true | hi = 4 |
| 3 | 3 | 4 | 3 | true | hi = 3 |
| End | 3 | 3 | — | — | return 3 |
The same code handles extreme cases without special branches:
| Predicate pattern | Meaning | Returned boundary |
|---|---|---|
| all values qualify | ||
| no values qualify | ||
| empty array | no indices exist |
The loop guard lo < hi is meaningful: it says that at least one unresolved element index remains. When lo == hi, the unresolved half-open interval is empty, and that shared position is the boundary.
Inclusive bounds: search actual indices, allow them to cross
An inclusive interval is written:
For an array, initialize it as:
Unlike the half-open form, an empty array begins with:
That is valid: the inclusive interval contains no indices.
With inclusive bounds, the natural loop guard is:
This says that at least one valid array index remains to inspect.
For the same first-true boundary contract, use:
template <class Pred>
int first_true_inclusive(int n, Pred P) {
int lo = 0;
int hi = n - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (P(mid)) {
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return lo; // First true index, or n if none exists.
}
This may look only one character different from the half-open version, but its logic is distinct.
Why does a true midpoint use hi = mid - 1 here?
In the inclusive formulation, hi denotes the last unclassified index. Once is true:
midis a valid candidate for the answer;- every index strictly to its right is also true;
- the only possible improvement is a smaller true index.
So the still-unclassified region must end at mid - 1:
The boundary position mid has not been lost. It is represented by the fact that eventually lo can cross hi:
At termination, lo is precisely the first true position, or if no true index was found.
A useful inclusive interpretation is:
- indices before
loare known false; - indices after
hiare known true; - the boundary belongs to .
The is important. It accounts for a boundary immediately after the final unresolved index.
Binary Search tutorial (C++ and Python)
Errichto Algorithms’ “Binary Search tutorial (C++ and Python)” compares an inclusive first-qualifying search with the broader false-then-true viewpoint. Watch it to connect each pointer movement with the goal of finding the leftmost valid result rather than merely any valid result.
First watch the leftmost search. Notice that a qualifying midpoint is saved and the inclusive right bound moves to mid - 1, because a better answer can only be to the left. Then watch the Boolean boundary view, which reframes the same task as locating the first true in a monotone predicate.
The video uses an ans variable to remember the best true index seen so far. That is also correct:
int ans = n;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (P(mid)) {
ans = mid;
hi = mid - 1;
} else {
lo = mid + 1;
}
}
return ans;
For a first-true predicate, ans and lo end with the same value. The version returning lo is often easier to reason about because it directly returns the boundary position. The ans version can feel more natural when the required fallback is problem-specific, such as -1, an optional result, or a special object.
The two conventions are equivalent—but their rules are not interchangeable
Both styles solve the same contract:
Their difference lies in what the bounds represent.
| Design choice | Half-open formulation | Inclusive formulation |
|---|---|---|
| Unresolved indices | ||
| Initial bounds | lo = 0, hi = n | lo = 0, hi = n - 1 |
| Empty-array state | lo == hi == 0 | lo = 0, hi = -1 |
| Loop guard | lo < hi | lo <= hi |
On P(mid) == true | hi = mid | hi = mid - 1 |
On P(mid) == false | lo = mid + 1 | lo = mid + 1 |
| Result | lo | lo |
| No true index | returns n | returns n |
The key distinction is the true case:
- In a half-open interval,
hi = midis progress becausemid < hibefore the update. - In an inclusive interval,
hi = midcan fail to make progress whenlo == hi == mid. Therefore it must behi = mid - 1.
This is not a stylistic preference. It is forced by the interval contract.
Two broken hybrids to recognize immediately
Hybrid 1: half-open initialization with an inclusive guard
int lo = 0, hi = n;
while (lo <= hi) {
// ...
}
With , a false predicate can move lo to 1. The next iteration still satisfies lo <= hi, and the code may attempt to evaluate P(1), which is outside the array.
Hybrid 2: inclusive initialization with a half-open guard
int lo = 0, hi = n - 1;
while (lo < hi) {
// ...
}
With a singleton array, lo == hi == 0 initially, so the loop does not inspect the only element at all.
A compact diagnostic rule is:
Bounds, loop guard, and updates form one unit. Borrowing a line from another template breaks the proof unless you re-derive the contract.
Choosing a convention in C++ problem solving
For boundary searches over arrays, half-open bounds are usually the strongest default.
Use the half-open form when:
- the answer is naturally an insertion position;
- “not found” should be represented by
n; - empty ranges should work without a special case;
- you want reasoning aligned with iterator ranges such as
[first, last); - you are conceptually implementing
std::lower_boundorstd::upper_bound.
Use an inclusive form when:
- the problem naturally states an integer domain with inclusive endpoints;
- you want to search all values in a closed domain such as through ;
- you deliberately maintain a best-known answer;
- you are implementing classic exact-match search, where exiting after
lo > hinaturally means the target is absent.
For example, suppose the candidate answer is a day from through , and feasible(day) is false then true. Both contracts are valid:
| Representation | Initial bounds | No feasible day |
|---|---|---|
| Half-open | returns 31 | |
| Inclusive | returns 31 after bounds cross |
In either case, 31 is a sentinel boundary, not a valid day. If the problem requires -1 instead, convert only after the search:
int day = first_true(...);
return day == 31 ? -1 : day;
Keep the internal search contract uniform; perform output-format conversion at the boundary of the function.
One C++ caution follows from this choice. Inclusive array bounds often require hi = -1 for an empty range, so size_t is a poor type for that convention because it is unsigned. A half-open index range avoids negative bounds and is friendlier to size_t, although competitive-programming code often uses signed int or long long after checking that the domain fits. Midpoint overflow and signed sentinel bounds will receive dedicated treatment later.
A short pre-code contract checklist
Before writing any binary-search loop, write these facts on paper or in a comment:
- Output: Is the answer an element index, an insertion position, a maximum feasible value, or a sentinel?
- Domain: Are candidates valid at , , or some other interval?
- Empty and extreme cases: What should all-false, all-true, and empty input return?
- Convention: Are bounds inclusive or half-open?
- Guard: Does it express “at least one unresolved candidate remains” for that convention?
- Updates: When the midpoint is true or false, which side is proved irrelevant, and does the chosen update strictly reduce the unresolved region?
If each answer is explicit, the code is usually mechanical. If one is vague, an off-by-one error is likely waiting in the implementation.
Conclusion
A binary-search interval is a proof contract, not a pair of variables chosen by habit.
- A robust first-true contract returns a boundary position in , with meaning “no true index exists.”
- Half-open bounds begin at , use
while (lo < hi), and update a true midpoint withhi = mid. - Inclusive bounds begin at , use
while (lo <= hi), and update a true midpoint withhi = mid - 1. - The same return value can arise from either convention, but their guards and updates cannot be mixed.
- Empty input, all-false input, and all-true input should follow directly from the stated contract rather than from special-case patches.
Next, you will make the half-open first-true formulation fully rigorous by stating its loop invariant: exactly what is known to be false, what is known to be true, and where the boundary must remain after every iteration.
Can't find a good explanation? Sign up and we'll make it for you
Sign up