576. Out of Boundary Paths - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Recursion - The base solutions use recursive depth-first exploration of grid cells
- Dynamic Programming (Memoization) - Caching overlapping subproblems with state (row, col, moves) is essential for efficiency
- 2D Grid/Matrix Traversal - Moving in four directions and handling boundary conditions
- Modular Arithmetic - Results must be computed modulo 10^9 + 7 to prevent overflow
1. Recursion
Intuition
We want to count all paths that start from a given cell and eventually move out of the grid, using at most maxMove moves. At each cell, we can move in four directions, and each move decrements our remaining moves.
The base cases are: if we step outside the grid, we found one valid path; if we run out of moves while still inside, this path does not count. We recursively explore all four directions and sum up the results.
Algorithm
- Define a recursive function
dfs(r, c, moves)that returns the number of paths to exit from cell(r, c)with the given number of moves remaining. - Base case 1: If
(r, c)is outside the grid, return1(we found a valid exit path). - Base case 2: If
movesis0, return0(no moves left, cannot exit). - Recursively call
dfsfor all four neighbors withmoves - 1. - Return the sum of all four directions, modulo 10^9 + 7.
class Solution:
def findPaths(self, m: int, n: int, maxMove: int, startRow: int, startColumn: int) -> int:
ROWS, COLS = m, n
MOD = 10**9 + 7
def dfs(r, c, moves):
if r < 0 or r >= ROWS or c < 0 or c >= COLS:
return 1
if moves == 0:
return 0
return (
dfs(r + 1, c, moves - 1) +
dfs(r - 1, c, moves - 1) +
dfs(r, c + 1, moves - 1) +
dfs(r, c - 1, moves - 1)
) % MOD
return dfs(startRow, startColumn, maxMove)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows, is the number of columns, and is the maximum number of allowed moves.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution has many overlapping subproblems since the same (row, col, moves) state can be reached through different paths. We can use memoization to cache results and avoid redundant computation.
Each state is defined by three parameters: current position (r, c) and remaining moves. Since there are m _ n _ maxMove possible states, memoization reduces the time complexity dramatically.
Algorithm
- Create a 3D cache indexed by
(row, col, moves). - Define
dfs(r, c, moves):- If out of bounds, return
1. - If
movesis0, return0. - If already computed, return the cached value.
- Otherwise, compute the sum of all four directions and cache it.
- If out of bounds, return
- Return
dfs(startRow, startColumn, maxMove).
class Solution:
def findPaths(self, m: int, n: int, maxMove: int, startRow: int, startColumn: int) -> int:
ROWS, COLS = m, n
MOD = 10**9 + 7
cache = {}
def dfs(r, c, moves):
if r < 0 or r >= ROWS or c < 0 or c >= COLS:
return 1
if moves == 0:
return 0
if (r, c, moves) in cache:
return cache[(r, c, moves)]
cache[(r, c, moves)] = (
dfs(r + 1, c, moves - 1) +
dfs(r - 1, c, moves - 1) +
dfs(r, c + 1, moves - 1) +
dfs(r, c - 1, moves - 1)
) % MOD
return cache[(r, c, moves)]
return dfs(startRow, startColumn, maxMove)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows, is the number of columns, and is the maximum number of allowed moves.
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of recursion, we can fill the DP table iteratively. We build up from 1 move to maxMove, computing how many ways each cell can reach the boundary with exactly that many moves remaining.
For each cell, we look at its four neighbors. If a neighbor is out of bounds, that contributes 1 path. If the neighbor is valid, we add its value from the previous move count.
Algorithm
- Create a 3D DP array
dp[r][c][moves]representing paths from(r, c)with the given moves. - Iterate
movesfrom1tomaxMove:- For each cell
(r, c), check all four neighbors. - If a neighbor is out of bounds, add
1todp[r][c][moves]. - Otherwise, add
dp[neighbor][moves - 1].
- For each cell
- Return
dp[startRow][startColumn][maxMove].
class Solution:
def findPaths(self, m: int, n: int, maxMove: int, startRow: int, startColumn: int) -> int:
MOD = 10**9 + 7
dp = [[[0] * (maxMove + 1) for _ in range(n)] for _ in range(m)]
for moves in range(1, maxMove + 1):
for r in range(m):
for c in range(n):
dp[r][c][moves] = (
(dp[r - 1][c][moves - 1] if r > 0 else 1) +
(dp[r + 1][c][moves - 1] if r < m - 1 else 1) +
(dp[r][c - 1][moves - 1] if c > 0 else 1) +
(dp[r][c + 1][moves - 1] if c < n - 1 else 1)
) % MOD
return dp[startRow][startColumn][maxMove]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows, is the number of columns, and is the maximum number of allowed moves.
4. Dynamic Programming (Space Optimized)
Intuition
Since each move layer only depends on the previous move layer, we do not need the full 3D array. We can use two 2D arrays: one for the current move count and one for the previous. After processing each move, we swap them.
This reduces space from O(m _ n _ maxMove) to O(m * n).
Algorithm
- Create two 2D arrays:
dpfor the previous move count andtmpfor the current. - For each move from
1tomaxMove:- Reset
tmpto zeros. - For each cell, add
1for each out-of-bound neighbor, or adddp[neighbor]for valid neighbors. - Swap
dpandtmp.
- Reset
- Return
dp[startRow][startColumn].
class Solution:
def findPaths(self, m: int, n: int, maxMove: int, startRow: int, startColumn: int) -> int:
MOD = 10**9 + 7
dp = [[0] * n for _ in range(m)]
for moves in range(1, maxMove + 1):
tmp = [[0] * n for _ in range(m)]
for r in range(m):
for c in range(n):
if r + 1 == m:
tmp[r][c] = (tmp[r][c] + 1) % MOD
else:
tmp[r][c] = (tmp[r][c] + dp[r + 1][c]) % MOD
if r - 1 < 0:
tmp[r][c] = (tmp[r][c] + 1) % MOD
else:
tmp[r][c] = (tmp[r][c] + dp[r - 1][c]) % MOD
if c + 1 == n:
tmp[r][c] = (tmp[r][c] + 1) % MOD
else:
tmp[r][c] = (tmp[r][c] + dp[r][c + 1]) % MOD
if c - 1 < 0:
tmp[r][c] = (tmp[r][c] + 1) % MOD
else:
tmp[r][c] = (tmp[r][c] + dp[r][c - 1]) % MOD
dp = tmp
return dp[startRow][startColumn]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows, is the number of columns, and is the maximum number of allowed moves.
Common Pitfalls
Forgetting to Apply Modulo at Each Step
The number of paths can grow extremely large, causing integer overflow. A common mistake is only applying the modulo operation at the end. You must apply % (10^9 + 7) after each addition to prevent overflow, especially when summing results from multiple recursive calls or DP transitions.
Incorrect Base Case for Out-of-Bounds
Some implementations check if moves are zero before checking if the position is out of bounds. The correct order is to first check if we are outside the grid (return 1 for a valid exit path), then check if moves are exhausted (return 0). Reversing this order means we would never count paths that step outside on their last move.
Using Wrong Memoization Key
When using memoization, the cache key must include all three state variables: row, column, and remaining moves. Forgetting to include the move count in the key leads to incorrect caching where different move states overwrite each other, producing wrong answers.