Topics › Dynamic Programming › 2D/3D DP and DP on Grids
Minimum Falling Path Sum
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Given a matrix of integers, a falling path starts at any cell of the first row and moves down one row at a time. From cell (i, j) the next cell is (i + 1, j - 1), (i + 1, j) or (i + 1, j + 1): down-left, straight down or down-right, staying inside the matrix. It ends at some cell of the last row. Return the minimum possible sum of the values along a falling path.
This is LeetCode 931, where the matrix is n × n with 1 ≤ n ≤ 100 and values in [-100, 100]. The Java methods work on any rectangular n × m matrix. Striver’s lecture on this pattern asks for the maximum falling path sum on a rectangular matrix; swapping every min for max (and the infinity for minus infinity) gives that version.
Example 1
matrix = [[2, 1, 3],
[6, 5, 4],
[7, 8, 9]]
answer = 13 // 1 -> 5 -> 7, or 1 -> 4 -> 8
Example 2
matrix = [[-19, 57],
[-40, -5]]
answer = -59 // -19 -> -40
What is new here
In Unique Paths the start and the end were fixed cells. Here both are variable: the path may start at any cell of row 0 and end at any cell of the last row. The standard way to handle this is to fix one end inside the recursion and loop over the other outside it. We define the state as “the path ends at (i, j)”, recurse upward to the first row, where any cell is a valid start (so that is the base case), and take the best over all cells of the last row.
Approach 1: Recursion
Intuition. Let f(i, j) be the minimum sum of a falling path that starts anywhere in row 0 and ends at (i, j). The step into (i, j) came from one of (i - 1, j - 1), (i - 1, j) or (i - 1, j + 1). Whichever it was, the part of the path above should be the cheapest path to that cell. So
f(i, j) = +infinity if j is outside the matrix
f(0, j) = matrix[0][j]
f(i, j) = matrix[i][j] + min( f(i-1, j-1), f(i-1, j), f(i-1, j+1) )
answer = min over j of f(n - 1, j)
“Infinity” here is 10^9, a value no real path sum can reach (the largest real magnitude is 100 · 100 = 10^4). It is only ever passed to min, and the straight-up neighbour is always inside the matrix, so infinity never wins and is never added to.
Algorithm.
- If
jis out of range, return infinity. - If
i == 0, returnmatrix[0][j]. - Return
matrix[i][j]plus the minimum of the three upward neighbours. - The answer is the minimum of
f(n - 1, j)over all columnsj.
static int recursive(int[][] matrix) {
int n = matrix.length, m = matrix[0].length;
int best = Integer.MAX_VALUE;
for (int j = 0; j < m; j++) best = Math.min(best, solve(n - 1, j, matrix));
return best;
}
static int solve(int i, int j, int[][] matrix) {
if (j < 0 || j >= matrix[0].length) return INF; // stepped off the side
if (i == 0) return matrix[0][j]; // a path may start at any cell of row 0
int up = solve(i - 1, j, matrix);
int upLeft = solve(i - 1, j - 1, matrix);
int upRight = solve(i - 1, j + 1, matrix);
return matrix[i][j] + Math.min(up, Math.min(upLeft, upRight));
}
Complexity. Time O(m · 3^n): each of the m end cells starts a recursion that branches three ways for n - 1 levels. Space O(n) for the recursion depth.
Approach 2: Memoization
Intuition. There are n · m states, and neighbouring end cells share almost all of their upward recursion. Cache f(i, j) in dp[i][j].
Values can be negative, so -1 is no longer a safe “not computed” marker: a real path sum can be -1. The table is filled with Integer.MAX_VALUE instead, which no real sum can equal.
Algorithm.
- Create
dp[n][m]filled withInteger.MAX_VALUE. - In the helper, apply the two base cases, then return
dp[i][j]if it was computed; otherwise compute, store and return it. - Take the minimum over the last row as before. The table is shared across the
mstarting calls, which is what makes the total workO(n · m).
static int memoization(int[][] matrix) {
int n = matrix.length, m = matrix[0].length;
int[][] dp = new int[n][m];
for (int[] row : dp) Arrays.fill(row, Integer.MAX_VALUE); // MAX_VALUE = not computed (sums can be negative)
int best = Integer.MAX_VALUE;
for (int j = 0; j < m; j++) best = Math.min(best, memo(n - 1, j, matrix, dp));
return best;
}
static int memo(int i, int j, int[][] matrix, int[][] dp) {
if (j < 0 || j >= matrix[0].length) return INF;
if (i == 0) return matrix[0][j];
if (dp[i][j] != Integer.MAX_VALUE) return dp[i][j];
int up = memo(i - 1, j, matrix, dp);
int upLeft = memo(i - 1, j - 1, matrix, dp);
int upRight = memo(i - 1, j + 1, matrix, dp);
return dp[i][j] = matrix[i][j] + Math.min(up, Math.min(upLeft, upRight));
}
Complexity. Time O(n · m): each cell is computed once with three lookups. Space O(n · m) plus O(n) stack.
Approach 3: Tabulation
Intuition. Row i only reads row i - 1, so fill the table from the top down. Row 0 is the base case, copied from the matrix. Out-of-range neighbours are replaced by infinity, just as in the recursion.
Algorithm.
dp[0][j] = matrix[0][j]for allj.- For each row
i ≥ 1and columnj:dp[i][j] = matrix[i][j] + min(dp[i-1][j], dp[i-1][j-1] if j > 0, dp[i-1][j+1] if j < m - 1). - Return the minimum of the last row.
static int tabulation(int[][] matrix) {
int n = matrix.length, m = matrix[0].length;
int[][] dp = new int[n][m];
for (int j = 0; j < m; j++) dp[0][j] = matrix[0][j];
for (int i = 1; i < n; i++) {
for (int j = 0; j < m; j++) {
int up = dp[i - 1][j];
int upLeft = j > 0 ? dp[i - 1][j - 1] : INF;
int upRight = j < m - 1 ? dp[i - 1][j + 1] : INF;
dp[i][j] = matrix[i][j] + Math.min(up, Math.min(upLeft, upRight));
}
}
int best = Integer.MAX_VALUE;
for (int j = 0; j < m; j++) best = Math.min(best, dp[n - 1][j]);
return best;
}
Dry run on Example 1:
| row | j = 0 | j = 1 | j = 2 |
|---|---|---|---|
| 0 | 2 | 1 | 3 |
| 1 | 6 + min(2, 1) = 7 | 5 + min(2, 1, 3) = 6 | 4 + min(1, 3) = 5 |
| 2 | 7 + min(7, 6) = 13 | 8 + min(7, 6, 5) = 13 | 9 + min(6, 5) = 14 |
The minimum of the last row is 13.
Complexity. Time O(n · m), space O(n · m).
Approach 4: Space optimized
Intuition. Only the previous row is ever read. Keep it in prev, build the new row in cur, and swap. Unlike Unique Paths, a single in-place array does not work here: cur[j] needs the old prev[j - 1], which an in-place left-to-right update would already have overwritten. Two arrays avoid the problem.
Algorithm.
prev = copy of matrix[0].- For each later row, compute
cur[j]fromprev[j - 1],prev[j],prev[j + 1], thenprev = cur. - Return the minimum of
prev.
static int spaceOptimized(int[][] matrix) {
int n = matrix.length, m = matrix[0].length;
int[] prev = matrix[0].clone();
for (int i = 1; i < n; i++) {
int[] cur = new int[m];
for (int j = 0; j < m; j++) {
int up = prev[j];
int upLeft = j > 0 ? prev[j - 1] : INF;
int upRight = j < m - 1 ? prev[j + 1] : INF;
cur[j] = matrix[i][j] + Math.min(up, Math.min(upLeft, upRight));
}
prev = cur;
}
int best = Integer.MAX_VALUE;
for (int v : prev) best = Math.min(best, v);
return best;
}
Complexity. Time O(n · m), space O(m).
Edge cases and pitfalls
- Negative values. They are allowed, so the memo sentinel cannot be
-1or0. Use a value outside the reachable range. - Infinity overflow. If “infinity” were
Integer.MAX_VALUEand code ever added a cell value to it, the sum would wrap to a large negative number and win everymin. Using10^9and only adding to the chosen minimum avoids this. - Edge columns. Column 0 has no down-left parent and column
m - 1no down-right parent. In a1-column matrix both are missing and the single path is forced; the test[[4], [-2], [6]]expects 8. - One row. The answer is the smallest value in that row.
- Greedy is wrong. Starting from the smallest value in row 0 and always stepping to the smallest neighbour can walk into an expensive region. On
[[1, 2, 3], [9, 9, 1]]greedy starts at 1 and must pay 9 next (total 10), while2 -> 1costs 3. The test file includes this case. - Testing. 200 seeded random matrices with negative values run through all four methods.
Related problems
- Triangle has a fixed start and a variable end, with two moves per step.
- Minimum sum path in the matrix fixes both ends and allows right and down moves.
- Grid Unique Paths : DP on Grids (DP8) introduces DP on grids by counting paths.
- Ninja and his Friends runs two falling paths at once, which needs a three-dimensional state.