Problem overview
The function min_path_sum receives a grid of non-negative integers as a Vec<Vec<i32>> with n rows and m columns. A path goes from the top-left cell to the bottom-right cell, moving only down or right. The function returns the smallest sum of the numbers along such a path.
For [[1, 3, 1], [1, 5, 1], [4, 2, 1]] the answer is 7, from the path 1, 3, 1, 1, 1.
Approach: turn each cell into a running total
Every path to a cell (i, j) arrives either from the cell above or from the cell to the left. So if best(i, j) is the cheapest way to reach (i, j) from the start, then:
best(0, 0)is the cell's own value;- in the first row, a cell can only come from its left neighbour, so
best(0, j) = grid[0][j] + best(0, j - 1); - in the first column, a cell can only come from above, so
best(i, 0) = grid[i][0] + best(i - 1, 0); - everywhere else,
best(i, j) = grid[i][j] + min(best(i - 1, j), best(i, j - 1)).
That is a table to fill row by row. Cells are visited in an order where the cell above and the cell to the left are already finished. Instead of allocating a second table, this lesson overwrites grid itself: after processing, grid[i][j] holds best(i, j), and the answer is the bottom-right cell.
On the example, the first row becomes 1, 4, 5 and the first column becomes 1, 2, 6. The middle cell 5 adds the smaller of 4 (above) and 2 (left), so it becomes 7. The cell at row 1, column 2 adds the smaller of 5 and 7, giving 6. The cell at row 2, column 1 adds the smaller of 7 and 6, giving 8. The last cell, 1, adds the smaller of 6 and 8, giving 7.
Step by step
- Make the grid mutable. The parameter isn't declared
mut, so rebind it withlet mut grid = grid;. The new variable takes over the same vector without copying it. - Read the dimensions:
nis the number of rows andmthe number of columns. - Fill the first row with a loop over
jin1..m, addinggrid[0][j - 1]togrid[0][j]. Cell (0, 0) stays as it is. - Fill the first column with a loop over
iin1..n, addinggrid[i - 1][0]togrid[i][0]. - Fill the rest with two nested loops over
1..nand1..m, adding the smaller of the cell above and the cell to the left. - Return the corner,
grid[n - 1][m - 1], as the final expression.
The solution in Rust
impl Solution {
fn min_path_sum(grid: Vec<Vec<i32>>) -> i32 {
let mut grid = grid;
let n = grid.len();
let m = grid[0].len();
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] += grid[i - 1][j].min(grid[i][j - 1]);
}
}
grid[n - 1][m - 1]
}
}
The Rust details worth a closer look:
- Shadowing.
let mut grid = grid;declares a new variable with the same name, which hides the parameter. The vector is moved, not copied. Writingmut grid: Vec<Vec<i32>>in the signature would do the same job. - Ownership. The function takes the grid by value, so the caller gives it up. Changing it here doesn't affect any variable the caller still has. A caller that needs the original must pass a clone.
- Ranges.
1..mstarts at 1 and stops beforem. Whenmis 1, the range is empty, so a grid with one column skips the first-row loop. The same holds for1..nand a single row. - Compound assignment.
grid[0][j] += grid[0][j - 1]reads one element and writes another. Both are plaini32values, so the borrow checker accepts it. - The min method.
grid[i - 1][j].min(grid[i][j - 1])compares the two neighbours. The method call needs both operands to have the same type,i32. - Overflow. The totals are
i32. A grid whose path sum exceeds 2,147,483,647 would wrap silently in an optimized build, and panic in a debug build.
Complexity
Time: O(n * m). Each cell is updated once: the two edge loops cover the first row and the first column, and the nested loops cover the remaining cells.
Space: O(1) extra. The table is the grid itself, which the function already owns. Only a few integers are added. This is less memory than the memoized recursion of the previous lesson, which needed a separate cache and a call stack.
Edge cases and common mistakes
- A single cell.
[[5]]gives 5: all the loops are empty and the corner is the start. - One row or one column.
[[1, 2, 3]]and[[1], [2], [3]]give 6, the sum of all the cells. - All zeros.
[[0, 0], [0, 0]]gives 0. - Empty grid. It isn't a valid input: reading
grid[0]panics with an out-of-bounds error. - Starting the inner loops at 0. Then
j - 1andi - 1would be computed on ausizeequal to 0, which panics in a debug build and indexes out of bounds in an optimized one. - Using the original values. The neighbours must be the already updated totals, so the order matters. Row by row, left to right is correct.
- Forgetting the edge loops. The middle formula would look for a cell above the first row or left of the first column.
Testing
[[1, 3, 1], [1, 5, 1], [4, 2, 1]]gives 7.[[5]]gives 5.[[1, 2, 3]]gives 6.[[1], [2], [3]]gives 6.[[1, 2], [1, 1]]gives 3.
