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) andn
(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 (return1
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 return0
- 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.
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:
- Where we currently are (position
(i, j)
) - 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:
- Outside the grid: If
not 0 <= i < m or not 0 <= j < n
, we're outside the grid boundaries. Return1
ifk >= 0
(we successfully escaped), otherwise return0
. - No moves left: If
k <= 0
while still inside the grid, we can't escape anymore, so return0
.
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)
wheredirs = (-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 modulo10^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 EvaluatorExample 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
- Up:
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!)
- Up:
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
- Up:
- 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:
- Up (1 move) - exits immediately
- Right → Up (2 moves)
- Right → Right (2 moves)
- Down → Up → Up (3 moves)
- 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:
- The memoization cache stores the result for each unique state
(i, j, k)
, which requiresO(m × n × maxMove)
space. - The recursion stack depth in the worst case is
O(maxMove)
, as we decreasek
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.
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!