Skip to content

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 O(n2)O(n^2).

The greedy insight collapses it to O(n)O(n) 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_reach scan, and why “if i > 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.

can_jump.py
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 0
can_jump.py
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 0

O(n)O(n) time, O(1)O(1) 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.

min_jumps.py
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 -> 4
min_jumps.py
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 -> 4

Pattern 3 — feasibility plus restart (Gas Station)

Gas Station has two independent halves, and separating them is what makes it easy:

  1. Is any circuit possible? Only if sum(gas) >= sum(cost)sum(gas) >= sum(cost). Otherwise return -1-1 immediately — no starting point can work.
  2. 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 to i + 1i + 1 and reset the tank.
gas_station.py
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]))   # 3
gas_station.py
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]))   # 3

Complexity

ProblemGreedyObvious DP / brute force
55 Jump GameO(n)O(n) / O(1)O(1)O(n2)O(n^2) / O(n)O(n)
45 Jump Game IIO(n)O(n) / O(1)O(1)O(n2)O(n^2) / O(n)O(n)
134 Gas StationO(n)O(n) / O(1)O(1)O(n2)O(n^2) — try every start

The variant map

VariantWhat you trackCanonical problem
Reachable at allOne max_reachmax_reach55 Jump Game
Minimum jumpscurrent_endcurrent_end + farthestfarthest (BFS levels)45 Jump Game II
Cover a range with intervalsSame two variables, over clips1024 Video Stitching · 1326
Circular, find the startFeasibility check + restart index134 Gas Station
Arbitrary jumps, may go backwardsGreedy fails — use real BFS1306 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 O(n)O(n). Space O(1)O(1).

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 O(n)O(n). Space O(1)O(1).

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 ii eliminates every candidate start up to ii at once — see the note above for why.

Time O(n)O(n), one pass plus two sums. Space O(1)O(1).

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

#ProblemDifficultyThe twist
55Jump GameMediumOne reachreach variable; reachability is a contiguous prefix
45Jump Game IIMediumBFS levels without a queue; mind the loop bound
134Gas StationMediumGlobal feasibility check + local restart rule
1024Video StitchingMediumLC 45’s greedy over intervals: cover [0, time][0, time] with fewest clips
1306Jump Game IIIMediumBackward jumps allowed — greedy fails, use BFS/DFS

Interview follow-ups

They askWhat they’re checkingThe answer
“Why doesn’t this need DP?”The key insightThe reachable set is a contiguous prefix [0, reach][0, reach], so one integer fully describes it
“Prove the minimum-jump greedy is optimal”RigourIt is BFS on an unweighted graph; each level is a contiguous range, so two integers replace the queue
“Why is Gas Station one pass?”DepthA 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?”CareThe global feasibility check plus the uniqueness guarantee make the surviving candidate valid
“What if you can jump backwards?”Knowing the boundaryThe prefix property dies; use BFS/DFS with visitedvisited (LC 1306)
“What if jumps were exact, not ‘up to’?”Same boundaryReachability stops being contiguous, so greedy fails
“Reconstruct the path?”BookkeepingRecord the launching index whenever farthestfarthest improves

Edge-case checklist

  • Single element[0][0]; LC 55 gives TrueTrue, LC 45 gives 00. 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] gives TrueTrue; 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 canonical FalseFalse.
  • Very large jump valuesi + nums[i]i + nums[i] may exceed the last index; harmless with maxmax, but do not index with it.
  • All zeros[0,0,0][0,0,0] is FalseFalse for 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-route00 is not negative, so the trip continues; only < 0< 0 triggers 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 — O(n)O(n) and O(1)O(1).
  • 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_end and farthestfarthest. Loop to len(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 coffee

Was this page helpful?

Let us know how we did