Problem overview
You get a grid of non-negative integers with m rows and n columns. Starting at the top-left cell, you move only down or right until you reach the bottom-right cell. The method MinPathSum returns the smallest possible sum of the numbers on the cells you visit, including both ends.
For this grid the answer is 7:
1 3 1
1 5 1
4 2 1
The path 1, 3, 1, 1, 1 (right, right, down, down) adds up to 7, and no other down/right path does better. This lesson solves it with recursion and a cache, in O(n * m) time and O(n * m) extra space.
Approach: the best path from a cell
Define MinSum(grid, cache, i, j) as the minimum path sum from cell (i, j) to the bottom-right corner. From any cell other than the corner you can only go down or right, so the best path is the cell's own value plus the better of the two continuations:
MinSum(i, j) = grid[i, j] + min(MinSum(i + 1, j), MinSum(i, j + 1))
If a move leaves the grid, that continuation doesn't exist. Returning int.MaxValue for it means the minimum never prefers it. If you are standing on the bottom-right cell, the path is that cell alone.
The plain recursion is slow because many paths cross the same cell. In a 3 x 3 grid, cell (1, 1) is reached from both (0, 1) and (1, 0), and its whole subtree is solved twice; in bigger grids the repetition multiplies. The answer for a cell never changes, so we store it in a 2D array shaped like the grid, cache, and reuse it. Each of the m * n cells is then computed once.
On the example, the values stored are 3 at (2, 1), 7 at (2, 0), 2 at (1, 2), 7 at (1, 1), 8 at (1, 0), 3 at (0, 2) and 6 at (0, 1). The top-left cell then gives 1 + min(8, 6) = 7.
Step by step
- Create the cache in
MinPathSumas anint[,]with the same dimensions as the grid:new int[grid.GetLength(0), grid.GetLength(1)]. It starts filled with zeros. - Return the helper's answer for the start cell:
MinSum(grid, cache, 0, 0). - Declare the helper above
MinPathSum, taking the grid, the cache and the indicesiandj. - Out of bounds: if
iis past the last row orjpast the last column, returnint.MaxValue. - Base case: on the bottom-right cell, return
grid[i, j]. Do this before using the cache, since the corner is never stored. - Cache hit: if
cache[i, j]is greater than 0, the cell was already computed, so return it. A cell still holding 0 has not been filled yet. - Compute and store the recurrence in
cache[i, j], usingMath.Minon the answers for the cell below and the cell to the right. - Return
cache[i, j].
The solution in C#
class Solution {
public int MinSum(int[,] grid, int[,] cache, 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];
}
if (cache[i, j] > 0) {
return cache[i, j];
}
cache[i, j] =
grid[i, j] + Math.Min(MinSum(grid, cache, i + 1, j), MinSum(grid, cache, i, j + 1));
return cache[i, j];
}
public int MinPathSum(int[,] grid) {
int[,] cache = new int[grid.GetLength(0), grid.GetLength(1)];
return MinSum(grid, cache, 0, 0);
}
};
C# details worth knowing
- Rectangular arrays.
int[,]is a true two-dimensional array, indexed asgrid[i, j]with a comma. It is different from a jagged arrayint[][], which is indexed asgrid[i][j]. Using the wrong syntax is a compile error. - Sizes with
GetLength. A rectangular array'sLengthis the total number of cells. To get the rows useGetLength(0)and for the columnsGetLength(1). - Zero as "not computed". New arrays are zero-filled, and this code treats 0 as "empty". A cell whose real answer is 0 (a path of all zeros) is never recognized as cached, so it is simply recomputed. The result is still correct, only slower for such cells.
- The large sentinel is safe.
int.MaxValueis never added to anything: the only cell that could have both moves out of bounds is the bottom-right one, and it returns first. Adding toint.MaxValuewould wrap around to a negative number.
Complexity
There are m * n cells. Each is computed once, and the work for a cell besides its two recursive calls is constant, while every repeated request is a cache hit. The time is O(n * m). The cache has m * n entries and the recursion is at most m + n calls deep, so the extra space is O(n * m).
Edge cases and common mistakes
- A single cell. The start is the bottom-right cell, so the method returns that cell's value.
- A single row or column. One of the two moves is always out of bounds and returns
int.MaxValue, so the path is forced. For the row[1, 2, 3]the answer is 6. - Checking the cache before the bounds. Reading
cache[i, j]withiorjout of range throwsIndexOutOfRangeException. Test the bounds first. - Using
grid.Lengthas the row count. It is the cell count (9 for a 3 x 3 grid), not the number of rows. - Forgetting to share the cache. Creating a new array inside the helper discards all previous work.