Problem overview
You start at index 0 of an array of non-negative integers. Each element is the maximum jump length from its position: from index i you can land on any index from i + 1 to i + nums[i]. The method Jump returns the minimum number of jumps needed to reach the last index.
For [2, 3, 1, 1, 4] the answer is 2: one step to index 1, then three steps to the end.
The lesson asks for an iterative version of the memoized recursion, still in O(n^2) time and O(n) extra space.
Approach: fill a table from right to left
The recursive solution defines the answer from index i in terms of answers from larger indices: it is one jump plus the best answer among the reachable cells. Since every answer depends only on cells to its right, you can compute them in a loop, starting at the end of the array and moving left, and no recursion or dictionary is needed.
Let dp[i] be the minimum number of jumps from index i to the last index. Then:
dp[last] = 0, because you are already there.dp[i] = min(1 + dp[j])over everyjfromi + 1toi + nums[i]that is inside the array.
Going right to left guarantees that each dp[j] you read is already final. The answer is dp[0].
On [2, 3, 1, 1, 4] the table starts as [5, 5, 5, 5, 0], where 5 is the placeholder "infinity". Then:
i = 3: onlyj = 4is reachable, sodp[3] = 1 + 0 = 1.i = 2: onlyj = 3, sodp[2] = 1 + 1 = 2.i = 1:jis 2, 3 or 4, giving 3, 2 and 1, sodp[1] = 1.i = 0:jis 1 or 2, giving 2 and 3, sodp[0] = 2.
The final table is [2, 1, 2, 1, 0] and the method returns 2.
Step by step
- Create the table as an
int[]of lengthnums.Length. A newintarray is filled with zeros, which would look like "zero jumps" for every cell, so set every entry except the last tonums.Length. No real path takes that many jumps (at mostnums.Length - 1), so it works as infinity until a real path replaces it. The last entry can stay 0. - State the base case explicitly with
dp[nums.Length - 1] = 0. It is already 0, but writing it makes the logic readable. - Loop right to left with
istarting atnums.Length - 2and decreasing whilei >= 0. The last cell is skipped because it is the base case. - Loop over the targets with
jfromi + 1whilej < nums.Lengthandj <= i + nums[i]. - Relax
dp[i]withMath.Min(dp[i], 1 + dp[j]): a first jump tojcosts one jump plus the best fromj. - Return
dp[0]once both loops finish.
The solution in C#
class Solution {
public int Jump(int[] nums) {
int[] dp = new int[nums.Length];
for (int i = 0; i < dp.Length - 1; ++i) {
dp[i] = nums.Length;
}
dp[nums.Length - 1] = 0;
for (int i = nums.Length - 2; i >= 0; --i) {
for (int j = i + 1; j < nums.Length && j <= i + nums[i]; ++j) {
dp[i] = Math.Min(dp[i], 1 + dp[j]);
}
}
return dp[0];
}
}
C# details worth knowing
- Arrays start zeroed.
new int[n]gives younzeros, so a "not computed" or "infinite" marker has to be written explicitly, as the first loop does.Array.Fill(dp, nums.Length)would fill the whole array in one call, and the lesson's loop plus the base-case line gets the same result. - Reusing the name
i. The two outerforloops each declare their owni. That is legal in C# because each variable's scope ends with its loop. Declaring a secondiwhile the first is still in scope would be a compile error. - Pre-decrement in the header.
--iandi--behave the same in aforupdate clause, because the value of the expression is not used.
Complexity
The outer loop runs about n times and the inner loop at most n - 1 times, so the time is O(n^2). The only extra storage is the dp array of n integers, so the space is O(n). Compared with the memoized recursion, the time is the same, but there are no recursive calls and no dictionary lookups, so there is no stack depth to worry about for long arrays.
Edge cases and common mistakes
- A one-element array. The outer loop starts at
nums.Length - 2, which is -1, so it never runs anddp[0]is the base case, 0. - Looping left to right. Then
dp[j]would still hold its placeholder when you read it, and the answers would be wrong. - Using
int.MaxValueas infinity. The placeholdernums.Lengthis safe because1 + dp[j]stays small. Withint.MaxValue, that addition would overflow and wrap to a large negative number, silently breaking the minimum. - Leaving the zeros in place. If you skip the first loop, every unfilled entry looks like a free cell and
Math.Minkeeps 0 forever, so the method would report 0 for most inputs. - Ignoring the array end. Without
j < nums.Length, a largenums[i]makesdp[j]throwIndexOutOfRangeException. - Unreachable end. For input like
[1, 0, 1]the last index can't be reached, and this code returnsnums.Length, here 3. The lesson assumes the end is reachable.
Testing
These results come from running the solution:
[2, 3, 1, 1, 4]returns 2.[1, 1, 1, 1]returns 3.[0]returns 0.