Problem overview
The function jump receives a slice nums of non-negative integers. You start at index 0, and nums[i] is the farthest you can jump forward from index i: any distance from 1 up to nums[i] is allowed. The function returns the minimum number of jumps needed to land on the last index.
For nums = [2, 3, 1, 1, 4] the answer is 2: jump 1 step from index 0 to index 1, then 3 steps from index 1 to the last index, 4.
Approach: minimum over the first jump, remembered
Think of minJumps(i) as "the fewest jumps to reach the last index when standing on index i". From i you can choose any landing index j from i + 1 to i + nums[i]. Taking that jump costs 1, plus minJumps(j) for the rest, so:
minJumps(i) = 1 + the smallest minJumps(j) over every reachable j, and minJumps(last) = 0.
Written as plain recursion, this recomputes the same index over and over. Many different first jumps lead to the same cell, and each time the whole subtree below it is searched again. For inputs where every cell can reach all the cells after it, the number of paths doubles with each extra element, so the running time is exponential.
The fix is memoization: minJumps(i) depends only on i, so compute it once, store it and reuse it. There are n possible values of i, each costs one loop over at most n choices, giving the O(n^2) time the lesson asks for, with O(n) extra space for the cache. A bottom-up version of the same idea appears in the next lesson on this problem.
Step by step
- Create the cache as a map literal that already holds the base case: the last index maps to 0, since no jumps are needed there. Seeding it means the helper needs no separate base-case check.
- Delegate to a helper and return
minJumps(nums, 0, cache), the answer from the starting index. - Declare
minJumpswith the slice, the indexiand the cache. - Look in the cache first, using the comma-ok form in the
ifstatement. A hit returns immediately. This covers both earlier results and the seeded last index. - Start
resultatlen(nums). No route needs that many jumps (at mostlen(nums) - 1), so it works as infinity. - Loop over the landing cells.
jruns fromi + 1while it stays inside the slice and within the jump lengthi+nums[i]. - Take the best. For each
j, a first jump tojcosts1+minJumps(nums, j, cache). Keep the smaller of that andresult. - Save and return. Store
resultunderiso the cell is never solved again.
The solution in Go
func jump(nums []int) int {
cache := map[int]int{len(nums) - 1: 0}
return minJumps(nums, 0, cache)
}
func minJumps(nums []int, i int, cache map[int]int) int {
if value, exists := cache[i]; exists {
return value
}
result := len(nums)
for j := i + 1; j < len(nums) && j <= i+nums[i]; j++ {
result = min(result, 1+minJumps(nums, j, cache))
}
cache[i] = result
return result
}
The Go details worth knowing:
- A map literal as the cache.
map[int]int{len(nums) - 1: 0}creates the map and stores the base case in one expression. The key is the index of the last element. - Comma-ok. Reading a missing key returns 0, which is also a valid answer here (the last cell needs 0 jumps).
value, exists := cache[i]separates the two cases. Thevalueandexistsnames are scoped to theifstatement. - The map is shared. A map argument refers to the same underlying table, so every recursive call reads and writes the cache created in
jump. - The built-in
min. Go 1.21 addedminandmaxas built-in functions for ordered types, somin(result, 1+minJumps(nums, j, cache))needs no import and no hand-written helper. - Compound loop condition.
j < len(nums) && j <= i+nums[i]stops at the end of the slice even when the jump length reaches past it. The&&evaluates left to right, soj < len(nums)is tested first.
Complexity
Time: O(n^2). Each index is solved once and then served from the cache. Solving an index runs a loop of at most n - 1 iterations, and each iteration is a cache lookup or the one-time computation of a new index.
Space: O(n). The cache holds at most n entries, and the recursion goes at most n calls deep: the first branch follows jumps of length 1, so the stack can reach the end of the slice.
Edge cases and common mistakes
- One element.
[0]returns 0, because the start is already the last index and the seeded cache has the answer. - A zero blocks the way. For
[1, 0, 1]the last index can't be reached. The loop at a zero cell never runs, so that cell keeps the "infinity" valuelen(nums), and the function returnslen(nums), which is 3 here. The lesson's problem assumes the last index can be reached, so this case doesn't arise in the tests; code that must handle it should check for that value. - Using 0 as "not cached". A stored 0 is a real answer for the last cell. The comma-ok test avoids confusing it with a missing key.
- Looping past the slice. Without
j < len(nums), a largenums[i]indexes beyond the end and panics. - Starting
jati. The cell would call itself with no progress and recurse forever. - Forgetting to store the result. The code still returns correct answers, but the cache never fills and the running time becomes exponential again.
Testing
[2, 3, 1, 1, 4]gives2.[2, 3, 0, 1, 4]gives2.[1, 1, 1, 1]gives3.[5, 1, 1]gives1.[0]gives0.