Problem overview
The method minPathSum receives a grid of non-negative numbers, an Array[Array[Int]] with n rows and m columns. You start in the top-left cell, finish in the bottom-right cell, and may only move down or right. The method 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: the path 1, 3, 1, 1, 1 goes right, right, down, down.
Approach: turn each cell into the best sum that reaches it
The earlier solutions of this problem work backwards from the goal with recursion. This one works forwards, with a loop, and it needs no extra table because it writes its results into the grid itself.
After the algorithm runs, grid(i)(j) holds the minimum sum of a path from the top-left cell to (i, j). The reasoning splits cells into four kinds:
- Cell
(0, 0)is the start. Its best path is itself, so it keeps its value. - A cell in the first row can be reached only from its left neighbour, because there is no row above. Its best sum is its own value plus the left neighbour's best sum.
- A cell in the first column can be reached only from above, so the same rule applies with the upper neighbour.
- Any other cell can be reached from above or from the left. The best way to get there is through whichever neighbour has the smaller best sum, so the cell adds the smaller of the two.
Going row by row, left to right, guarantees that both neighbours are final by the time a cell is processed. The answer is the value that ends up in the bottom-right cell.
On the example, the first row becomes [1, 4, 5] and the first column [1, 2, 6]. Cell (1, 1) adds min(4, 2) = 2 to its 5 and becomes 7. Cell (1, 2) adds min(5, 7) = 5 to its 1 and becomes 6. Cell (2, 1) adds min(7, 6) = 6 to its 2 and becomes 8. Finally cell (2, 2) adds min(6, 8) = 6 to its 1 and becomes 7, which is the answer.
Step by step
- Store the sizes. Declare
nasgrid.length(rows) andmasgrid(0).length(columns) withval, because they are used in every loop header. - Fill the first row. For
jover1 until m, addgrid(0)(j - 1)togrid(0)(j). Starting at 1 guarantees the left neighbour exists, and it already holds its updated sum. - Fill the first column. For
iover1 until n, addgrid(i - 1)(0)togrid(i)(0). - Fill the rest. For
iover1 until nand, inside it,jover1 until m, add the smaller ofgrid(i - 1)(j)andgrid(i)(j - 1)togrid(i)(j), usingmath.min. - Return
grid(n - 1)(m - 1)as the last expression of the method.
The solution in Scala
class Solution {
def minPathSum(grid: Array[Array[Int]]): Int = {
val n = grid.length
val m = grid(0).length
for (j <- 1 until m) {
grid(0)(j) += grid(0)(j - 1)
}
for (i <- 1 until n) {
grid(i)(0) += grid(i - 1)(0)
}
for (i <- 1 until n) {
for (j <- 1 until m) {
grid(i)(j) += math.min(grid(i - 1)(j), grid(i)(j - 1))
}
}
grid(n - 1)(m - 1)
}
}
The Scala details worth a closer look:
- Compound assignment on array elements.
grid(0)(j) += xis shorthand forgrid(0)(j) = grid(0)(j) + x. It works becauseArrayis mutable, even thoughgriditself is a parameter and can't be reassigned. - The input is modified. Arrays are passed by reference, so the caller's grid is overwritten with the running sums. After the call on the example, it holds
[[1, 4, 5], [2, 7, 6], [6, 8, 7]]. Copy the grid first, for example withgrid.map(_.clone), if the original is still needed. untilexcludes the end.1 until mcounts 1, 2, ...,m - 1. Ifmis 1, the range is empty and the loop body never runs.- Two indexes, two parentheses. A cell is
grid(i)(j): first the row, then the column. - Overflow. Sums are stored in
Int, which wraps silently beyond about 2.1 billion. For the lesson's grids this does not matter.
Complexity
Time: O(n * m). Each cell is updated exactly once, with a constant amount of work.
Space: O(1) extra. The running sums overwrite the input, so no second table is allocated and there is no recursion. This is the difference from the memoized version, which needs a cache of n * m entries and a call stack.
Edge cases and common mistakes
- A single cell. For
[[5]]all three loops are empty and the answer is 5. - A single row or column.
[[1, 2, 3]]and[[1], [2], [3]]both give 6, from the first-row or first-column loop alone. - Skipping the border loops. Running only the nested loop from index 1 would leave the first row and column as plain values, so the paths along the edges would be missed.
- Looping from 0. Starting the nested loop at 0 would read
grid(-1)(j)and throwArrayIndexOutOfBoundsException. - Using the original values. Adding a neighbour's original value instead of its updated sum gives a result that only looks one step back.
- Reusing the grid. Calling the method twice on the same array keeps accumulating, and the second answer is wrong.
Testing
[[1, 3, 1], [1, 5, 1], [4, 2, 1]]gives7.[[5]]gives5.[[1, 2, 3]]gives6.[[1], [2], [3]]gives6.[[0, 0], [0, 0]]gives0.
