【LeetCode題解】動態規劃:從新手到專家(一)
【LeetCode題解】動態規劃:從新手到專家(一)
文章標題借用了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[..]a[..],求解maxa[j]−a[i]j>imaxa[j]−a[i]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。其他情況則有遞推式:
ci,j=max(ci,j−1, max(ci−1,t−pt)+pj),0≤t<jci,j=max(ci,j−1, max(ci−1,t−pt)+pj),0≤t<j
其中,ci,jci,j表示在tt時刻共ii次交易產生的最大收益。
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共有幾種加法?
假定didi表示加到i的種數,那麼就有遞推式di=di−1+di−2di=di−1+di−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
題目大意:求解從左上角到右下角的路徑數。
路徑數遞推式:ci,j=ci−1,j+ci,j−1ci,j=ci−1,j+ci,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
題目大意:從矩陣的左上角到右下角的最短路徑。
加權路徑值ci,j=max(ci−1,j,ci,j−1)+wi,jci,j=max(ci−1,j,ci,j−1)+wi,j,其中,wi,jwi,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];
}
如需轉載,請註明作者及出處.
作者:Treant