skip to content

A reachability scan takes the maximum of i + a[i] over every stone but never compares i to that maximum — what breaks?

level: middleimportance: should knowfreq 42%

answer

  1. the scan trusts every index equally
  2. some stones are never stood on
  3. the maximum must aggregate reachable stones only
  4. try leaps 1, 0, 0, 5
  5. compare the loop index against the reach first

basics

~20 s

It credits stones you can never stand on, so it reports crossings that are impossible. With leaps [1, 0, 0, 5] you stall on the second stone, but the unguarded scan folds in the fourth stone's range and answers yes. The fix is to stop as soon as the index passes the recorded reach.

solid answer

~40 s

The invariant is supposed to be "`maxReach` is the furthest index reachable using stones **proven reachable** so far". An unguarded loop silently changes that to "using **all** stones so far", which is a different and much larger quantity. Trace leaps `[1, 0, 0, 5]`: index 0 sets reach 1; index 1 has range 0, reach stays 1; index 2 is already unreachable but the loop folds it in anyway (reach 2); index 3 is unreachable too, yet contributes `3 + 5 = 8`, so the final test `maxReach >= n - 1` says yes when the true answer is no. The one-line fix is a guard at the top of the body: if `i > maxReach`, return not-crossable immediately. Everything else stays the same, and the pass is still O(n) time and O(1) space.

code

pseudocode · 6 lines
pseudocode
// stone i lets you leap at most a[i] stones forward
maxReach = 0
for i in 0..n-1:
    maxReach = max(maxReach, i + a[i])
    ...
return maxReach >= n - 1

go deeper

for a junior

Know that the furthest-reach scan must skip nothing and trust nothing: an index only contributes its range if the running reach already covers it. Be able to check a short input by hand.

for a middle

State the invariant in words, then show the input class that violates it — a gap followed by a large range. Explain why the guard has to precede the reach update rather than follow it.

for a senior

Test by invariant rather than by intuition: name one input per failure mode — the gap-then-large-range case, the single-element case, the jumped-over zero, and the near-ceiling values that overflow a fixed-width sum.

for a principal

Treat over-estimating reachability as the dangerous direction of failure and set the review expectation accordingly: a false yes strands the caller silently, so the boundary cases belong in the test suite, not in a reviewer's memory.

## What the loop is supposed to maintain A furthest-reach reachability scan over stones — stone `i` permits a leap of at most `a[i]` forward — rests on one invariant: > After processing index `i`, `maxReach` equals the furthest index reachable from stone `0` using only stones `0..i`. The word doing all the work is *reachable*. A stone contributes its range to the frontier only if you can actually stand on it. Drop the guard and the loop computes something else entirely: the maximum of `i + a[i]` over all indices, reachable or not. That quantity is an over-estimate, and over-estimating reachability produces **false positives** — the worst kind of bug here, because the algorithm confidently answers yes on inputs that strand you. ## The trace that exposes it Leaps `[1, 0, 0, 5]`, so `n = 4` and the goal is index `3`. | i | a[i] | true reachable? | unguarded maxReach | |---|---|---|---| | 0 | 1 | yes | max(0, 0+1) = 1 | | 1 | 0 | yes | max(1, 1+0) = 1 | | 2 | 0 | **no** | max(1, 2+0) = 2 | | 3 | 5 | **no** | max(2, 3+5) = 8 | Final test `8 >= 3` returns crossable. The truth: from stone `0` you can only leap to stone `1`, whose range is `0`, so you are stuck. Index `2` was never standable, and the moment the loop folded index `2` into the reach it manufactured a foothold that does not exist — after which index `3` looked reachable, and its enormous range finished the job. Note how the failure is engineered: an early dead stone, then a large range parked just past the gap. Random inputs rarely expose it, which is exactly why this bug survives casual testing and shows up in a review. ## The fix Guard first, extend second: 1. If `i > maxReach`, the current index sits past the reachable prefix — return not-crossable. 2. Otherwise `maxReach = max(maxReach, i + a[i])`. Order matters: extending before guarding would let the current stone rescue itself. Some write the equivalent as a loop bound (`while i <= maxReach and i < n`), which is the same idea expressed in the control flow rather than the body. ## The other boundary cases in the same family - **A single stone.** `n = 1` means you are already across, and the answer must be yes before any leap is attempted. A loop that unconditionally requires a positive range on the first stone gets this wrong. - **Trailing zero range.** The last stone's range is irrelevant — you need to arrive, never to leave. Code that special-cases it is confused. - **Fixed-width overflow.** If ranges can approach the maximum of a signed 32-bit accumulator, `i + a[i]` wraps to a negative value and the reach *collapses* rather than growing, turning a crossable input into a reported failure. The safe comparison is `a[i] >= n - 1 - i`, which never adds two large values, or accumulate in a wider type. - **Zero-length input**, if the caller can produce one, needs a defined answer before the loop, not inside it. ## How to talk about it in an interview When asked to test your own code, do not read the loop back line by line. Name the *class* of input each bug needs: one input where a gap precedes a large range (catches the missing guard), one input of length one (catches the initialisation), one input with a zero range that is jumped over rather than landed on (catches over-eager failure), and one input with values near the accumulator's ceiling (catches overflow). Four inputs, each aimed at a specific way the invariant can be violated, is a far stronger answer than a dozen random arrays — and it demonstrates that you know what the invariant *is*, which is the actual thing being assessed.

  • Why does the guard have to run before the reach update rather than after it?
    Because updating first lets an unreachable stone rescue itself: index `i` would contribute `i + a[i]` to the reach, and the subsequent comparison against the now-inflated reach would pass. The guard's whole job is to decide whether this stone is standable *given only earlier stones*, so it must be evaluated against the reach as it stood before this index was seen.
  • How would you write the reach comparison so that huge range values cannot overflow a fixed-width accumulator?
    Compare without summing: test `a[i] >= n - 1 - i` to see whether this stone alone finishes the crossing, and keep the running reach clamped at `n - 1` since nothing beyond the last stone matters. Clamping makes the stored value bounded by the input size regardless of how large individual ranges are.
  • Does a backwards scan avoid this bug class?
    It trades it for a different one. Scanning from the far bank and tracking the leftmost index known to reach it is also linear and has no unreachable-stone problem, but it has its own boundary: the known-good marker must be initialised to the final index, and forgetting that makes every input report failure. Neither direction is bug-free by construction.

saying these in an interview costs you the question

  • Assumes every index in the loop is a stone you can stand on
  • Tests only at the end instead of failing fast on a gap
  • Updates the furthest reach before checking the index is reachable
  • Believes random test inputs would surface the missing guard
  • Ignores that i + a[i] can wrap in a fixed-width accumulator

context