文章標題借用了Hawstein的譯文《動態規划:從新手到專家》。
1. 概述
動態規划( Dynamic Programming, DP)是最優化問題的一種解決方法,本質上狀態空間的狀態轉移。所謂狀態轉移是指每個階段的最優狀態(對應於子問題的解)可以從之前的某一個或幾個階段的狀態中得到,這個性質叫做最優子結構。而不管之前這個狀態是如何得到的,這被稱之為無后效性。
DP問題中最經典的莫過於01背包問題:
有N件物品和一個容量為V的背包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入背包可使價值總和最大。
用子問題定義狀態:即f[i][v]表示前i件物品恰放入一個容量為v的背包可以獲得的最大價值;則其狀態轉移方程:
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]。
2. 題解
| 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。
53. Maximum Subarray
子數組最大和問題,求解方法可用Kadane算法。
121. Best Time to Buy and Sell Stock
題目大意:給定數組\(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}\)表示在\(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];
}
