emmm…动态规划就练几个简单的题吧,其他的搞不定,告辞告辞~
如何想到使用DP
- 找最大值/最小值(maximum/minimum)
- 判断是否可行(yes/no)
- 所有可能结果的数量(count)
解题思路
- Matrix DP
- Sequence DP
- Two Sequences DP
- Interval DP
- 其他
Matrix DP
[120] Triangle
自底向上:
public int minimumTotal(List<List<Integer>> triangle) {
if (triangle.size() == 1) return triangle.get(0).get(0);
int n = triangle.size();
int[][] dp = new int[n][n];
// 先算出最底层的值
for (int i = 0; i < triangle.get(n - 1).size(); i++) {
dp[n - 1][i] = triangle.get(n - 1).get(i);
}
// 自底向上
for (int i = n - 2; i >= 0; i--) {
for (int j = 0; j <= i; j++) {
dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + triangle.get(i).get(j);
}
}
return dp[0][0];
}
自顶向下:
public int minimumTotal(List<List<Integer>> triangle) {
if (triangle.size() == 1) return triangle.get(0).get(0);
int n = triangle.size();
int[][] dp = new int[n][n];
dp[0][0] = triangle.get(0).get(0);
// 先要初始化对角线所有点以及左侧边界所有点
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0] + triangle.get(i).get(0);
dp[i][i] = dp[i - 1][i - 1] + triangle.get(i).get(i);
}
// 自顶向下
for (int i = 1; i < n; i++) {
for (int j = 0; j <= i; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i - 1][j - 1]) + triangle.get(i).get(j);
}
}
// 结果要在最后一行里找最大值
int res = dp[n - 1][0];
for (int i = 1; i < n; i++) {
res = Math.min(res, dp[n - 1][i]);
}
return res;
}
参考讲解:
- https://www.youtube.com/watch?v=x1CmmsD7H3E
- https://www.youtube.com/watch?v=ITV2CglqkWU
- https://www.youtube.com/watch?v=37iLUvLGQOs
[62] Unique Paths
public int uniquePaths(int m, int n) {
if (m == 0 || n == 0) return 0;
int[][] dp = new int[m][n];
// 初始化:第1行和第1列某个位置的来源都只有一种情况
// (0,j-1)->(0,j), (j-1,1)->(j,1)
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
// 要么从上边来,要么从左边来
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
[63] Unique Paths II
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
if (m == 0 || n == 0) return 0;
int[][] dp = new int[m][n];
for (int i = 0; i < m; i++) {
if (obstacleGrid[i][0] == 0) dp[i][0] = 1;
else break;
}
for (int j = 0; j < n; j++) {
if (obstacleGrid[0][j] == 0) dp[0][j] = 1;
else break;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 0) dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
else dp[i][j] = 0;
}
}
return dp[m - 1][n - 1];
}
[64] Minimum Path Sum
public int minPathSum(int[][] grid) {
if (grid == null || grid.length == 0 || grid[0].length == 0) return -1;
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m][n];
// 初始化
dp[0][0] = grid[0][0];
for (int i = 1; i < m; i++) dp[i][0] = dp[i - 1][0] + grid[i][0];
for (int j = 1; j < n; j++) dp[0][j] = dp[0][j - 1] + grid[0][j];
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = grid[i][j] + Math.min(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[m - 1][n - 1];
}
Sequence DP
[70] Climbing Stairs
问题:爬楼梯要爬n步才能到达楼顶,每次只能爬1步或者2步,一共有多少种方式爬到楼顶?
思路:斐波那契数列的典型应用。
0级台阶:0
1级台阶:1
2级台阶:2
3级台阶:1+2=3
4级台阶:2+3=5
……
n级台阶:f(n-2)+f(n-1)=f(n)
令dp[i]表示爬到第i阶楼梯可行的不同方式数目,接着寻找当前子问题dp[i]与前面子问题的关系。
- 如果是用两步跳过来的(爬到第i阶楼梯)则
dp[i]=dp[i-2]
,因为这两步已经确定了,那么只有dp[i-2]种可能 - 如果是用一步跳过来的(爬到第i阶楼梯)则
dp[i]=dp[i-1]
,因为这一步已经确定了,那么只有dp[i-1]种可能
public int climbStairs(int n) {
if (n <= 2) return n;
int[] dp = new int[n + 1];
// 初始条件:一级楼梯是一种解法,两级楼梯是两种解法
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
// 可以从第i-1层阶梯过来,也可以从第i-2层阶梯过来,那么到达第i层阶梯的方法数是两个的和
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
由于动态规划的时候只用到了前面2步,因此可以用两个变量记录一下,这样就将空间复杂度降到O(1)了。
public int climbStairs(int n) {
int result = 0;
int num1 = 0, num2 = 1;
for (int i = 1; i <= n; i++) {
result = num1 + num2;
num1 = num2;
num2 = result;
}
return result;
}
参考讲解:
[55] Jump Game
public boolean canJump(int[] nums) {
boolean[] dp = new boolean[nums.length];
dp[0] = true;
for (int i = 1; i < nums.length; i++) {
// 枚举上一步跳到哪儿了,也就是i可以是从哪个j跳过来的
for (int j = 0; j < i; j++) {
if (dp[j] && j + nums[j] >= i) {
dp[i] = true;
break;
}
}
}
return dp[nums.length - 1];
}
public boolean canJump2(int nums[]) {
int maxLocation = 0;
for (int i = 0; i < nums.length; i++) {
// 如果先前的maxLocation小于i,意味着不能到达i位置,因此返回false
if (maxLocation < i) return false;
// greedy
maxLocation = (i + nums[i]) > maxLocation ? i + nums[i] : maxLocation;
}
return true;
}
[300] Longest Increasing Subsequence
public int lengthOfLIS(int[] nums) {
if (nums == null || nums.length == 0) return 0;
int n = nums.length;
// dp[i]表示前i个数并且第i个数是LIS中最后一个数的最长LIS的长度
int[] dp = new int[n];
int max = 0;
for (int i = 0; i < n; i++) {
dp[i] = 1;
// 枚举倒数第二个数是什么
for (int j = 0; j < i; j++) {
// 前面的数比第i个数小,满足递增条件
if (nums[j] < nums[i]) {
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
max = Math.max(max, dp[i]);
}
return max;
}
Two Sequences DP
[72] Edit Distance
问题:给出两个单词word1和word2,计算出将word1 转换为word2的最少操作次数。你总共三种操作方法:插入一个字符、删除一个字符、替换一个字符。
思路:
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
// dp[i][j]表示word1的第i个字符匹配上word2的前j个字符
int[][] dp = new int[m + 1][n + 1];
// 删掉i个字符
for (int i = 0; i < m + 1; i++) dp[i][0] = i;
// 删掉j个字符
for (int j = 0; j < n + 1; j++) dp[0][j] = j;
// 枚举word1的所有位置
for (int i = 1; i < m + 1; i++) {
// 枚举word2的所有位置
for (int j = 1; j < n + 1; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1]));
}
}
}
return dp[m][n];
}
[LintCode 77] Longest Common Subsequence
问题:给出两个字符串,找到最长公共子序列(LCS),返回LCS的长度。
思路:
public int longestCommonSubsequence(String A, String B) {
int m = A.length();
int n = B.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
if (A.charAt(i - 1) == B.charAt(j - 1))
dp[i][j] = dp[i - 1][j - 1] + 1;
}
}
return dp[m][n];
}
[322] Coin Change
问题:给定一组硬币和一个目标金额,返回最少用几个硬币能凑出目标金额,如果不能返回-1。
思路:数组dp用来记录能凑出从1到amount最少的硬币数量。
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
for (int i = 1; i <= amount; i++) {
// dp[i]表示凑齐钱数i需要的最少硬币数
dp[i] = Integer.MAX_VALUE;
for (int j = 0; j < coins.length; j++)
if (i >= coins[j] && dp[i - coins[j]] != Integer.MAX_VALUE)
// 用当前的硬币能组合成啥,取最小
// dp[i + coins[j] ] = min(dp[i + coins[j] ] , dp[i] + 1)
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
参考讲解:
[746] Min Cost Climbing Stairs 爬楼梯的最小代价
问题:我们爬楼梯,每一步都会有代价,例如第i步代价为cost[i],你可以从第0个台阶爬起,也可以从第一个台阶爬起,同时,每次你可以跳一步,也可以跳两步。求爬完楼梯的最小代价是多少。
思路:
- Recursion + Memorization 记忆化递归
dp[n]表示爬到第n阶楼梯的最小代价,我们来思考一下如何才能到第n层呢?是不是只有两种可能性,一个是从第n-2层上直接跳上来,一个是从第n-1层上跳上来。那么dp[n] = min(dp(n-1)+cost[n-1], dp(n-2)+cost[n-2])
,也就是说我可以从第n-1阶爬1步到达第n阶楼梯,但需要cost[n-1]的代价;我也可以从第n-2阶爬2步到达第n阶楼梯,但需要cost[n-2]的代价。最后我们返回最后一个数字dp[n]即可。
public int minCostClimbingStairs(int[] cost) {
int m[] = new int[cost.length + 1];
return dp(cost, m, cost.length);
}
private int dp(int[] cost, int[] m, int i) {
// 在第0阶或第1阶开始爬是不需要代价的
if (i <= 1) return 0;
// 已求解过子问题,直接返回
if (m[i] > 0) return m[i];
return m[i] = Math.min(dp(cost, m, i - 1) + cost[i - 1],
dp(cost, m, i - 2) + cost[i - 2]);
}
- DP
// DP,空间复杂度O(n)
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
// dp[i]表示爬到第i阶楼梯的最小代价
int[] dp = new int[n + 1];
for (int i = 2; i <= n; i++) {
// 要到达第i阶楼梯,可以从第i-1层上花cost[i-1]爬一步上来,也可以从第i-2层上花cost[i-2]爬两步上来
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[n];
}
// 滚动数组,空间复杂度O(1)
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int dp1 = 0;
int dp2 = 0;
for (int i = 2; i <= n; i++) {
int dp = Math.min(dp1 + cost[i - 1], dp2 + cost[i - 2]);
dp2 = dp1;
dp1 = dp;
}
// 因为dp1是最后的dp
return dp1;
}
参考讲解:
- https://www.youtube.com/watch?v=v3WqNLmmBdk
- https://www.youtube.com/watch?v=2Ry-HaMfMjU
- https://zhuanlan.zhihu.com/p/32980698
[LintCode 20] Dice Sum
http://lintcode.com/en/problem/dices-sum
问题:扔n个骰子,所有骰子朝上一面的点数之和为s。输入骰子个数n,求点数之和为s出现的概率。
思路:假设dp(n,x)表示投第n个骰子的时候,点数之和为x出现的次数,投第n个骰子的点数之和只与投第n-1个骰子有关。我们得到递归方程:
f(n,x)=f(n-1,x-1)+f(n-1,x-2)+f(n-1,x-3)+f(n-1,x-4)+f(n-1,x-5)+f(n-1,x-6)
,表示本轮点数之和为n出现次数等于上一轮点数之和为n-1,n-2,n-3,n-4,n-5,n-6出现的次数之和。
public double probability(int n, int x) {
// dp(n,x)表示投第n个骰子的时候,点数之和为x出现的次数,投第n个骰子的点数之和只与投第n-1个骰子有关
int[][] dp = new int[n + 1][6 * n + 1];
// 初始化n=1的情况
for (int j = 1; j <= 6; j++) {
dp[1][j] = 1;
System.out.println("dp[" + 1 + "][" + j + "] = " + dp[1][j]);
}
for (int i = 2; i <= n; i++) {
// i个筛子至少得到i点,至多得到6*i点
for (int j = i; j <= 6 * i; j++) {
// k表示最后一个筛子能取的点数
for (int k = 1; k <= 6; k++) {
// 表示本轮点数之和为n出现次数等于上一轮点数之和为n-1,n-2,n-3,n-4,n-5,n-6出现的次数之和
if (j - k >= 0) dp[i][j] += dp[i - 1][j - k];
}
System.out.println("dp[" + i + "][" + j + "] = " + dp[i][j]);
}
}
// 事件总数为6^n,dp[n][x]除以它就是发生的概率
return dp[n][x] / (Math.pow(6, n));
}
而LintCode上是要我们求所有可能的x出现的概率:
public List<Map.Entry<Integer, Double>> dicesSum(int n) {
// dp(n,s)表示投第n个骰子的时候,点数之和为s出现的次数,投第n个骰子的点数之和只与投第n-1个骰子有关
List<Map.Entry<Integer, Double>> results = new ArrayList<>();
for (int s = n; s <= 6*n; s++) {
// 要用long存储,否则过不了后面大的Test Case
long[][] dp = new long[n + 1][6 * n + 1];
for (int j = 1; j <= 6; j++) {
dp[1][j] = 1;
}
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= 6 * i; j++) {
for (int k = 1; k <= 6; k++) {
// 表示本轮点数之和为n出现次数等于上一轮点数之和为n-1,n-2,n-3,n-4,n-5,n-6出现的次数之和
if (j - k >= 0) dp[i][j] += dp[i - 1][j - k];
}
}
}
// 事件总数为6^n,dp[n][s]除以它就是发生的概率
double probability = dp[n][s] / (Math.pow(6, n));
results.add(new AbstractMap.SimpleEntry<Integer, Double>(s, probability));
}
return results;
}
参考讲解: