Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 column j
  • 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:

  1. Initialize: Start with f = [0] * n, representing zero cost before processing any rows.

  2. 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 from f[j] where j ≠ i
      • The min((f[j] for j in range(n) if j != i), default=0) handles this
      • The default=0 handles the edge case when n=1
    • Update f = g for the next iteration
  3. Return result: After processing all rows, f contains the minimum sums to reach each position in the last row. Return min(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 Evaluator

Example 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 size n to store the minimum falling path sums from the previous row
  • Array g of size n (created as row[:]) 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 of f
  • curr_row_min_sums instead of g
  • col or curr_col instead of i or j
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More