55. Jump Game
First-principles: from "how far can I go?" to the greedy one-pass solution

๐Ÿ“‹ Problem Statement

You are given an integer array nums. You are initially positioned at the array's first index, and each element in the array represents your maximum jump length at that position.

Return true if you can reach the last index, or false otherwise.

Example 1

Input: nums = [2, 3, 1, 1, 4] Output: true Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.

Example 2

Input: nums = [3, 2, 1, 0, 4] Output: false Explanation: You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index.

Constraints

We're going to derive the solution from scratch โ€” not just memorize the loop. By the end, you'll understand why tracking a single variable works, why greedy beats DP here, and how to spot the same pattern in other reachability problems.

1. What are we really being asked?

You start at index 0. At each position i, the number nums[i] tells you the maximum distance you can leap forward โ€” but you can choose any smaller jump too. You want to know: can I land on the last index?

Let's think about what makes this possible or impossible:

Two equivalent ways to see the problem

Forward view: From where I am, how far can I reach? At each step, expand my "reachable frontier." If it ever falls short of a position I need to reach โ†’ stuck.

Backward view: Starting from the last index, work backwards. Which positions can jump to a position that's already "good"? If index 0 is good โ†’ success.

The forward view is simpler โ€” and that's the greedy approach.

Concrete examples:

Example 1: [2, 3, 1, 1, 4] โ€” starting with 2 at index 0, you can reach indices 1 and 2. From index 1 (value 3), you can reach all the way to index 4 (the end). โœ… Reachable.

Example 2: [3, 2, 1, 0, 4] โ€” from index 0 (3) you can reach up to index 3. But at index 3, the value is 0 โ€” you can't move further. No matter which path you take, you get trapped at index 3. โŒ Unreachable.

The "aha" moment

You don't need to plan a full path. You just need to know: as you scan left to right, is there any point where the furthest reachable index falls behind your current position?

If maxReach < i at any point โ†’ you're stuck. Otherwise, keep going and update maxReach = max(maxReach, i + nums[i]).

2. The greedy recurrence

Define maxReach = the furthest index you can reach so far.

Initial state: maxReach = 0 // Initially, you can only reach index 0 (your start) At each position i: if maxReach < i โ†’ return false // You can't even get to position i maxReach = max( maxReach, i + nums[i] ) โ””โ”€ stay where I am โ””โ”€ jump from i as far as I can If we finish the loop without getting stuck: return true

Break this down:

Why this is greedy (and why greedy works here)

At each step, you take the locally optimal action: jump as far as you can from wherever you are. But unlike many greedy problems, there's no tradeoff โ€” reaching further can never hurt you. Here's why:

  1. If you can reach index j, you can also reach every index between your current position and j (just take smaller jumps).
  2. So "can reach" is monotonic โ€” from index i, you create a continuous reachable interval [i, i+nums[i]].
  3. The union of reachable intervals from all visited positions is [0, maxReach] โ€” a single contiguous block from the start.
  4. Since maxReach only grows, there's never a reason to be "strategic" about which position to jump to โ€” farther is always better.

This monotonicity is what makes greedy optimal. If jumps were exact (not "up to"), or if there were costs, greedy would fail โ€” and that's when DP takes over.

The key insight: the question "can I reach position i?" is answered by one value: maxReach. We don't need to try every path (O(2โฟ)) or build a DP array of reachable positions (O(nยฒยทm)). One scan, one variable โ€” O(n) time, O(1) space.

3. Interactive walkthrough

Choose an example and step through the greedy scan:

โ€” Click "Step Forward" to begin
0.6s

4. Why the greedy one-pass is optimal

The key property that makes greedy correct here is monotonicity of reachability.

Proof sketch

Claim: After processing positions [0..i], the set of reachable positions is exactly [0..maxReach] โ€” a single contiguous prefix.

Base case (i=0): You can reach [0, nums[0]] โ€” a contiguous segment from 0.

Inductive step: Assume after processing [0..i-1], reachable = [0..R] where R = maxReach. At position i:

  • If i > R: position i is unreachable โ†’ we're stuck.
  • If i โ‰ค R: we can be at i, and from i we can reach up to i + nums[i]. So the new reachable set is [0..max(R, i + nums[i])] โ€” still a contiguous prefix.

This invariant proves: maxReach is sufficient to track everything. No DP needed.

When greedy fails: if the problem asked for the minimum number of jumps (LeetCode 45 โ€” Jump Game II), greedy on maxReach alone doesn't work. You need to track "when does the current jump's range end?" โ€” which adds a second state variable. Even then, it's still greedy, but the decision is more subtle.

5. The complete solutions

5a. Greedy โ€” O(n) time, O(1) space (optimal)

/** * Jump Game โ€” Greedy * Track the furthest reachable index as we scan. * If we ever encounter an index beyond maxReach โ†’ stuck. */ function canJump(nums) { let maxReach = 0; for (let i = 0; i < nums.length; i++) { // Can I even be at this position? if (maxReach < i) return false; // Extend my reach maxReach = Math.max(maxReach, i + nums[i]); // Early exit: already can reach the end if (maxReach >= nums.length - 1) return true; } return true; }

5b. Optimized Greedy โ€” loop only while reachable

/** * Jump Game โ€” Alternative greedy phrasing * Loop condition itself encodes the "stuck" check: * - Continue only while i โ‰ค maxReach (i is reachable) * - If loop finishes naturally and maxReach โ‰ฅ last index โ†’ success */ function canJump(nums) { let maxReach = 0; for (let i = 0; i <= maxReach; i++) { maxReach = Math.max(maxReach, i + nums[i]); if (maxReach >= nums.length - 1) return true; } return false; // maxReach < n-1 and we can't advance further }

5c. DP โ€” O(nยฒ) time, O(n) space (for learning)

/** * Jump Game โ€” DP * dp[i] = true if index i is reachable from the start * * Recurrence: * dp[i] = true if there exists j < i such that * dp[j] == true AND j + nums[j] >= i * * This is the "backward look" approach โ€” at each i, look * at all previous positions to see if any can reach i. */ function canJumpDP(nums) { const n = nums.length; const dp = new Array(n).fill(false); dp[0] = true; // Base case: we start at index 0 for (let i = 0; i < n; i++) { if (!dp[i]) continue; // Can't propagate from an unreachable position // Mark all positions reachable from i const maxJump = Math.min(n - 1, i + nums[i]); for (let j = i + 1; j <= maxJump; j++) { dp[j] = true; if (j === n - 1) return true; // Early exit } } return dp[n - 1]; }

Greedy Approach

  • One variable: maxReach
  • O(n) time, O(1) space
  • Exploits the contiguous reachable interval property
  • Standard interview answer

DP Approach

  • Explicit dp[i] = "is i reachable?"
  • O(nยฒ) time, O(n) space โ€” too slow for n=10โด
  • Easier to derive if you think "backwards from the answer"
  • Useful stepping stone โ€” then optimize to greedy

6. The backward DP (an alternative lens)

There's a third approach that's also O(n) โ€” scanning right to left:

Backward DP / "good position" approach

Define lastGood = the rightmost index we know can reach the end. Initially, lastGood = n - 1 (the last index can trivially reach itself).

Scan from right to left. At position i, ask: can I jump from i to anywhere โ‰ฅ lastGood? If i + nums[i] โ‰ฅ lastGood, then i is also "good" โ€” set lastGood = i.

At the end: if lastGood === 0 โ†’ we can reach the end from the start.

/** * Jump Game โ€” Backward DP * O(n) time, O(1) space */ function canJumpBwd(nums) { let lastGood = nums.length - 1; for (let i = nums.length - 2; i >= 0; i--) { if (i + nums[i] >= lastGood) { lastGood = i; // This position can reach a "good" position } } return lastGood === 0; }
The backward scan is elegant and also O(n) / O(1). It's the "reverse" of the forward greedy. Choose whichever feels more natural โ€” in an interview, showing either one demonstrates solid reasoning. The backward version is especially nice when the follow-up is "find the minimum number of jumps" (Jump Game II), because the "good position" concept generalizes well.

7. Recognizing this pattern in other problems

Problem Core Idea Pattern
Jump Game maxReach = max(maxReach, i + nums[i]) Reachability frontier
Jump Game II maxReach + currentEnd + jumps counter Reachability + level-order BFS in 1D
Can Place Flowers greedy place if no adjacent conflict Local optimal = global optimal
Gas Station track surplus, reset when negative Contiguous segment with maintenance of invariant
Video Stitching / Min Taps to Water Garden maxReach from current coverage window Interval coverage + greedy frontier

The common thread: a monotonically expanding frontier where "further is always better." When you spot that property, greedy is on the table โ€” and it's usually the optimal answer.

8. First-principles derivation checklist

When you see a "can you reach the end?" problem, ask:

  1. Is reachability monotonic? โ€” Can I reach every position between my start and the furthest reachable? If yes, the reachable set is a contiguous prefix โ†’ one variable suffices.
  2. What's the state? โ€” For Jump Game I, it's just maxReach. For Jump Game II (minimum jumps), you need maxReach + currentEnd + jumps. The complexity of the state tells you whether it's simple greedy or layered greedy.
  3. Does "further" always help? โ€” If there's a cost, penalty, or diminishing return associated with jumping far, greedy fails. That's your cue to reach for DP.
  4. Can I early-exit? โ€” As soon as maxReach โ‰ฅ lastIndex, return true. As soon as i > maxReach, return false. These early exits often speed up real-world performance dramatically on sparse inputs.

9. Submissions โ€” click to expand

โ–ถ Submission #1 โ€” Forward greedy with early exit (your code) 2 bugs 3 issues
Click to expand

Your submitted code:

class Solution: def canJump(self, nums: List[int]) -> bool: max_reach = 0 for idx, num in enumerate(nums): if max_reach < idx: return False if max_reach >= len(nums): โ† BUG #1: off-by-one on the last index return True max_reach = max(max_reach, idx + num) return False โ† BUG #2: wrong fallback value
Interviewer verdict: Would not pass Correct structure, correct greedy thinking โ€” but two logic bugs produce wrong answers on core examples

What you got right first

Let me start with a genuine compliment: you understand the greedy approach correctly. The structure โ€” stuck check, reach expansion, early exit โ€” is exactly right. You're tracking max_reach as the frontier and using enumerate() for clean iteration. The max_reach < idx stuck check is in the right place. This is the right algorithm โ€” it's just two fragile mistakes in the numbers.

๐Ÿ”ด Blocking Bugs

Bug #1 โ€” max_reach >= len(nums) is an off-by-one error BLOCKING

The last index of an array of length n is n - 1, not n. By comparing against len(nums) instead of len(nums) - 1, your early exit never fires when the answer is True โ€” because max_reach can never exceed the array bounds. When max_reach == n - 1 (exactly at the last index), the condition max_reach >= n is False, so it keeps scanning โ€” and eventually hits the return False fallback.

Counterexample: [2, 3, 1, 1, 4] โ€” the main LeetCode example. Expected: True. Your code: False.

idx num max_reach (before) stuck?
(max_reach < idx)
early exit?
(max_reach >= 5)
max_reach (after)
0 2 0 No (0โ‰ฅ0) No (0<5) 2
1 3 2 No (2โ‰ฅ1) No (2<5) 4
2 1 4 No (4โ‰ฅ2) No (4<5) โ† SHOULD BE TRUE! 4
3 1 4 No No (4<5) 4
4 4 4 No No (4<5) 8
loop end โ€” โ€” โ€” โ€” return False โ† WRONG

At idx=1, max_reach becomes 4, which equals len(nums) - 1 = 4. The last index is reachable! But because your check tests max_reach >= 5, it misses this. The code runs to completion and returns False.

โš ๏ธ This bug always fires for valid inputs where max_reach lands exactly on the last index. It's not an edge case โ€” it's the normal case for the problem's own example.

Fix:
if max_reach >= len(nums) - 1: return True

The last valid index is n-1. When max_reach covers it, you're done.

Bug #2 โ€” return False at the end is logically backwards BLOCKING

Even after fixing Bug #1, this bug would still bite on the single-element case. Consider [0] (n=1):

idx num max_reach (before) stuck? early exit?
(max_reach >= 0)
max_reach (after)
0 0 0 No Yes! (0โ‰ฅ0) โ€”

With Bug #1 fixed, the single-element case would work. But conceptually, the return False is wrong. Think about what it means for the loop to complete without returning:

  • The stuck check (max_reach < idx) never fired โ†’ we were never stuck at any position.
  • The early exit (max_reach >= n - 1) never fired โ†’ max_reach never covered the last index.

But if we processed every index without getting stuck, that means we visited every position including the last one. By definition, the last index is reachable. The correct fallback after a complete scan is return True.

โš ๏ธ In practice, with Bug #1 fixed and the early exit working, this return False would only be hit on edge cases where the early exit somehow never fires despite reaching the end. But semantically return True is the correct invariant, and writing return False signals to any reviewer that you haven't thought through what "loop completed" actually means.

Fix:
return True โ€” if we scanned the whole array without getting stuck, the last index is reachable.

๐ŸŸก Design / Code Quality Issues

Issue #3 โ€” Trailing whitespace and inconsistent blank lines

max_reach = 0 is followed by 8 trailing spaces before the newline. Then there's a blank line before the for loop. This is minor, but in FAANG interviews, clean formatting is a baseline expectation โ€” it tells the interviewer you're careful. The fix: one blank line between logical blocks (initialization โ†’ loop) with no trailing whitespace.

Issue #4 โ€” Early-exit check positioned before the max_reach update

Your early-exit check (max_reach >= len(nums)) happens before the max() update. This means if the current position's jump extends reach to the end, you won't catch it until the next iteration โ€” wasting a loop cycle. It's not a correctness bug, but it's a minor inefficiency and a sign you haven't fully traced the control flow.

# Your order: check BEFORE update โ€” needs one extra iteration max_reach = max(max_reach, idx + num) if max_reach >= n - 1: # put this AFTER the update return True

With your order, for [2,3,1,1,4]: at idx=1, max_reach updates to 4. But you already checked 2 โ‰ฅ 5 earlier in the iteration. So you need idx=2 to check 4 โ‰ฅ 5 โ€” still fails. It doesn't matter here because your condition is wrong anyway, but with the correct condition it would add one unnecessary iteration. Move the check after the update.

Issue #5 โ€” return False is the opposite of what the loop-invariant says

After a complete scan without hitting the stuck check, you've proven every position from 0 to n-1 is reachable. The loop-invariant: "after processing i, all positions โ‰ค max_reach are reachable." If the loop completes, max_reach โ‰ฅ n-1 (otherwise the stuck check would have fired at some point). The return value must be True. Writing return False contradicts the invariant you just maintained.

This is exactly the kind of thing a FAANG interviewer would ask: "Walk me through a case that hits that final return. What does it mean when the loop finishes normally?" If you can't answer that, or realize it should never happen, the return False becomes a red flag.

๐ŸŸข Fixed Solution

What changed and why:
  • Fixed the off-by-one: len(nums) - 1 instead of len(nums)
  • Fixed the fallback: return True at the end โ€” loop completion means we visited every index without getting stuck
  • Moved the early-exit check: after the max() update, so we can exit in the same iteration
  • Cleaned whitespace: removed trailing spaces, consistent blank line after initialization
class Solution: def canJump(self, nums: List[int]) -> bool: max_reach = 0 for idx, num in enumerate(nums): if max_reach < idx: # Stuck at a gap return False max_reach = max(max_reach, idx + num) # Extend reach first if max_reach >= len(nums) - 1: # Then check โ€” can we reach the last index? return True return True # Scanned whole array โ€” no gaps โ†’ reachable

Or the even more concise version (same correctness):

class Solution: def canJump(self, nums: List[int]) -> bool: max_reach = 0 for idx, num in enumerate(nums): if max_reach < idx: return False max_reach = max(max_reach, idx + num) return True

๐ŸŽฏ Key Takeaway

You have the algorithm. You understand the greedy insight. The stuck-check / reach-expansion / early-exit structure is exactly what a FAANG interviewer wants to see. This is a "getting warmer" submission โ€” the thinking is correct, the implementation has two small but fatal number-errors.

But in a real interview, these bugs would sink you. Here's why โ€” and how to catch them before the interviewer does:

  • Off-by-one on array indices is interview enemy #1. Last index = n - 1, not n. Always. Whenever you write len(nums) in a comparison, pause and ask: "Am I comparing against the length or the last index?" If it's the latter, subtract 1.
  • Trace the problem's own example. Before saying "done," manually trace [2,3,1,1,4] through your code. At idx=1, max_reach becomes 4, and n=5. 4 โ‰ฅ 4? The check should return True โ€” but it doesn't. Tracing catches the off-by-one instantly.
  • Every return should tell a story. You have three returns:
    1๏ธโƒฃ return False when stuck โ†’ correct
    2๏ธโƒฃ return True on early exit โ†’ correct intent
    3๏ธโƒฃ return False at the end โ†’ "loop finished and... we failed?"
    The loop-invariant says: "if the loop finishes, we processed the entire array without getting stuck." That means success, not failure. Your final return must match your invariant.
  • Check order matters. Update max_reach before checking whether it reaches the end โ€” otherwise you need an extra iteration to catch it. This is a code-smell a sharp interviewer will notice: "Why are you checking before you've updated?"
  • Clean formatting is part of the signal. Trailing whitespace and inconsistent spacing aren't logic bugs, but they send a message about attention to detail. In a 10-line function, every character is visible.

Bottom line: You're 10 characters away from the optimal solution. - 1 on the comparison and True on the fallback โ€” that's it. The algorithm is right. Now make the numbers right.