Unlike the standard falling path problem where we can move to adjacent columns, here we must pick a different column in each row. For each cell in a row, we recursively try all columns in the next row except the current one. This constraint increases complexity since we need to consider more transitions. The brute force approach explores all valid paths.
helper(r, c) that returns the minimum path sum from cell (r, c) to the last row.r == n-1, return grid[r][c] (we've reached the last row).next_col in the next row where next_col != c:grid[r][c] plus the minimum path sum found.0 and return the minimum result.class Solution:
def minFallingPathSum(self, grid: List[List[int]]) -> int:
N = len(grid)
def helper(r, c):
if r == N - 1:
return grid[r][c]
res = float("inf")
for next_col in range(N):
if c != next_col:
res = min(res, grid[r][c] + helper(r + 1, next_col))
return res
res = float("inf")
for c in range(N):
res = min(res, helper(0, c))
return resThe recursive solution recomputes the same (r, c) states multiple times. By storing computed results in a memoization table, we ensure each state is solved only once. This reduces time complexity from exponential to O(n^3), since for each of the n^2 cells, we may consider up to n transitions.
helper(r, c) that returns the minimum path sum from (r, c) to the last row.(r, c) is already computed, return the cached value.r == n-1, return grid[r][c].next_col != c, recursively compute and track the minimum.grid[r][c] plus the minimum found.class Solution:
def minFallingPathSum(self, grid: List[List[int]]) -> int:
N = len(grid)
cache = {}
def helper(r, c):
if r == N - 1:
return grid[r][c]
if (r, c) in cache:
return cache[(r, c)]
res = float("inf")
for next_col in range(N):
if c != next_col:
res = min(res, grid[r][c] + helper(r + 1, next_col))
cache[(r, c)] = res
return res
res = float("inf")
for c in range(N):
res = min(res, helper(0, c))
return resWe can build the solution iteratively from the last row upward. For each cell, we compute the minimum path sum by considering all columns in the next row except the current one. This bottom-up approach fills a 2D DP table where dp[r][c] represents the minimum path sum from cell (r, c) to any cell in the last row.
dp[n-1][c] = grid[n-1][c] for all columns.r from n-2 down to 0:c:next_col != c:dp[r][c] = min(dp[r][c], grid[r][c] + dp[r+1][next_col]).dp[0].class Solution:
def minFallingPathSum(self, grid: List[List[int]]) -> int:
N = len(grid)
dp = [[float("inf")] * N for _ in range(N)]
for c in range(N):
dp[N - 1][c] = grid[N - 1][c]
for r in range(N - 2, -1, -1):
for c in range(N):
for next_col in range(N):
if c != next_col:
dp[r][c] = min(dp[r][c], grid[r][c] + dp[r + 1][next_col])
return min(dp[0])Since each row only depends on the next row's values, we can reduce space from O(n^2) to O(n) by using a single 1D array. We process rows from top to bottom, updating the array for each row while referencing the values from the previous iteration.
r:curr_c:prev_c != curr_c, track the minimum from the previous row.next_dp[curr_c] = grid[r][curr_c] + min_prev.class Solution:
def minFallingPathSum(self, grid: List[List[int]]) -> int:
N = len(grid)
dp = grid[0]
for r in range(1, N):
next_dp = [float("inf")] * N
for curr_c in range(N):
for prev_c in range(N):
if prev_c != curr_c:
next_dp[curr_c] = min(
next_dp[curr_c],
grid[r][curr_c] + dp[prev_c]
)
dp = next_dp
return min(dp)The O(n^3) complexity comes from checking all n columns for each of n^2 cells. We can optimize by observing that when transitioning to a cell, we usually want the minimum from the previous row, unless that minimum is in the same column. So we only need to track the two smallest values from each row: if the current column matches the smallest, use the second smallest; otherwise, use the smallest.
class Solution:
def minFallingPathSum(self, grid: List[List[int]]) -> int:
def get_min_two(row):
two_smallest = []
for val, idx in row:
if len(two_smallest) < 2:
two_smallest.append((val, idx))
elif two_smallest[1][0] > val:
two_smallest.pop()
two_smallest.append((val, idx))
two_smallest.sort()
return two_smallest
N = len(grid)
first_row = [(val, idx) for idx, val in enumerate(grid[0])]
dp = get_min_two(first_row)
for r in range(1, N):
next_dp = []
for curr_c in range(N):
curr_val = grid[r][curr_c]
min_val = float("inf")
for prev_val, prev_c in dp:
if curr_c != prev_c:
min_val = min(min_val, curr_val + prev_val)
next_dp.append((min_val, curr_c))
dp = get_min_two(next_dp)
return min(val for val, idx in dp)We can further optimize by not storing an entire array of pairs. Instead, we only track four values: the index and value of the smallest element, and the index and value of the second smallest element from the previous row. For each cell in the current row, we add the appropriate previous minimum based on column matching. Then we update our tracked values for the next iteration. This achieves O(n^2) time with O(1) extra space.
dpIdx1, dpIdx2 (indices of first and second smallest) and dpVal1, dpVal2 (their values) to represent the previous row's state.j:j != dpIdx1, add dpVal1; otherwise add dpVal2.dpVal1 (the overall minimum).class Solution:
def minFallingPathSum(self, grid: List[List[int]]) -> int:
n = len(grid)
if n == 1:
return grid[0][0]
dp_idx1 = dp_idx2 = -1
dp_val1 = dp_val2 = 0
for i in range(n):
nextDp_idx1 = nextDp_idx2 = -1
nextDp_val1 = nextDp_val2 = float("inf")
for j in range(n):
cur = dp_val1 if j != dp_idx1 else dp_val2
cur += grid[i][j]
if nextDp_idx1 == -1 or cur < nextDp_val1:
nextDp_idx2, nextDp_val2 = nextDp_idx1, nextDp_val1
nextDp_idx1, nextDp_val1 = j, cur
elif nextDp_idx2 == -1 or cur < nextDp_val2:
nextDp_idx2, nextDp_val2 = j, cur
dp_idx1, dp_idx2, dp_val1, dp_val2 = nextDp_idx1, nextDp_idx2, nextDp_val1, nextDp_val2
return dp_val1Unlike the standard falling path problem where you move to adjacent columns, this variant requires choosing a different column in each row. Using the adjacent-column logic from the standard problem will produce incorrect results.
A naive approach checks all n columns from the previous row for each of n^2 cells, resulting in O(n^3) time. The key optimization is tracking only the two smallest values from each row, since you only need the second smallest when the current column matches the smallest.
When n = 1, there is only one column, making it impossible to pick a different column in each row after the first. The answer is simply grid[0][0] since a 1x1 grid has only one cell. Failing to handle this edge case causes index errors or infinite loops.
When optimizing to O(n^2), you must correctly track both the minimum and second minimum values along with their column indices. Common mistakes include not updating both values properly, using the wrong index for comparison, or forgetting to handle ties when multiple columns have the same minimum value.
Grid values can be negative, so initializing with INT_MIN as a sentinel can cause overflow when added to grid values. Use a distinct sentinel value or nullable type to distinguish uncomputed states from valid negative sums.