931. Minimum Falling Path Sum - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Dynamic Programming Fundamentals - Understanding how to break problems into overlapping subproblems and use memoization or tabulation
- 2D Grid/Matrix Traversal - Ability to navigate and process elements in a 2D array with boundary checks
- Recursion with Memoization - Converting recursive solutions to top-down DP by caching computed results
1. Recursion
Intuition
A falling path starts at any cell in the first row and moves to an adjacent cell in the next row (directly below, diagonally left, or diagonally right). We want to find the path with the minimum sum. For each starting column in the first row, we recursively explore all valid paths and track the minimum total. This brute force approach considers all possible paths, leading to exponential time complexity.
Algorithm
- Define
dfs(r, c)that returns the minimum path sum starting from cell(r, c)to the bottom row. - Base cases:
- If
r == n, we've gone past the last row, return0. - If
cis out of bounds, returnInfinity(invalid path).
- If
- Return
matrix[r][c]plus the minimum ofdfs(r+1, c-1),dfs(r+1, c), anddfs(r+1, c+1). - Try starting from each column in row
0and return the minimum result.
class Solution:
def minFallingPathSum(self, matrix: List[List[int]]) -> int:
N = len(matrix)
def dfs(r, c):
if r == N:
return 0
if c < 0 or c >= N:
return float("inf")
return matrix[r][c] + min(
dfs(r + 1, c - 1),
dfs(r + 1, c),
dfs(r + 1, c + 1)
)
res = float("inf")
for c in range(N):
res = min(res, dfs(0, c))
return resTime & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution has overlapping subproblems: the same (r, c) state is computed multiple times. By caching results in a memoization table, we avoid redundant calculations. Each unique (r, c) pair is computed only once, reducing time complexity from exponential to polynomial.
Algorithm
- Create a
cache(dictionary or 2D array) to store computed results. - Define
dfs(r, c)that returns the minimum path sum from(r, c)to the bottom. - Before computing, check if
(r, c)is already in thecache; if so, return the cached value. - Compute the result as
matrix[r][c]plus the minimum of the three possible moves. - Store the result in the
cacheand return it. - Return the minimum among all starting columns in row
0.
class Solution:
def minFallingPathSum(self, matrix: List[List[int]]) -> int:
N = len(matrix)
cache = {}
def dfs(r, c):
if r == N:
return 0
if c < 0 or c >= N:
return float("inf")
if (r, c) in cache:
return cache[(r, c)]
cache[(r, c)] = matrix[r][c] + min(
dfs(r + 1, c - 1),
dfs(r + 1, c),
dfs(r + 1, c + 1)
)
return cache[(r, c)]
res = float("inf")
for c in range(N):
res = min(res, dfs(0, c))
return resTime & Space Complexity
- Time complexity:
- Space complexity:
3. Dynamic Programming (Bottom-Up)
Intuition
We can solve this iteratively by building up solutions row by row. For each cell, the minimum path sum to reach it equals its value plus the minimum of the three cells above it that could lead here. By processing rows from top to bottom and only keeping the previous row's values, we achieve O(n) space complexity.
Algorithm
- Initialize a 1D
dparray with the first row of the matrix. - For each subsequent row
r:- Track
leftUp(the previous row's value to the left) to avoid overwriting issues. - For each column
c:midUp = dp[c](directly above).rightUp = dp[c+1]if within bounds, else infinity.- Update
dp[c] = matrix[r][c] + min(leftUp, midUp, rightUp). - Set
leftUp = midUpfor the next iteration.
- Track
- Return the minimum value in the final
dparray.
class Solution:
def minFallingPathSum(self, matrix: List[List[int]]) -> int:
N = len(matrix)
dp = matrix[0][:]
for r in range(1, N):
leftUp = float('inf')
for c in range(N):
midUp = dp[c]
rightUp = dp[c + 1] if c < N - 1 else float('inf')
dp[c] = matrix[r][c] + min(midUp, leftUp, rightUp)
leftUp = midUp
return min(dp)Time & Space Complexity
- Time complexity:
- Space complexity:
4. Dynamic Programming (In-Place)
Intuition
If we are allowed to modify the input matrix, we can avoid using any extra space for DP. We update each cell in place to store the minimum path sum to reach that cell. This is the most space-efficient approach, using only O(1) extra space beyond the input.
Algorithm
- Start from the second row (index
1). - For each cell
(r, c):- Compute the minimum of the three cells above:
(r-1, c-1),(r-1, c),(r-1, c+1)(treating out-of-bounds as infinity). - Add this minimum to
matrix[r][c].
- Compute the minimum of the three cells above:
- After processing all rows, return the minimum value in the last row.
class Solution:
def minFallingPathSum(self, matrix: List[List[int]]) -> int:
N = len(matrix)
for r in range(1, N):
for c in range(N):
mid = matrix[r - 1][c]
left = matrix[r - 1][c - 1] if c > 0 else float("inf")
right = matrix[r - 1][c + 1] if c < N - 1 else float("inf")
matrix[r][c] = matrix[r][c] + min(mid, left, right)
return min(matrix[-1])Time & Space Complexity
- Time complexity:
- Space complexity: extra space.
Common Pitfalls
Out-of-Bounds Column Access
When computing the minimum from the three cells above (left-diagonal, directly above, right-diagonal), forgetting to check column boundaries causes index errors. The leftmost column has no left-diagonal parent, and the rightmost column has no right-diagonal parent. Always handle these edge cases by treating out-of-bounds values as infinity.
Modifying Input While Reading
In the in-place DP approach, you update matrix[r][c] using values from the previous row. Since you read from the same row you just wrote to, there is no conflict. However, if you mistakenly read from cells you have already updated in the current row, you will get incorrect results. Process each row independently from the previous row.
Forgetting to Check All Starting Columns
The path can start from any column in the first row. A common mistake is starting only from column 0 or assuming the minimum value in the first row is the optimal start. You must either try all starting columns explicitly or let the DP naturally propagate all possibilities.