Create your own
Lesson illustration

Deriving Last-True Search from First-True Boundaries

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.

The left side shows a false-then-true predicate, where `hi` ends at the first true position. The right side shows a true-then-false predicate: searching for its first false position leaves `lo` there, so `lo - 1` is the last true position.

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 mid cannot be the first false position. It belongs to the known-true prefix, so discard it:

    lo = mid + 1;
    
  • If is false, then mid could 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 objectivePredicate patternMidpoint is in candidate boundary position when…Update
First trueP(mid) is truehi = mid
Last trueQ(mid) is false, because it may be the first falsehi = 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 lo are proved true;
  • positions from hi onward 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 .

SteplohimidResulting knowledge
Initial07No positions classified
1073trueIndices through are true; set lo = 4
2475falseIndices and are false; set hi = 5
3454falseIndex is false; set hi = 4
Final44First 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 domainFinal lo / first falseReturned last true
Empty domain

Two details matter:

  1. 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.

  2. 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_true returns , the last occurrence of .

  • For target = 7, the predicate is still:

    and last_true again returns . But a[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:

  1. Define : “is valid?”
  2. Prove the pattern is true then false as increases.
  3. Search for the first where is false.
  4. Return one position before that boundary.
  5. 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, lo is the first false position and lo - 1 is 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