TL;DR
Sweep left to right tracking the farthest reachable index; if a position ever lies beyond the frontier you are stuck β O(n) time, O(1) space.
Approach 1 β Dynamic programming: reachability of every index
Let reach[i] be True if index i is reachable. reach[0] = True; index i is reachable if some earlier reachable j can jump to it (j + nums[j] >= i).
from typing import List
class Solution:
def canJump(self, nums: List[int]) -> bool:
n = len(nums)
reach = [False] * n
reach[0] = True
for i in range(1, n):
for j in range(i):
if reach[j] and j + nums[j] >= i:
reach[i] = True
break
return reach[n - 1]
Complexity: O(n^2) time, O(n) space. Correct, but the nested scan is wasteful β reachability is monotone, which the greedy exploits.
Approach 2 β Forward greedy: track the farthest frontier
The greedy-choice property (why one frontier number is enough): The set of reachable indices is a contiguous prefix [0, farthest] β if you can reach index i, you can reach every index below it too (just stop short). So all the information you need about βeverything reachable so farβ collapses to a single number: the maximum index you can reach, farthest. Scanning left to right, at each index i you only need it to be within reach (i <= farthest); if so, greedily extend the frontier to max(farthest, i + nums[i]). Taking the maximum reach at every reachable cell is optimal because a larger frontier can only ever include the same-or-more indices β there is no downside to reaching farther, and no cleverer choice of intermediate landings could reach an index the frontier cannot. The moment i exceeds farthest, a gap of unreachable cells has opened and nothing after it can be reached.
from typing import List
class Solution:
def canJump(self, nums: List[int]) -> bool:
farthest = 0
last = len(nums) - 1
for i in range(len(nums)):
if i > farthest: # fell into an unreachable gap
return False
farthest = max(farthest, i + nums[i])
if farthest >= last:
return True
return True
Walkthrough on nums = [3,2,1,0,4]:
i=0: 0 <= 0 ok. farthest = max(0, 0+3) = 3. Not yet >= 4.
i=1: 1 <= 3 ok. farthest = max(3, 1+2) = 3.
i=2: 2 <= 3 ok. farthest = max(3, 2+1) = 3.
i=3: 3 <= 3 ok. farthest = max(3, 3+0) = 3.
i=4: 4 > 3 β fell into the gap β return False. β
And on nums = [2,3,1,1,4]: frontier grows 2 β 4 at i=1 (1+3), which is >= 4, so return True.
Complexity: O(n) time, O(1) space.
Approach 3 β Backward greedy: shrink the goalpost
The insight: work right to left. Keep the leftmost index good from which the end is reachable, starting at the last index. Index i is βgoodβ if it can jump to a good index (i + nums[i] >= good); if so, move the goalpost to i. The start is reachable iff good == 0 at the end.
from typing import List
class Solution:
def canJump(self, nums: List[int]) -> bool:
good = len(nums) - 1
for i in range(len(nums) - 2, -1, -1):
if i + nums[i] >= good:
good = i
return good == 0
Complexity: O(n) time, O(1) space β the mirror image of the forward greedy, equally valid; pick whichever reads more clearly to you.
Common pitfalls
- Returning
False too eagerly on seeing a 0. A 0 is only fatal if no earlier jump can vault over it β the frontier check handles this correctly.
- Off-by-one on the target: reaching
farthest >= len(nums) - 1 (the last index), not len(nums).
- Iterating past the frontier without the
i > farthest guard β you would read indices that are actually unreachable and wrongly extend the frontier.
Pattern takeaway
When reachability (or any property) is monotone/contiguous, collapse βall states reachable so farβ into one boundary value and extend it greedily. Reaching as far as possible at each step is safe precisely because a larger frontier dominates a smaller one.