Problem overview
The method minPathSum(_ grid: [[Int]]) -> Int receives a rectangular grid of non-negative integers. A path begins at the top-left cell, ends at the bottom-right cell, and moves only down or right. The method returns the smallest sum of the numbers on any such path, counting both end cells.
For [[1, 3, 1], [1, 5, 1], [4, 2, 1]] the answer is 7: the path 1, 3, 1, 1, 1 goes right, right, down, down, and no other path adds up to less.
Approach: build the answer cell by cell from the top-left
The key observation is that a cell can only be entered from the cell above it or from the cell to its left. Let best(i, j) be the smallest path sum from the top-left corner to cell (i, j). Then:
best(0, 0)is justgrid[0][0].- On the first row, the only way in is from the left, so
best(0, j) = grid[0][j] + best(0, j - 1). - In the first column, the only way in is from above, so
best(i, 0) = grid[i][0] + best(i - 1, 0). - Every other cell takes the cheaper of the two ways in:
best(i, j) = grid[i][j] + min(best(i - 1, j), best(i, j - 1)).
The answer is best of the bottom-right cell. Filling the cells row by row, left to right, means both neighbours are always final before they are used, so no recursion is needed. Each cell is computed in constant time, so the whole algorithm runs in O(n * m) for a grid of n rows and m columns.
This lesson stores the values in the grid itself, overwriting each cell with its best value. For the example, the grid becomes [[1, 4, 5], [2, 7, 6], [6, 8, 7]], and the bottom-right cell holds 7.
Step by step
- Make a mutable copy:
var grid = grid. Function parameters are constants in Swift, so the method couldn't write into the original. Because arrays are values, the copy is independent of the caller's grid. - Name the dimensions:
let n = grid.count, m = grid[0].count. They are the number of rows and the number of columns. - Fill the first row. A loop with
jover1..<maddsgrid[0][j - 1]togrid[0][j]. Cell(0, 0)stays as it is, and starting at 1 guarantees the left neighbour exists. - Fill the first column. A loop with
iover1..<naddsgrid[i - 1][0]togrid[i][0]. Each step builds on the already updated value above. - Fill the rest. Two nested loops,
iover1..<nandjover1..<m, addmin(grid[i - 1][j], grid[i][j - 1])togrid[i][j]. - Return
grid[n - 1][m - 1], the bottom-right cell.
The solution in Swift
class Solution {
func minPathSum(_ grid: [[Int]]) -> Int {
var grid = grid
let n = grid.count, m = grid[0].count
for j in 1..<m {
grid[0][j] += grid[0][j - 1]
}
for i in 1..<n {
grid[i][0] += grid[i - 1][0]
}
for i in 1..<n {
for j in 1..<m {
grid[i][j] += min(grid[i - 1][j], grid[i][j - 1])
}
}
return grid[n - 1][m - 1]
}
}
Swift details worth a closer look:
var grid = gridshadows the parameter. Inside the method, the namegridnow refers to the new mutable variable. It is legal and common. Swift copies lazily, so the actual duplication happens at the first write, and the caller's array is never changed. After the call, the grid that was passed in still holds its original numbers.- Half-open ranges.
1..<mcounts from 1 up tom - 1. Whenmis 1, the range1..<1is empty, so a one-column grid skips the loop. That works because Swift accepts a range whose two ends are equal. A range whose upper bound is smaller than the lower bound, such as1..<0, crashes, which is what an empty row would trigger. - Compound assignment on nested arrays.
grid[i][j] += ...reads the element, adds, and writes it back in place. It works becausegridis avarand the element type isInt. - Two constants in one
let.let n = grid.count, m = grid[0].countdeclares two separate constants, both inferred asInt. - Overflow.
Intis 64 bits, and overflow would stop the program. Sums of reasonable grids are far below that.
Complexity
Time: O(n * m). The three loops together touch every cell once, doing constant work for each.
Space: O(n * m) as written. The method copies the grid, and the copy serves as the table. The extra space is therefore one value per cell. Modifying the caller's array directly would avoid the copy, but the Swift version can't do that for a plain parameter. A single row of m numbers is enough to hold the running values, which would give O(m) extra space.
Edge cases and common mistakes
- A single cell.
[[5]]returns 5: all three loops run over empty ranges. - A single row or column.
[[1, 2, 3]]returns 6 and[[1], [2], [3]]returns 6, because the first row (or first column) fill-in does the whole job. - Forgetting the first row and column. The general formula reads
grid[i - 1]andgrid[i][j - 1], which don't exist on the border. These cells need their own rule. - Using
0..<m. Starting at 0 would readgrid[0][-1], an index out of range that crashes. - Taking the wrong neighbour. Using the maximum, or adding both neighbours, answers a different question. For the minimum, take the smaller of the two.
- Swapping
nandm. The bottom-right cell isgrid[n - 1][m - 1]:nis the row count andmthe column count. Swapping them returns a wrong cell, or crashes on a non-square grid. - Empty grid.
grid[0]traps, so the method needs at least one cell.
Testing
[[1, 3, 1], [1, 5, 1], [4, 2, 1]]gives7.[[1, 2], [1, 1]]gives3.[[1, 2, 3]]gives6.[[1], [2], [3]]gives6.[[5]]gives5.
