由于对动态规划DP算法 掌握得不是很好,所以决定进行动态规划专项训练。
动态规划五部曲
①确定dp[i]含义
②递推公式
③dp数组如何初始化
④遍历顺序
⑤打印dp数组(debug)
除了第五条在力扣上不开会员无法实现外,其余四项就是做出dp类型题目的关键,后续的训练中将按照这四步来进行解题。
力扣第509题-斐波那契数

1.本题的动态规划四部曲:
①dp[i]含义:代表第i个斐波那契数的值。
②递推公式:题目中已经告诉我们了,dp[i] = dp[i -- 1] + dp[i -- 2]。
③初始化:也在题目中,dp[0] = 0,dp[1] = 1。
④遍历顺序:从前向后
2.本题是动态规划入门级题目,可写出完整代码如下:
cpp
1. int fib(int n) {
2. int dp[31] = {0};
3. dp[0] = 0;
4. dp[1] = 1;
5.
6. if (n >= 2){
7. for (int i = 2; i <= n; i++){
8. dp[i] = dp[i - 1] + dp[i - 2];
9. }
10. }
11.
12. return dp[n];
13. }
该算法时间复杂度为O(n),空间复杂度为O(n)。可以通过只保存当前遍历下标的前两个元素来将空间复杂度降为O(1):
cpp
1. int fib(int n) {
2. int a = 0;
3. int b = 1;
4. if (n == 0) return a;
5. if (n == 1) return b;
6.
7. for (int i = 2; i <= n; i++){
8. int tmp = a;
9. a = b;
10. b += tmp;
11. }
12.
13. return b;
14.
15. }
3.需要注意的是,对于定义数组并赋值为0的代码:
cpp
1. int dp[10] = {0}; // ✅ 完全正确
2. int n = 5;
3. int dp[n + 1]; // ✅ 定义可以
4. int dp[n + 1] = {0}; // ❌ 初始化 不可以!
固定长度的数组可以直接进行赋值为0的初始化,而变长数组(数组长度中含有变量)只能进行定义,不能进行赋值为0的初始化操作。
4.本题也是递归类型题目的入门题,所以用递归再写一遍:
cpp
1. int recursion(int n, int start, int a, int b){
2. if (start == n){
3. return a + b;
4. }
5.
6. int tmp = a;
7. a = b;
8. b = tmp + b;
9. return recursion(n, start + 1, a, b);
10. }
11.
12. int fib(int n) {
13. if (n == 0){
14. return 0;
15. }
16. if (n == 1){
17. return 1;
18. }
19.
20. return recursion(n, 2, 0, 1);
21. }
该算法时间复杂度为O(n),空间复杂度为O(1)。
力扣第70题-爬楼梯

1.本题的动态规划四部曲:
①dp[i]含义:代表爬到第i层台阶的方法数。
②递推公式:爬到该层楼梯只能通过1步或者2步到达,所以方法数等于前1层台阶的方法数与前2层台阶的方法数之和,dp[i] = dp[i -- 1] + dp[i -- 2]。
③初始化:到达第1层有1种方法,所以dp[1] = 1,而到达第二层有两种方法,即1步1步到达与直接2步到达,所以dp[2] = dp[1] + dp[0] = 2,初始化dp[0] = 1。
④遍历顺序:从前向后
2.基于以上思想,可写出完整代码如下:
cpp
1. // 爬楼梯:滚动变量(迭代)写法 ------ 空间 O(1),最优解
2. int climbStairs(int n) {
3. // 定义两个滚动变量
4. // a 保存 dp[i-2] 的值(前前台阶的方法数)
5. int a = 1;
6. // b 保存 dp[i-1] 的值(前一台阶的方法数)
7. int b = 1;
8.
9. // 特殊情况:n=1,直接返回 b(只有1种方法)
10. if (n == 1) return b;
11.
12. // 从第 2 阶开始,一直递推到第 n 阶
13. for (int i = 2; i <= n; i++){
14. int tmp = a; // 临时保存旧的 a(dp[i-2])
15. a = b; // a 滚动更新:变成 dp[i-1]
16. b += tmp; // b 滚动更新:新b = 旧b + 旧a → dp[i] = dp[i-1]+dp[i-2]
17. }
18.
19. // 循环结束后,b 就是 dp[n],即答案
20. return b;
21. }
该算法时间复杂度为O(n),空间复杂度为O(1)。
力扣第746题-使用最小花费爬楼梯

1.本题的动态规划四部曲:
①dp[i]含义:因为从该层台阶向上跳才会收取费用,所以本题dp[i]的含义为在第i层台阶向上跳所需要的最小花费。
②递推公式:爬到该层楼梯只能通过1步或者2步到达,所以爬到本层的最小花费应该是爬到前两层的最少花费与本层花费之和,dp[i] = cost[i] + fmin(dp[i - 1], dp[i - 2])。需要注意的是,楼顶的下标为costSize而不是costSize -- 1,所以最后一次循环的时候需要使用的代码为:dp[costSize] = fmin(dp[costSize - 1], dp[costSize - 2])。
③初始化:dp[0] = cost[0],dp[1] = cost[1](相比于从0层爬到1层,肯定是直接从1层出发花费更少)。
④遍历顺序:从前向后
2.基于以上思想,可写出完整代码如下:
cpp
1. int minCostClimbingStairs(int* cost, int costSize) {
2. // 1. 定义dp数组
3. // dp[i] = 到达第 i 个台阶时,累计的最小花费
4. // 题目限制 costSize <= 1000,所以开 1001 足够
5. int dp[1001] = {0};
6.
7. // 2. 初始化基准条件
8. // 可以从下标 0 或 1 开始爬,所以:
9. dp[0] = cost[0]; // 到达第 0 阶的最小花费 = cost[0]
10. dp[1] = cost[1]; // 到达第 1 阶的最小花费 = cost[1]
11.
12. // 3. 特殊情况:只有 2 阶台阶时,直接取两者最小值(可以从0或1直接到顶)
13. if (costSize == 2) return fmin(dp[0], dp[1]);
14.
15. // 4. 递推计算:从第 2 阶开始,一直算到楼梯顶部(第 costSize 阶)
16. for (int i = 2; i <= costSize; i++){
17. // 5. 特殊处理:到达楼梯顶部(i == costSize)
18. // 顶部不需要支付 cost,所以直接取前两阶的最小值
19. if (i == costSize){
20. dp[i] = fmin(dp[i - 1], dp[i - 2]);
21. }
22. // 6. 普通台阶:到达第 i 阶的最小花费 = cost[i] + 前两阶的最小值
23. // 因为要到达 i 阶,必须先付 cost[i],再从 i-1 或 i-2 阶爬上来
24. else {
25. dp[i] = cost[i] + fmin(dp[i - 1], dp[i - 2]);
26. }
27. }
28.
29. // 7. 最终答案:到达楼梯顶部(第 costSize 阶)的最小花费
30. return dp[costSize];
31. }
该算法的时间复杂度和空间复杂度均为O(n)。
3.实际上本题更符合题意、更清晰的做法应该是:
①dp的含义为到达该层台阶所需要的最小花费。
②递推公式为dp[i] = fmin(cost[i - 1] + dp[i - 1], cost[i - 2] + dp[i - 2]),即要么从前1层跳上来,要么从前2层跳上来,最小花费为跳到前面那层的费用以及从那层起跳的费用。
③初始化:dp[0] = 0,dp[1] = 0。
这样做就不需要额外讨论costSize == 2的边界情况。完整代码如下:
cpp
1. int minCostClimbingStairs(int* cost, int costSize) {
2. int dp[1001];
3.
4. // dp[i] = 到达第 i 级台阶(还没往上跳)时的最小花费
5. // 重点:我们站在台阶上时,还没支付这个台阶的费用!
6.
7. dp[0] = 0; // 站在第 0 级台阶,不需要花费
8. dp[1] = 0; // 站在第 1 级台阶,不需要花费
9.
10. // 从第 2 级开始,一直推到 顶部(costSize)
11. for (int i = 2; i <= costSize; i++){
12. // 核心状态转移方程(超级关键)
13. // 到达第 i 级台阶,有两种方法:
14. // 1. 从 i-1 跳 1 步上来 → 花费 cost[i-1] + dp[i-1]
15. // 2. 从 i-2 跳 2 步上来 → 花费 cost[i-2] + dp[i-2]
16. // 取最小的那个!
17. dp[i] = fmin(cost[i - 1] + dp[i - 1], cost[i - 2] + dp[i - 2]);
18. }
19.
20. // dp[costSize] 就是到达楼梯顶部的最小花费
21. return dp[costSize];
22. }