Frog Jump II
MedPrompt
You are given a 0-indexed integer array stones sorted in strictly increasing order representing the positions of stones in a river.
A frog, initially on the first stone, wants to travel to the last stone and then return to the first stone. However, it can jump to any stone at most once.
The length of a jump is the absolute difference between the position of the stone the frog is currently on and the position of the stone to which the frog jumps.
- More formally, if the frog is at
stones[i]and is jumping tostones[j], the length of the jump is|stones[i] - stones[j]|.
The cost of a path is the maximum length of a jump among all jumps in the path.
Return the minimum cost of a path for the frog.
Example 1:
Input: stones = [0,2,5,6,7]
Output: 5
Explanation: The above figure represents one of the optimal paths the frog can take.
The cost of this path is 5, which is the maximum length of a jump.
Since it is not possible to achieve a cost of less than 5, we return it.Example 2:
Input: stones = [0,3,9]
Output: 9
Explanation:
The frog can jump directly to the last stone and come back to the first stone.
In this case, the length of each jump will be 9. The cost for the path will be max(9, 9) = 9.
It can be shown that this is the minimum achievable cost.
Constraints:
2 <= stones.length <= 1050 <= stones[i] <= 109stones[0] == 0stonesis sorted in a strictly increasing order.
Approaches
2 approaches with complexity analysis and trade-offs.
A common strategy for problems that ask to minimize a maximum value (or maximize a minimum value) is to binary search on the answer. We can binary search for the minimum possible cost. For a given cost C, we then need to determine if it's possible for the frog to make the round trip where no single jump exceeds C.
To check if a cost C is feasible, we can use a greedy approach. The frog must travel from stones[0] to stones[n-1] and then back. These two paths (forward and backward) cannot share any intermediate stones. To leave as many options as possible for the return trip, the forward trip should be constructed by making the largest possible jumps at each step, as long as the jump length does not exceed C. This way, we use the minimum number of intermediate stones for the forward journey.
After simulating the forward path, we do the same for the backward path using the remaining unvisited stones. If both the forward and backward paths can be completed, then a cost of C is achievable. The overall binary search will find the smallest C for which this is true.
Algorithm
- The problem asks for the minimum cost, where the cost is the maximum jump length. This structure suggests that we can binary search on the answer (the cost).
- If a cost
Cis achievable, any costC' > Cis also achievable. IfCis not achievable, anyC' < Cis also not achievable. This monotonicity allows for binary search. - We define a search range for the cost. A lower bound is
0, and an upper bound isstones[n-1] - stones[0]. - For each
midvalue in our binary search, we need a functioncanAchieve(cost)to check if it's possible to complete the round trip with all jumps having a length of at mostcost. - The
canAchieve(cost)function works as follows:- It uses a greedy strategy. To maximize the chances of success for the return trip, the forward trip should use as few stones as possible. This is achieved by making the longest possible jumps at each step.
- Forward Path: Start at
stones[0]. From the current stonestones[i], find the furthest stonestones[j](j > i) such thatstones[j] - stones[i] <= cost. Jump tostones[j]. Repeat untilstones[n-1]is reached. Keep track of all visited stones. - If
stones[n-1]cannot be reached,costis not achievable. - Backward Path: Start at
stones[n-1]. From the current stonestones[i], find the furthest stonestones[j](j < i) that was not visited on the forward path, such thatstones[i] - stones[j] <= cost. Jump tostones[j]. Repeat untilstones[0]is reached. - If
stones[0]cannot be reached,costis not achievable. - If both paths are successfully constructed,
costis achievable.
- The greedy traversals in
canAchievecan be implemented efficiently inO(N)time using a two-pointer technique, rather than searching for the next stone from scratch at every step. - The binary search will then narrow down the range until the minimum possible cost is found.
Walkthrough
The core of this approach is the canAchieve(cost) function. This function determines if a round trip is possible with a maximum jump length of cost.
Here's a step-by-step breakdown of canAchieve(cost):
- Initialize a boolean array
visitedof sizento keep track of the stones used in the forward path. - Forward Path Simulation:
- Start at
current_stone_index = 0. Markvisited[0] = true. - While
current_stone_index < n - 1:- Find the largest index
next_stone_index > current_stone_indexsuch thatstones[next_stone_index] - stones[current_stone_index] <= cost. - To do this efficiently, we can use a search pointer that we advance from its previous position.
- If no such stone exists (i.e., we can't even jump to the next stone), it's impossible to reach the end. Return
false. - Move to this
next_stone_indexand mark it as visited.
- Find the largest index
- Start at
- Backward Path Simulation:
- Start at
current_stone_index = n - 1. - While
current_stone_index > 0:- Find the smallest index
next_stone_index < current_stone_indexsuch thatstones[current_stone_index] - stones[next_stone_index] <= costANDvisited[next_stone_index]isfalse(ornext_stone_indexis 0, which is always a valid endpoint). - If no such stone can be found, the return trip is impossible. Return
false. - Move to this
next_stone_index.
- Find the smallest index
- Start at
- If both simulations complete successfully, it means a valid path exists for the given
cost. Returntrue.
With this checker function, the main logic is a standard binary search:
class Solution { public int maxJump(int[] stones) { int low = 0, high = stones[stones.length - 1] - stones[0]; int ans = high; while (low <= high) { int mid = low + (high - low) / 2; if (canAchieve(stones, mid)) { ans = mid; high = mid - 1; } else { low = mid + 1; } } return ans; } private boolean canAchieve(int[] stones, int cost) { int n = stones.length; boolean[] visited = new boolean[n]; // Forward path int currentIdx = 0; visited[0] = true; while (currentIdx < n - 1) { int nextIdx = -1; for (int i = currentIdx + 1; i < n; i++) { if (stones[i] - stones[currentIdx] <= cost) { nextIdx = i; } else { break; } } if (nextIdx == -1) { return false; // Cannot reach the end } currentIdx = nextIdx; visited[currentIdx] = true; } // Backward path currentIdx = n - 1; while (currentIdx > 0) { int nextIdx = -1; for (int i = currentIdx - 1; i >= 0; i--) { if (stones[currentIdx] - stones[i] <= cost) { if (!visited[i] || i == 0) { nextIdx = i; } } else { break; } } if (nextIdx == -1) { return false; // Cannot reach the start } currentIdx = nextIdx; } return true; }}Note: The inner loops in canAchieve can be optimized to a single pass with two pointers, but this version illustrates the logic more clearly.
Complexity
Time
O(N * log(K)), where N is the number of stones and K is the maximum possible jump length (`stones[n-1] - stones[0]`). The `canAchieve` function takes O(N) time, and it's called O(log K) times by the binary search.
Space
O(N) to store the `visited` status of each stone.
Trade-offs
Pros
A standard and robust approach for minimax problems.
Relatively straightforward to reason about and implement once the binary search pattern is identified.
Cons
Slightly more complex to implement than the optimal linear scan solution.
Not the most optimal time complexity, though it is efficient enough to pass within typical time limits.
A more direct and efficient approach comes from a key observation about the structure of the problem. The cost of any path is the length of its longest jump. To minimize this cost, we need to avoid long jumps.
Long jumps occur when the frog skips one or more stones. The shortest possible 'skip' jump is one that hops over a single stone, i.e., from stones[i] to stones[i+2]. The length of such a jump is stones[i+2] - stones[i].
Consider any three consecutive stones stones[i], stones[i+1], stones[i+2]. For the frog to travel from the region ...stones[i] to stones[i+2]... and back, the paths must somehow cover this segment. If one path takes a jump from stones[i] to stones[i+2], the other path must handle stones[i+1]. This jump from i to i+2 has a length of stones[i+2] - stones[i]. Any valid round-trip path will have a cost of at least the maximum of these i-to-i+2 jumps. It turns out this is not just a lower bound, but the exact minimum cost.
Therefore, the problem reduces to a simple linear scan through the stones array, calculating stones[i+2] - stones[i] for each i and finding the maximum among them.
Algorithm
- The core idea is to find a logical lower bound for the cost and then show that this lower bound is always achievable.
- Consider any three consecutive stones:
stones[i],stones[i+1], andstones[i+2]. - The frog's two paths (forward and backward) must somehow navigate past this triplet of stones.
- If one path handles all three stones in sequence (e.g.,
... -> i -> i+1 -> i+2 -> ...), the jumps are short. However, this forces the other path to make a very long jump to bypass this entire block of stones, likely leading to a high cost. - To balance the jump lengths between the two paths, they must be interleaved. A common strategy is for one path to handle
stones[i]andstones[i+2], while the other handlesstones[i+1]. - If a path includes the jump
i -> i+2, its length isstones[i+2] - stones[i]. This jump skipsstones[i+1], which must be visited by the other path. - This implies that for every
i, some jump in the total path must be at least as long as the shortest way to 'skip' a stone, which isstones[i+2] - stones[i]. Therefore, the final cost must be at leastmax(stones[i+2] - stones[i])over all possiblei. - It can be shown that this lower bound is always achievable. A path can be constructed where one leg of the journey primarily uses even-indexed stones and the other uses odd-indexed stones, and the maximum jump length in such a path is exactly this value.
- Thus, the problem simplifies to finding the maximum value of
stones[i+2] - stones[i]forifrom0ton-3.
Walkthrough
The logic hinges on the realization that the bottleneck (the longest jump) is determined by jumps that skip exactly one stone. Let's formalize this:
-
Lower Bound: Let the minimum cost be
C. Consider anyifrom0ton-3. The stonesstones[i],stones[i+1], andstones[i+2]must be visited. The forward and backward paths are disjoint on intermediate stones. This meansstones[i+1]can't be on both paths. Suppose the forward path goes fromstones[i]tostones[i+2]. This jump has lengthstones[i+2] - stones[i]. Thus, the costCmust be at least this large. If the path doesn't jump fromitoi+2directly, but viai+1(i -> i+1 -> i+2), then the other path must jump over this entire block, which would be an even longer jump. This forces the costCto be at leastmax(stones[i+2] - stones[i])for alli. -
Achievability: We can argue that a cost equal to this lower bound, let's call it
L, is always achievable. A path can be constructed where jumps are either between adjacent stones (i -> i+1) or second-adjacent stones (i -> i+2). The length of any jumpstones[i+1] - stones[i]is less thanstones[i+2] - stones[i]. So, the maximum jump length will indeed beL.
This simplifies the algorithm to a single loop:
- Initialize a variable
max_cost = 0. - If
n == 2, the only path is0 -> 1 -> 0, so the cost isstones[1] - stones[0]. - Iterate from
i = 0ton - 3. - In each iteration, calculate the jump length
stones[i+2] - stones[i]. - Update
max_cost = max(max_cost, stones[i+2] - stones[i]). - Return
max_cost.
This approach is extremely efficient as it only requires a single pass through the array.
class Solution { public int maxJump(int[] stones) { int n = stones.length; if (n == 2) { return stones[1] - stones[0]; } int maxCost = 0; // The first jump on the return path is from the last stone to some other stone. // The longest possible first jump on the return path is to the first stone, but // that would mean all other stones were visited on the way out. The cost would be // max(stones[i+1]-stones[i]) and stones[n-1]-stones[0]. // The optimal path interleaves stones. The cost is determined by jumps over one stone. maxCost = stones[1] - stones[0]; // The jump 1->0 on the return path is one candidate for (int i = 0; i <= n - 3; i++) { maxCost = Math.max(maxCost, stones[i + 2] - stones[i]); } // The provided solution in the loop is sufficient. Let's simplify. // The cost is max of all (i -> i+2) jumps. // The jump 1->0 is not of this form, but let's check the logic. // Path 0->2->... and ...->3->1->0. The jumps are (i, i+2) and (j+2, j) and (1,0). // stones[1]-stones[0] is smaller than stones[2]-stones[0]. So we don't need to check it separately. int result = 0; for (int i = 2; i < n; i++) { result = Math.max(result, stones[i] - stones[i-2]); } return result; }}The simplified code correctly captures the logic that the cost is the maximum of all stones[i] - stones[i-2] jumps.
Complexity
Time
O(N) because we perform a single pass through the `stones` array.
Space
O(1) as we only use a few variables to keep track of the maximum cost.
Trade-offs
Pros
Extremely efficient, with optimal time and space complexity.
Very simple to implement once the core logic is understood.
Cons
The underlying proof that this lower bound is always achievable is non-trivial and requires careful reasoning about path construction.
It relies on a key insight that may not be immediately obvious.
Solutions
Solution
class Solution {public int maxJump(int[] stones) { int ans = stones[1] - stones[0]; for (int i = 2; i < stones.length; ++i) { ans = Math.max(ans, stones[i] - stones[i - 2]); } return ans; }}class Solution {public: int maxJump(vector<int> &stones) { int ans = stones[1] - stones[0]; for (int i = 2; i < stones.size(); ++i) ans = max(ans, stones[i] - stones[i - 2]); return ans; }};class Solution: def maxJump(self, stones: List[int]) -> int: ans = stones[1] - stones[0] for i in range(2, len(stones)): ans = max(ans, stones[i] - stones[i - 2]) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.