Problem overview
The function minPathSum(grid) receives a rectangular grid of non-negative integers as a [][]int. A path starts in the top-left cell and ends in the bottom-right cell, moving only down or right. The sum of a path is the total of all the cells it visits, both ends included. The function returns the smallest possible sum.
For this grid the answer is 7:
[1 3 1]
[1 5 1]
[4 2 1]
The path 1, 3, 1, 1, 1 goes right, right, down, down and adds up to 7.
Approach: the best path from a cell
Let minSum(grid, i, j) be the smallest path sum from cell (i, j) to the bottom-right cell. From any cell there are only two moves, down to (i + 1, j) or right to (i, j + 1). The best path from a cell is its own value plus the better of the two continuations. The answer to the problem is minSum(grid, 0, 0).
The recursion needs two base cases. At the bottom-right cell the path is that single cell, so the sum is its value. Moves that fall off the grid are not paths at all. Instead of special handling, such a move returns math.MaxInt, the largest int, so the minimum of the two continuations never selects it.
This version is a backtracking solution: it follows every route and keeps the cheapest. The same cell is reached over and over by different routes, so the number of calls grows quickly. For a 3 by 3 grid it makes 27 calls, for 5 by 5 it makes 363, for 10 by 10 it makes 272,271 and for 15 by 15 it makes 230,001,839. A later lesson on this problem saves each cell's answer and reduces the work to one computation per cell, which is O(n * m).
Step by step
- Declare the helper
minSumaboveminPathSum. It takesgrid [][]intand the indicesi, j int, which share one type name, and returns anint. - Off the grid. If
i >= len(grid)orj >= len(grid[0]), returnmath.MaxInt. - Bottom-right cell. If
iequalslen(grid)-1andjequalslen(grid[0])-1, returngrid[i][j]. This stops the recursion. - Recurse. Return
grid[i][j]plus the smaller ofminSum(grid, i+1, j)andminSum(grid, i, j+1). - Start at the corner.
minPathSumreturnsminSum(grid, 0, 0).
The solution in Go
func minSum(grid [][]int, i, j int) int {
if i >= len(grid) || j >= len(grid[0]) {
return math.MaxInt
}
if i == len(grid)-1 && j == len(grid[0])-1 {
return grid[i][j]
}
return grid[i][j] + min(minSum(grid, i+1, j), minSum(grid, i, j+1))
}
func minPathSum(grid [][]int) int {
return minSum(grid, 0, 0)
}
Go details worth knowing:
- The import.
math.MaxIntcomes from themathpackage, which a complete program must import. It equals 9223372036854775807 on a 64-bit build. - Why
MaxIntdoesn't overflow. Adding to the largestintwould wrap to a huge negative number, since Go integers overflow silently. That never happens here. A call only seesMaxIntfrom a neighbour that fell off the grid, and the other neighbour is on the grid except at the bottom-right cell, which returns before the addition. So the minimum always picks a real sum, andMaxIntis never added to anything. - Built-in
min. Since Go 1.21,minworks on ordered types without an import. - Order of the checks. The bounds test must come first. For a cell outside the grid, the final
returnwould indexgrid[i][j]outside the grid and panic. len(grid[0])is the width. A[][]intis a slice of row slices, so the number of columns is the length of any row, here the first. This assumes thatgridhas at least one row.
Complexity
Time: exponential in the grid size. Every cell inside the grid except the bottom-right one makes two calls, and the number of routes to a cell grows combinatorially, so the same subproblems are solved many times.
Space: O(m + n) for the call stack, where m and n are the number of rows and columns. Every call moves one step down or right, so a route has at most m + n - 1 cells, and nothing else is allocated.
Edge cases and common mistakes
- One cell.
[[5]]is the bottom-right cell already, so the answer is 5. - One row or one column.
[[1, 2, 3]]and a column[[1], [2], [3]]both give 6, because there is only one path. The off-grid moves returnMaxIntand the minimum ignores them. - Counting the start and end. Both corner cells are part of the path. A recursion that returns 0 at the end would miss the last cell's value.
- Greedy choices. Always stepping to the smaller neighbour can fail, because a small cell can lead into expensive ones. The recursion compares whole paths, not single steps.
- Using 0 for off-grid moves. That would make a move off the grid look free and the minimum would pick it. A large value is needed.
- Grids that are too large. The call count grows so fast that a 15 by 15 grid already takes hundreds of millions of calls.
Testing
The grid [[1, 3, 1], [1, 5, 1], [4, 2, 1]] gives 7. [[5]] gives 5. [[1, 2, 3]] gives 6.