jump-game
Difficulty: Medium · Topic: 1-D Dynamic Programming · Practice it on parikshan: /practice/jump-game/start
The story
A Cloudflare Workers deployment publishes a chain of edge handlers. Each handler exposes a number representing how many downstream handlers it is willing to forward to in a single hop (the rate budget). The platform team wants to know one thing: starting at the first handler, can a request reach the final handler at all? Some handlers have a budget of 0 (a dead-end sink); if all forwarding paths terminate at one of these before hitting the last handler, deployment is broken.
That reachability question is jump-game. The same shape appears in dependency resolution chains (will the build reach the target if every step has bounded look-ahead?), in failover topologies, in courseware progression where each lesson unlocks at most k ahead, and in firmware rollout pipelines on Tesla and Amazon Devices fleets where each gateway forwards updates up to a budget.
This problem also has a beautiful twin in the same statement: the DP solution is correct and educational, but a greedy solution beats it. The lesson is recognising when greedy works.
What's actually being asked
You have an array nums of non-negative integers. You start at index 0. From index i you may jump to any index in [i+1, i+nums[i]]. Return true if you can reach index n-1, otherwise false.
Examples:
nums = [2, 3, 1, 1, 4]→ true. Jump 0 → 1 → 4.nums = [3, 2, 1, 0, 4]→ false. Every path lands on the0at index 3 and stalls.nums = [0]→ true. Already at the last index, no jumps needed.nums = [1, 0, 1, 0]→ false. From index 1 you cannot reach index 2.
The naive approach
Recursion on every legal jump:
def can_jump(i, nums):
if i >= len(nums) - 1: return True
for k in range(1, nums[i] + 1):
if can_jump(i + k, nums): return True
return False
Exponential. For n = 30 this is already huge; for n = 10⁵ it is impossible.
The key insight
The DP framing is correct and educational. Let dp[i] be True if index i is reachable from 0. Then dp[0] = True and dp[i] = True iff some earlier index j < i is True and j + nums[j] >= i.
# DP recurrence:
# dp[i] = OR over j in [0..i-1] of ( dp[j] AND j + nums[j] >= i )
# dp[0] = True
# Answer = dp[n-1]
That works. It is O(n²): for each i, scan backwards over potential predecessors. Acceptable up to n ~ 5000. Past that, you need something better.
The key insight: then the greedy
The DP is wasteful because reachability is monotone: if any index j <= i was reachable, then every index in [0, j + nums[j]] is also reachable. So instead of recording a True/False per index, just track one number: the furthest index reachable so far, call it reach.
# Greedy / collapsed DP:
# reach = 0
# For i from 0 to n-1:
# if i > reach: return False (no predecessor lands here)
# reach = max(reach, i + nums[i])
# if reach >= n - 1: return True
# return True
This is O(n), O(1) space. Why does greedy succeed here when greedy fails on coin-change? Because the local choice cannot make a future state unreachable. If you can reach i at all, you can reach i+1, i+2, ..., up to i + nums[i]. There is no scenario where jumping "longer" forecloses a future jump, because shorter jumps are always available from the same launchpad. The reachable region is a contiguous prefix, summarised by its right edge.
The four DP questions, in greedy collapsed form:
- State:
reach, a single integer: the furthest index reachable using any prefix of decisions. - Base case:
reach = 0at the start (you are at index 0, so 0 is reachable). - Transition:
reach = max(reach, i + nums[i])for eachiyou can reach. If at any pointi > reach, you cannot get toi, so you cannot get further. - Answer:
reach >= n - 1once the loop ends, orTruethe momentreach >= n - 1mid-scan.
Step-by-step approach
The pure greedy (recommended in production):
def can_jump(nums):
n = len(nums)
reach = 0
for i in range(n):
if i > reach:
return False
if i + nums[i] > reach:
reach = i + nums[i]
if reach >= n - 1:
return True
return True
The DP form (educational, O(n²)):
def can_jump_dp(nums):
n = len(nums)
dp = [False] * n
dp[0] = True
for i in range(1, n):
for j in range(i):
if dp[j] and j + nums[j] >= i:
dp[i] = True
break
return dp[n-1]
Both return the right answer; only the greedy is fast enough at n = 10⁵.
Worked example
nums = [2, 3, 1, 1, 4], n = 5
| i | nums[i] | i > reach? | i + nums[i] | reach | reach >= n-1? |
|---|---|---|---|---|---|
| 0 | 2 | 0 > 0 false | 2 | 2 | 2 >= 4 false |
| 1 | 3 | 1 > 2 false | 4 | 4 | 4 >= 4 true |
Return true on i=1. Verified.
nums = [3, 2, 1, 0, 4], n = 5
| i | nums[i] | i > reach? | i + nums[i] | reach | reach >= n-1? |
|---|---|---|---|---|---|
| 0 | 3 | 0 > 0 false | 3 | 3 | 3 >= 4 false |
| 1 | 2 | 1 > 3 false | 3 | 3 | false |
| 2 | 1 | 2 > 3 false | 3 | 3 | false |
| 3 | 0 | 3 > 3 false | 3 | 3 | false |
| 4 | 4 | 4 > 3 true | . | . | . |
Return false at i=4. Verified.
Complexity
- DP form: O(n²) time, O(n) space.
- Greedy form: O(n) time, O(1) space.
The greedy is the only acceptable production answer at n = 10⁵.
Common pitfalls
- Greedy without monotonicity check: writing the greedy without "if i > reach return False" silently scans past unreachable indices and updates
reachfrom invalid launchpads. Wrong answer. - Stopping at
reach == n - 1instead ofreach >= n - 1: an over-shoot is still valid; you do not need to land onn-1precisely. - Using
>= nums[i]instead of<= nums[i]: a classic confusion.nums[i]is the maximum jump, not the exact jump. - Treating
nums[i] = 0as a fatal error: it is not. A zero at indexionly blocks if no earlier launchpad could overshoot toi + 1. The recurrence catches this; do not write special cases.
Where this shows up in the enterprise
- Cloudflare Workers chains: can a request reach the final handler given per-handler forwarding budgets?
- PagerDuty alert escalation: each level can forward up to
klevels above; can the page reach the executive on-call? - Apple Maps tile fetching: each tile lists how many downstream tiles to prefetch; will the prefetch chain cover the route?
- Amazon Devices firmware staging: each gateway can forward updates up to a per-gateway budget; can the firmware reach every leaf device?
- Google Search index propagation: shards forward to followers with bounded fanout; can the latest snapshot reach every replica?
- NLP tokenisation candidate paths (HuggingFace BPE merge dynamics): given per-position merge ranges, is the full token covered?
- Genomics scaffolding (Illumina assemblies): contigs anchor to others within a max overlap window; is the genome covered end-to-end?
The pattern is universal: "given per-step bounded look-ahead, is the end reachable?" Greedy with a single reach variable answers it in linear time.
Variants you'll see elsewhere
jump-game-ii: not "can you reach", but "minimum number of jumps to reach the end". Greedy still, but you track BFS levels. The next article in this cluster.jump-game-iii: from each index you may jump exactlynums[i]left or right; reach any index with value 0. BFS / DFS over a graph.jump-game-iv: jump left, right, or to any index with the same value; BFS with adjacency by value.jump-game-v: jumps constrained by intermediate-element values; usually DP with memoisation.frog-jump: each jump depends on the previous jump size; DP with(position, last_jump_size)state.
The jump-game family is a fantastic survey of "what is the right algorithmic family for this twist": linear greedy, BFS, DP, or graph search.
How parikshan turns this into a learning loop
This problem is the textbook "DP is correct but greedy is better" lesson.
- Read this article and write the DP recurrence first. Then realise the DP collapses to a single number because reachability is monotone. That progression is the entire pedagogical payoff.
- Practice mode with no AI: code the DP, time it on
n = 1000, watch it pass. Time it onn = 10⁵; watch it TLE. Now code the greedy and watch it pass instantly. - AI training: Optimise. Submit the O(n²) DP and ask the assistant to optimise. The good response is the greedy collapse; the mediocre response is "use early termination" without the collapse.
- AI training: Explain. Ask "why does greedy work here but not on coin-change?". The answer (local choice does not foreclose future reachability) is the entire skill of recognising the greedy family.
- Exam mode: redo cold. The bank's adversarial test on this problem includes
[0](singleton zero),[1, 0, 1, 0](looks tractable, is not), and[100000, 0, 0, ..., 0](one big jump). The greedy handles all three; sloppy DP TLEs on the last.
In the AI-integrated workspace
The classic AI failures on this problem:
- The over-engineered DP: an agent asked "is the end reachable" produces a memoised recursion with a visited set. It works but is O(n²); fails the stress test.
- The greedy bug: an agent collapses to
reachbut forgets thei > reachguard. Sample tests pass; the adversarial[3, 2, 1, 0, 4]returns true (wrong) because the greedy overshoots past the dead0without noticing it cannot get to index 3. - The strict-equality bug: returns false because the loop terminates exactly when
reach == n - 1but the early-return check uses>instead of>=. Off-by-one classic.
The 2027 engineer's discipline:
- State the question precisely: "can you reach", not "how many jumps", not "shortest path". Different questions, different algorithms.
- Ask for greedy with a single state variable. "Maintain
reach, the furthest index reachable. For eachi, ifi > reachreturn False, else updatereach." - Verify on
[3, 2, 1, 0, 4]. The answer must be false. If the agent's code returns true, the greedy guard is wrong; reject the code. - Verify on
[2, 0]. The answer must be true (jump from 0 to 1, which is the last index). Off-by-one catcher. - Do not let the agent escalate to a graph BFS unless the variant truly demands one (which
jump-game-iiiand beyond do). On the basejump-game, greedy is the right answer.
A candidate who can articulate "this is reach-based greedy because reachability is monotone" in seven seconds is the candidate who can direct an AI agent to produce a correct, linear-time, ten-line solution. That is the skill the parikshan loop is built to train.