Problem overview
The method MinPathSum receives a rectangular grid of non-negative integers as an int[,]. It must find the path from the top-left cell to the bottom-right cell with the smallest total, where every step moves either down or right, and return that total.
For this grid:
1 3 1
1 5 1
4 2 1
the answer is 7, from the path 1, 3, 1, 1, 1 (right, right, down, down). The other paths cost more: going down first gives 1 + 1 + 4 + 2 + 1 = 9.
Approach: ask each cell for its best continuation
The cheapest path from a cell is the cell's own value plus the cheaper of its two continuations: the best path from the cell below, or the best path from the cell to the right. That sentence is already the algorithm. Write a helper MinSum(grid, i, j) for "the minimum path sum from cell (i, j) to the bottom-right cell", and let it call itself for the two neighbours.
Two special cases end the recursion:
- At the bottom-right cell the path is that cell alone, so the answer is its value.
- Stepping outside the grid is not allowed. The helper reports that with
int.MaxValue, a value so large that the minimum never prefers it. This also takes care of the last row and last column, where only one move is available: the move that leaves the grid returns the huge value and loses to the one that stays inside.
Evaluating the recursion from the bottom-right corner back to the top-left gives these best totals from each cell to the target:
7 6 3
8 7 2
7 3 1
The corner is 1. The cell above it holds 1 and can only move down, so its total is 1 + 1 = 2. The middle cell holds 5 and chooses between the cell below (total 3) and the cell to its right (total 2), so its total is 5 + 2 = 7. At the top left, the choice is between 8 (down) and 6 (right), so the answer is 1 + 6 = 7.
Step by step
- Declare
MinSum. It is apublicmethod aboveMinPathSumthat takes theint[,]grid and the cell's rowiand columnj, and returns anint. - Handle leaving the grid. If
i >= grid.GetLength(0)orj >= grid.GetLength(1), the move was illegal. Returnint.MaxValue. - Handle the target. If
iis the last row andjis the last column, returngrid[i, j]. This is the base case. - Combine. Return
grid[i, j]plus the smaller ofMinSum(grid, i + 1, j)(move down) andMinSum(grid, i, j + 1)(move right), usingMath.Min. - Start the search.
MinPathSumreturnsMinSum(grid, 0, 0), because every path begins at the top-left cell.
The solution in C#
class Solution {
public int MinSum(int[,] grid, int i, int j) {
if (i >= grid.GetLength(0) || j >= grid.GetLength(1)) {
return int.MaxValue;
}
if (i == grid.GetLength(0) - 1 && j == grid.GetLength(1) - 1) {
return grid[i, j];
}
return grid[i, j] + Math.Min(MinSum(grid, i + 1, j), MinSum(grid, i, j + 1));
}
public int MinPathSum(int[,] grid) {
return MinSum(grid, 0, 0);
}
};
The C# details worth a closer look:
- Rectangular arrays.
int[,]is a single block of rows times columns cells, indexed with a comma:grid[i, j]. It is different from a jagged arrayint[][], which is an array of separate arrays and is indexedgrid[i][j]. Mixing the two styles doesn't compile. - Sizes.
grid.GetLength(0)is the number of rows andgrid.GetLength(1)is the number of columns.grid.Lengthwould be the total number of cells, andgrid.Rankis the number of dimensions. - Why
int.MaxValueis safe here. Adding to it would wrap around silently to a negative number, becauseintarithmetic is unchecked by default. The code never does that: the only cell where both moves leave the grid is the bottom-right cell, and the secondifreturns before the sum. Every other cell has at least one move that stays inside, so the minimum is a real total, never the huge value. - Trailing semicolon. The
;after the class's closing brace is legal in C# and does nothing. - Short-circuit
||and&&. The bounds check comes first, sogrid[i, j]is only read wheniandjare valid.
Complexity
Time: exponential. Every cell makes two recursive calls, and different paths reach the same cell again and again. A 3 by 3 grid makes 27 calls; a 4 by 4 grid, 99; an 8 by 8 grid, 18,875; a 12 by 12 grid, 3,997,447. Each extra row and column multiplies the work. The number of distinct cells is only m * n, so almost all of the calls repeat work already done. Caching each cell's answer removes the repetition and reduces the time to O(m * n), which is where the next lessons on this problem go.
Space: O(m + n). No extra arrays are created. The call stack is as deep as the longest path from the start, which has m + n - 1 cells, and that is the only memory used.
Edge cases and common mistakes
- A single cell.
{{7}}is both the start and the target, so the answer is 7. - A single row or column. For
{{1, 2, 3}}the only move is right (the move down leaves the grid), so the answer is 6. A column works the same way. - Using
-1or0for out-of-bounds. With0, the minimum would prefer leaving the grid, and the result would be wrong. The out-of-bounds value has to be larger than any real total. - Adding to
int.MaxValue. Doing something likegrid[i, j] + int.MaxValuewraps around to a negative number. Here the structure of the recursion avoids it; if you change the base cases, keep that in mind. - Mixing up the dimensions. Row count is
GetLength(0), column count isGetLength(1). Swapping them reads past the edge on grids that aren't square and throws anIndexOutOfRangeException. - Checking the target before the bounds. Reading
grid[i, j]before the bounds test throws on the first step outside the grid. - Large grids. The exponential time makes this version suitable only for small grids.
Testing
- The grid
{{1, 3, 1}, {1, 5, 1}, {4, 2, 1}}gives 7. {{7}}gives 7.{{1, 2, 3}}gives 6, and{{1}, {2}, {3}}gives 6.