212 lines
8.9 KiB
YAML
212 lines
8.9 KiB
YAML
title: Minimum Path Sum
|
||
slug: minimum-path-sum
|
||
difficulty: medium
|
||
leetcode_id: 64
|
||
leetcode_url: https://leetcode.com/problems/minimum-path-sum/
|
||
categories:
|
||
- arrays
|
||
- dynamic-programming
|
||
patterns:
|
||
- dynamic-programming
|
||
- matrix-traversal
|
||
|
||
description: |
|
||
Given a `m x n` `grid` filled with non-negative numbers, find a path from top left to bottom right, which minimizes the sum of all numbers along its path.
|
||
|
||
**Note:** You can only move either down or right at any point in time.
|
||
|
||
constraints: |
|
||
- `m == grid.length`
|
||
- `n == grid[i].length`
|
||
- `1 <= m, n <= 200`
|
||
- `0 <= grid[i][j] <= 200`
|
||
|
||
examples:
|
||
- input: "grid = [[1,3,1],[1,5,1],[4,2,1]]"
|
||
output: "7"
|
||
explanation: "Because the path 1 → 3 → 1 → 1 → 1 minimizes the sum."
|
||
- input: "grid = [[1,2,3],[4,5,6]]"
|
||
output: "12"
|
||
explanation: "The path 1 → 2 → 3 → 6 gives the minimum sum of 12."
|
||
|
||
explanation:
|
||
intuition: |
|
||
Imagine you're navigating a grid where each cell has a cost, and you want to find the cheapest route from the top-left corner to the bottom-right corner.
|
||
|
||
The key insight is that to reach any cell `(i, j)`, you can only arrive from either the cell directly above `(i-1, j)` or the cell directly to the left `(i, j-1)`. This is because you can only move **down** or **right**.
|
||
|
||
Think of it like this: if you know the minimum cost to reach every cell above and to the left of your current position, then the minimum cost to reach your current cell is simply the current cell's value plus the **smaller** of the two incoming paths.
|
||
|
||
This is the essence of **dynamic programming** — we build up the solution by solving smaller subproblems (finding minimum paths to earlier cells) and combining them to solve larger ones.
|
||
|
||
The first row and first column are special cases: cells in the first row can only be reached from the left, and cells in the first column can only be reached from above.
|
||
|
||
approach: |
|
||
We solve this using a **Dynamic Programming** approach:
|
||
|
||
**Step 1: Create a DP table**
|
||
|
||
- Create a 2D array `dp` of the same dimensions as `grid`
|
||
- `dp[i][j]` will store the minimum path sum to reach cell `(i, j)`
|
||
|
||
|
||
|
||
**Step 2: Initialise the starting point**
|
||
|
||
- `dp[0][0] = grid[0][0]`: The cost to reach the starting cell is just its own value
|
||
|
||
|
||
|
||
**Step 3: Fill the first row**
|
||
|
||
- For each cell in the first row, you can only come from the left
|
||
- `dp[0][j] = dp[0][j-1] + grid[0][j]`
|
||
|
||
|
||
|
||
**Step 4: Fill the first column**
|
||
|
||
- For each cell in the first column, you can only come from above
|
||
- `dp[i][0] = dp[i-1][0] + grid[i][0]`
|
||
|
||
|
||
|
||
**Step 5: Fill the rest of the table**
|
||
|
||
- For each remaining cell `(i, j)`, take the minimum of coming from above or from the left
|
||
- `dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]`
|
||
|
||
|
||
|
||
**Step 6: Return the result**
|
||
|
||
- Return `dp[m-1][n-1]`: The minimum path sum to reach the bottom-right corner
|
||
|
||
common_pitfalls:
|
||
- title: Forgetting Boundary Conditions
|
||
description: |
|
||
A common mistake is trying to apply the general recurrence `min(dp[i-1][j], dp[i][j-1])` to all cells, including the first row and column.
|
||
|
||
For cells in the first row, there's no cell above (`i-1` would be `-1`). Similarly, for cells in the first column, there's no cell to the left. These boundary cases must be handled separately.
|
||
wrong_approach: "Applying the same formula to all cells without boundary checks"
|
||
correct_approach: "Handle first row and first column separately before the main loop"
|
||
|
||
- title: Using BFS/DFS Without Memoisation
|
||
description: |
|
||
You might think to use BFS or DFS to explore all paths. While this works, without memoisation you'll recompute the same subproblems many times.
|
||
|
||
For a `200 x 200` grid, the number of unique paths is astronomically large (`C(398, 199)`), and naive exploration will result in **Time Limit Exceeded (TLE)**.
|
||
|
||
Dynamic programming ensures each cell is computed exactly once, giving O(m × n) time complexity.
|
||
wrong_approach: "Recursive DFS exploring all paths without caching"
|
||
correct_approach: "Bottom-up DP filling the table systematically"
|
||
|
||
- title: Not Considering Space Optimisation
|
||
description: |
|
||
While a full `m x n` DP table works, you only need the previous row to compute the current row. This allows reducing space from O(m × n) to O(n).
|
||
|
||
This optimisation is optional but demonstrates deeper understanding of the DP pattern.
|
||
|
||
key_takeaways:
|
||
- "**2D DP pattern**: When a problem involves a grid with constrained movement, consider building a DP table where each cell depends on previously computed cells"
|
||
- "**Optimal substructure**: The minimum path to any cell is determined by the minimum paths to its predecessors — a hallmark of DP problems"
|
||
- "**Space optimisation**: Since each row only depends on the previous row, you can reduce space from O(m × n) to O(n) by reusing a single row array"
|
||
- "**Related problems**: This pattern extends to Unique Paths, Unique Paths II, Triangle, and other grid-based DP problems"
|
||
|
||
time_complexity: "O(m × n). We visit each cell in the grid exactly once to compute its minimum path sum."
|
||
space_complexity: "O(m × n) for the standard solution using a full DP table. Can be optimised to O(n) by reusing a single row, or O(1) by modifying the input grid in place."
|
||
|
||
solutions:
|
||
- approach_name: Dynamic Programming (2D Table)
|
||
is_optimal: true
|
||
code: |
|
||
def min_path_sum(grid: list[list[int]]) -> int:
|
||
m, n = len(grid), len(grid[0])
|
||
|
||
# Create DP table to store minimum path sums
|
||
dp = [[0] * n for _ in range(m)]
|
||
|
||
# Starting point - cost is just the cell value
|
||
dp[0][0] = grid[0][0]
|
||
|
||
# Fill first row - can only come from the left
|
||
for j in range(1, n):
|
||
dp[0][j] = dp[0][j - 1] + grid[0][j]
|
||
|
||
# Fill first column - can only come from above
|
||
for i in range(1, m):
|
||
dp[i][0] = dp[i - 1][0] + grid[i][0]
|
||
|
||
# Fill the rest - take minimum of above or left
|
||
for i in range(1, m):
|
||
for j in range(1, n):
|
||
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
|
||
|
||
# Return minimum path sum to bottom-right corner
|
||
return dp[m - 1][n - 1]
|
||
explanation: |
|
||
**Time Complexity:** O(m × n) — We iterate through each cell once.
|
||
|
||
**Space Complexity:** O(m × n) — We use a separate DP table of the same size as the grid.
|
||
|
||
This approach builds up the solution systematically. Each cell stores the minimum cost to reach it, and we use previously computed values to find the answer efficiently.
|
||
|
||
- approach_name: Space-Optimised DP (Single Row)
|
||
is_optimal: true
|
||
code: |
|
||
def min_path_sum(grid: list[list[int]]) -> int:
|
||
m, n = len(grid), len(grid[0])
|
||
|
||
# Use single row - reuse for each row of the grid
|
||
dp = [0] * n
|
||
|
||
for i in range(m):
|
||
for j in range(n):
|
||
if i == 0 and j == 0:
|
||
# Starting point
|
||
dp[j] = grid[0][0]
|
||
elif i == 0:
|
||
# First row - can only come from left
|
||
dp[j] = dp[j - 1] + grid[i][j]
|
||
elif j == 0:
|
||
# First column - can only come from above
|
||
dp[j] = dp[j] + grid[i][j]
|
||
else:
|
||
# General case - min of above (dp[j]) or left (dp[j-1])
|
||
dp[j] = min(dp[j], dp[j - 1]) + grid[i][j]
|
||
|
||
return dp[n - 1]
|
||
explanation: |
|
||
**Time Complexity:** O(m × n) — Same as the 2D approach.
|
||
|
||
**Space Complexity:** O(n) — We only store one row at a time.
|
||
|
||
This optimisation works because when computing row `i`, we only need values from row `i-1`. The value `dp[j]` before update represents the cell above, and `dp[j-1]` after update represents the cell to the left.
|
||
|
||
- approach_name: In-Place Modification
|
||
is_optimal: false
|
||
code: |
|
||
def min_path_sum(grid: list[list[int]]) -> int:
|
||
m, n = len(grid), len(grid[0])
|
||
|
||
# Modify grid in place - fill first row
|
||
for j in range(1, n):
|
||
grid[0][j] += grid[0][j - 1]
|
||
|
||
# Fill first column
|
||
for i in range(1, m):
|
||
grid[i][0] += grid[i - 1][0]
|
||
|
||
# Fill rest of grid
|
||
for i in range(1, m):
|
||
for j in range(1, n):
|
||
grid[i][j] += min(grid[i - 1][j], grid[i][j - 1])
|
||
|
||
return grid[m - 1][n - 1]
|
||
explanation: |
|
||
**Time Complexity:** O(m × n) — Same traversal pattern.
|
||
|
||
**Space Complexity:** O(1) — No extra space used, but modifies input.
|
||
|
||
This approach achieves O(1) space by using the input grid itself as the DP table. However, it **mutates the input**, which may not be acceptable in all contexts. Use this only when input modification is explicitly allowed.
|