1289. Minimum Falling Path Sum II
Problem Description
You are given an n x n
integer matrix called grid
. Your task is to find the minimum sum of a falling path with non-zero shifts.
A falling path with non-zero shifts means:
- You must select exactly one element from each row of the matrix
- No two elements selected from adjacent rows can be in the same column (this is the "non-zero shift" requirement)
In other words, if you pick an element from column j
in row i
, then in row i+1
, you must pick an element from any column except column j
.
For example, if you have a 3x3 matrix and you pick element at position [0][1]
(row 0, column 1), then in row 1 you can pick from columns 0 or 2, but not column 1.
The goal is to find such a path from the first row to the last row that gives you the minimum possible sum of all selected elements.
The solution uses dynamic programming where f[j]
represents the minimum sum to reach position j
in the current row. For each row, it calculates the minimum possible sum to reach each column by considering all valid previous columns (those that are different from the current column) and adds the current cell's value. The answer is the minimum value among all possible ending positions in the last row.
Intuition
When we look at this problem, we need to find an optimal path from top to bottom with a constraint - we can't pick elements from the same column in consecutive rows.
Let's think about this step by step. If we're at a certain position in row i
, how did we get there? We must have come from some position in row i-1
, but not from the same column. To reach any position optimally, we want to come from the position in the previous row that gives us the minimum cumulative sum.
This naturally leads us to dynamic programming. For each cell in the current row, we need to know: "What's the minimum sum to reach this cell from the top?"
To calculate this, we consider all possible previous positions (all columns except the current one in the previous row) and pick the one with the minimum cumulative sum. Then we add the current cell's value to get the minimum sum to reach the current position.
The key insight is that we don't need to track the entire path - we only need to know the minimum sum to reach each position in the previous row. This is because the decision at each row only depends on the immediately previous row due to the constraint.
Starting from the first row, we can build up these minimum sums row by row. For each cell (i, j)
, we compute:
- The minimum sum among all positions in row
i-1
except columnj
- Add the current cell's value
grid[i][j]
to this minimum
By the time we reach the last row, we'll have the minimum sum to reach each position in that row. The answer is simply the minimum among all these values.
The solution elegantly uses a rolling array technique where f
stores the minimum sums for the previous row, and g
computes the new minimum sums for the current row, which then becomes the new f
for the next iteration.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming with space optimization. Let's break down the implementation:
Dynamic Programming State Definition:
We define f[j]
to represent the minimum sum to reach column j
in the current row. Initially, f = [0] * n
represents that we haven't processed any rows yet.
State Transition:
For each row, we create a new array g
that will store the minimum sums for the current row. The transition formula is:
g[j] = row[j] + min(f[k] for all k ≠ j)
This means for position j
in the current row, we add:
- The current cell's value
row[j]
- The minimum sum from all positions in the previous row except column
j
Implementation Details:
-
Initialize: Start with
f = [0] * n
, representing zero cost before processing any rows. -
Process each row: For each row in the grid:
- Create
g = row[:]
- copy the current row values as the base - For each column
i
in the current row:- Add to
g[i]
the minimum value fromf[j]
wherej ≠ i
- The
min((f[j] for j in range(n) if j != i), default=0)
handles this - The
default=0
handles the edge case whenn=1
- Add to
- Update
f = g
for the next iteration
- Create
-
Return result: After processing all rows,
f
contains the minimum sums to reach each position in the last row. Returnmin(f)
.
Space Optimization:
Instead of maintaining a 2D DP table f[i][j]
of size n×n
, we use a rolling array technique with just two 1D arrays of size n
. This reduces space complexity from O(n²)
to O(n)
.
Time Complexity: O(n³)
- For each of the n
rows, we process n
columns, and for each column, we find the minimum among n-1
values.
Space Complexity: O(n)
- We only maintain arrays f
and g
of size n
.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with a 3×3 matrix:
grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
Initial State:
f = [0, 0, 0]
(no cost before processing any rows)
Processing Row 0: [1, 2, 3]
- Create
g = [1, 2, 3]
(copy of current row) - For column 0:
g[0] = 1 + min(f[1], f[2]) = 1 + min(0, 0) = 1
- For column 1:
g[1] = 2 + min(f[0], f[2]) = 2 + min(0, 0) = 2
- For column 2:
g[2] = 3 + min(f[0], f[1]) = 3 + min(0, 0) = 3
- Update:
f = [1, 2, 3]
Processing Row 1: [4, 5, 6]
- Create
g = [4, 5, 6]
(copy of current row) - For column 0:
g[0] = 4 + min(f[1], f[2]) = 4 + min(2, 3) = 4 + 2 = 6
- Can't come from column 0, must come from column 1 or 2
- For column 1:
g[1] = 5 + min(f[0], f[2]) = 5 + min(1, 3) = 5 + 1 = 6
- Can't come from column 1, must come from column 0 or 2
- For column 2:
g[2] = 6 + min(f[0], f[1]) = 6 + min(1, 2) = 6 + 1 = 7
- Can't come from column 2, must come from column 0 or 1
- Update:
f = [6, 6, 7]
Processing Row 2: [7, 8, 9]
- Create
g = [7, 8, 9]
(copy of current row) - For column 0:
g[0] = 7 + min(f[1], f[2]) = 7 + min(6, 7) = 7 + 6 = 13
- For column 1:
g[1] = 8 + min(f[0], f[2]) = 8 + min(6, 7) = 8 + 6 = 14
- For column 2:
g[2] = 9 + min(f[0], f[1]) = 9 + min(6, 6) = 9 + 6 = 15
- Update:
f = [13, 14, 15]
Final Result:
min(f) = min(13, 14, 15) = 13
The minimum path is: grid[0][0]=1 → grid[1][1]=5 → grid[2][0]=7
, giving sum = 1 + 5 + 7 = 13.
Another optimal path exists: grid[0][1]=2 → grid[1][0]=4 → grid[2][0]=7
, giving sum = 2 + 4 + 7 = 13.
Solution Implementation
1class Solution:
2 def minFallingPathSum(self, grid: List[List[int]]) -> int:
3 """
4 Find the minimum sum of a falling path with non-zero shifts.
5 A falling path starts from any element in the first row and chooses
6 one element from each row. The next row's choice must be in a different
7 column from the previous row.
8
9 Args:
10 grid: n x n integer matrix
11
12 Returns:
13 The minimum sum of a falling path through the grid
14 """
15 n = len(grid)
16
17 # dp[i] represents the minimum path sum ending at column i of the previous row
18 dp = [0] * n
19
20 # Process each row of the grid
21 for row in grid:
22 # Create a new dp array for the current row
23 new_dp = row[:] # Copy current row values as base costs
24
25 # For each column in the current row
26 for col in range(n):
27 # Add the minimum from previous row, excluding the same column
28 # This ensures we don't pick from the same column as previous row
29 min_prev = min((dp[prev_col] for prev_col in range(n) if prev_col != col), default=0)
30 new_dp[col] += min_prev
31
32 # Update dp for the next iteration
33 dp = new_dp
34
35 # Return the minimum value from the last row
36 return min(dp)
37
1class Solution {
2 public int minFallingPathSum(int[][] grid) {
3 int n = grid.length;
4
5 // dp array to store minimum path sum ending at each column of previous row
6 int[] previousRow = new int[n];
7
8 // Large value representing infinity for comparison
9 final int INFINITY = 1 << 30;
10
11 // Process each row of the grid
12 for (int[] currentGridRow : grid) {
13 // Create a copy of current row values to build new dp state
14 int[] currentRow = currentGridRow.clone();
15
16 // For each column in current row
17 for (int currentCol = 0; currentCol < n; ++currentCol) {
18 // Find minimum value from previous row, excluding same column
19 int minFromPreviousRow = INFINITY;
20
21 for (int previousCol = 0; previousCol < n; ++previousCol) {
22 // Skip if it's the same column (no same column in consecutive rows)
23 if (previousCol != currentCol) {
24 minFromPreviousRow = Math.min(minFromPreviousRow, previousRow[previousCol]);
25 }
26 }
27
28 // Add minimum from previous row to current cell
29 // If this is the first row (minFromPreviousRow == INFINITY), add 0
30 currentRow[currentCol] += (minFromPreviousRow == INFINITY ? 0 : minFromPreviousRow);
31 }
32
33 // Update dp array for next iteration
34 previousRow = currentRow;
35 }
36
37 // Return the minimum value from the last row
38 return Arrays.stream(previousRow).min().getAsInt();
39 }
40}
41
1class Solution {
2public:
3 int minFallingPathSum(vector<vector<int>>& grid) {
4 int n = grid.size();
5
6 // dp[i] represents the minimum sum ending at column i in the current row
7 vector<int> dp(n, 0);
8 const int INF = 1e9;
9
10 // Process each row of the grid
11 for (const auto& currentRow : grid) {
12 // Create a new dp array for the current row
13 vector<int> newDp = currentRow;
14
15 // For each column in the current row
16 for (int col = 0; col < n; ++col) {
17 // Find the minimum value from the previous row, excluding the same column
18 int minPrevious = INF;
19 for (int prevCol = 0; prevCol < n; ++prevCol) {
20 if (prevCol != col) {
21 minPrevious = min(minPrevious, dp[prevCol]);
22 }
23 }
24
25 // Add the minimum from previous row to current cell value
26 // If this is the first row (minPrevious == INF), add 0
27 newDp[col] += (minPrevious == INF ? 0 : minPrevious);
28 }
29
30 // Update dp array for the next iteration
31 dp = move(newDp);
32 }
33
34 // Return the minimum value from the last row
35 return *min_element(dp.begin(), dp.end());
36 }
37};
38
1/**
2 * Finds the minimum sum of a falling path through a grid where no two
3 * elements are in the same column for consecutive rows
4 * @param grid - 2D array representing the grid of numbers
5 * @returns The minimum falling path sum
6 */
7function minFallingPathSum(grid: number[][]): number {
8 const gridSize: number = grid.length;
9
10 // Dynamic programming array to store minimum path sums ending at each column
11 let previousRowMinSums: number[] = Array(gridSize).fill(0);
12
13 // Process each row in the grid
14 for (const currentRow of grid) {
15 // Create a copy of the current row to store new minimum sums
16 const currentRowMinSums: number[] = [...currentRow];
17
18 // For each column in the current row
19 for (let currentCol: number = 0; currentCol < gridSize; ++currentCol) {
20 // Find the minimum sum from the previous row, excluding the same column
21 let minPreviousSum: number = Infinity;
22
23 for (let previousCol: number = 0; previousCol < gridSize; ++previousCol) {
24 // Skip if it's the same column (can't fall straight down)
25 if (previousCol !== currentCol) {
26 minPreviousSum = Math.min(minPreviousSum, previousRowMinSums[previousCol]);
27 }
28 }
29
30 // Add the minimum from previous row to current cell value
31 // If no valid previous sum exists (first row), add 0
32 currentRowMinSums[currentCol] += minPreviousSum === Infinity ? 0 : minPreviousSum;
33 }
34
35 // Update the previous row sums for the next iteration
36 previousRowMinSums.splice(0, gridSize, ...currentRowMinSums);
37 }
38
39 // Return the minimum value from the last row
40 return Math.min(...previousRowMinSums);
41}
42
Time and Space Complexity
Time Complexity: O(n³)
The algorithm iterates through each row of the grid (n
rows). For each row, it processes each element (n
elements), and for each element i
, it computes the minimum of all previous row values except position i
(which takes O(n)
time). Therefore, the total time complexity is O(n × n × n) = O(n³)
.
Space Complexity: O(n)
The algorithm uses a rolling array optimization technique. Instead of maintaining a 2D DP table of size n × n
, it only keeps track of:
- Array
f
of sizen
to store the minimum falling path sums from the previous row - Array
g
of sizen
(created asrow[:]
) to compute the current row's values
Since only two 1D arrays of size n
are used at any time, and the algorithm alternates between them (rolling array approach), the space complexity is optimized from O(n²)
to O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Inefficient O(n³) Time Complexity
The current solution has a time complexity of O(n³) because for each cell (n² cells total), we're finding the minimum among n-1 values. This becomes problematic for large matrices.
The Pitfall:
# Inefficient: For each column, we iterate through all other columns
for col in range(n):
min_prev = min((dp[prev_col] for prev_col in range(n) if prev_col != col), default=0)
Solution - Optimize to O(n²): Instead of finding the minimum for each column separately, we can precompute:
- The overall minimum value from the previous row
- The second minimum value from the previous row
- Track which column contains the minimum
class Solution:
def minFallingPathSum(self, grid: List[List[int]]) -> int:
n = len(grid)
# Handle edge case
if n == 1:
return grid[0][0]
# Initialize with first row
dp = grid[0][:]
# Process remaining rows
for i in range(1, n):
# Find first minimum and second minimum from previous row
min1 = min2 = float('inf')
min1_idx = -1
for j in range(n):
if dp[j] < min1:
min2 = min1
min1 = dp[j]
min1_idx = j
elif dp[j] < min2:
min2 = dp[j]
# Build new dp array
new_dp = [0] * n
for j in range(n):
# If current column is same as min column, use second min
if j == min1_idx:
new_dp[j] = grid[i][j] + min2
else:
new_dp[j] = grid[i][j] + min1
dp = new_dp
return min(dp)
2. Edge Case: Single Column Matrix (n=1)
When n=1, there's only one column, making it impossible to choose different columns in consecutive rows.
The Pitfall:
The original code uses default=0
which incorrectly handles this case:
min_prev = min((dp[prev_col] for prev_col in range(n) if prev_col != col), default=0)
When n=1 and col=0, the generator is empty, so it returns 0, which may not be correct.
Solution: Handle n=1 as a special case:
if n == 1: return grid[0][0] # Only one element in the grid
3. Unnecessary Array Copying
The line new_dp = row[:]
creates a copy of the row which might be confusing and slightly inefficient.
The Pitfall:
new_dp = row[:] # Creates a copy but might be unclear why
Solution - More explicit initialization:
new_dp = [0] * n
for col in range(n):
new_dp[col] = grid[row_idx][col] + min_prev_value
4. Variable Naming Confusion
Using generic names like f
, g
can make the code harder to understand and maintain.
Solution: Use descriptive variable names:
prev_row_min_sums
instead off
curr_row_min_sums
instead ofg
col
orcurr_col
instead ofi
orj
Which of the following array represent a max heap?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!