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
Recursion with Memoization - Converting recursive solutions to top-down DP by caching computed results
Space Optimization in DP - Reducing 2D DP to 1D by only keeping track of the previous row's state
1. Recursion
Intuition
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.
Algorithm
Define helper(r, c) that returns the minimum path sum from cell (r, c) to the last row.
Base case: if r == n-1, return grid[r][c] (we've reached the last row).
For each column next_col in the next row where next_col != c:
Recursively compute the path sum and track the minimum.
Return grid[r][c] plus the minimum path sum found.
Try starting from each column in row 0 and return the minimum result.
classSolution:defminFallingPathSum(self, grid: List[List[int]])->int:
N =len(grid)defhelper(r, c):if r == N -1:return grid[r][c]
res =float("inf")for next_col inrange(N):if c != next_col:
res =min(res, grid[r][c]+ helper(r +1, next_col))return res
res =float("inf")for c inrange(N):
res =min(res, helper(0, c))return res
publicclassSolution{privateinthelper(int[][] grid,int r,int c){intN= grid.length;if(r ==N-1){return grid[r][c];}int res =Integer.MAX_VALUE;for(int nextCol =0; nextCol <N; nextCol++){if(c != nextCol){
res =Math.min(res, grid[r][c]+helper(grid, r +1, nextCol));}}return res;}publicintminFallingPathSum(int[][] grid){intN= grid.length;int res =Integer.MAX_VALUE;for(int c =0; c <N; c++){
res =Math.min(res,helper(grid,0, c));}return res;}}
classSolution{inthelper(vector<vector<int>>& grid,int r,int c){int N = grid.size();if(r == N -1){return grid[r][c];}int res = INT_MAX;for(int nextCol =0; nextCol < N; nextCol++){if(c != nextCol){
res =min(res, grid[r][c]+helper(grid, r +1, nextCol));}}return res;}public:intminFallingPathSum(vector<vector<int>>& grid){int N = grid.size();int res = INT_MAX;for(int c =0; c < N; c++){
res =min(res,helper(grid,0, c));}return res;}};
classSolution{/**
* @param {number[][]} grid
* @return {number}
*/minFallingPathSum(grid){constN= grid.length;consthelper=(r, c)=>{if(r ===N-1)return grid[r][c];let res =Infinity;for(let nextCol =0; nextCol <N; nextCol++){if(c !== nextCol){
res = Math.min(res, grid[r][c]+helper(r +1, nextCol));}}return res;};let res =Infinity;for(let c =0; c <N; c++){
res = Math.min(res,helper(0, c));}return res;}}
publicclassSolution{publicintMinFallingPathSum(int[][] grid){int N = grid.Length;int res =int.MaxValue;for(int c =0; c < N; c++){
res = Math.Min(res,Helper(grid,0, c, N));}return res;}privateintHelper(int[][] grid,int r,int c,int N){if(r == N -1){return grid[r][c];}int res =int.MaxValue;for(int nextCol =0; nextCol < N; nextCol++){if(c != nextCol){
res = Math.Min(res, grid[r][c]+Helper(grid, r +1, nextCol, N));}}return res;}}
funcminFallingPathSum(grid [][]int)int{
n :=len(grid)var helper func(r, c int)int
helper =func(r, c int)int{if r == n-1{return grid[r][c]}
res := math.MaxInt32
for nextCol :=0; nextCol < n; nextCol++{if c != nextCol {
res =min(res, grid[r][c]+helper(r+1, nextCol))}}return res
}
res := math.MaxInt32
for c :=0; c < n; c++{
res =min(res,helper(0, c))}return res
}
class Solution {funminFallingPathSum(grid: Array<IntArray>): Int {val n = grid.size
funhelper(r: Int, c: Int): Int {if(r == n -1)return grid[r][c]var res = Int.MAX_VALUE
for(nextCol in0 until n){if(c != nextCol){
res =minOf(res, grid[r][c]+helper(r +1, nextCol))}}return res
}var res = Int.MAX_VALUE
for(c in0 until n){
res =minOf(res,helper(0, c))}return res
}}
classSolution{funcminFallingPathSum(_ grid:[[Int]])->Int{let n = grid.count
funchelper(_ r:Int,_ c:Int)->Int{if r == n -1{return grid[r][c]}var res =Int.max
for nextCol in0..<n {if c != nextCol {
res =min(res, grid[r][c]+helper(r +1, nextCol))}}return res
}var res =Int.max
for c in0..<n {
res =min(res,helper(0, c))}return res
}}
Time & Space Complexity
Time complexity: O(nn)
Space complexity: O(n) for recursion stack.
2. Dynamic Programming (Top-Down)
Intuition
The 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.
Algorithm
Create a memo table (dictionary or 2D array) to cache results.
Define helper(r, c) that returns the minimum path sum from (r, c) to the last row.
If (r, c) is already computed, return the cached value.
Base case: if r == n-1, return grid[r][c].
For each next_col != c, recursively compute and track the minimum.
Cache and return grid[r][c] plus the minimum found.
classSolution:defminFallingPathSum(self, grid: List[List[int]])->int:
N =len(grid)
cache ={}defhelper(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 inrange(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 inrange(N):
res =min(res, helper(0, c))return res
publicclassSolution{privateint[][] memo;privateinthelper(int[][] grid,int r,int c){intN= grid.length;if(r ==N-1){return grid[r][c];}if(memo[r][c]!=Integer.MIN_VALUE){return memo[r][c];}int res =Integer.MAX_VALUE;for(int nextCol =0; nextCol <N; nextCol++){if(c != nextCol){
res =Math.min(res, grid[r][c]+helper(grid, r +1, nextCol));}}
memo[r][c]= res;return res;}publicintminFallingPathSum(int[][] grid){intN= grid.length;
memo =newint[N][N];for(int[] row : memo){Arrays.fill(row,Integer.MIN_VALUE);}int res =Integer.MAX_VALUE;for(int c =0; c <N; c++){
res =Math.min(res,helper(grid,0, c));}return res;}}
classSolution{
vector<vector<int>> memo;inthelper(vector<vector<int>>& grid,int r,int c){int N = grid.size();if(r == N -1){return grid[r][c];}if(memo[r][c]!= INT_MIN){return memo[r][c];}int res = INT_MAX;for(int nextCol =0; nextCol < N; nextCol++){if(c != nextCol){
res =min(res, grid[r][c]+helper(grid, r +1, nextCol));}}
memo[r][c]= res;return res;}public:intminFallingPathSum(vector<vector<int>>& grid){int N = grid.size();
memo.assign(N,vector<int>(N, INT_MIN));int res = INT_MAX;for(int c =0; c < N; c++){
res =min(res,helper(grid,0, c));}return res;}};
classSolution{/**
* @param {number[][]} grid
* @return {number}
*/minFallingPathSum(grid){constN= grid.length;const memo = Array.from({length:N},()=>Array(N).fill(-Infinity));consthelper=(r, c)=>{if(r ===N-1)return grid[r][c];if(memo[r][c]!==-Infinity)return memo[r][c];let res =Infinity;for(let nextCol =0; nextCol <N; nextCol++){if(c !== nextCol){
res = Math.min(res, grid[r][c]+helper(r +1, nextCol));}}
memo[r][c]= res;return res;};let res =Infinity;for(let c =0; c <N; c++){
res = Math.min(res,helper(0, c));}return res;}}
publicclassSolution{privateint[,] memo;publicintMinFallingPathSum(int[][] grid){int N = grid.Length;
memo =newint[N, N];for(int i =0; i < N; i++){for(int j =0; j < N; j++){
memo[i, j]=int.MinValue;}}int res =int.MaxValue;for(int c =0; c < N; c++){
res = Math.Min(res,Helper(grid,0, c, N));}return res;}privateintHelper(int[][] grid,int r,int c,int N){if(r == N -1)return grid[r][c];if(memo[r, c]!=int.MinValue)return memo[r, c];int res =int.MaxValue;for(int nextCol =0; nextCol < N; nextCol++){if(c != nextCol){
res = Math.Min(res, grid[r][c]+Helper(grid, r +1, nextCol, N));}}
memo[r, c]= res;return res;}}
funcminFallingPathSum(grid [][]int)int{
n :=len(grid)
memo :=make([][]int, n)for i :=range memo {
memo[i]=make([]int, n)for j :=range memo[i]{
memo[i][j]= math.MinInt32
}}var helper func(r, c int)int
helper =func(r, c int)int{if r == n-1{return grid[r][c]}if memo[r][c]!= math.MinInt32 {return memo[r][c]}
res := math.MaxInt32
for nextCol :=0; nextCol < n; nextCol++{if c != nextCol {
res =min(res, grid[r][c]+helper(r+1, nextCol))}}
memo[r][c]= res
return res
}
res := math.MaxInt32
for c :=0; c < n; c++{
res =min(res,helper(0, c))}return res
}
class Solution {funminFallingPathSum(grid: Array<IntArray>): Int {val n = grid.size
val memo =Array(n){IntArray(n){ Int.MIN_VALUE }}funhelper(r: Int, c: Int): Int {if(r == n -1)return grid[r][c]if(memo[r][c]!= Int.MIN_VALUE)return memo[r][c]var res = Int.MAX_VALUE
for(nextCol in0 until n){if(c != nextCol){
res =minOf(res, grid[r][c]+helper(r +1, nextCol))}}
memo[r][c]= res
return res
}var res = Int.MAX_VALUE
for(c in0 until n){
res =minOf(res,helper(0, c))}return res
}}
classSolution{funcminFallingPathSum(_ grid:[[Int]])->Int{let n = grid.count
var memo =Array(repeating:Array(repeating:Int.min, count: n), count: n)funchelper(_ r:Int,_ c:Int)->Int{if r == n -1{return grid[r][c]}if memo[r][c]!=Int.min {return memo[r][c]}var res =Int.max
for nextCol in0..<n {if c != nextCol {
res =min(res, grid[r][c]+helper(r +1, nextCol))}}
memo[r][c]= res
return res
}var res =Int.max
for c in0..<n {
res =min(res,helper(0, c))}return res
}}
Time & Space Complexity
Time complexity: O(n3)
Space complexity: O(n2)
3. Dynamic Programming (Bottom-Up)
Intuition
We 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.
Algorithm
Create a 2D DP table initialized to infinity.
Fill the last row: dp[n-1][c] = grid[n-1][c] for all columns.
classSolution:defminFallingPathSum(self, grid: List[List[int]])->int:
N =len(grid)
dp =[[float("inf")]* N for _ inrange(N)]for c inrange(N):
dp[N -1][c]= grid[N -1][c]for r inrange(N -2,-1,-1):for c inrange(N):for next_col inrange(N):if c != next_col:
dp[r][c]=min(dp[r][c], grid[r][c]+ dp[r +1][next_col])returnmin(dp[0])
publicclassSolution{publicintminFallingPathSum(int[][] grid){intN= grid.length;int[][] dp =newint[N][N];for(int c =0; c <N; c++){
dp[N-1][c]= grid[N-1][c];}for(int r =N-2; r >=0; r--){for(int c =0; c <N; c++){
dp[r][c]=Integer.MAX_VALUE;for(int nextCol =0; nextCol <N; nextCol++){if(c != nextCol){
dp[r][c]=Math.min(dp[r][c], grid[r][c]+ dp[r +1][nextCol]);}}}}int res =Integer.MAX_VALUE;for(int c =0; c <N; c++){
res =Math.min(res, dp[0][c]);}return res;}}
classSolution{public:intminFallingPathSum(vector<vector<int>>& grid){int N = grid.size();
vector<vector<int>>dp(N,vector<int>(N, INT_MAX));for(int c =0; c < N; c++){
dp[N -1][c]= grid[N -1][c];}for(int r = N -2; r >=0; r--){for(int c =0; c < N; c++){
dp[r][c]= INT_MAX;for(int nextCol =0; nextCol < N; nextCol++){if(c != nextCol){
dp[r][c]=min(dp[r][c], grid[r][c]+ dp[r +1][nextCol]);}}}}int res = INT_MAX;for(int c =0; c < N; c++){
res =min(res, dp[0][c]);}return res;}};
classSolution{/**
* @param {number[][]} grid
* @return {number}
*/minFallingPathSum(grid){constN= grid.length;const dp = Array.from({length:N},()=>Array(N).fill(Infinity));for(let c =0; c <N; c++){
dp[N-1][c]= grid[N-1][c];}for(let r =N-2; r >=0; r--){for(let c =0; c <N; c++){for(let nextCol =0; nextCol <N; nextCol++){if(c !== nextCol){
dp[r][c]= Math.min(
dp[r][c],
grid[r][c]+ dp[r +1][nextCol],);}}}}return Math.min(...dp[0]);}}
publicclassSolution{publicintMinFallingPathSum(int[][] grid){int N = grid.Length;int[,] dp =newint[N, N];for(int c =0; c < N; c++){
dp[N -1, c]= grid[N -1][c];}for(int r = N -2; r >=0; r--){for(int c =0; c < N; c++){
dp[r, c]=int.MaxValue;for(int nextCol =0; nextCol < N; nextCol++){if(c != nextCol){
dp[r, c]= Math.Min(dp[r, c], grid[r][c]+ dp[r +1, nextCol]);}}}}int res =int.MaxValue;for(int c =0; c < N; c++){
res = Math.Min(res, dp[0, c]);}return res;}}
funcminFallingPathSum(grid [][]int)int{
n :=len(grid)
dp :=make([][]int, n)for i :=range dp {
dp[i]=make([]int, n)for j :=range dp[i]{
dp[i][j]= math.MaxInt32
}}for c :=0; c < n; c++{
dp[n-1][c]= grid[n-1][c]}for r := n -2; r >=0; r--{for c :=0; c < n; c++{for nextCol :=0; nextCol < n; nextCol++{if c != nextCol {
dp[r][c]=min(dp[r][c], grid[r][c]+dp[r+1][nextCol])}}}}
res := math.MaxInt32
for c :=0; c < n; c++{
res =min(res, dp[0][c])}return res
}
class Solution {funminFallingPathSum(grid: Array<IntArray>): Int {val n = grid.size
val dp =Array(n){IntArray(n){ Int.MAX_VALUE }}for(c in0 until n){
dp[n -1][c]= grid[n -1][c]}for(r in n -2 downTo 0){for(c in0 until n){for(nextCol in0 until n){if(c != nextCol){
dp[r][c]=minOf(dp[r][c], grid[r][c]+ dp[r +1][nextCol])}}}}return dp[0].minOrNull()!!}}
classSolution{funcminFallingPathSum(_ grid:[[Int]])->Int{let n = grid.count
var dp =Array(repeating:Array(repeating:Int.max, count: n), count: n)for c in0..<n {
dp[n -1][c]= grid[n -1][c]}for r instride(from: n -2, through:0, by:-1){for c in0..<n {for nextCol in0..<n {if c != nextCol {
dp[r][c]=min(dp[r][c], grid[r][c]+ dp[r +1][nextCol])}}}}return dp[0].min()!}}
Time & Space Complexity
Time complexity: O(n3)
Space complexity: O(n2)
4. Dynamic Programming (Space Optimized)
Intuition
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.
Algorithm
Initialize DP array with the first row of the grid.
For each subsequent row r:
Create a new DP array for the current row.
For each column curr_c:
For each prev_c != curr_c, track the minimum from the previous row.
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.
Algorithm
Initialize DP by computing the two smallest (value, index) pairs from the first row.
For each subsequent row:
For each column, compute the path sum using the smallest previous value if columns differ, else the second smallest.
Collect all results and find the new two smallest pairs.
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.
Algorithm
Initialize dpIdx1, dpIdx2 (indices of first and second smallest) and dpVal1, dpVal2 (their values) to represent the previous row's state.
For each row:
Track new smallest and second smallest values and indices.
For each column j:
If j != dpIdx1, add dpVal1; otherwise add dpVal2.
Update the running smallest and second smallest for this row.
After processing all rows, return dpVal1 (the overall minimum).
funcminFallingPathSum(grid [][]int)int{
n :=len(grid)if n ==1{return grid[0][0]}
dpIdx1, dpIdx2 :=-1,-1
dpVal1, dpVal2 :=0,0for i :=0; i < n; i++{
nextDpIdx1, nextDpIdx2 :=-1,-1
nextDpVal1, nextDpVal2 := math.MaxInt32, math.MaxInt32
for j :=0; j < n; j++{
cur := dpVal1
if j == dpIdx1 {
cur = dpVal2
}
cur += grid[i][j]if nextDpIdx1 ==-1|| cur < nextDpVal1 {
nextDpIdx2, nextDpVal2 = nextDpIdx1, nextDpVal1
nextDpIdx1, nextDpVal1 = j, cur
}elseif nextDpIdx2 ==-1|| cur < nextDpVal2 {
nextDpIdx2, nextDpVal2 = j, cur
}}
dpIdx1, dpIdx2, dpVal1, dpVal2 = nextDpIdx1, nextDpIdx2, nextDpVal1, nextDpVal2
}return dpVal1
}
class Solution {funminFallingPathSum(grid: Array<IntArray>): Int {val n = grid.size
if(n ==1)return grid[0][0]var dpIdx1 =-1var dpIdx2 =-1var dpVal1 =0var dpVal2 =0for(i in0 until n){var nextDpIdx1 =-1var nextDpIdx2 =-1var nextDpVal1 = Int.MAX_VALUE
var nextDpVal2 = Int.MAX_VALUE
for(j in0 until n){var cur =if(j != dpIdx1) dpVal1 else dpVal2
cur += grid[i][j]if(nextDpIdx1 ==-1|| cur < nextDpVal1){
nextDpIdx2 = nextDpIdx1
nextDpVal2 = nextDpVal1
nextDpIdx1 = j
nextDpVal1 = cur
}elseif(nextDpIdx2 ==-1|| cur < nextDpVal2){
nextDpIdx2 = j
nextDpVal2 = cur
}}
dpIdx1 = nextDpIdx1
dpIdx2 = nextDpIdx2
dpVal1 = nextDpVal1
dpVal2 = nextDpVal2
}return dpVal1
}}
classSolution{funcminFallingPathSum(_ grid:[[Int]])->Int{let n = grid.count
if n ==1{return grid[0][0]}var dpIdx1 =-1, dpIdx2 =-1var dpVal1 =0, dpVal2 =0for i in0..<n {var nextDpIdx1 =-1, nextDpIdx2 =-1var nextDpVal1 =Int.max, nextDpVal2 =Int.max
for j in0..<n {var cur =(j != dpIdx1)? dpVal1 : dpVal2
cur += grid[i][j]if nextDpIdx1 ==-1|| cur < nextDpVal1 {
nextDpIdx2 = nextDpIdx1
nextDpVal2 = nextDpVal1
nextDpIdx1 = j
nextDpVal1 = cur
}elseif nextDpIdx2 ==-1|| cur < nextDpVal2 {
nextDpIdx2 = j
nextDpVal2 = cur
}}
dpIdx1 = nextDpIdx1
dpIdx2 = nextDpIdx2
dpVal1 = nextDpVal1
dpVal2 = nextDpVal2
}return dpVal1
}}
Time & Space Complexity
Time complexity: O(n2)
Space complexity: O(1) extra space.
Common Pitfalls
Confusing with Standard Falling Path Sum
Unlike 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.
O(n^3) Time Complexity Trap
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.
Edge Case with Single Column Grid
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.
Incorrect Second Minimum Tracking
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.
Integer Overflow with Large Negative Values
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.