Problem overview
The function jump receives a vector nums of non-negative integers. You start at index 0, and nums[i] is the farthest you can jump forward from index i. The task is the minimum number of jumps to reach the last index.
For [2, 3, 1, 1, 4] the answer is 2: one step from index 0 to index 1, then three steps to the last index. This lesson converts the memoized recursion of the previous one into a loop. It keeps the O(n^2) time and O(n) extra space.
Approach: fill a table from the right
Let dp[i] be the minimum number of jumps needed to reach the last index from index i. The recursive version asked for these values on demand and cached them. The iterative version computes every one of them in an order that guarantees the values it depends on are ready.
The recurrence is the same: dp[last] is 0, and for any other index, dp[i] is 1 plus the smallest dp[j] among the indices j that a jump from i can land on. Every such j is to the right of i, so if the loop walks from the right end towards index 0, every dp[j] is final by the time dp[i] needs it. There is no recursion, so deep inputs can't overflow the call stack.
On [2, 3, 1, 1, 4] the table fills as follows: dp[4] = 0. Index 3 can reach index 4, so dp[3] = 1. Index 2 can reach only index 3, so dp[2] = 2. Index 1 can reach indices 2, 3 and 4, and the best is index 4, so dp[1] = 1. Index 0 can reach indices 1 and 2, and the better is index 1, so dp[0] = 2.
Step by step
- Allocate the table. Store the length in
n, then createdpwithvec![n as i32; n]. Every entry starts atn, which is larger than any real answer (at mostn - 1jumps), so it works as infinity until a path is found. - Set the base. The last index needs no jumps:
dp[n - 1] = 0. - Walk right to left with
for i in (0..n - 1).rev(), from indexn - 2down to 0. - Find the reach.
max_reachisi + nums[i] as usize, capped atn - 1with.min. - Try each landing index
jin(i + 1)..=max_reachand updatedp[i]to the smaller of itself and1 + dp[j]. - Return
dp[0]as the final expression.
The solution in Rust
impl Solution {
fn jump(nums: Vec<i32>) -> i32 {
let n = nums.len();
let mut dp = vec![n as i32; n];
dp[n - 1] = 0;
for i in (0..n - 1).rev() {
let max_reach = (i + nums[i] as usize).min(n - 1);
for j in (i + 1)..=max_reach {
dp[i] = dp[i].min(1 + dp[j]);
}
}
dp[0]
}
}
The Rust details worth a closer look:
- Repeating a value.
vec![x; n]builds a vector ofncopies ofx. The castn as i32is needed becausenis ausizeand the entries arei32. - Reversing a range.
0..n - 1excludesn - 1, and.rev()iterates it backwards, soitakes the valuesn - 2, n - 3, ... 0. Forn = 1the range is empty and the loop never runs, which is right: the answer is alreadydp[0] = 0. - Cast precedence. In
i + nums[i] as usize, the cast applies tonums[i]first, and then the sum is of twousizevalues. The elements are non-negative, so the cast keeps their value. A negativei32would turn into a hugeusize. - Capping the reach. Without
.min(n - 1),dp[j]would index past the end of the vector and panic. - Updating in place.
dp[i] = dp[i].min(1 + dp[j])reads and writes the same element. That is fine fori32, which isCopy, and the read finishes before the write. - The tail expression.
dp[0]has no semicolon, so its value is returned.
Complexity
Time: O(n^2). The outer loop runs n - 1 times and the inner loop covers up to n - 1 landing indices, each with constant work. The worst case is a vector of large values, where every index can reach the end.
Space: O(n). The table dp has one entry per index. Nothing else grows with the input.
Edge cases and common mistakes
- A single element.
[0]gives 0. - Zeros in the middle.
[3, 0, 0, 0]gives 1: the zeros are only a problem if the path has to land on them. - An unreachable end. The statement doesn't say what to return. This code leaves
dp[0]atnin that case:[1, 0, 5]gives 3. - Empty input. An empty vector isn't a valid input. The expression
dp[n - 1]would use0usize - 1, which in an optimized build wraps and then indexes out of bounds. - Looping left to right. The table entries to the right would still hold the starting value
nwhen they are read, and the answers would be wrong. On[2, 3, 1, 1, 4]such a loop returns 5 instead of 2, because the entries it reads have not been computed yet. - Starting the table at 0. A starting value of 0 would win every
min, so the result would always be 0. The large starting value is what lets real counts replace it.
Testing
[2, 3, 1, 1, 4]gives 2.[0]gives 0.[1, 1, 1, 1]gives 3.[3, 0, 0, 0]gives 1.[5, 1, 1, 1, 1, 1, 1]gives 2.
