Good to see you again. In the previous lesson, you derived a robust half-open first-true search: it finds the first index where a false-then-true predicate changes state, returning n if no true value exists.
This lesson derives last true rather than treating it as a second template to memorize. The central move is to recognize that “last true” is the position immediately before the first false. You will translate that observation into a loop, a contract, and a C++ implementation that handles all-true, all-false, and empty domains cleanly.
From last true to first true
Suppose has this monotonic form over indices through :
The problem is to find the greatest index for which is true. If no index is true, the natural sentinel answer is .
Instead of inventing a new kind of search, negate the predicate:
Now the sequence becomes:
That is exactly the first-true form from the previous lesson.
Let be the first index where is true:
Since means is false, is the first false index for . Therefore the last true index is one position earlier:
Equivalently,
This identity is the derivation. The rest is just expressing it faithfully in code.

Consider a concrete predicate:
Negating it gives:
The first true index of is , hence the last true index of is:
The subtraction is not a trick or a patch. It expresses the exact relationship between adjacent boundary positions.
Get Binary Search Right Every Time, Explained Without Code
Read the short last-occurrence example in Nil Mamano’s article. It uses the useful “before region / after region” view: for a last occurrence, the desired element is the final item in the before region.
In the article’s “The reduction” discussion, begin at the paragraph starting with the choice of transition point. Focus on why the predicate x <= target places every target occurrence in the before region, making the final such index the desired answer.
Rewrite the first-true loop mechanically
Recall the first-true template for a predicate :
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;
}
To obtain last true for , call this function with :
template <class Pred>
int last_true(int n, Pred Q) {
return first_true(n, [&](int i) {
return !Q(i);
}) - 1;
}
This version is excellent during development because the relationship is explicit. It says precisely what the algorithm is doing: find the first invalid position, then step back once.
For competitive programming or interviews, you will often write the inlined version. Start with the substitution:
The first-true branch
if (P(mid)) {
hi = mid;
} else {
lo = mid + 1;
}
becomes:
if (!Q(mid)) {
hi = mid;
} else {
lo = mid + 1;
}
and, written in the more natural branch order:
if (Q(mid)) {
lo = mid + 1;
} else {
hi = mid;
}
So the complete half-open last-true template is:
template <class Pred>
int last_true(int n, Pred Q) {
int lo = 0;
int hi = n;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (Q(mid)) {
lo = mid + 1;
} else {
hi = mid;
}
}
return lo - 1;
}
The endpoint lo is the first false position. The final answer is the element immediately before it.
Why the updates reverse
The update directions can be derived from the goal of finding the first false:
-
If is true, then
midcannot be the first false position. It belongs to the known-true prefix, so discard it:lo = mid + 1; -
If is false, then
midcould be the first false position. Retain it as a candidate:hi = mid;
This is the same asymmetry as first true, with true and false interchanged.
| Search objective | Predicate pattern | Midpoint is in candidate boundary position when… | Update |
|---|---|---|---|
| First true | P(mid) is true | hi = mid | |
| Last true | Q(mid) is false, because it may be the first false | hi = mid |
For last true, you are not directly retaining a “last true candidate” during the half-open loop. You are locating the first false boundary and converting that boundary to the final true index afterward.
The invariant, stated in the new vocabulary
A correct implementation should have a contract you can say aloud before writing code.
For last_true(n, Q), assume is monotone in the form:
Equivalently:
During the loop, maintain:
Thus:
- positions before
loare proved true; - positions from
hionward are proved false; - the unknown transition lies in .
When the loop finishes, lo == hi. Call this position . It is the first false position, including the possibility . Consequently,
Notice how the sentinel arises without special-case code. If , then no index can be true, and .
A trace
Let:
The expected last true index is .
| Step | lo | hi | mid | Resulting knowledge | |
|---|---|---|---|---|---|
| Initial | 0 | 7 | — | — | No positions classified |
| 1 | 0 | 7 | 3 | true | Indices through are true; set lo = 4 |
| 2 | 4 | 7 | 5 | false | Indices and are false; set hi = 5 |
| 3 | 4 | 5 | 4 | false | Index is false; set hi = 4 |
| Final | 4 | 4 | — | — | First false is ; return |
The loop did not search right merely because it “wanted a larger answer.” A true midpoint proved that the first false must occur strictly after mid. Every move follows from the boundary contract.
Edge cases are part of the contract
The formula return lo - 1 deals with the important cases uniformly.
| Predicate over the domain | Final lo / first false | Returned last true |
|---|---|---|
| Empty domain | ||
Two details matter:
-
All true: there is no actual false element, but the conceptual first-false position is the one-past-the-end position . Returning correctly gives the final valid index.
-
All false: the first-false position is . Returning correctly signals that no valid index exists.
Avoid changing the loop condition or adding a separate ans variable merely to handle these cases. The half-open boundary contract has already accounted for them.
Sorted-array application: last occurrence through a boundary
For a sorted vector a, define:
Because the vector is sorted, this predicate has the required shape:
Therefore:
int pos = last_true(static_cast<int>(a.size()),
[&](int i) { return a[i] <= target; });
pos is the greatest index whose value is at most target.
That is close to “last occurrence of target,” but not identical. If target is absent, pos may identify the predecessor of target, not an occurrence. Validate the result:
int last_occurrence(const vector<int>& a, int target) {
int pos = last_true(static_cast<int>(a.size()),
[&](int i) { return a[i] <= target; });
if (pos >= 0 && a[pos] == target) {
return pos;
}
return -1;
}
For example, with:
-
For
target = 4, the predicate is:so
last_truereturns , the last occurrence of . -
For
target = 7, the predicate is still:and
last_trueagain returns . Buta[3]is , not , so the target is absent.
This distinction will matter whenever a boundary is used to answer an exact-match question: the boundary identifies a structural position; a final value check determines whether the requested value actually occurs.
Binary search - finding first or last occurrence of a number
Watch mycodeschool’s “Binary search – finding first or last occurrence of a number” for a visual trace of the conventional “store an answer and keep searching right” approach. Compare it with the boundary formulation here: both seek the same final index, but the boundary version derives it as one less than the first false position.
Watch the last occurrence trace. Notice that after a matching midpoint, the search continues right; translate that behavior into the predicate Q(i)\equiv a[i]\leq\texttt{target}, where a true midpoint causes lo = mid + 1.
A compact implementation checklist
When a problem asks for the greatest integer or index satisfying a condition, use this sequence:
- Define : “is valid?”
- Prove the pattern is true then false as increases.
- Search for the first where is false.
- Return one position before that boundary.
- Decide whether is an appropriate “no valid answer” sentinel for the problem.
For the implementation, retain the four lines that encode the derivation:
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (Q(mid)) lo = mid + 1;
else hi = mid;
}
return lo - 1;
For a short practice session, implement last_true first by calling your existing first_true with !Q, then inline the loop. Differential-test both versions on randomly generated true-prefix/false-suffix Boolean vectors, including an empty vector and the all-true/all-false cases. They should always produce the same answer.
Conclusion
Last true is not a disconnected binary-search template:
The half-open loop therefore maintains a known-true prefix and a known-false suffix:
- a true midpoint is definitely before the first false, so set
lo = mid + 1; - a false midpoint may be the first false, so set
hi = mid; - at termination,
lois the first false position andlo - 1is the last true position.
Next, you will address a lower-level reliability issue that affects every variant: computing the midpoint safely when signed integer bounds can be large.
Can't find a good explanation? Sign up and we'll make it for you
Sign up