文章標題借用了Hawstein的譯文《動態規劃:重新手到專家》。html
動態規劃( Dynamic Programming, DP)是最優化問題的一種解決方法,本質上狀態空間的狀態轉移。所謂狀態轉移是指每一個階段的最優狀態(對應於子問題的解)能夠從以前的某一個或幾個階段的狀態中獲得,這個性質叫作最優子結構。而無論以前這個狀態是如何獲得的,這被稱之爲無後效性。java
DP問題中最經典的莫過於01揹包問題:算法
有N件物品和一個容量爲V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可以使價值總和最大。數組
用子問題定義狀態:即f[i][v]表示前i件物品恰放入一個容量爲v的揹包能夠得到的最大價值;則其狀態轉移方程:post
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
「將前i件物品放入容量爲v的揹包中」這個子問題,若只考慮第i件物品的策略(放或不放),那麼就能夠轉化爲一個只牽扯前i-1件物品的問題。若是不放第i件物品,那麼問題就轉化爲「前i-1件物品放入容量爲v的揹包中」,價值爲f[i-1][v];若是放第i件物品,那麼問題就轉化爲「前i-1件物品放入剩下的容量爲v-c[i]的揹包中」,此時能得到的最大價值就是f[i-1][v-c[i]]再加上經過放入第i件物品得到的價值w[i]。優化
LeetCode題目 | 歸類 |
---|---|
53. Maximum Subarray | 子數組最大和 |
121. Best Time to Buy and Sell Stock | 子數組最大和 |
122. Best Time to Buy and Sell Stock II | 子序列最大和 |
123. Best Time to Buy and Sell Stock III | |
188. Best Time to Buy and Sell Stock IV | |
55. Jump Game | |
70. Climbing Stairs | |
62. Unique Paths | |
63. Unique Paths II | |
64. Minimum Path Sum | 最短路徑 |
91. Decode Ways |
如下代碼既有Java,也有Go。spa
53. Maximum Subarraycode
子數組最大和問題,求解方法可用Kadane算法。htm
121. Best Time to Buy and Sell Stockblog
題目大意:給定數組\(a[..]\),求解\(\max a[j] - a[i] \quad j > i\)。
解決思路:將數組a的相鄰值相減(右邊減左邊)變換成數組b,上述問題轉變成了求數組b的子數組最大和問題.
// Kadane algorithm to solve Maximum subArray problem public int maxProfit(int[] prices) { int maxEndingHere = 0, maxSoFar = 0; for (int i = 1; i < prices.length; i++) { maxEndingHere += prices[i] - prices[i - 1]; maxEndingHere = Math.max(maxEndingHere, 0); maxSoFar = Math.max(maxEndingHere, maxSoFar); } return maxSoFar; }
122. Best Time to Buy and Sell Stock II
以前問題Best Time to Buy and Sell Stock的升級版,對交易次數沒有限制,至關於求解相鄰相減後造成的子序列最大和——只要爲正數,則應計算在子序列內。
public int maxProfit(int[] prices) { int max = 0; for (int i = 1; i < prices.length; i++) { if (prices[i] > prices[i - 1]) { max += (prices[i] - prices[i - 1]); } } return max; }
123. Best Time to Buy and Sell Stock III
最多容許交易兩次。
public int maxProfit(int[] prices) { int sell1 = 0, sell2 = 0; int buy1 = Integer.MIN_VALUE, buy2 = Integer.MIN_VALUE; for (int price : prices) { buy1 = Math.max(buy1, -price); // borrow sell1 = Math.max(sell1, buy1 + price); buy2 = Math.max(buy2, sell1 - price); sell2 = Math.max(sell2, buy2 + price); } return sell2; }
188. Best Time to Buy and Sell Stock IV
最多容許交易k次。當k >= n/2時,在任意時刻均可以進行交易(一次交易包括買、賣),所以該問題退化爲了問題122. Best Time to Buy and Sell Stock II。其餘狀況則有遞推式:
\[ c_{i,j} = \max (c_{i,j-1}, \ \max (c_{i-1,t} - p_t) + p_j),\quad 0 \leq t < j \]
其中,\(c_{i,j}\)表示在\(t\)時刻共\(i\)次交易產生的最大收益。
public int maxProfit(int k, int[] prices) { int n = prices.length; if (n <= 1) { return 0; } // make transaction at any time else if (k >= n / 2) { return maxProfit122(prices); } int[][] c = new int[k + 1][n]; for (int i = 1; i <= k; i++) { int localMax = -prices[0]; for (int j = 1; j < n; j++) { c[i][j] = Math.max(c[i][j - 1], localMax + prices[j]); localMax = Math.max(localMax, c[i - 1][j] - prices[j]); } } return c[k][n - 1]; } public int maxProfit122(int[] prices) { int max = 0; for (int i = 1; i < prices.length; i++) { if (prices[i] > prices[i - 1]) { max += (prices[i] - prices[i - 1]); } } return max; }
55. Jump Game
限制當前最大跳躍數,問是否能到達最後一個index。須要反向日後推演。
public boolean canJump(int[] nums) { int n = nums.length, index = n - 1; for (int i = n - 2; i >= 0; i--) { if (i + nums[i] >= index) index = i; } return index <= 0; }
70. Climbing Stairs
題目大意:每一次能夠加1或加2,那麼從0加到n共有幾種加法?
假定\(d_i\)表示加到i的種數,那麼就有遞推式\(d_i = d_{i-1} + d_{i-2}\)。
func climbStairs(n int) int { if(n < 1) { return 0; } d := make([]int, n+1) d[1] = 1 if n >= 2 { d[2] = 2 } for i := 3; i<=n; i++ { d[i] = d[i-1] + d[i-2] } return d[n] }
62. Unique Paths
題目大意:求解從左上角到右下角的路徑數。
路徑數遞推式:\(c_{i,j}= c_{i-1,j} + c_{i,j-1}\)。
func uniquePaths(m int, n int) int { f := make([][]int, m) for i := range f { f[i] = make([]int, n) } // handle boundary condition: f[][0] and f[0][] f[0][0] = 1 for i := 1; i < m; i++ { f[i][0] = 1 } for j := 1; j < n; j++ { f[0][j] = 1 } for i := 1; i < m; i++ { for j := 1; j < n; j++ { f[i][j] = f[i][j - 1] + f[i - 1][j] } } return f[m-1][n-1] }
63. Unique Paths II
加了限制條件,有的點爲obstacle——不容許經過。上面的遞推式依然成立,只不過要加判斷條件。另外,在實現過程當中能夠用一維數組代替二維數組,好比說按行或按列計算。
public int uniquePathsWithObstacles(int[][] obstacleGrid) { int columnSize = obstacleGrid[0].length; int[] c = new int[columnSize]; c[0] = 1; for (int[] row : obstacleGrid) { for (int j = 0; j < columnSize; j++) { if (row[j] == 1) c[j] = 0; else if (j >= 1) c[j] += c[j - 1]; } } return c[columnSize - 1]; }
64. Minimum Path Sum
題目大意:從矩陣的左上角到右下角的最短路徑。
加權路徑值\(c_{i,j}= \max (c_{i-1,j},c_{i,j-1}) + w_{i,j}\),其中,\(w_{i,j}\)爲圖中邊的權值。
// the shortest path for complete directed graph func minPathSum(grid [][]int) int { var m, n = len(grid), len(grid[0]) f := make([][]int, m) for i := range f { f[i] = make([]int, n) } // handle boundary condition: f[][0] and f[0][] f[0][0] = grid[0][0] for i := 1; i < m; i++ { f[i][0] = f[i - 1][0] + grid[i][0] } for j := 1; j < n; j++ { f[0][j] = f[0][j-1] + grid[0][j] } for i :=1; i < m; i++ { for j := 1; j<n; j++ { if(f[i-1][j] < f[i][j-1]) { f[i][j] = f[i-1][j] + grid[i][j] } else { f[i][j] = f[i][j-1] + grid[i][j] } } } return f[m-1][n-1] }
91. Decode Ways
求解共有多少種解碼狀況。
public int numDecodings(String s) { int n = s.length(); if (n == 0 || (n == 1 && s.charAt(0) == '0')) return 0; int[] d = new int[n+1]; d[n] = 1; d[n - 1] = s.charAt(n - 1) == '0' ? 0 : 1; for (int i = n-2; i >= 0; i--) { if(s.charAt(i) == '0') continue; else if(Integer.parseInt(s.substring(i, i+2)) <= 26) d[i] += d[i + 2]; d[i] += d[i + 1]; } return d[0]; }