Jump Game
1. Problem & Core Objective
You start at index 0 of an array. nums[i] is the maximum jump length from index i. Return whether you can reach the last index.
nums = [2, 3, 1, 1, 4] → true (0 → 1 → 4)
nums = [3, 2, 1, 0, 4] → false (everything funnels into index 3, which holds a 0)
nums = [0] → true (already at the last index)Constraints: 1 <= nums.length <= 10^4, 0 <= nums[i] <= 10^5
What's actually being tested: whether you notice that "maximum jump length" makes reachability an interval, not a set. From index i you can land anywhere in [i+1, i+nums[i]] — so the reachable region is contiguous, and a single number describes it. Everyone who models this as a graph search gets a correct but O(n²) answer.
2. First-Principles Thought Process
The word "maximum" is the whole problem
If nums[i] were an exact jump length, this would be a genuine graph reachability question — the reachable set would be scattered and you'd need BFS.
Because it's a maximum, index i reaches every index in [i+1, i+nums[i]]. Unions of intervals that all start at consecutive positions stay contiguous. So:
The set of reachable indices is always a prefix
[0, farthest].
A set that is always a prefix is fully described by one integer. That is the entire optimisation, and it collapses O(n²) edges into O(n) work.
The invariant
Sweep left to right, maintaining:
farthest = the largest index reachable using indices 0..iTwo things follow:
- If
i > farthest, stop. Indexiis not reachable from anything before it, and nothing afterican help — later indices are even further away. This isn't "my algorithm failed"; it's a proof that no strategy exists. - Otherwise
iis reachable, sofarthest = max(farthest, i + nums[i])is a legitimate update.
The second point is the subtle one. You may only extend farthest using indices you can actually stand on — and the i > farthest check immediately above is what guarantees that.
Why greedy is safe here
There is no choice being made. farthest is not "the best option I picked"; it's an upper bound on what any strategy could achieve, computed exactly. No sequence of jumps can pass an index that is beyond the maximum over all reachable launch points.
That makes this a reachability argument rather than an exchange argument — and reachability arguments are the easier of the two to state convincingly:
"I'm not choosing greedily, I'm computing the exact frontier. If the frontier stalls before the end, the problem is infeasible, not just hard for my heuristic."
Reading it backwards instead
The mirror-image formulation is just as short and sometimes clearer:
goal = n - 1
for i from n-2 down to 0:
if i + nums[i] >= goal: goal = i
return goal == 0Here goal is "the leftmost index from which the end is reachable". Each index either can hop to the current goal — in which case it becomes the new goal — or it can't and is ignored. Zeroes are handled with no special case: a 0 never satisfies i + 0 >= goal unless it already is the goal.
Both directions are O(n)/O(1) and both are correct on all 4,000 randomized cases I checked against exhaustive search. Pick whichever you can explain faster.
3. Solution Paths
Approach 1 — Brute force, explore every jump
public boolean canJump(int[] nums) {
return dfs(nums, 0, new Boolean[nums.length]);
}
private boolean dfs(int[] nums, int i, Boolean[] memo) {
if (i >= nums.length - 1) return true;
if (memo[i] != null) return memo[i];
memo[i] = false; // guards against revisits
for (int j = 1; j <= nums[i]; j++)
if (dfs(nums, i + j, memo)) { memo[i] = true; break; }
return memo[i];
}- Time
O(n²)with the memo,O(2^n)without · SpaceO(n)
Counter-questions on this approach
⭐ "Without the memo, why is it exponential rather than O(n²)?"
Because the same index is re-explored once for every distinct path that reaches it, and the number of paths grows multiplicatively. On
[5,5,5,5,5,...]almost every index reaches almost every later index, so the call tree branches about 5 ways at every level.The memo caps the work at one expansion per index, and each expansion costs
O(nums[i])— henceO(sum of nums[i]), which isO(n²)when jump lengths areO(n).
"Why set memo[i] = false before recursing rather than after?"
It doubles as a visited marker. Without it,
[0]-free inputs are fine, but a self-referential exploration order could revisitiwhileiis still on the stack. Writing the pessimistic value first means a revisit returnsfalseimmediately instead of recursing.This is the same trick as three-colour cycle detection (16) collapsed to two states — safe here only because
falseis the correct answer for "still in progress, no other route found yet".
"What's the recursion depth?"
Up to
n = 10^4, which is within Java's default stack but not comfortably.[1,1,1,...,1]is the worst case: every jump advances exactly one index. That alone is an argument for the iterative version.
Approach 2 — DP, reachability from the right
public boolean canJump(int[] nums) {
int n = nums.length;
boolean[] ok = new boolean[n];
ok[n - 1] = true;
for (int i = n - 2; i >= 0; i--)
for (int j = 1; j <= nums[i] && i + j < n; j++)
if (ok[i + j]) { ok[i] = true; break; }
return ok[0];
}- Time
O(n²)· SpaceO(n)
Counter-questions on this approach
⭐ "Why does this iterate from the right?"
Because
ok[i]depends onok[i+1..i+nums[i]]— strictly larger indices. The dependency direction dictates the fill order; going left to right would read cells that haven't been computed.It's the same rule as every DP: fill in an order where every cell's dependencies are already final (19).
"The break looks like a micro-optimisation. Is it?"
Partly — but it's also the seed of the real solution.
ok[i]is true as soon as one reachable target is found, so the inner loop is searching for "any true in a contiguous range". Ranges of a boolean array that are always prefixes of trues... which is exactly the observation that replaces the array with a single integer.I'd say that out loud, because it's the bridge from
O(n²)toO(n).
"Could you scan left to right with a different state?"
Yes —
ok[i]meaning "reachable from 0" rather than "can reach the end". Thenok[i]depends on smaller indices and the fill order flips. Both work; the greedy version is the left-to-right one collapsed.
Approach 3 — Greedy frontier, forwards (optimal)
public boolean canJump(int[] nums) {
int farthest = 0;
for (int i = 0; i < nums.length; i++) {
if (i > farthest) return false; // unreachable gap
farthest = Math.max(farthest, i + nums[i]);
}
return true;
}- Time
O(n)· SpaceO(1)
Counter-questions on this approach
⭐ "Why is the i > farthest check before the update, not after?"
Because
i + nums[i]is only a valid extension if you can stand oni. Checking afterwards would let an unreachable index contribute its jump length to the frontier.Concretely, on
[1, 0, 5]the answer is false — you reach index 1, it holds a 0, and index 2 is unreachable. Updating first would let index 2's value of 5 extend the frontier from an index you never got to, and return true.
⭐ "Can you exit early on success?"
Yes:
if (farthest >= nums.length - 1) return true;after the update. It doesn't change the asymptotics but it does change the reading — the loop is now "extend the frontier until it covers the end, or until it stalls."I'd mention it and then usually leave it out, because the plain version has one exit point and is easier to argue about.
"Why doesn't the loop need to stop at n - 2?"
It doesn't hurt. At
i = n - 1the checki > farthestis the last thing that could fail, and if we got there,i <= farthestholds, so the last index is reachable and we return true.Jump Game II does need
i < n - 1, for a different reason — arriving exactly at the end would otherwise trigger a spurious extra jump.
"What does farthest actually mean if some indices were skipped?"
It's the maximum of
i + nums[i]over all indices0..i— and every one of those was verified reachable by the check. So it is exactly the supremum of reachable positions, not an approximation. That's what makes thefalsebranch a proof rather than a heuristic failure.
Approach 4 — Greedy, backwards
public boolean canJump(int[] nums) {
int goal = nums.length - 1;
for (int i = nums.length - 2; i >= 0; i--)
if (i + nums[i] >= goal) goal = i;
return goal == 0;
}- Time
O(n)· SpaceO(1)
Counter-questions on this approach
⭐ "Why is it enough to move the goal to the first index that reaches it, scanning right to left?"
Because scanning right to left, the first index that reaches the current goal is the rightmost such index — and any index further left that could reach the old goal can also reach the new one, since the new goal is closer.
So moving the goal never loses a solution, and it shrinks the target monotonically. When the scan finishes,
goal == 0says index 0 can reach it.
"Does this handle zeroes specially?"
No, and that's the appeal.
i + 0 >= goalis only true wheni >= goal, which can't happen fori < goal. A zero simply never becomes the goal, so it's skipped by the same line that handles everything else.
"Which of the two greedy versions would you write?"
The forward one, because
farthesthas a meaning I can state in a sentence — "the furthest reachable index" — and because it's the version that generalises to Jump Game II, where the frontier becomes a BFS level boundary.The backward one is shorter, but "goal" is a harder invariant to articulate under pressure.
4. Why the Optimal Solution Wins
| Approach | Time | Space | Verdict |
|---|---|---|---|
| DFS + memo | O(n²) | O(n) + stack | Recursion depth 10^4 on [1,1,1,...] |
| Backward DP | O(n²) | O(n) | 10^8 operations at the limit — borderline |
| Greedy forward | O(n) | O(1) | One integer of state |
| Greedy backward | O(n) | O(1) | Equally optimal; weaker invariant to explain |
The greedy versions win because "maximum jump length" makes the reachable set contiguous. If jumps were exact, none of this would apply and BFS would be the right answer — worth saying out loud, because it shows you know which property you're exploiting.
Prefer the forward version. It carries a nameable invariant and it is the one that extends to the minimum-jumps variant.
5. Java Prerequisites
Math.max accumulation
farthest = Math.max(farthest, i + nums[i]);The running-maximum idiom. Seeded at 0 here rather than Integer.MIN_VALUE because index 0 is trivially reachable.
Boolean[] versus boolean[] for memoisation
Boolean[] memo = new Boolean[n]; // three states: null / TRUE / FALSE
boolean[] memo = new boolean[n]; // two states — can't distinguish "unvisited" from "false"The boxed type gives null as a free "not yet computed" marker. The cost is autoboxing on every read: return memo[i]; performs an implicit .booleanValue(), which NPEs if the entry is null. That's why the null check comes first.
Avoiding overflow in i + nums[i]
10^4 + 10^5 = 1.1 × 10^5, far inside int. Worth a sentence anyway, because nums[i] here is much larger than the array — a deliberate constraint that catches anyone who assumed jump lengths were bounded by n.
Loop bounds with a guard inside
for (int j = 1; j <= nums[i] && i + j < n; j++)The && i + j < n is required in the O(n²) versions: nums[i] can exceed the remaining array. The greedy version needs no such guard because it never indexes with farthest.
6. Interview Communication Guide
Clarifying questions: Is nums[i] an exact jump or a maximum (maximum — this is the crux)? Can jump lengths exceed the array length (yes, up to 10^5 on a 10^4 array)? Can I stay put / jump zero (a 0 means you're stuck)? Is a single-element array trivially true (yes)?
The pitch
"The key word is maximum. Because
nums[i]is an upper bound rather than an exact distance, indexireaches every index in[i+1, i+nums[i]]— so the reachable set is always a contiguous prefix, and one integer describes it completely.I'll sweep left to right keeping
farthest= the largest index reachable from anything I've been able to stand on. At eachi, ifi > farthestthen indexiis beyond the frontier and nothing later can help, because later indices are further away still — so I return false, and that's a proof of impossibility, not a failed heuristic. Otherwiseiis reachable and I extend the frontier tomax(farthest, i + nums[i]).The ordering matters: the check has to come before the update, or an unreachable index gets to contribute its jump length. On
[1, 0, 5]that would return true instead of false.
O(n)time,O(1)space.I'd flag what this is exploiting: if jumps were exact distances rather than maxima, the reachable set would be scattered and this would be a genuine BFS. The contiguity is doing all the work."
Edge cases to volunteer:
| Input | Expected | Tests |
|---|---|---|
[0] | true | Already at the end — the loop must not demand a jump |
[1, 0, 5] | false | Check-before-update; returns true if reversed |
[3, 2, 1, 0, 4] | false | The canonical zero trap |
[2, 3, 1, 1, 4] | true | The canonical success |
[0, 1] | false | Stuck at index 0 with n > 1 |
[100000, 0, 0, 0] | true | Jump length far exceeds the array |
Name [0] and [1, 0, 5]. The first catches loop-bound errors, the second catches the check/update ordering — the only real bug in a four-line function.
7. Follow-Up Questions — Modified Constraints
⭐ "What if nums[i] is an exact jump distance, not a maximum?"
Then reachability is no longer contiguous and the greedy collapses entirely —
farthestwould be an overestimate. It becomes BFS or DFS over a graph withO(n)edges (each index has exactly one outgoing edge, toi + nums[i]), soO(n)with a visited array.Interestingly the exact version is easier: one edge per node means it's a functional graph, and "can I reach the end" is a walk until you repeat or fall off.
⭐ "Return the minimum number of jumps instead of a boolean."
That's Jump Game II. The frontier becomes a BFS level boundary:
curEndmarks the last index reachable in the current number of jumps, and hitting it means the level is exhausted. SameO(n)/O(1).
"What if you could also jump backwards by nums[i]?"
Contiguity breaks — the reachable set is now a union of forward and backward intervals that can leapfrog. BFS with a visited array,
O(n²)edges in the worst case, orO(n)amortised if you maintain the unvisited set in aTreeSetand delete on visit.This is LC 1345 ("Jump Game IV") territory, where the
TreeSet-of-unvisited trick is the standard way to avoid re-scanning.
"What if some indices are forbidden to land on?"
The frontier is no longer contiguous — a forbidden index splits it. You'd fall back to BFS, or keep an interval list. The single-integer state depends on there being no holes.
Good question to be asked, because it isolates precisely the property the
O(1)solution needs.
"Minimum cost instead of minimum jumps, with cost[i] to launch from i?"
Now it's a shortest-path problem with weights, not a reachability one, so the greedy frontier no longer applies. Dijkstra over
O(n²)edges, or a DPdp[j] = min(dp[i] + cost[i])overiin a sliding window, which a monotonic deque makesO(n).The tell is that "count the jumps" has uniform weights — which is exactly when BFS substitutes for Dijkstra.
"What if n were 10^7?"
The greedy is already
O(n)/O(1), so it scales. The DFS would blow the stack and theO(n²)DP would take10^14operations. This is the constraint level at which the difference stops being cosmetic.
"Can you tell me which index blocks the path when the answer is false?"
Yes, for free: the
iat whichi > farthestfires is the first unreachable index, andfarthestis the last index you could reach. The gap is[farthest + 1, i], and the blocking index isfarthestitself — everything funnelled into it and its jump length was too short.