A clear guide to finding the minimum path sum in a grid using dynamic programming.
Problem Restatement
We are given an m x n grid filled with non-negative integers.
We start at the top-left cell and want to reach the bottom-right cell.
At each step, we may move only:
- Right
- Down
A path sum is the sum of all numbers on the path, including the starting cell and the ending cell.
We need to return the minimum possible path sum.
The official constraints are 1 <= m, n <= 200 and 0 <= grid[i][j] <= 200. (leetcode.com)
Input and Output
| Item | Meaning |
|---|---|
| Input | A 2D grid of non-negative integers |
| Output | The minimum path sum from top-left to bottom-right |
| Start | (0, 0) |
| Target | (m - 1, n - 1) |
| Allowed moves | Right or down |
Function shape:
def minPathSum(grid: list[list[int]]) -> int:
...Examples
For:
grid = [
[1, 3, 1],
[1, 5, 1],
[4, 2, 1],
]The minimum path is:
1 -> 3 -> 1 -> 1 -> 1The sum is:
7So the answer is:
7For:
grid = [
[1, 2, 3],
[4, 5, 6],
]One minimum path is:
1 -> 2 -> 3 -> 6The sum is:
12So the answer is:
12First Thought: Try Every Path
A direct recursive solution tries every possible path.
From each cell, we try:
- Move right
- Move down
Then we take the smaller path sum.
This idea is correct, but it repeats work. Many paths may reach the same cell and recompute the minimum cost from that cell again.
Since the grid may be as large as 200 x 200, repeated recursion is unnecessary.
Key Insight
To reach a cell (row, col), the previous step must come from either:
- The cell above:
(row - 1, col) - The cell on the left:
(row, col - 1)
So the minimum cost to reach (row, col) is:
grid[row][col] + min(
cost from above,
cost from left
)This gives the dynamic programming transition:
dp[row][col] = grid[row][col] + min(
dp[row - 1][col],
dp[row][col - 1],
)Base Cases
The starting cell has only one path:
dp[0][0] = grid[0][0]The first row can only be reached from the left:
dp[0][col] = dp[0][col - 1] + grid[0][col]The first column can only be reached from above:
dp[row][0] = dp[row - 1][0] + grid[row][0]Algorithm
Create a DP table with the same size as the grid.
Set the top-left value.
Fill the first row.
Fill the first column.
Then fill every remaining cell using:
dp[row][col] = grid[row][col] + min(
dp[row - 1][col],
dp[row][col - 1],
)Return:
dp[m - 1][n - 1]Correctness
The robot may move only right or down. Therefore every path to cell (row, col) must arrive from either the cell above or the cell on the left.
For the first row, the robot can only move right, so each cell has exactly one possible path from the start. For the first column, the robot can only move down, so each cell also has exactly one possible path.
For every other cell, the best path must use the cheaper of the best path to the cell above and the best path to the cell on the left, then add the current cell value.
The algorithm computes cells in row-major order, so when it computes dp[row][col], both needed previous values are already known. Thus every DP value is correct, and the bottom-right value is the minimum path sum.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(m * n) | Every cell is processed once |
| Space | O(m * n) | The DP table stores one value per cell |
Implementation
class Solution:
def minPathSum(self, grid: list[list[int]]) -> int:
m = len(grid)
n = len(grid[0])
dp = [[0] * n for _ in range(m)]
dp[0][0] = grid[0][0]
for col in range(1, n):
dp[0][col] = dp[0][col - 1] + grid[0][col]
for row in range(1, m):
dp[row][0] = dp[row - 1][0] + grid[row][0]
for row in range(1, m):
for col in range(1, n):
dp[row][col] = grid[row][col] + min(
dp[row - 1][col],
dp[row][col - 1],
)
return dp[m - 1][n - 1]Code Explanation
First get the grid size:
m = len(grid)
n = len(grid[0])Create a DP table:
dp = [[0] * n for _ in range(m)]Initialize the starting cell:
dp[0][0] = grid[0][0]Fill the first row:
for col in range(1, n):
dp[0][col] = dp[0][col - 1] + grid[0][col]Fill the first column:
for row in range(1, m):
dp[row][0] = dp[row - 1][0] + grid[row][0]For each inner cell, choose the cheaper previous route:
dp[row][col] = grid[row][col] + min(
dp[row - 1][col],
dp[row][col - 1],
)Finally return the bottom-right cell:
return dp[m - 1][n - 1]Space Optimization
Each cell only depends on:
- The value above it
- The value to its left
So we can keep one row.
Let dp[col] mean the minimum path sum to the current row at column col.
When processing a cell:
dp[col] = grid[row][col] + min(dp[col], dp[col - 1])Here:
| Expression | Meaning |
|---|---|
dp[col] before update | Cost from above |
dp[col - 1] | Cost from left |
Optimized Implementation
class Solution:
def minPathSum(self, grid: list[list[int]]) -> int:
m = len(grid)
n = len(grid[0])
dp = [0] * n
dp[0] = grid[0][0]
for col in range(1, n):
dp[col] = dp[col - 1] + grid[0][col]
for row in range(1, m):
dp[0] += grid[row][0]
for col in range(1, n):
dp[col] = grid[row][col] + min(
dp[col],
dp[col - 1],
)
return dp[-1]Testing
def run_tests():
s = Solution()
assert s.minPathSum([
[1, 3, 1],
[1, 5, 1],
[4, 2, 1],
]) == 7
assert s.minPathSum([
[1, 2, 3],
[4, 5, 6],
]) == 12
assert s.minPathSum([[5]]) == 5
assert s.minPathSum([[1, 2, 3]]) == 6
assert s.minPathSum([
[1],
[2],
[3],
]) == 6
assert s.minPathSum([
[0, 0],
[0, 0],
]) == 0
print("all tests passed")
run_tests()| Test | Why |
|---|---|
3 x 3 grid | Standard example |
2 x 3 grid | Rectangular example |
| Single cell | Start equals target |
| Single row | Only right moves |
| Single column | Only down moves |
| All zeros | Minimum sum can be zero |