Before attempting this problem, you should be comfortable with:
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.
dfs(r, c, moves) that returns the number of paths to exit from cell (r, c) with the given number of moves remaining.(r, c) is outside the grid, return 1 (we found a valid exit path).moves is 0, return 0 (no moves left, cannot exit).dfs for all four neighbors with moves - 1.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)Where is the number of rows, is the number of columns, and is the maximum number of allowed moves.
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.
(row, col, moves).dfs(r, c, moves):1.moves is 0, return 0.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)Where is the number of rows, is the number of columns, and is the maximum number of allowed moves.
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.
dp[r][c][moves] representing paths from (r, c) with the given moves.moves from 1 to maxMove:(r, c), check all four neighbors.1 to dp[r][c][moves].dp[neighbor][moves - 1].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]Where is the number of rows, is the number of columns, and is the maximum number of allowed moves.
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).
dp for the previous move count and tmp for the current.1 to maxMove:tmp to zeros.1 for each out-of-bound neighbor, or add dp[neighbor] for valid neighbors.dp and tmp.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]Where is the number of rows, is the number of columns, and is the maximum number of allowed moves.
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.
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.
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.