Learning/Greedy/Jump Game
Medium LeetCode 55 · 13 min read

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

Two things follow:

  1. If i > farthest, stop. Index i is not reachable from anything before it, and nothing after i can help — later indices are even further away. This isn't "my algorithm failed"; it's a proof that no strategy exists.
  2. Otherwise i is reachable, so farthest = 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 == 0

Here 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

Java
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 · Space O(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]) — hence O(sum of nums[i]), which is O(n²) when jump lengths are O(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 revisit i while i is still on the stack. Writing the pessimistic value first means a revisit returns false immediately instead of recursing.

This is the same trick as three-colour cycle detection (16) collapsed to two states — safe here only because false is 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

Java
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²) · Space O(n)

Counter-questions on this approach

⭐ "Why does this iterate from the right?"

Because ok[i] depends on ok[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²) to O(n).

"Could you scan left to right with a different state?"

Yes — ok[i] meaning "reachable from 0" rather than "can reach the end". Then ok[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)

Java
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) · Space O(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 on i. 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 - 1 the check i > farthest is the last thing that could fail, and if we got there, i <= farthest holds, 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 indices 0..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 the false branch a proof rather than a heuristic failure.

Approach 4 — Greedy, backwards

Java
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) · Space O(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 == 0 says index 0 can reach it.

"Does this handle zeroes specially?"

No, and that's the appeal. i + 0 >= goal is only true when i >= goal, which can't happen for i < 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 farthest has 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

ApproachTimeSpaceVerdict
DFS + memoO(n²)O(n) + stackRecursion depth 10^4 on [1,1,1,...]
Backward DPO(n²)O(n)10^8 operations at the limit — borderline
Greedy forwardO(n)O(1)One integer of state
Greedy backwardO(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

Java
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

Java
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

Java
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, index i reaches 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 each i, if i > farthest then index i is 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. Otherwise i is reachable and I extend the frontier to max(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:

InputExpectedTests
[0]trueAlready at the end — the loop must not demand a jump
[1, 0, 5]falseCheck-before-update; returns true if reversed
[3, 2, 1, 0, 4]falseThe canonical zero trap
[2, 3, 1, 1, 4]trueThe canonical success
[0, 1]falseStuck at index 0 with n > 1
[100000, 0, 0, 0]trueJump 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 — farthest would be an overestimate. It becomes BFS or DFS over a graph with O(n) edges (each index has exactly one outgoing edge, to i + nums[i]), so O(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: curEnd marks the last index reachable in the current number of jumps, and hitting it means the level is exhausted. Same O(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, or O(n) amortised if you maintain the unvisited set in a TreeSet and 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 DP dp[j] = min(dp[i] + cost[i]) over i in a sliding window, which a monotonic deque makes O(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 the O(n²) DP would take 10^14 operations. 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 i at which i > farthest fires is the first unreachable index, and farthest is the last index you could reach. The gap is [farthest + 1, i], and the blocking index is farthest itself — everything funnelled into it and its jump length was too short.