1. 动态规划理论基础
- 动态规划与贪心:动态规划是由前一个状态推导出来;贪心是局部选最优;
- 动态规划五部曲:
- 确定 dp 数组以及下标的含义;
- 确定递推公式;
- 确定 dp 数组如何初始化;
- 确定遍历顺序;
- 举例推导 dp 数组;
- 动态规划 debug:如果写出的代码没通过,先打印出来 dp 数组,看和自己的预期是不是一样,如果不一样那就是代码实现有问题;如果和预期一样,那就是 dp 数组初始化、递推公式、遍历顺序有问题。
2. 斐波那契数
- 题目链接:https://leetcode.cn/problems/fibonacci-number/description/
- 思路:注意求 n 为0、1时的边界条件不能用动态规划递推;然后 n > 1 时递推公式已经给了
dp[n] = dp[n-2] + dp[n-1]
,初始化为0、1,从前往后遍历;注意可以直接用一个二维数组代替 dp 数组。 - 代码实现:
class Solution {
public int fib(int n) {
if(n < 2) {
return n;
}
int[] dp = new int[2];
dp[0] = 0;
dp[1] = 1;
for(int i = 2; i <= n; ++i) {
int temp = dp[1];
dp[1] = dp[1] + dp[0];
dp[0] = temp;
}
return dp[1];
}
}
3. 爬楼梯
- 题目链接:https://leetcode.cn/problems/climbing-stairs/
- 思路:第一层直接爬一阶上来,第二层可以走两个一阶或者一次走一个两阶上来–两种方法,第三层可以从第一层一次两阶上来也可以从第二层爬一阶上来–到达第三层的方法就是到达第一层和第二层的方法之和。dp[i] – 到达第 i 层有多少种方法,状态转移:
dp[i] = dp[i - 1] + dp[i - 2]
,初始条件:第1层为1种,第2层为2种,第0层不用管。 - 代码实现:
class Solution {
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 + 1; ++i) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
4. 使用最小花费爬楼梯
- 题目链接:https://leetcode.cn/problems/min-cost-climbing-stairs/
- 思路:动态规划
- dp[i] – 达到第 i 层需要花费的体力;
- 递推公式:第 i 层可以从 i-1 或者 i-2 层爬上来,那么花费的体力应该是到达这两层花费的体力和离开这一层的成本之和的小者,即
dp[i] = Min{dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]}
; - 初始化:dp[0] = 0, dp[1] = 0;
- 遍历顺序:从前往后
- 代码实现:
class Solution {
public int minCostClimbingStairs(int[] cost) {
int[] dp = new int[cost.length + 1];
dp[0] = 0;
dp[1] = 0;
for(int i = 2; i < dp.length; ++i) {
dp[i] = Math.min(dp[i - 2] + cost[i - 2], dp[i - 1] + cost[i - 1]);
}
return dp[dp.length - 1];
}
}
5. 动态规划周总结
6. 不同路径
- 题目链接:https://leetcode.cn/problems/unique-paths/
- 思路:
- dp二维数组,dp[i][j],到达 i,j 位置有多少种路径;
- 递推公式:每一个位置只能从其上边或者左边移动过来 –
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
; - 初始化:第一行和第一列只有一种路径过来;
- 遍历:从左上到右下,已经初始化的第一行第一列不需要遍历
- 代码实现:
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i = 0; i < m; ++i) {
dp[i][0] = 1;
}
for(int i = 0; i < n; ++i) {
dp[0][i] = 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];
}
}
7. 不同路径 II
- 题目链接:https://leetcode.cn/problems/unique-paths-ii/description/
- 思路:与不同路径基本相同,对于障碍物的处理,在初始化时如果有障碍物,第一行/列障碍物之后的位置都无法到达;对于状态转移方程,如果当前单元格是障碍物,就无法到达,那么到达路径数直接设为0,否则是从正上方或者正左方到达的路径之和。
- 代码实现:
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
for(int i = 0; i < m; ++i) {
if(obstacleGrid[i][0] != 0) {
break;
}
dp[i][0] = 1;
}
for(int i = 0; i < n; ++i) {
if(obstacleGrid[0][i] != 0) {
break;
}
dp[0][i] = 1;
}
for(int i = 1; i < m; ++i) {
for(int j = 1; j < n; ++j) {
dp[i][j] = obstacleGrid[i][j] == 0 ? dp[i - 1][j] + dp[i][j - 1] : 0;
}
}
return dp[m - 1][n - 1];
}
}
8. 整数拆分
- 题目链接:https://leetcode.cn/problems/integer-break/
- 思路:
- dp[i]:数值i能够拆分出来的最大乘积;
- 递推公式:
dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
,j * (i - j) – 对i只拆成两个数、j * dp[i - j] – 对i拆成两个及两个以上的数; - 初始化:0、1没有拆分的意义,初始化2的拆分 dp[2] = 1;
- 遍历:从3开始。
- 代码实现:
class Solution {
public int integerBreak(int n) {
int[] dp = new int[n + 1];
dp[2] = 1;
for(int i = 3; i <= n; ++i) {
for(int j = 1; j < i; ++j) {
dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
}
}
return dp[n];
}
}
9. 不同的二叉搜索树
- 题目链接:https://leetcode.cn/problems/unique-binary-search-trees/description/
- 思路:i个节点组成的二叉搜索树的种类可以取从1到i分别作为根节点,将每个根节点情况的搜索树的种类累加;每种情况下树的种类是左右子树的种类之即积,对应每种情况下左右子树的情况是:
- 1为根节点–左子树0个节点组成搜索树的种类*右子树i-1个节点做成搜索树的种类;
- 2为根节点–左子树1个节点组成搜索树的种类*右子树i-2个节点组成搜索树的种类;
- …
- i为根节点–做字数i-1个节点组成搜索数的种类*右子树0个节点组成搜索树的种类;
- 0个节点组成搜索树的种类就是一个空树,种类为1;
- 1个节点组成的搜索树和单独一个节点组成的搜索树的种类数相同。
- 代码实现:
class Solution {
public int numTrees(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
for(int i = 1; i <= n; ++i) {
for(int j = 0; j < i; ++j) {
dp[i] += dp[j] * dp[i - j - 1];
}
}
return dp[n];
}
}
10. 动态规划周总结
11. 0-1背包理论基础1
- 题目链接:null
- 思路:二维dp数组实现0-1背包
- dp数组及下标含义:dp[i][j] – 从物品0~i任意取,放进容量为j的背包能够得到的最大价值;
- 递推公式:如果容量j放得下当前物品i,那么可以选择放该物品也可以不放该物品;放该物品 – 最大价值应该是当前物品价值 value[i] + 放该物品之前剩余容量能够获得的最大价值dp[i - 1][j - weight[i]];
- 初始化:对于j = 0,容量为0,什么都放不了,价值为0,即第一列为 0;对于第一行i = 0,物品0放进不同容量的背包,最大价值就看能不能放下,能放下价值就是物品0的价值value[0],否则就什么也没放,价值为0;
- 遍历顺序:可以先遍历物品,每遍历到一个物品,然后遍历依次增大的背包容量,确定对应背包容量下能够得到的最大价值;也可以先遍历背包容量,然后遍历该容量下不断增加可以选择的物品种类时对应的最大价值;本质上遍历到每个元素,用到的都是矩阵左上角已经计算过的结果,这里选择先遍历物品再遍历背包容量。
- 代码实现:
package com.dp;
public class Knapsack01 {
public static void main(String[] args) {
int[] weight = {1,3,4};
int[] value = {15,20,30};
int bagSize = 4;
testWeightBagProblem(weight,value,bagSize);
}
private static void testWeightBagProblem(int[] weight, int[] value, int bagSize) {
int[][] dp = new int[weight.length][bagSize + 1];
for(int j = 0; j <= bagSize; ++j) {
if(j >= weight[0]) {
dp[0][j] = value[0];
}
}
for(int i = 1; i < weight.length; ++i) {
for(int j = 1; j <= bagSize; ++j) {
if (weight[i] > j) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
for(int i = 0; i < dp.length; ++i) {
for (int j = 0; j < dp[0].length; j++) {
System.out.print(dp[i][j] + "\t");
}
System.out.println();
}
}
}
12. 0-1背包理论基础2
- 题目链接:null
- 思路:通过滚动数组减少空间复杂度,使用一维数组实现0-1背包;
- dp数组及下标含义:dp[j] – 容量为j的背包能够获得的最大价值,随着物品的遍历更新;
- 递推公式:当前容量能够放下当前物品才有放进去递推更新的必要 – 不放当前的物品,直接就是原来的 dp[j],放当前物品,就是 value[i] + dp[j - weight[i]],取二者之间的大的;
- 初始化:j为0时最大价值肯定为0,其他的都可以根据后面的遍历来获得;
- 遍历顺序:先遍历物品,再倒序遍历背包容量;如果正序遍历背包容量,那么后面的容量取dp[j - weight[i]]时对应的元素就被覆盖了,所以只能倒序遍历背包容量;而如果先倒序遍历背包容量,再遍历物品,每个dp[j]就只会放一个物品。归根到底,二维数组中是右下角的值依赖于其左上方的,使用滚动一维数组,就要先遍历物品再遍历背包容量,不能提前覆盖–倒序。
- 代码实现:
package com.dp;
public class Knapsack01 {
public static void main(String[] args) {
int[] weight = {1,3,4};
int[] value = {15,20,30};
int bagSize = 4;
testWeightBagProblem2(weight,value,bagSize);
}
private static void testWeightBagProblem2(int[] weight, int[] value, int bagSize) {
int[] dp = new int[bagSize + 1];
dp[0] = 0;
for(int i = 0; i < weight.length; ++i) {
for(int j = bagSize; j >= weight[i]; --j) {
dp[j] = Math.max(dp[j], value[i] + dp[j - weight[i]]);
}
}
for (int j = 0; j < dp.length; j++) {
System.out.print(dp[j] + "\t");
}
}
13. 分割等和子集
- 题目链接:https://leetcode.cn/problems/partition-equal-subset-sum/
- 思路:先统计数组的总和,然后将总和的一半
target
作为背包容量 – 0-1背包:数组nums
中的每个元素都是物品,物品的重量和价值都是元素的值,确定背包容量为target
时能够装进去的最大价值是否和target
相同 – 相同就是刚好有不同的元素和为总和一半;
- dp数组:
dp[j]
容量为j的背包能够装进去的最大价值; - 递推公式:
dp[j] = Math.max(dp[j], nums[i] + dp[j - nums[i]])
; - 遍历顺序:先正序遍历物品,再倒序遍历背包容量;每次背包容量遍历完,判断是否满足
dp[target] == target
;
- 代码实现:
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for(int num : nums) {
sum += num;
}
if(sum % 2 != 0) {
return false;
}
int target = sum / 2;
int[] dp = new int[target + 1];
for(int i = 0; i < nums.length; ++i) {
for(int j = target; j >= nums[i]; --j) {
dp[j] = Math.max(dp[j], nums[i] + dp[j - nums[i]]);
}
if(target == dp[target]) {
return true;
}
}
return false;
}
}
14. 斐波那契数
15. 斐波那契数
16. 斐波那契数
17. 斐波那契数
18. 斐波那契数
19. 斐波那契数
20. 斐波那契数
21. 斐波那契数