931. Minimum Falling Path Sum
Problem Description
You are given an n x n
matrix of integers. Your task is to find the minimum sum of any falling path through the matrix.
A falling path is defined as follows:
- Start at any element in the first row
- From each position
(row, col)
, you can move to the next row by choosing one of three positions:(row + 1, col - 1)
- diagonally left(row + 1, col)
- directly below(row + 1, col + 1)
- diagonally right
- Continue this process until you reach the last row
- The path must stay within the matrix boundaries
The sum of a falling path is the total of all elements visited along the path. You need to find the path that gives the smallest possible sum.
For example, if you have a 3x3 matrix, you could start at any element in row 0, then move to one of the three allowed positions in row 1, and finally to one of the three allowed positions in row 2. The goal is to find which combination of moves produces the minimum sum.
The solution uses dynamic programming where f[j]
represents the minimum sum to reach position j
in the current row. For each new row, it calculates the minimum sum to reach each position by considering all valid previous positions (up to 3 positions from the previous row) and adding the current element's value. The final answer is the minimum value in the last row's results.
Intuition
The key insight is that we need to find the optimal path from top to bottom, and at each step, we have limited choices. This naturally suggests a dynamic programming approach.
Think about it this way: if we're standing at any position in a row, the minimum sum to reach that position depends on which position we came from in the previous row. Since we can only arrive from three possible positions (directly above, diagonally left-above, or diagonally right-above), we just need to pick the one that gives us the smallest cumulative sum.
Instead of trying all possible paths from the top (which would be exponential in complexity), we can build up the solution row by row. For each position in the current row, we ask: "What's the minimum sum needed to reach this position?" The answer is the minimum sum to reach any of the valid previous positions, plus the current cell's value.
The beauty of this approach is that we don't need to keep track of the entire path - we only need to know the minimum sums to reach each position in the previous row. This is why the solution uses two arrays: f
stores the minimum sums for the previous row, and g
calculates the minimum sums for the current row.
The expression min(f[l:r]) + x
captures this perfectly: it finds the minimum among the valid previous positions (considering boundary constraints with max(0, j-1)
and min(n, j+2)
) and adds the current element's value. By processing each row sequentially, we guarantee that when we reach the last row, we have the minimum sum to reach each position in that row. The final answer is simply the minimum among all positions in the last row.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a space-optimized dynamic programming approach using a row-by-row processing strategy.
Data Structure:
- We use a 1D array
f
of sizen
to store the minimum path sums for the previous row - A temporary array
g
of the same size to compute the minimum path sums for the current row
Algorithm Steps:
-
Initialize: Start with
f = [0] * n
, representing that we haven't processed any rows yet (the cost to reach the "row before" the first row is 0). -
Process each row: For each
row
in the matrix:- Create a new array
g
to store the minimum sums for the current row - For each position
j
with valuex
in the current row:- Calculate the valid range of previous positions:
l = max(0, j - 1)
ensures we don't go below index 0r = min(n, j + 2)
ensures we don't exceed the array bounds (note:j + 2
because Python slicing is exclusive of the end)
- Compute
g[j] = min(f[l:r]) + x
f[l:r]
gives us the slice of valid previous positions (could be 1, 2, or 3 positions depending on boundaries)min(f[l:r])
finds the minimum sum among those valid positions- Adding
x
gives us the minimum sum to reach positionj
in the current row
- Calculate the valid range of previous positions:
- After processing all positions in the current row, update
f = g
to prepare for the next row
- Create a new array
-
Return the result: After processing all rows,
f
contains the minimum sums to reach each position in the last row. Returnmin(f)
to get the overall minimum falling path sum.
Time Complexity: O(n²)
where n is the dimension of the matrix. We process each of the n²
elements once, and for each element, we perform a constant-time operation (finding minimum among at most 3 values).
Space Complexity: O(n)
as we only maintain two arrays of size n
at any time, rather than storing the entire DP table of size n²
.
The elegance of this solution lies in its space optimization - instead of maintaining a 2D DP table, we only keep track of the previous row's results, which is sufficient for computing the current row's minimum sums.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with this 3×3 matrix:
[2, 1, 3] [6, 5, 4] [7, 8, 9]
Goal: Find the minimum sum falling path from top to bottom.
Step 1: Initialize
f = [0, 0, 0]
(no cost to reach positions before the first row)
Step 2: Process Row 0 [2, 1, 3]
- Create
g = [0, 0, 0]
for storing new values - For position 0 (value=2):
- Valid previous positions:
l=max(0,0-1)=0
,r=min(3,0+2)=2
f[0:2] = [0, 0]
, minimum is 0g[0] = 0 + 2 = 2
- Valid previous positions:
- For position 1 (value=1):
- Valid previous positions:
l=max(0,1-1)=0
,r=min(3,1+2)=3
f[0:3] = [0, 0, 0]
, minimum is 0g[1] = 0 + 1 = 1
- Valid previous positions:
- For position 2 (value=3):
- Valid previous positions:
l=max(0,2-1)=1
,r=min(3,2+2)=3
f[1:3] = [0, 0]
, minimum is 0g[2] = 0 + 3 = 3
- Valid previous positions:
- Update:
f = [2, 1, 3]
Step 3: Process Row 1 [6, 5, 4]
- Create
g = [0, 0, 0]
- For position 0 (value=6):
- Can come from positions 0 or 1 in previous row
f[0:2] = [2, 1]
, minimum is 1g[0] = 1 + 6 = 7
- For position 1 (value=5):
- Can come from positions 0, 1, or 2 in previous row
f[0:3] = [2, 1, 3]
, minimum is 1g[1] = 1 + 5 = 6
- For position 2 (value=4):
- Can come from positions 1 or 2 in previous row
f[1:3] = [1, 3]
, minimum is 1g[2] = 1 + 4 = 5
- Update:
f = [7, 6, 5]
Step 4: Process Row 2 [7, 8, 9]
- Create
g = [0, 0, 0]
- For position 0 (value=7):
- Can come from positions 0 or 1 in previous row
f[0:2] = [7, 6]
, minimum is 6g[0] = 6 + 7 = 13
- For position 1 (value=8):
- Can come from positions 0, 1, or 2 in previous row
f[0:3] = [7, 6, 5]
, minimum is 5g[1] = 5 + 8 = 13
- For position 2 (value=9):
- Can come from positions 1 or 2 in previous row
f[1:3] = [6, 5]
, minimum is 5g[2] = 5 + 9 = 14
- Update:
f = [13, 13, 14]
Step 5: Return Result
min(f) = min([13, 13, 14]) = 13
The minimum falling path sum is 13, which corresponds to the path: 1 → 5 → 7 (or alternatively 1 → 4 → 8).
Solution Implementation
1class Solution:
2 def minFallingPathSum(self, matrix: List[List[int]]) -> int:
3 """
4 Find the minimum sum of a falling path through the matrix.
5 A falling path starts at any element in the first row and chooses
6 one element from each row. The next row's choice must be in a column
7 that is different from the previous row's column by at most one.
8
9 Args:
10 matrix: n x n matrix of integers
11
12 Returns:
13 The minimum sum of any falling path through the matrix
14 """
15 n = len(matrix)
16
17 # Initialize dp array for the previous row's minimum path sums
18 # dp[j] represents the minimum path sum ending at column j
19 previous_row_dp = [0] * n
20
21 # Process each row in the matrix
22 for row in matrix:
23 # Create a new dp array for the current row
24 current_row_dp = [0] * n
25
26 # Calculate minimum path sum for each column in current row
27 for col_index, value in enumerate(row):
28 # Determine the valid range of columns from the previous row
29 # Can come from column (col_index - 1), col_index, or (col_index + 1)
30 left_bound = max(0, col_index - 1)
31 right_bound = min(n, col_index + 2)
32
33 # Find minimum path sum from valid previous columns and add current value
34 current_row_dp[col_index] = min(previous_row_dp[left_bound:right_bound]) + value
35
36 # Update previous row dp for next iteration
37 previous_row_dp = current_row_dp
38
39 # Return the minimum value from the last row's dp array
40 return min(previous_row_dp)
41
1class Solution {
2 public int minFallingPathSum(int[][] matrix) {
3 int n = matrix.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 // Process each row of the matrix
9 for (int[] currentRow : matrix) {
10 // Create a new array for current row's minimum path sums
11 int[] currentRowDp = previousRow.clone();
12
13 // Calculate minimum path sum for each column in current row
14 for (int col = 0; col < n; col++) {
15 // Check if we can come from top-left diagonal
16 if (col > 0) {
17 currentRowDp[col] = Math.min(currentRowDp[col], previousRow[col - 1]);
18 }
19
20 // Check if we can come from top-right diagonal
21 if (col + 1 < n) {
22 currentRowDp[col] = Math.min(currentRowDp[col], previousRow[col + 1]);
23 }
24
25 // Add current cell value to the minimum path sum
26 currentRowDp[col] += currentRow[col];
27 }
28
29 // Update previous row for next iteration
30 previousRow = currentRowDp;
31 }
32
33 // Find the minimum value in the last row
34 int minPathSum = Integer.MAX_VALUE;
35 for (int pathSum : previousRow) {
36 minPathSum = Math.min(minPathSum, pathSum);
37 }
38
39 return minPathSum;
40 }
41}
42
1class Solution {
2public:
3 int minFallingPathSum(vector<vector<int>>& matrix) {
4 int n = matrix.size();
5
6 // dp[j] represents the minimum path sum ending at column j of the previous row
7 vector<int> dp(n, 0);
8
9 // Process each row of the matrix
10 for (const auto& currentRow : matrix) {
11 // Create a temporary array to store the new minimum path sums for current row
12 vector<int> newDp(n);
13
14 // Calculate minimum path sum for each column in the current row
15 for (int col = 0; col < n; ++col) {
16 // Start with the value from directly above (same column in previous row)
17 newDp[col] = dp[col];
18
19 // Check if we can come from the left diagonal (previous row, col-1)
20 if (col > 0) {
21 newDp[col] = min(newDp[col], dp[col - 1]);
22 }
23
24 // Check if we can come from the right diagonal (previous row, col+1)
25 if (col + 1 < n) {
26 newDp[col] = min(newDp[col], dp[col + 1]);
27 }
28
29 // Add the current cell's value to the minimum path sum
30 newDp[col] += currentRow[col];
31 }
32
33 // Update dp array for the next iteration
34 dp = move(newDp);
35 }
36
37 // Return the minimum value from the last row
38 return *min_element(dp.begin(), dp.end());
39 }
40};
41
1/**
2 * Finds the minimum sum of a falling path through the matrix.
3 * A falling path starts at any element in the first row and chooses
4 * one element from each row. The next row's choice must be in a column
5 * that is different from the previous row's column by at most one.
6 *
7 * @param matrix - A square matrix of integers
8 * @returns The minimum sum of any falling path through the matrix
9 */
10function minFallingPathSum(matrix: number[][]): number {
11 const matrixSize: number = matrix.length;
12
13 // Dynamic programming array to store minimum path sums ending at each column
14 // Initially all zeros, will be updated row by row
15 let previousRowMinSums: number[] = new Array(matrixSize).fill(0);
16
17 // Process each row of the matrix
18 for (const currentRow of matrix) {
19 // Create a copy of the previous row's minimum sums to calculate current row
20 const currentRowMinSums: number[] = previousRowMinSums.slice();
21
22 // Calculate minimum path sum for each column in the current row
23 for (let columnIndex = 0; columnIndex < matrixSize; ++columnIndex) {
24 // Check if we can come from the left diagonal (column - 1)
25 if (columnIndex > 0) {
26 currentRowMinSums[columnIndex] = Math.min(
27 currentRowMinSums[columnIndex],
28 previousRowMinSums[columnIndex - 1]
29 );
30 }
31
32 // Check if we can come from the right diagonal (column + 1)
33 if (columnIndex + 1 < matrixSize) {
34 currentRowMinSums[columnIndex] = Math.min(
35 currentRowMinSums[columnIndex],
36 previousRowMinSums[columnIndex + 1]
37 );
38 }
39
40 // Add the current cell's value to the minimum path sum
41 currentRowMinSums[columnIndex] += currentRow[columnIndex];
42 }
43
44 // Update the previous row's minimum sums for the next iteration
45 previousRowMinSums.splice(0, matrixSize, ...currentRowMinSums);
46 }
47
48 // Return the minimum value from the last row's minimum path sums
49 return Math.min(...previousRowMinSums);
50}
51
Time and Space Complexity
Time Complexity: O(n²)
The algorithm iterates through each row of the n×n
matrix (outer loop runs n
times). For each row, it iterates through each element (inner loop runs n
times). Within the inner loop, the slicing operation f[l:r]
and min()
function take at most O(3)
time since we're only considering at most 3 adjacent elements from the previous row. Therefore, the overall time complexity is O(n) × O(n) × O(1) = O(n²)
.
Space Complexity: O(n)
The algorithm uses two arrays f
and g
, each of size n
, to store the minimum falling path sums. Instead of maintaining the entire DP table of size n×n
, it optimizes space by only keeping track of the previous row's results in f
and computing the current row's results in g
. The space used is O(n) + O(n) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Boundary Handling in Column Range Calculation
One of the most common mistakes is incorrectly calculating the valid column range from the previous row, especially with the slicing bounds.
Pitfall Example:
# INCORRECT: Using wrong bounds left_bound = j - 1 # Can be negative! right_bound = j + 1 # Misses the rightmost valid column # This would cause: # - IndexError when j = 0 (left_bound = -1) # - Missing valid transitions when using previous_row_dp[left_bound:right_bound]
Correct Solution:
# CORRECT: Properly handle boundaries
left_bound = max(0, j - 1) # Ensures non-negative index
right_bound = min(n, j + 2) # j+2 because slicing is exclusive of end
# This ensures:
# - When j = 0: can come from columns [0, 1]
# - When j = n-1: can come from columns [n-2, n-1]
# - When 0 < j < n-1: can come from columns [j-1, j, j+1]
2. Initializing the DP Array with Wrong Values
Another pitfall is initializing the first row's DP values incorrectly.
Pitfall Example:
# INCORRECT: Initializing with the first row values
previous_row_dp = matrix[0] # Creates a reference, not a copy!
# Or
previous_row_dp = [float('inf')] * n # Wrong initial values
# This causes incorrect calculations for the second row
Correct Solution:
# CORRECT: Initialize with zeros previous_row_dp = [0] * n # The first iteration will correctly compute: # current_row_dp[j] = min([0, 0, ...]) + matrix[0][j] = matrix[0][j] # This properly sets up the base case
3. Modifying Arrays In-Place Without Proper Copying
A subtle but critical error occurs when trying to optimize space by reusing arrays incorrectly.
Pitfall Example:
# INCORRECT: Modifying the same array in place
dp = [0] * n
for row in matrix:
for j, x in enumerate(row):
# Modifying dp while still reading from it!
dp[j] = min(dp[max(0, j-1):min(n, j+2)]) + x
# This corrupts the values needed for subsequent columns
Correct Solution:
# CORRECT: Use two separate arrays
previous_row_dp = [0] * n
for row in matrix:
current_row_dp = [0] * n # Fresh array for current row
for j, x in enumerate(row):
current_row_dp[j] = min(previous_row_dp[max(0, j-1):min(n, j+2)]) + x
previous_row_dp = current_row_dp # Safe to reassign after processing entire row
4. Edge Case: Single Element Matrix
While the provided solution handles this correctly, it's worth noting that some implementations might fail on a 1x1 matrix.
Pitfall Example:
# INCORRECT: Assuming matrix has at least 2 rows
def minFallingPathSum(self, matrix):
n = len(matrix)
dp = matrix[0][:] # Copy first row
for i in range(1, n): # Skips if n = 1!
# ... process rows
return min(dp)
Correct Solution: The provided solution naturally handles this case because:
- With n=1, there's only one iteration
min([0]) + matrix[0][0]
correctly returns the single element- No special case handling needed
In a binary min heap, the minimum element can be found in:
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!