Greedy Reachability and Jumps
Jump Game looks like dynamic programming. “Can I reach the end?” invites a
dp[i] = can I reach index i?dp[i] = can I reach index i? table, and that works — in .
The greedy insight collapses it to and one variable:
You do not need to know which path reaches an index. You only need to know how far you can reach at all.
Reachability is a single number, not a set. Because it only ever grows, one
forward pass maintaining max_reachmax_reach answers the question completely.
What you’ll learn
- The
max_reachmax_reachscan, and why “ifi > reachi > reach, fail” is a complete test. - The level-BFS view of minimum jumps — why LC 45 counts jumps without ever choosing one.
- The two-part structure of Gas Station: a feasibility check plus a restart rule.
- Why these greedies are provably optimal, and where the same shape fails.
- Three real LeetCode problems solved in the browser: 55, 45, 134.
The cue
Pattern 1 — can I get there at all?
Sweep left to right maintaining the furthest index reachable so far. If the loop ever arrives at an index beyond that reach, there is a gap nothing can cross.
def can_jump(nums):
reach = 0
for i, jump in enumerate(nums):
if i > reach:
return False # a gap: nothing reaches index i
reach = max(reach, i + jump)
return True
print(can_jump([2, 3, 1, 1, 4])) # True
print(can_jump([3, 2, 1, 0, 4])) # False -- stuck at the 0def can_jump(nums):
reach = 0
for i, jump in enumerate(nums):
if i > reach:
return False # a gap: nothing reaches index i
reach = max(reach, i + jump)
return True
print(can_jump([2, 3, 1, 1, 4])) # True
print(can_jump([3, 2, 1, 0, 4])) # False -- stuck at the 0time, space.
Pattern 2 — minimum jumps, as implicit BFS
For the number of jumps, think in levels, exactly like BFS: level 0 is index 0; level 1 is everything reachable in one jump; level 2 everything reachable in two. The answer is the level containing the last index.
You never decide which jump to take. You only notice when the current level is exhausted and a new one must begin.
def min_jumps(nums):
jumps = 0
current_end = 0 # right edge of the current BFS level
farthest = 0 # right edge of the NEXT level
for i in range(len(nums) - 1): # note: stop BEFORE the last index
farthest = max(farthest, i + nums[i])
if i == current_end: # current level exhausted
jumps += 1 # commit to one more jump
current_end = farthest # descend into the next level
return jumps
print(min_jumps([2, 3, 1, 1, 4])) # 2 -> 0 -> 1 -> 4def min_jumps(nums):
jumps = 0
current_end = 0 # right edge of the current BFS level
farthest = 0 # right edge of the NEXT level
for i in range(len(nums) - 1): # note: stop BEFORE the last index
farthest = max(farthest, i + nums[i])
if i == current_end: # current level exhausted
jumps += 1 # commit to one more jump
current_end = farthest # descend into the next level
return jumps
print(min_jumps([2, 3, 1, 1, 4])) # 2 -> 0 -> 1 -> 4Pattern 3 — feasibility plus restart (Gas Station)
Gas Station has two independent halves, and separating them is what makes it easy:
- Is any circuit possible? Only if
sum(gas) >= sum(cost)sum(gas) >= sum(cost). Otherwise return-1-1immediately — no starting point can work. - Given that one exists, which index? Sweep once with a running tank.
Whenever the tank goes negative at index
ii, no start in[current_start, i][current_start, i]can work, so jump the candidate start toi + 1i + 1and reset the tank.
def can_complete_circuit(gas, cost):
if sum(gas) < sum(cost):
return -1 # globally impossible
start = 0
tank = 0
for i in range(len(gas)):
tank += gas[i] - cost[i]
if tank < 0: # cannot get from `start` past i
start = i + 1 # so try starting after i
tank = 0
return start
print(can_complete_circuit([1, 2, 3, 4, 5], [3, 4, 5, 1, 2])) # 3def can_complete_circuit(gas, cost):
if sum(gas) < sum(cost):
return -1 # globally impossible
start = 0
tank = 0
for i in range(len(gas)):
tank += gas[i] - cost[i]
if tank < 0: # cannot get from `start` past i
start = i + 1 # so try starting after i
tank = 0
return start
print(can_complete_circuit([1, 2, 3, 4, 5], [3, 4, 5, 1, 2])) # 3Complexity
| Problem | Greedy | Obvious DP / brute force |
|---|---|---|
| 55 Jump Game | / | / |
| 45 Jump Game II | / | / |
| 134 Gas Station | / | — try every start |
The variant map
| Variant | What you track | Canonical problem |
|---|---|---|
| Reachable at all | One max_reachmax_reach | 55 Jump Game |
| Minimum jumps | current_endcurrent_end + farthestfarthest (BFS levels) | 45 Jump Game II |
| Cover a range with intervals | Same two variables, over clips | 1024 Video Stitching · 1326 |
| Circular, find the start | Feasibility check + restart index | 134 Gas Station |
| Arbitrary jumps, may go backwards | Greedy fails — use real BFS | 1306 Jump Game III |
Practice — real LeetCode problems
LC 55 — Jump Game · Medium
Problem. You start at index 00 of numsnums, where nums[i]nums[i] is the
maximum jump length from index ii. Return TrueTrue if you can reach the
last index.
Constraints. 1 <= len(nums) <= 10^41 <= len(nums) <= 10^4, 0 <= nums[i] <= 10^50 <= nums[i] <= 10^5.
Examples. [2,3,1,1,4][2,3,1,1,4] gives TrueTrue · [3,2,1,0,4][3,2,1,0,4] gives FalseFalse
(the 00 at index 3 is a wall) · [0][0] gives TrueTrue (already there)
Editorial — approach, complexity, follow-uups
Maintain reachreach, the furthest index attainable using any sequence of jumps
from indices already visited. Standing at index ii with i <= reachi <= reach means
ii is genuinely attainable, so its jump extends the frontier. If the scan
reaches an ii with i > reachi > reach, every index from there on is unreachable and
the answer is FalseFalse.
Time . Space .
The correctness rests on reachability being a contiguous prefix — see the note above. That is the answer to “why doesn’t this need DP?“.
Test-case notes: [0][0] gives TrueTrue because you begin at the last index —
no jumping required. [2,0,0][2,0,0] gives TrueTrue because the first jump of 2 clears
both zeros. [1,0,1,0][1,0,1,0] gives FalseFalse: from index 0 you reach only index 1,
whose value is 00, so index 2 is unreachable.
A backward variant exists and is worth mentioning: scan right to left tracking
the leftmost index known to reach the end, and check whether index 00
qualifies. Same complexity, and some find the invariant easier to state.
Follow-ups you should expect: “Minimum number of jumps?” — LC 45, below.
“What if nums[i]nums[i] were an exact jump length rather than a maximum?” — the
prefix property breaks and it becomes a BFS. “Can you jump backwards?” —
LC 1306, also BFS. “Reconstruct the path?” — record the launching index each
time reachreach improves.
LC 45 — Jump Game II · Medium
Problem. Same setup, but you are guaranteed to be able to reach the last index. Return the minimum number of jumps required.
Constraints. 1 <= len(nums) <= 10^41 <= len(nums) <= 10^4, 0 <= nums[i] <= 10000 <= nums[i] <= 1000, and
reaching the end is always possible.
Examples. [2,3,1,1,4][2,3,1,1,4] gives 22 (index 0 → 1 → 4) ·
[2,3,0,1,4][2,3,0,1,4] gives 22 · [0][0] gives 00
Editorial — approach, complexity, follow-ups
Group indices by the minimum number of jumps needed to reach them. Level 0 is
{0}{0}; level 1 is everything reachable from level 0; and so on. The answer is
the level of the last index — which is precisely BFS shortest path on an
unweighted graph, with the levels being contiguous ranges so no queue is
needed.
While scanning within a level, farthestfarthest accumulates the union of everything
that level can reach. When ii hits current_endcurrent_end, the level is finished:
increment the jump count and adopt farthestfarthest as the new boundary.
Time . Space .
The loop bound is the one real trap. [0][0] must give 00: you start at the
last index. With range(len(nums))range(len(nums)), i = 0 == current_endi = 0 == current_end triggers an
increment and returns 11. Stopping at len(nums) - 1len(nums) - 1 means arriving is never
charged a jump.
[1,1,1,1][1,1,1,1] giving 33 is a good sanity check that levels are being counted
rather than positions — each jump advances exactly one index.
Follow-ups you should expect: “Prove it is optimal” — it is BFS, and BFS
gives shortest paths on unweighted graphs; the greedy is just BFS with the
queue replaced by two integers, valid because each level is a contiguous
range. “Return the actual jump sequence?” — record the index achieving
farthestfarthest for each level. “What if reaching the end were not guaranteed?” —
detect current_endcurrent_end failing to advance (farthest == current_endfarthest == current_end before the
end) and return -1-1. “Weighted jump costs?” — no longer BFS; Dijkstra or DP.
LC 134 — Gas Station · Medium
Problem. There are nn gas stations in a circle. gas[i]gas[i] is the fuel
available at station ii, and cost[i]cost[i] is the fuel needed to travel from ii
to i+1i+1. Starting with an empty tank, return the index of the station from
which you can complete the circuit, or -1-1 if none exists. The answer is
guaranteed unique.
Constraints. 1 <= len(gas) == len(cost) <= 10^51 <= len(gas) == len(cost) <= 10^5,
0 <= gas[i], cost[i] <= 10^40 <= gas[i], cost[i] <= 10^4.
Examples. gas = [1,2,3,4,5], cost = [3,4,5,1,2]gas = [1,2,3,4,5], cost = [3,4,5,1,2] gives 33 ·
gas = [2,3,4], cost = [3,4,3]gas = [2,3,4], cost = [3,4,3] gives -1-1
Editorial — approach, complexity, follow-ups
Splitting the problem is the whole idea:
- Feasibility is global. Total fuel must cover total cost; otherwise no starting point helps. This check also removes any need to handle wrap-around explicitly.
- Location is local. One left-to-right sweep suffices, because a failure
at index
iieliminates every candidate start up toiiat once — see the note above for why.
Time , one pass plus two sums. Space .
The subtlety is that the loop never wraps around, yet the answer is correct for a circular route. That is what the feasibility check buys: given a non-negative global surplus, the single surviving candidate is guaranteed to make it round, so you never have to simulate the wrap.
([3,1,1],[1,2,2])([3,1,1],[1,2,2]) returning 00 is worth tracing: totals are 55 and 55, so
feasible. From index 0 the tank runs +2, +1, 0+2, +1, 0 and never goes negative, so
startstart stays 00.
Follow-ups you should expect: “Why don’t you need to simulate the
wrap-around?” — the most likely question; the feasibility check plus
uniqueness guarantee it. “What if multiple valid starts existed?” — this pass
returns the first; collecting all of them needs a second pass. “What if you
begin with kk fuel already?” — add kk to the initial tank and the same
argument holds. “Do it with a
prefix sum?”
— yes, and it is elegant: the answer is the index just after the minimum
prefix sum of gas[i] - cost[i]gas[i] - cost[i], which is an equivalent framing worth
mentioning.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 55 | Jump Game | Medium | One reachreach variable; reachability is a contiguous prefix |
| 45 | Jump Game II | Medium | BFS levels without a queue; mind the loop bound |
| 134 | Gas Station | Medium | Global feasibility check + local restart rule |
| 1024 | Video Stitching | Medium | LC 45’s greedy over intervals: cover [0, time][0, time] with fewest clips |
| 1306 | Jump Game III | Medium | Backward jumps allowed — greedy fails, use BFS/DFS |
Interview follow-ups
| They ask | What they’re checking | The answer |
|---|---|---|
| “Why doesn’t this need DP?” | The key insight | The reachable set is a contiguous prefix [0, reach][0, reach], so one integer fully describes it |
| “Prove the minimum-jump greedy is optimal” | Rigour | It is BFS on an unweighted graph; each level is a contiguous range, so two integers replace the queue |
| “Why is Gas Station one pass?” | Depth | A failure at ii eliminates every start up to ii, because those runs are suffixes of an already non-negative prefix |
| “Why no wrap-around simulation?” | Care | The global feasibility check plus the uniqueness guarantee make the surviving candidate valid |
| “What if you can jump backwards?” | Knowing the boundary | The prefix property dies; use BFS/DFS with visitedvisited (LC 1306) |
| “What if jumps were exact, not ‘up to’?” | Same boundary | Reachability stops being contiguous, so greedy fails |
| “Reconstruct the path?” | Bookkeeping | Record the launching index whenever farthestfarthest improves |
Edge-case checklist
- Single element —
[0][0]; LC 55 givesTrueTrue, LC 45 gives00. The loop-bound test. - Leading zero with length > 1 —
[0, 1][0, 1]; unreachable,FalseFalse. - A zero you can jump over —
[2,0,0][2,0,0]givesTrueTrue; a zero is only fatal if you must land and stop on it. - Zero that traps you —
[3,2,1,0,4][3,2,1,0,4]; the canonicalFalseFalse. - Very large jump values —
i + nums[i]i + nums[i]may exceed the last index; harmless withmaxmax, but do not index with it. - All zeros —
[0,0,0][0,0,0]isFalseFalsefor LC 55 (length > 1). - Gas equals cost exactly — feasible with zero slack;
([5],[4])([5],[4])and([3,1,1],[1,2,2])([3,1,1],[1,2,2])cover the boundary. - Single station (LC 134) — valid if
gas[0] >= cost[0]gas[0] >= cost[0]. - Tank exactly zero mid-route —
00is not negative, so the trip continues; only< 0< 0triggers a restart.
Recap
- Track how far you can reach, not how you got there. Reachability is a contiguous prefix, so one integer replaces an entire DP array — and .
- Can I reach the end? Fail the moment the scan stands beyond
reachreach. - Minimum jumps is BFS by levels with the queue replaced by
current_endcurrent_endandfarthestfarthest. Loop tolen(nums) - 1len(nums) - 1, or a single element wrongly costs a jump. - Gas Station splits into a global feasibility check (
sum(gas) >= sum(cost)sum(gas) >= sum(cost)) and a local restart rule, which together avoid simulating the wrap-around. - Covering a range sorts by start and extends furthest; selecting non-overlapping intervals sorts by end. Ask which you are doing.
- The greedy dies when the reachable set stops being contiguous — backward or exact-length jumps mean real BFS.
Next: Sorting with Custom Comparators — the greedy prerequisite, and how to express an ordering that is not just “ascending”.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
