Topics › Dynamic Programming › 1D DP
Frog Jump
Java source ↗ Edit this article ↗
Jump to code:
Problem
There are n stairs, and stair i has height h[i]. A frog starts on stair 0 and wants to reach stair n - 1. From stair i it can jump to stair i + 1 or stair i + 2. A jump from stair i to stair j costs |h[i] - h[j]| energy. Return the minimum total energy needed to reach the last stair.
Constraints (the Coding Ninjas version used by the sheet): 1 ≤ n ≤ 10^5, 0 ≤ h[i] ≤ 10^4. The worst total is below 10^9, so int is enough. This is not LeetCode 403, which shares the name but is a different problem about stones in a river.
Example 1
h = [10, 20, 30, 10]
answer = 20 // 0 -> 1 costs 10, then 1 -> 3 costs |20 - 10| = 10
Example 2
h = [10, 50, 10]
answer = 0 // jump 0 -> 2 directly, both heights are 10
Why greedy fails
The tempting approach is to always take the cheaper of the two next jumps. On h = [30, 10, 60, 10, 60, 50] greedy goes 0 -> 1 (cost 20, cheaper than 30), then 1 -> 3 (cost 0), then 3 -> 5 (cost 40), total 60. The best route is 0 -> 2 -> 4 -> 5 with cost 30 + 0 + 10 = 40. Paying more on the first jump put the frog on a stair from which the rest of the climb was cheap. A locally cheap jump can lead somewhere expensive, so we have to compare all routes, and DP is how we do that without enumerating them.
Approach 1: Recursion
Intuition. Let f(i) be the minimum energy to reach stair i from stair 0. The frog arrives at stair i by exactly one last jump, either from i - 1 or from i - 2. Whichever it was, the route up to that earlier stair should itself be the cheapest possible, because the cost of the remaining jump does not depend on how the frog got there. So
f(0) = 0
f(i) = min( f(i - 1) + |h[i] - h[i - 1]|,
f(i - 2) + |h[i] - h[i - 2]| ) // second option only when i ≥ 2
The answer is f(n - 1). This “the best route to i is built from the best route to an earlier stair” property is exactly the optimal substructure that DP needs.
Algorithm.
- If
i == 0, return 0. one = f(i - 1) + |h[i] - h[i - 1]|.two = f(i - 2) + |h[i] - h[i - 2]|ifi > 1, otherwise “infinity”.- Return
min(one, two).
static int recursive(int[] h) {
return solve(h.length - 1, h);
}
static int solve(int i, int[] h) {
if (i == 0) return 0; // already standing on stair 0
int one = solve(i - 1, h) + Math.abs(h[i] - h[i - 1]); // last jump came from i - 1
int two = Integer.MAX_VALUE;
if (i > 1) two = solve(i - 2, h) + Math.abs(h[i] - h[i - 2]); // last jump came from i - 2
return Math.min(one, two);
}
Complexity. Time O(2^n): two branches per call, with the same stairs re-solved over and over. Space O(n) for the recursion depth.
The “infinity” is Integer.MAX_VALUE, and it is only ever compared, never added to. Adding anything to it would overflow into a negative number and win every min.
Approach 2: Memoization
Intuition. f(i) depends only on i, so there are n distinct states. Cache each one.
Algorithm.
- Create
dpof sizen, filled with-1. Real costs are never negative, so-1cannot be confused with an answer. - In the helper, after the base case, return
dp[i]if it is filled; otherwise compute it as in Approach 1 and store it.
static int memoization(int[] h) {
int[] dp = new int[h.length];
Arrays.fill(dp, -1); // -1 = not computed (real costs are >= 0)
return memo(h.length - 1, h, dp);
}
static int memo(int i, int[] h, int[] dp) {
if (i == 0) return 0;
if (dp[i] != -1) return dp[i];
int one = memo(i - 1, h, dp) + Math.abs(h[i] - h[i - 1]);
int two = Integer.MAX_VALUE;
if (i > 1) two = memo(i - 2, h, dp) + Math.abs(h[i] - h[i - 2]);
return dp[i] = Math.min(one, two);
}
Complexity. Time O(n), space O(n) for the table plus O(n) recursion stack. With n = 10^5, that stack depth will overflow Java’s default thread stack, which is the practical reason to move on to tabulation.
Approach 3: Tabulation
Intuition. f(i) only depends on smaller indices, so compute dp[0], dp[1], ..., dp[n - 1] in order. Each cell is one comparison.
Algorithm.
dp[0] = 0.- For
ifrom 1 ton - 1, setdp[i]to the minimum ofdp[i - 1] + |h[i] - h[i - 1]|and, wheni > 1,dp[i - 2] + |h[i] - h[i - 2]|. - Return
dp[n - 1].
static int tabulation(int[] h) {
int n = h.length;
int[] dp = new int[n];
dp[0] = 0;
for (int i = 1; i < n; i++) {
int one = dp[i - 1] + Math.abs(h[i] - h[i - 1]);
int two = Integer.MAX_VALUE;
if (i > 1) two = dp[i - 2] + Math.abs(h[i] - h[i - 2]);
dp[i] = Math.min(one, two);
}
return dp[n - 1];
}
Dry run on h = [30, 10, 60, 10, 60, 50], the input that fools greedy:
| i | h[i] | from i - 1 | from i - 2 | dp[i] |
|---|---|---|---|---|
| 0 | 30 | - | - | 0 |
| 1 | 10 | 0 + 20 = 20 | - | 20 |
| 2 | 60 | 20 + 50 = 70 | 0 + 30 = 30 | 30 |
| 3 | 10 | 30 + 50 = 80 | 20 + 0 = 20 | 20 |
| 4 | 60 | 20 + 50 = 70 | 30 + 0 = 30 | 30 |
| 5 | 50 | 30 + 10 = 40 | 20 + 40 = 60 | 40 |
Complexity. Time O(n), space O(n).
Approach 4: Space optimized
Intuition. Computing dp[i] reads dp[i - 1] and dp[i - 2] and nothing older. Two variables are enough: prev for dp[i - 1] and prev2 for dp[i - 2].
Algorithm.
prev = prev2 = 0.- For
ifrom 1 ton - 1: computecurexactly as in tabulation usingprevandprev2, then shiftprev2 = prev,prev = cur. - Return
prev. With one stair the loop does not run and the answer is 0.
At i = 1, prev2 is a placeholder that is never used, because the i > 1 guard skips the two-step option.
static int spaceOptimized(int[] h) {
int prev2 = 0, prev = 0; // dp[i - 2] and dp[i - 1]
for (int i = 1; i < h.length; i++) {
int one = prev + Math.abs(h[i] - h[i - 1]);
int two = Integer.MAX_VALUE;
if (i > 1) two = prev2 + Math.abs(h[i] - h[i - 2]);
int cur = Math.min(one, two);
prev2 = prev;
prev = cur;
}
return prev;
}
Complexity. Time O(n), space O(1).
Edge cases and pitfalls
- One stair. The frog is already at the end; every method returns 0.
- Two stairs. Only the single jump
0 -> 1exists; the two-step option must be skipped, not computed with index-1. - Infinity arithmetic. Never add a cost to
Integer.MAX_VALUE. Keep the “impossible” value out of additions, as the code does, or use a smaller sentinel such as10^9. - Equal heights. Jumps between equal heights are free, so flat stretches give 0 cost. The test
[4, 4, 4, 4, 4]checks that. - Greedy is wrong. It fails on
[30, 10, 60, 10, 60, 50]; the Java file includes that case and 200 seeded random staircases on which all four methods must agree. - Overflow. With the given bounds the total stays below
10^9. If heights could reach10^9, switch the sums tolong.
Related problems
- Introduction to DP explains the four forms used here on Fibonacci.
- Climbing stairs has the same two possible last moves, but counts routes instead of pricing them.
- Frog jump with K distances lets the frog jump up to
kstairs, turning the two options into a loop. - Maximum sum of non adjacent elements is the next 1D DP, with a pick / not-pick choice.