文章标题借用了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]; }