1 題目描述
給定一個m x n的矩陣,找到一條從左上角到右下角的最小路徑和。每次只能向右或是向下移動。
2 解法
這題與LeetCode 課前預習 - 掌握 Dynamic Programming 的思維指南的題目類似,如果你會做Leetcode-62-Unique Paths,這題也是一樣的概念,只是這題是找最小路徑和。
2.1 Recursive
首先我們很清楚,我們只能往右或是往下走,假設i是row,j是column,
所以我們可以寫出以下的關係式
- 往右:
helper(i, j+1)
column + 1
- 往下:
helper(i+1, j)
row + 1
- 然後關係式:
min(往右, 往下) + 當前
1
| min(helper(i, j+1), helper(i+1, j)) + grid[i][j]
|
接下來我們來解決最小問題的狀況:
- 如果只有一行或是一列,直接回傳總和,因為只有一條路可以走
- 何時知道走到底,當
i==rows-1
和j==cols-1
時,就是走到底了,直接回傳grid[i][j]
1 2
| if i == rows-1 and j == cols-1: return grid[i][j]
|
我們就可以開始寫程式了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def minPathSum(self, grid: List[List[int]]) -> int: def helper(i: int, j: int) -> int: if row == 1 or column == 1: return sum(grid) if i == row - 1 and j == column - 1: return grid[i][j] right, down = float('inf'), float('inf') if j < column - 1: right = helper(i, j + 1) if i < row - 1: down = helper(i + 1, j) return min(right, down) + grid[i][j]
row = len(grid) column = len(grid[0]) return helper(0, 0)
|
2.2 Recursive + Memoization
接下來下一步是為了減少重複運算,我們加入Memoization,這樣就不用一直重複計算了。這裡我們可以用一個dp
的list來記錄已經計算過的值,我們可以先建立一個與grid
一樣大小的list,然後每次計算過的值就存進去,下次再遇到就直接回傳。
建立與matrix相同大小的dp,該dp紀錄起點走到dp[i][j]
的最佳解
1 2 3
| row = len(grid) column = len(grid[0]) dp = [[0]*column for _ in range(row)]
|
然後設計斷點
- 如果走到底了,直接回傳
grid[i][j]
- 如果只有一行或是一列,直接回傳總和
- 如果dp不是0,直接回傳dp已經算過的內容
1 2 3 4 5 6 7
| if row == 1 or column == 1: return sum(grid) if dp[i][j] != 0: return dp[i][j] if i == row - 1 and j == column - 1: dp[i][j] = grid[i][j] return dp[i][j]
|
那我們就可以開始寫完整的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| def minPathSum2(self, grid: List[List[int]]) -> int: def helper(i: int, j: int) -> int: if row == 1 or column == 1: return sum(grid) elif dp[i][j] != -1: return dp[i][j] elif i == row - 1 and j == column - 1: dp[i][j] = grid[i][j] return dp[i][j]
right, down = float('inf'), float('inf') if j < column - 1: right = helper(i, j + 1) if i < row - 1: down = helper(i + 1, j) dp[i][j] = min(right, down) + grid[i][j] return dp[i][j]
row = len(grid) column = len(grid[0]) dp = [[-1 for _ in range(column)] for _ in range(row)] helper(0, 0) return dp[0][0]
|
2.3 Iterative + Tabulation
接下來我們使用Iterative就不會因為stack size超過導致出錯。這裡我們可以從終點走到起點,一步步計算出所有cell的最佳解,最終走到起點,回傳dp[0][0]
就是答案。
建立一個dp
的list,裡面的值是當前的最佳解
1 2 3
| row = len(grid) column = len(grid[0]) dp = [[0 * column] for _ in range(row)]
|
然後我們從終點慢慢往起點計算
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| for i in range(row-1, -1, -1): for j in range(column-1, -1, -1): if i == row-1 and j == column - 1: dp[i][j] = grid[i][j] elif i == row - 1: dp[i][j] = grid[i][j] + dp[i][j+1] elif j == column - 1: dp[i][j] = grid[i][j] + dp[i+1][j] else: dp[i][j] = grid[i][j] + min(dp[i][j+1], dp[i+1][j])
|
最後完整程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| def minPathSum3(self, grid: List[List[int]]) -> int: row = len(grid) column = len(grid[0]) dp = [[0 * column] for _ in range(row)] for i in range(row-1, -1, -1): for j in range(column-1, -1, -1): if i == row-1 and j == column - 1: dp[i][j] = grid[i][j] elif i == row - 1: dp[i][j] = grid[i][j] + dp[i][j+1] elif j == column - 1: dp[i][j] = grid[i][j] + dp[i+1][j] else: dp[i][j] = grid[i][j] + min(dp[i][j+1], dp[i+1][j]) return dp[0][0]
|
他還有另一個變化,如果不想要寫這麼多if else
可以把dp
都塞flout('inf')
,並且在第一次塞dp
也就是塞終點的時候,在比較min
的時候,確保第一次比較的值是0
即可。大概要做以下工作:
- 多擴展一層,這樣for loop的時候
i+1
跟j+1
不會超過邊界
- 然後塞終點到
dp
時,要確保第一個比較的值是0也就是dp[row-1][column] = 0
1 2 3 4
| row, column = len(grid), len(grid[0])
dp = [[float('inf')] * (column + 1) for _ in range(row + 1)] dp[row-1][column] = 0
|
for loop從終點開始往起點塞dp
1 2 3 4
| for i in range(row-1, -1, -1): for j in range(column-1, -1, -1): dp[i][j] = grid[i][j] + min(dp[i][j+1], dp[i+1][j])
|
完整程式碼
1 2 3 4 5 6 7 8 9 10 11 12
| def minPathSum3_2(self, grid: List[List[int]]) -> int: row, column = len(grid), len(grid[0]) dp = [[float('inf')] * (column + 1) for _ in range(row + 1)] dp[row-1][column] = 0
for i in range(row-1, -1, -1): for j in range(column-1, -1, -1): dp[i][j] = grid[i][j] + min(dp[i][j+1], dp[i+1][j]) return dp[0][0]
|
2.4 Iterative with Constant Space
最後老樣子,我們能不能讓dp
的空間更省,其實當我們計算完一排row時,我們只會用到上一排計算完row的值,上上次的就不會用到了,因此dp
的row其實只要兩排就好了。
1 2 3
| row, column = len(grid), len(grid[0]) dp = [[float('inf')] * (column+1) for _ in range(2)] dp[0][column] = 0
|
但是要注意的是,我們每次走完一排,就要把算完的dp[0]
更新到dp[1]
並且,dp[0][column]
要設定為flout('inf')
,這樣下一次才能正確的更新dp[0][j]
。
1 2 3 4 5 6
| for i in range(row - 1, -1, -1): for j in range(column - 1, -1, -1): dp[0][j] = grid[i][j] + min(dp[0][j + 1], dp[1][j]) dp[1] = dp[0] dp[0][column] = float('inf') return dp[0][0]
|
完整程式碼
1 2 3 4 5 6 7 8 9 10 11 12
| def minPathSum4(self, grid: List[List[int]]) -> int: row, column = len(grid), len(grid[0]) dp = [[float('inf')] * (column+1) for _ in range(2)] dp[0][column] = 0
for i in range(row - 1, -1, -1): for j in range(column - 1, -1, -1): dp[0][j] = grid[i][j] + min(dp[0][j + 1], dp[1][j]) dp[1] = dp[0] dp[0][column] = float('inf') return dp[0][0]
|
3 總結
看到這種matrix要找到最佳路徑有幾個準則:
- Recursive:
min(左邊, 下面)+當前
就可以找到最佳解
- Interative: 從終點開始往起點找到最佳解