Facebook Pixel

403. Frog Jump

Problem Description

You have a frog that needs to cross a river by jumping on stones. The river is divided into units, and stones are placed at certain unit positions. The frog starts on the first stone and must reach the last stone without jumping into the water.

The key rules for the frog's movement are:

  1. Starting position: The frog begins on the first stone (at position stones[0])
  2. First jump: The first jump must be exactly 1 unit
  3. Jump distance rule: If the frog's last jump was k units, the next jump must be either:
    • k - 1 units
    • k units
    • k + 1 units
  4. Direction: The frog can only jump forward (to higher position values)
  5. Valid landing: The frog must land on a stone - it cannot jump into water

Given an array stones containing the positions of stones in ascending order, you need to determine if the frog can successfully jump from the first stone to the last stone following these rules.

For example, if stones = [0, 1, 3, 5, 6, 8, 12, 17], the frog can cross by following this path:

  • Jump 1 unit from stone 0 to stone 1
  • Jump 2 units from stone 1 to stone 3
  • Jump 2 units from stone 3 to stone 5
  • Jump 3 units from stone 5 to stone 8
  • Jump 4 units from stone 8 to stone 12
  • Jump 5 units from stone 12 to stone 17

The function should return true if the frog can reach the last stone, and false otherwise.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The problem asks us to find if there's a valid path from the first stone to the last stone. At each stone, we have multiple choices for our next jump (based on the previous jump distance), which creates a branching decision tree. This naturally suggests a recursive exploration approach.

Think about standing on any stone during the journey. We need to know two things:

  1. Which stone we're currently on (position i)
  2. How we got here (the last jump distance k)

Why do we need the last jump distance? Because it determines our next possible moves. If we jumped k units to get here, we can only jump k-1, k, or k+1 units next.

The key insight is that the same stone can be reached with different jump distances. For example, stone at position 5 could be reached by:

  • Jumping 2 units from position 3
  • Jumping 4 units from position 1

Each arrival creates a different state because the allowed next jumps are different. This means our state is defined by the pair (current_stone_index, last_jump_distance).

Since we might visit the same state multiple times through different paths, we can use memoization to avoid recalculating whether a particular (stone, jump) combination can lead to the destination.

The hash table pos is used for quick lookups - given a position value, we can instantly find if there's a stone there and get its index. This is crucial because when we're at stone i and want to jump j units, we need to check if position stones[i] + j has a stone.

Starting from stone 0 with an initial jump of 0 (which forces the first actual jump to be 1), we recursively explore all possible paths until we either reach the last stone or exhaust all possibilities.

Solution Approach

The solution uses Dynamic Programming with Memoization combined with Depth-First Search (DFS) to explore all possible jumping paths.

Data Structures Used

  1. Hash Table (pos): Maps each stone position to its index in the array. This allows O(1) lookup to check if a target position has a stone.

    pos = {s: i for i, s in enumerate(stones)}
  2. Memoization Cache: The @cache decorator automatically memoizes the results of dfs(i, k) to avoid redundant calculations.

Core Algorithm - DFS Function

The recursive function dfs(i, k) represents:

  • i: Current stone index
  • k: Last jump distance used to reach stone i
  • Returns: True if we can reach the last stone from position i, False otherwise

Implementation Steps

  1. Base Case: If we're at the last stone (i == n - 1), we've successfully crossed the river, return True.

  2. Recursive Exploration: For the current stone at index i, try all possible next jumps:

    • Generate jump distances: j ∈ [k-1, k, k+1]
    • For each valid jump distance j:
      • Check if j > 0 (must be positive)
      • Calculate target position: stones[i] + j
      • Check if target position has a stone using pos
      • If stone exists, recursively check dfs(pos[stones[i] + j], j)
      • If any recursive call returns True, we've found a valid path
  3. Return False: If no valid jump leads to success, return False.

Starting Point

The algorithm starts with dfs(0, 0):

  • Start at stone index 0
  • Initial k = 0 ensures the first actual jump must be 1 (since next jumps can be k-1=(-1), k=0, or k+1=1, only 1 is valid)

Time and Space Complexity

  • Time Complexity: O(n²) - At most n stones, and from each stone, we can have at most n different jump distances
  • Space Complexity: O(n²) - For the memoization cache storing results for each (stone_index, jump_distance) pair

The memoization is crucial here - without it, the algorithm would explore the same states multiple times, leading to exponential time complexity.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through a small example with stones = [0, 1, 3, 6, 7].

Initial Setup:

  • Create position map: pos = {0:0, 1:1, 3:2, 6:3, 7:4}
  • Start with dfs(0, 0) (at stone index 0, last jump was 0)

Step 1: From stone 0 (position 0), k=0

  • Possible jumps: k-1=-1 (invalid), k=0 (invalid), k+1=1 ✓
  • Jump 1 unit → position 0+1=1
  • Stone exists at position 1 (index 1)
  • Call dfs(1, 1)

Step 2: From stone 1 (position 1), k=1

  • Possible jumps: k-1=0 (invalid), k=1 ✓, k+1=2 ✓
  • Try jump 1: position 1+1=2 → no stone, skip
  • Try jump 2: position 1+2=3 → stone exists (index 2)
  • Call dfs(2, 2)

Step 3: From stone 2 (position 3), k=2

  • Possible jumps: k-1=1 ✓, k=2 ✓, k+1=3 ✓
  • Try jump 1: position 3+1=4 → no stone, skip
  • Try jump 2: position 3+2=5 → no stone, skip
  • Try jump 3: position 3+3=6 → stone exists (index 3)
  • Call dfs(3, 3)

Step 4: From stone 3 (position 6), k=3

  • Possible jumps: k-1=2 ✓, k=3 ✓, k+1=4 ✓
  • Try jump 1: position 6+1=7 → stone exists (index 4)
  • Call dfs(4, 1)

Step 5: At stone 4 (position 7), k=1

  • This is the last stone (index 4 = n-1)
  • Return True

The recursion unwinds, returning True all the way back. The frog successfully crosses!

Path taken: 0 → 1 (jump 1) → 3 (jump 2) → 6 (jump 3) → 7 (jump 1)

Note how memoization helps: If we later explore another path that reaches stone 3 with k=2, we don't need to recalculate - we already know dfs(2, 2) returns True.

Solution Implementation

1class Solution:
2    def canCross(self, stones: List[int]) -> bool:
3        """
4        Determine if a frog can cross the river by jumping on stones.
5        The frog starts at the first stone and each jump must be k-1, k, or k+1 units,
6        where k is the distance of the previous jump.
7      
8        Args:
9            stones: List of stone positions in ascending order
10          
11        Returns:
12            True if the frog can reach the last stone, False otherwise
13        """
14        from functools import cache
15      
16        @cache
17        def dfs(current_index: int, last_jump_distance: int) -> bool:
18            """
19            Depth-first search to check if frog can reach the last stone.
20          
21            Args:
22                current_index: Current stone index the frog is on
23                last_jump_distance: Distance of the previous jump
24              
25            Returns:
26                True if frog can reach the last stone from current position
27            """
28            # Base case: reached the last stone
29            if current_index == num_stones - 1:
30                return True
31          
32            # Try all possible jump distances: k-1, k, k+1
33            for next_jump_distance in range(last_jump_distance - 1, last_jump_distance + 2):
34                # Calculate the target position after jumping
35                target_position = stones[current_index] + next_jump_distance
36              
37                # Check if jump is valid (distance > 0) and target position has a stone
38                if next_jump_distance > 0 and target_position in stone_position_to_index:
39                    # Recursively check if we can reach the end from the new position
40                    target_index = stone_position_to_index[target_position]
41                    if dfs(target_index, next_jump_distance):
42                        return True
43          
44            # No valid path found from current position
45            return False
46      
47        # Initialize variables
48        num_stones = len(stones)
49        # Create a mapping from stone position to its index for O(1) lookup
50        stone_position_to_index = {position: index for index, position in enumerate(stones)}
51      
52        # Start DFS from first stone (index 0) with initial jump distance 0
53        return dfs(0, 0)
54
1class Solution {
2    // Memoization table: dp[currentStone][lastJumpSize] = can reach end from here
3    private Boolean[][] dp;
4  
5    // Maps stone position to its index in the stones array
6    private Map<Integer, Integer> stoneToIndex;
7  
8    // Array of stone positions
9    private int[] stones;
10  
11    // Total number of stones
12    private int totalStones;
13
14    /**
15     * Determines if a frog can cross the river by jumping on stones.
16     * The frog starts at the first stone and must reach the last stone.
17     * From stone i with last jump of k units, the next jump can be k-1, k, or k+1 units.
18     * 
19     * @param stones Array of stone positions in ascending order
20     * @return true if the frog can reach the last stone, false otherwise
21     */
22    public boolean canCross(int[] stones) {
23        // Initialize variables
24        totalStones = stones.length;
25        dp = new Boolean[totalStones][totalStones];
26        this.stones = stones;
27        stoneToIndex = new HashMap<>();
28      
29        // Build position to index mapping for O(1) lookup
30        for (int i = 0; i < totalStones; i++) {
31            stoneToIndex.put(stones[i], i);
32        }
33      
34        // Start DFS from first stone with initial jump size of 0
35        return dfs(0, 0);
36    }
37
38    /**
39     * Recursive DFS with memoization to check if we can reach the last stone.
40     * 
41     * @param currentIndex Index of the current stone
42     * @param lastJumpSize Size of the jump that brought us to current stone
43     * @return true if we can reach the last stone from current position
44     */
45    private boolean dfs(int currentIndex, int lastJumpSize) {
46        // Base case: reached the last stone
47        if (currentIndex == totalStones - 1) {
48            return true;
49        }
50      
51        // Check memoization table
52        if (dp[currentIndex][lastJumpSize] != null) {
53            return dp[currentIndex][lastJumpSize];
54        }
55      
56        // Try all possible jump sizes: k-1, k, k+1
57        for (int nextJumpSize = lastJumpSize - 1; nextJumpSize <= lastJumpSize + 1; nextJumpSize++) {
58            // Jump size must be positive
59            if (nextJumpSize > 0) {
60                // Calculate the landing position
61                int targetPosition = stones[currentIndex] + nextJumpSize;
62              
63                // Check if there's a stone at the target position
64                if (stoneToIndex.containsKey(targetPosition)) {
65                    int targetIndex = stoneToIndex.get(targetPosition);
66                  
67                    // Recursively check if we can reach the end from the target stone
68                    if (dfs(targetIndex, nextJumpSize)) {
69                        // Cache and return successful result
70                        return dp[currentIndex][lastJumpSize] = true;
71                    }
72                }
73            }
74        }
75      
76        // No valid path found, cache and return false
77        return dp[currentIndex][lastJumpSize] = false;
78    }
79}
80
1class Solution {
2public:
3    bool canCross(vector<int>& stones) {
4        int n = stones.size();
5      
6        // dp[i][k] represents whether we can reach stone i with last jump of k units
7        // -1: not computed, 0: false, 1: true
8        vector<vector<int>> dp(n, vector<int>(n, -1));
9      
10        // Map stone position to its index for O(1) lookup
11        unordered_map<int, int> stoneToIndex;
12        for (int i = 0; i < n; ++i) {
13            stoneToIndex[stones[i]] = i;
14        }
15      
16        // DFS with memoization
17        // currentIndex: current stone index
18        // lastJumpSize: the jump size used to reach current stone
19        function<bool(int, int)> dfs = [&](int currentIndex, int lastJumpSize) -> bool {
20            // Base case: reached the last stone
21            if (currentIndex == n - 1) {
22                return true;
23            }
24          
25            // Return memoized result if already computed
26            if (dp[currentIndex][lastJumpSize] != -1) {
27                return dp[currentIndex][lastJumpSize];
28            }
29          
30            // Try jumps of size k-1, k, k+1 where k is the last jump size
31            for (int nextJumpSize = lastJumpSize - 1; nextJumpSize <= lastJumpSize + 1; ++nextJumpSize) {
32                // Jump size must be positive
33                if (nextJumpSize > 0) {
34                    int nextPosition = stones[currentIndex] + nextJumpSize;
35                  
36                    // Check if the next position has a stone and recursively explore
37                    if (stoneToIndex.count(nextPosition)) {
38                        int nextIndex = stoneToIndex[nextPosition];
39                        if (dfs(nextIndex, nextJumpSize)) {
40                            return dp[currentIndex][lastJumpSize] = 1;
41                        }
42                    }
43                }
44            }
45          
46            // No valid jump found from current position
47            return dp[currentIndex][lastJumpSize] = 0;
48        };
49      
50        // Start from stone 0 with initial jump size 0
51        // (First jump must be 1 unit, which is handled by k+1 when k=0)
52        return dfs(0, 0);
53    }
54};
55
1/**
2 * Determines if a frog can cross a river by jumping on stones
3 * @param stones - Array of stone positions in ascending order
4 * @returns true if the frog can reach the last stone, false otherwise
5 */
6function canCross(stones: number[]): boolean {
7    const stoneCount: number = stones.length;
8  
9    // Map to store stone position to its index for O(1) lookup
10    const positionToIndex: Map<number, number> = new Map();
11    for (let i = 0; i < stoneCount; i++) {
12        positionToIndex.set(stones[i], i);
13    }
14  
15    // Memoization table: memo[stoneIndex][lastJumpDistance] 
16    // -1: not computed, 0: cannot reach end, 1: can reach end
17    const memo: number[][] = Array.from(
18        { length: stoneCount }, 
19        () => new Array(stoneCount).fill(-1)
20    );
21  
22    /**
23     * DFS helper function to check if frog can reach the last stone
24     * @param currentIndex - Current stone index
25     * @param lastJumpDistance - Distance of the last jump
26     * @returns true if can reach the last stone from current position
27     */
28    const depthFirstSearch = (currentIndex: number, lastJumpDistance: number): boolean => {
29        // Base case: reached the last stone
30        if (currentIndex === stoneCount - 1) {
31            return true;
32        }
33      
34        // Check memoization table
35        if (memo[currentIndex][lastJumpDistance] !== -1) {
36            return memo[currentIndex][lastJumpDistance] === 1;
37        }
38      
39        // Try jumps of distance k-1, k, or k+1 units
40        for (let nextJumpDistance = lastJumpDistance - 1; nextJumpDistance <= lastJumpDistance + 1; nextJumpDistance++) {
41            // Jump distance must be positive
42            if (nextJumpDistance > 0) {
43                const targetPosition: number = stones[currentIndex] + nextJumpDistance;
44              
45                // Check if there's a stone at the target position
46                if (positionToIndex.has(targetPosition)) {
47                    const targetIndex: number = positionToIndex.get(targetPosition)!;
48                  
49                    // Recursively check if we can reach the end from the target stone
50                    if (depthFirstSearch(targetIndex, nextJumpDistance)) {
51                        memo[currentIndex][lastJumpDistance] = 1;
52                        return true;
53                    }
54                }
55            }
56        }
57      
58        // Cannot reach the end from current position with given last jump
59        memo[currentIndex][lastJumpDistance] = 0;
60        return false;
61    };
62  
63    // Start from first stone (index 0) with initial jump distance 0
64    return depthFirstSearch(0, 0);
65}
66

Time and Space Complexity

The time complexity is O(n²) where n is the number of stones.

Time Complexity Analysis:

  • The algorithm uses memoized DFS with two parameters: position index i and jump distance k
  • There are at most n possible positions (from 0 to n-1)
  • For each position, the maximum jump distance k is bounded by n (in the worst case, the frog could jump at most n units to reach the last stone)
  • Therefore, there are at most O(n × n) = O(n²) unique states in the memoization
  • Each state is computed once due to the @cache decorator
  • For each state, we explore at most 3 jump options (k-1, k, k+1), which is O(1)
  • Total time complexity: O(n²) × O(1) = O(n²)

The space complexity is O(n²) where n is the number of stones.

Space Complexity Analysis:

  • The pos dictionary stores n key-value pairs: O(n)
  • The memoization cache stores at most O(n²) states (as analyzed above)
  • The recursion stack depth is at most n in the worst case (jumping through all stones): O(n)
  • Total space complexity: O(n) + O(n²) + O(n) = O(n²)

The dominant factor in both time and space complexity is the memoization cache which can store up to states.

Common Pitfalls

1. Incorrect Handling of the First Jump

Pitfall: A common mistake is misunderstanding how to enforce that the first jump must be exactly 1 unit. Some implementations might:

  • Start with dfs(0, 1) thinking the first jump is already 1
  • Forget to validate that the first jump can only be 1 unit

Why it's wrong: Starting with dfs(0, 1) would mean the frog has already made a jump of 1 unit, which would allow the next jump to be 0, 1, or 2 units. This violates the constraint that the first actual jump must be exactly 1.

Correct approach: Start with dfs(0, 0). When k=0, the possible next jumps are k-1=-1 (invalid), k=0 (invalid), and k+1=1 (valid). This naturally enforces that the first jump must be 1 unit.

2. Missing Edge Case: Second Stone Not at Position 1

Pitfall: Not checking if it's even possible to make the first jump. If stones = [0, 2, 3, 4, 8], the frog cannot make the first jump since position 1 has no stone.

Solution: Add an early termination check:

# Early termination: if second stone is not at position 1, impossible to cross
if len(stones) > 1 and stones[1] != 1:
    return False

3. Integer Overflow in Large Jump Calculations

Pitfall: For very large stone positions, calculating stones[i] + j might cause issues in some languages (though Python handles big integers well).

Solution: Add bounds checking or use the constraint that jump distances are limited:

# The maximum jump distance from any stone cannot exceed n (number of stones)
if next_jump_distance > num_stones:
    continue  # Skip impossibly large jumps

4. Not Using Memoization or Using It Incorrectly

Pitfall: Forgetting to memoize the DFS function or using incorrect memoization keys. Without memoization, the time complexity becomes exponential.

Wrong approach:

# Wrong: only memoizing by current index
@cache
def dfs(current_index):  # Missing last_jump_distance parameter
    ...

Why it's wrong: The same stone can be reached with different previous jump distances, leading to different possible next jumps. The state must include both position AND how we got there.

Correct approach: Always include both (current_index, last_jump_distance) in the memoization key.

5. Allowing Zero or Negative Jumps

Pitfall: Forgetting to check next_jump_distance > 0 before attempting a jump. This could lead to:

  • Infinite loops (jumping 0 units stays at the same position)
  • Invalid backward jumps (negative distances)

Solution: Always validate jump distance:

if next_jump_distance > 0 and target_position in stone_position_to_index:
    # Process valid jump

Complete Improved Solution with Pitfall Fixes:

class Solution:
    def canCross(self, stones: List[int]) -> bool:
        from functools import cache
      
        # Early termination: impossible if first jump can't be made
        if len(stones) > 1 and stones[1] != 1:
            return False
      
        num_stones = len(stones)
        stone_position_to_index = {position: index for index, position in enumerate(stones)}
      
        @cache
        def dfs(current_index: int, last_jump_distance: int) -> bool:
            if current_index == num_stones - 1:
                return True
          
            for next_jump_distance in range(last_jump_distance - 1, last_jump_distance + 2):
                # Critical checks: positive jump and not exceeding reasonable bounds
                if next_jump_distance > 0 and next_jump_distance <= num_stones:
                    target_position = stones[current_index] + next_jump_distance
                  
                    if target_position in stone_position_to_index:
                        target_index = stone_position_to_index[target_position]
                        if dfs(target_index, next_jump_distance):
                            return True
          
            return False
      
        return dfs(0, 0)
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More