Facebook Pixel

576. Out of Boundary Paths

Problem Description

You have an m x n grid with a ball placed at a starting position [startRow, startColumn]. The ball can move in four directions: up, down, left, or right. Each move takes the ball to an adjacent cell, and the ball can move outside the grid boundaries.

Given:

  • Grid dimensions: m (rows) and n (columns)
  • Maximum number of moves allowed: maxMove
  • Starting position: [startRow, startColumn]

Your task is to find the total number of different paths that can take the ball outside the grid boundaries within maxMove moves or less. Since the answer can be very large, return it modulo 10^9 + 7.

The solution uses a depth-first search with memoization. The function dfs(i, j, k) calculates the number of valid paths starting from position (i, j) with k moves remaining.

The key logic:

  • If the current position (i, j) is already outside the grid, it counts as one valid path (return 1 if we have non-negative moves remaining)
  • If we're inside the grid but have no moves left (k <= 0), we can't reach outside, so return 0
  • Otherwise, try moving in all four directions and sum up the paths from each new position with k-1 moves remaining

The @cache decorator memoizes results to avoid recalculating paths from the same position with the same number of moves remaining. The modulo operation ensures the result stays within bounds.

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

Intuition

When we think about counting paths that lead the ball outside the grid, we need to consider that from any position, we have multiple choices (up, down, left, right), and each choice leads to a new position with its own set of choices. This naturally suggests a recursive approach.

The key insight is that the number of valid paths from any position depends on:

  1. Where we currently are (position (i, j))
  2. How many moves we have left (k)

If we're already outside the grid, we've found a valid path. If we're inside but have no moves left, we can't escape. Otherwise, the total paths from our current position equals the sum of paths from all four neighboring positions with one less move available.

Notice that we might revisit the same position with the same number of moves remaining multiple times through different paths. For example, starting from (1, 1) with 3 moves, we might go right then left, or up then down, both returning to (1, 1) with 1 move left. The number of escape paths from (1, 1) with 1 move is always the same, regardless of how we got there.

This observation leads us to use memoization - we can cache the result for each unique state (i, j, k). Once we calculate how many paths exist from position (i, j) with k moves, we store it and reuse it whenever we encounter the same state again.

The recursive nature with overlapping subproblems makes this a classic dynamic programming problem, where we build up the solution by combining results from smaller subproblems (positions with fewer moves remaining).

Learn more about Dynamic Programming patterns.

Solution Approach

The solution implements a memoized depth-first search (DFS) to count all possible paths that lead the ball outside the grid.

Core Function Definition: The function dfs(i, j, k) represents the number of paths to move out of the boundary starting from position (i, j) with k moves remaining.

Base Cases:

  1. Outside the grid: If not 0 <= i < m or not 0 <= j < n, we're outside the grid boundaries. Return 1 if k >= 0 (we successfully escaped), otherwise return 0.
  2. No moves left: If k <= 0 while still inside the grid, we can't escape anymore, so return 0.

Recursive Case: For each valid position inside the grid with moves remaining:

  • Initialize ans = 0 to accumulate paths
  • Iterate through all four directions using pairwise(dirs) where dirs = (-1, 0, 1, 0, -1) represents direction vectors
  • For each direction (a, b), calculate next position (x, y) = (i + a, j + b)
  • Recursively call dfs(x, y, k - 1) to get paths from the next position
  • Add the result to ans and apply modulo 10^9 + 7 to prevent overflow

Direction Encoding: The dirs = (-1, 0, 1, 0, -1) array cleverly encodes four directions:

  • pairwise(dirs) generates pairs: (-1, 0), (0, 1), (1, 0), (0, -1)
  • These represent movements: up, right, down, left respectively

Memoization: The @cache decorator automatically memoizes the dfs function, storing results for each unique (i, j, k) combination. This prevents redundant calculations when the same state is reached through different paths, reducing time complexity from exponential to polynomial.

Main Function: Simply calls dfs(startRow, startColumn, maxMove) to get the total number of paths starting from the initial position with all moves available.

The modulo operation 10^9 + 7 is applied during accumulation to keep numbers manageable and provide the final answer in the required format.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a small example with a 2×3 grid, starting at position [0, 1] with maxMove = 3.

Grid visualization:
[_][S][_]  (S = start at [0,1])
[_][_][_]

We call dfs(0, 1, 3) to find all paths that exit the grid within 3 moves.

Step 1: From (0, 1) with 3 moves

  • Position is inside grid (0 ≤ 0 < 2 and 0 ≤ 1 < 3) ✓
  • We have moves remaining (3 > 0) ✓
  • Try all four directions:
    • Up: dfs(-1, 1, 2) → Outside grid! Returns 1
    • Right: dfs(0, 2, 2) → Still inside, continue recursion
    • Down: dfs(1, 1, 2) → Still inside, continue recursion
    • Left: dfs(0, 0, 2) → Still inside, continue recursion

Step 2: Expand dfs(0, 2, 2) - top-right corner with 2 moves

  • Try all directions:
    • Up: dfs(-1, 2, 1) → Outside! Returns 1
    • Right: dfs(0, 3, 1) → Outside! Returns 1
    • Down: dfs(1, 2, 1) → Inside, continue
    • Left: dfs(0, 1, 1) → Inside, continue (this state gets memoized!)

Step 3: Expand dfs(0, 1, 1) - back at start with 1 move

  • Try all directions:
    • Up: dfs(-1, 1, 0) → Outside with k=0, returns 1
    • Right: dfs(0, 2, 0) → Inside with k=0, returns 0
    • Down: dfs(1, 1, 0) → Inside with k=0, returns 0
    • Left: dfs(0, 0, 0) → Inside with k=0, returns 0
  • Total: 1 path

Key Memoization Example: When we later call dfs(1, 1, 2) from the initial position going down:

  • It will try going up and reach dfs(0, 1, 1)
  • But we've already computed dfs(0, 1, 1) = 1 and cached it!
  • We immediately return the cached value instead of recalculating

Path Examples: Some valid escape paths from our starting position:

  1. Up (1 move) - exits immediately
  2. Right → Up (2 moves)
  3. Right → Right (2 moves)
  4. Down → Up → Up (3 moves)
  5. Left → Up (2 moves)

The algorithm systematically explores all possibilities, using memoization to avoid redundant calculations, and returns the total count modulo 10^9 + 7.

Solution Implementation

1from functools import lru_cache
2from itertools import pairwise
3
4class Solution:
5    def findPaths(
6        self, m: int, n: int, maxMove: int, startRow: int, startColumn: int
7    ) -> int:
8        """
9        Find the number of paths to move a ball out of grid boundary.
10      
11        Args:
12            m: Number of rows in the grid
13            n: Number of columns in the grid
14            maxMove: Maximum number of moves allowed
15            startRow: Starting row position
16            startColumn: Starting column position
17          
18        Returns:
19            Number of paths to move the ball out of boundary within maxMove moves
20        """
21      
22        MOD = 10**9 + 7
23        # Direction vectors for moving up, right, down, left
24        # Stored as consecutive pairs: (-1,0), (0,1), (1,0), (0,-1)
25        DIRECTIONS = (-1, 0, 1, 0, -1)
26      
27        @lru_cache(None)
28        def dfs(row: int, col: int, moves_left: int) -> int:
29            """
30            Depth-first search to count paths from current position.
31          
32            Args:
33                row: Current row position
34                col: Current column position
35                moves_left: Number of moves remaining
36              
37            Returns:
38                Number of valid paths from current position
39            """
40            # Check if current position is out of bounds
41            if not (0 <= row < m) or not (0 <= col < n):
42                # Return 1 if we reached out of bounds with valid moves remaining
43                return int(moves_left >= 0)
44          
45            # No valid paths if no moves left and still in bounds
46            if moves_left <= 0:
47                return 0
48          
49            # Count paths from all four directions
50            path_count = 0
51            for dx, dy in pairwise(DIRECTIONS):
52                next_row = row + dx
53                next_col = col + dy
54                path_count = (path_count + dfs(next_row, next_col, moves_left - 1)) % MOD
55          
56            return path_count
57      
58        # Start DFS from the initial position
59        return dfs(startRow, startColumn, maxMove)
60
1class Solution {
2    // Grid dimensions
3    private int rows, cols;
4    // Memoization table: dp[row][col][movesRemaining] = number of paths
5    private Integer[][][] dp;
6    // Modulo constant for large numbers
7    private final int MOD = (int) 1e9 + 7;
8
9    /**
10     * Finds the number of paths to move the ball out of grid boundary.
11     * @param m - number of rows in the grid
12     * @param n - number of columns in the grid
13     * @param maxMove - maximum number of moves allowed
14     * @param startRow - starting row position
15     * @param startColumn - starting column position
16     * @return number of paths to move out of boundary within maxMove moves
17     */
18    public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
19        this.rows = m;
20        this.cols = n;
21        // Initialize memoization table
22        this.dp = new Integer[m][n][maxMove + 1];
23      
24        // Start DFS from the starting position with maxMove moves
25        return dfs(startRow, startColumn, maxMove);
26    }
27
28    /**
29     * DFS helper function to count paths from current position.
30     * @param currentRow - current row position
31     * @param currentCol - current column position
32     * @param movesRemaining - number of moves remaining
33     * @return number of valid paths from current position
34     */
35    private int dfs(int currentRow, int currentCol, int movesRemaining) {
36        // Base case: if out of bounds, we found a valid path
37        if (currentRow < 0 || currentRow >= rows || currentCol < 0 || currentCol >= cols) {
38            // Return 1 if we still have moves (or exactly 0 moves), 0 otherwise
39            return movesRemaining >= 0 ? 1 : 0;
40        }
41      
42        // Base case: no moves left and still in bounds
43        if (movesRemaining <= 0) {
44            return 0;
45        }
46      
47        // Check memoization table
48        if (dp[currentRow][currentCol][movesRemaining] != null) {
49            return dp[currentRow][currentCol][movesRemaining];
50        }
51      
52        // Calculate paths from current position
53        int pathCount = 0;
54      
55        // Direction vectors for up, right, down, left
56        final int[] directions = {-1, 0, 1, 0, -1};
57      
58        // Try all four directions
59        for (int dir = 0; dir < 4; dir++) {
60            int nextRow = currentRow + directions[dir];
61            int nextCol = currentCol + directions[dir + 1];
62          
63            // Add paths from next position and apply modulo
64            pathCount = (pathCount + dfs(nextRow, nextCol, movesRemaining - 1)) % MOD;
65        }
66      
67        // Store result in memoization table and return
68        dp[currentRow][currentCol][movesRemaining] = pathCount;
69        return pathCount;
70    }
71}
72
1class Solution {
2public:
3    int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
4        // dp[i][j][k] represents the number of paths to move the ball out of boundary
5        // from position (i, j) with exactly k moves remaining
6        int dp[m][n][maxMove + 1];
7        memset(dp, -1, sizeof(dp));
8      
9        const int MOD = 1e9 + 7;
10        // Direction vectors: up, right, down, left
11        const int directions[5] = {-1, 0, 1, 0, -1};
12      
13        // Recursive DFS with memoization
14        auto dfs = [&](this auto&& dfs, int row, int col, int movesLeft) -> int {
15            // Base case: ball is out of bounds
16            if (row < 0 || row >= m || col < 0 || col >= n) {
17                // Return 1 if we still have moves left (valid path), 0 otherwise
18                return movesLeft >= 0 ? 1 : 0;
19            }
20          
21            // Base case: no moves left and ball is still in bounds
22            if (movesLeft <= 0) {
23                return 0;
24            }
25          
26            // Return memoized result if already computed
27            if (dp[row][col][movesLeft] != -1) {
28                return dp[row][col][movesLeft];
29            }
30          
31            // Try all four directions
32            int pathCount = 0;
33            for (int dir = 0; dir < 4; ++dir) {
34                int nextRow = row + directions[dir];
35                int nextCol = col + directions[dir + 1];
36              
37                // Recursively calculate paths from the next position
38                pathCount = (pathCount + dfs(nextRow, nextCol, movesLeft - 1)) % MOD;
39            }
40          
41            // Memoize and return the result
42            return dp[row][col][movesLeft] = pathCount;
43        };
44      
45        // Start DFS from the initial position with maxMove moves
46        return dfs(startRow, startColumn, maxMove);
47    }
48};
49
1/**
2 * Finds the number of paths to move the ball out of grid boundary.
3 * The ball can move at most maxMove times in 4 directions (up, down, left, right).
4 * 
5 * @param m - Number of rows in the grid
6 * @param n - Number of columns in the grid
7 * @param maxMove - Maximum number of moves allowed
8 * @param startRow - Starting row position of the ball
9 * @param startColumn - Starting column position of the ball
10 * @returns Number of paths to move the ball out of boundary, modulo 10^9 + 7
11 */
12function findPaths(
13    m: number,
14    n: number,
15    maxMove: number,
16    startRow: number,
17    startColumn: number,
18): number {
19    // Initialize 3D memoization array: memo[row][col][remainingMoves]
20    // -1 indicates unvisited state
21    const memo: number[][][] = Array.from({ length: m }, () =>
22        Array.from({ length: n }, () => 
23            Array(maxMove + 1).fill(-1)
24        )
25    );
26  
27    // Modulo value to prevent integer overflow
28    const MOD = 1000000007;
29  
30    // Direction vectors for moving: up, right, down, left
31    // Used as pairs: (dirs[i], dirs[i+1]) represents (deltaRow, deltaCol)
32    const directions = [-1, 0, 1, 0, -1];
33  
34    /**
35     * Depth-first search with memoization to count valid paths.
36     * 
37     * @param row - Current row position
38     * @param col - Current column position
39     * @param movesRemaining - Number of moves remaining
40     * @returns Number of valid paths from current position
41     */
42    const dfs = (row: number, col: number, movesRemaining: number): number => {
43        // Base case: if out of bounds, we found a valid path (if we have non-negative moves)
44        if (row < 0 || row >= m || col < 0 || col >= n) {
45            return movesRemaining >= 0 ? 1 : 0;
46        }
47      
48        // Base case: no moves remaining and still inside grid
49        if (movesRemaining <= 0) {
50            return 0;
51        }
52      
53        // Return memoized result if already computed
54        if (memo[row][col][movesRemaining] !== -1) {
55            return memo[row][col][movesRemaining];
56        }
57      
58        // Calculate total paths from current position
59        let totalPaths = 0;
60      
61        // Try all 4 directions
62        for (let dirIndex = 0; dirIndex < 4; dirIndex++) {
63            const nextRow = row + directions[dirIndex];
64            const nextCol = col + directions[dirIndex + 1];
65          
66            // Add paths from next position with one less move
67            totalPaths = (totalPaths + dfs(nextRow, nextCol, movesRemaining - 1)) % MOD;
68        }
69      
70        // Memoize and return the result
71        memo[row][col][movesRemaining] = totalPaths;
72        return totalPaths;
73    };
74  
75    // Start DFS from the initial position with all moves available
76    return dfs(startRow, startColumn, maxMove);
77}
78

Time and Space Complexity

The time complexity is O(m × n × maxMove). The DFS function with memoization (using @cache) explores each unique state (i, j, k) at most once, where i ranges from 0 to m-1, j ranges from 0 to n-1, and k ranges from 0 to maxMove. For each state, we explore 4 directions (up, down, left, right), which takes constant time O(1). Therefore, the total time complexity is O(m × n × maxMove × 4) = O(m × n × maxMove).

The space complexity is O(m × n × maxMove). This comes from two sources:

  1. The memoization cache stores the result for each unique state (i, j, k), which requires O(m × n × maxMove) space.
  2. The recursion stack depth in the worst case is O(maxMove), as we decrease k by 1 in each recursive call until it reaches 0.

Since O(m × n × maxMove) > O(maxMove), the overall space complexity is O(m × n × maxMove).

Here, m and n are the dimensions of the grid, and maxMove represents the maximum number of moves allowed (denoted as k in the reference answer, where k = maxMove ≤ 50).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Boundary Check Logic

A common mistake is checking if we've reached the boundary after consuming a move, rather than before. This leads to incorrect counting of valid paths.

Incorrect approach:

def dfs(row, col, moves_left):
    # Wrong: checking moves_left < 0 instead of >= 0
    if not (0 <= row < m) or not (0 <= col < n):
        return 1 if moves_left > 0 else 0  # Bug: should be >= 0

Why it's wrong: When the ball moves out of bounds, it should count as a valid path even when moves_left = 0 (using the last move to exit). The condition should be moves_left >= 0.

2. Forgetting to Apply Modulo Operation

Without applying modulo at each accumulation step, intermediate results can cause integer overflow in languages with fixed integer sizes, or lead to performance issues with very large numbers.

Incorrect approach:

path_count = 0
for dx, dy in pairwise(DIRECTIONS):
    next_row = row + dx
    next_col = col + dy
    path_count += dfs(next_row, next_col, moves_left - 1)
return path_count % MOD  # Wrong: only applying modulo at the end

Correct approach: Apply modulo during each addition to keep numbers manageable:

path_count = (path_count + dfs(next_row, next_col, moves_left - 1)) % MOD

3. Cache Key Collision with Negative Indices

When the ball moves outside the grid, coordinates can become negative. If not handled properly, this could cause issues with memoization.

Potential issue: Some memoization implementations might have problems with negative indices, or you might accidentally treat negative positions incorrectly.

Solution: The current implementation handles this correctly by checking bounds first and returning immediately for out-of-bounds positions. The @lru_cache decorator handles negative values properly as part of the tuple key (row, col, moves_left).

4. Off-by-One Error in Move Counting

A subtle mistake is miscounting when to stop recursion, especially confusing "moves remaining" vs "moves used".

Incorrect approach:

def dfs(row, col, moves_left):
    if not (0 <= row < m) or not (0 <= col < n):
        return 1
    if moves_left == 0:  # Wrong: should be <= 0
        return 0

Why it's wrong: This would allow one extra move when moves_left = 0, letting the ball continue moving when it shouldn't.

5. Direction Vector Implementation Error

Using the compact direction array (-1, 0, 1, 0, -1) with pairwise is elegant but can be error-prone.

Common mistake:

DIRECTIONS = [-1, 0, 1, 0]  # Missing the last -1
# pairwise would only generate 3 pairs instead of 4

Alternative clear approach if confused:

DIRECTIONS = [(0, 1), (0, -1), (1, 0), (-1, 0)]
for dx, dy in DIRECTIONS:
    next_row = row + dx
    next_col = col + dy
    # ...

This is more explicit and less prone to errors, though slightly less elegant than the pairwise approach.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

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

Load More