理论基础
对于动态规划问题,可以拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!
动规五部曲:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化 🦄
- 确定遍历顺序
- 举例推导dp数组
一般人都是凭感觉做题。
509.斐波那契数
题目链接:509.斐波那契数
难度指数:😀😐
很简单的动规入门题,但简单题是用来掌握方法论的,还是要用动规五部曲来分析。
一般来说,斐波那契数用递归来写 (代码比较简单)
不过本题,我们要使用**动态规划(DP)**的思想来求斐波那契数列。
1️⃣ 确定dp数组(dp table)以及下标的含义
在做动态规划题目时,都要定义一个一维或二维数组 dp[ ](用来做状态转移)
dp[i]:第i个斐波那契数值为 dp[i]。
2️⃣ 确定递推公式 (核心)
本题很简单,因为**题目已经把递推公式直接给我们了:
状态转移方程(递推公式) 为 dp[i] = dp[i - 1] + dp[i - 2];
3️⃣ dp数组如何初始化 ?
4️⃣ 确定遍历顺序
从递推公式 dp[i] = dp[i - 1] + dp[i - 2]; 中可以看出:dp[i] 是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的。
(很多人做动态规划的题目,会很自然地认为一定是从前往后遍历,其实有些题目是从后向前遍历等,具体情况具体分析)
5️⃣ 打印dp数组
这一步主要是用来deBug的,
在力扣上写代码如果通过不了,把dp数组打印出来,看看和我们想象中的dp数组的样子是否一样。
AC代码: (核心代码模式)
class Solution {
public:
int fib(int N) {
if (N <= 1) return N;
//确定DP数组及下标的含义
vector<int> dp(N + 1); //为啥是N + 1? 答:N + 1是数组长度,下标是0 ~ N
//DP数组如何初始化
dp[0] = 0;
dp[1] = 1;
//遍历顺序:从前到后
for (int i = 2; i <= N; i++) {
//
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[N];
}
};
AC代码: (核心代码模式)
class Solution {
public:
int fib(int N) {
if (N <= 1) return N;
int dp[2];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= N; i++) {
int sum = dp[0] + dp[1];
dp[0] = dp[1];
dp[1] = sum;
}
return dp[1];
}
};
很多动态规划的题目都可以做状态压缩,因为当前状态只依赖于前面2个状态;或者说当前层只依赖于上一层,那我们就维护2层就可以了。
70.爬楼梯
题目链接:70.爬楼梯
难度指数:😀😕
比上一题难一点 ,不过依旧是 入门题。
本题大家先自己想一想,之后会发现,和 斐波那契数 有点关系。
题目思路:(动规五部曲)
1️⃣ 确定dp数组(dp table)以及下标的含义
dp[i]:到达 i 级台阶有 dp[i] 种方法。
2️⃣ 确定递推公式
这道题相比斐波那契数那道题,难在递推公式需要自己推出来(抽象问题的能力)
dp[i] = dp[i - 2] + dp[i - 1];
3️⃣ dp数组如何初始化
dp[1] = 1; dp[2] = 2; (本来想dp[0] = 0,有些题解还dp[0] = 1呢❌,但题目要求n > 0)
4️⃣ 确定遍历顺序
从前往后遍历,因为 dp[i] 依赖前面2个状态。
5️⃣ 打印dp数组
思考:debug的时候,dp数组如何打印?
AC代码: (核心代码模式)
class Solution {
public:
int climbStairs(int n) {
if (n <= 1) {
return n;
}
vector<int> dp(n + 1);
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
746.使用最小花费爬楼梯
题目链接:746.使用最小花费爬楼梯
难度指数:😐☹️😟
比上一题难 !
这道题目力扣改了题目描述了,现在的题目描述清晰很多,相当于明确说:第一步是不用花费的。
eg:
输入:cost = [10,15,20] 输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。
一开始站在
0号台阶 or 一开始站在1号台阶,不花费体力值;🦄只有往上跳,才需要花体力。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯,开始往上跳才要花费体力值。
题目思路:(动规五部曲)
1️⃣ 🦄确定 dp数组 以及 下标 的含义
dp[i]:到达第 i 个台阶所花费的最少体力为 dp[i] 。 (注意这里认为是第一步一定是要花费,现在题目描述清晰了)
dp数组的含义很重要,后面哪一步卡壳了,都要先明确dp数组的含义是什么
2️⃣ 确定递推公式
可以有两个途径得到 dp[i] ,一个是 dp[i-1] ;一个是 dp[i-2]。
说人话:dp[i] 是 由
dp[i - 1]跳一步得到dp[i];或者由dp[i - 2]跳两步得到dp[i]。
从 dp[i - 1] 往上跳的花费是 cost[i - 1] :dp[i - 1] + cost[i - 1]
从 dp[i - 2] 往上跳的花费是 cost[i - 2] :dp[i - 2] + cost[i - 2]
一定是选最小的,所以 dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
(注意这里为什么是加cost[i],而不是cost[i-1],cost[i-2]之类的,因为题目中说了:每当你爬上一个阶梯你都要花费对应的体力值)
3️⃣ dp数组如何初始化
很多人以为动态规划只需要想清楚递推公式,其他的就简单。
其实不然,dp数组如何初始化,遍历顺序也很重要。
(如果只认为动态规划题目里面,递推公式最重要,那说明题目做得不够多。)
dp[3]由前两个即(dp[1]和dp[2])推出来;同理dp[4],……
说明整个递推公式的基础在 dp[0] 和 dp[1]
只需要初始化dp[0]和dp[1]:
一开始选择站在0号台阶,所需要的花费是0;一开始站在1号台阶,所需要的花费也是0。
dp[0] = 0; dp[1] = 0; 一开始就到达0 和 1都是不需要花费的
4️⃣ 确定遍历顺序
从前向后遍历
(不是所有题目都是从前向后;有的从后向前;有的从下往上)
5️⃣ 打印dp数组
[1,100,1,1,1,100,1,1,100,1]
题目描述修改前:
AC代码: (核心代码模式)
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size());
dp[0] = cost[0];
dp[1] = cost[1];
for (int i = 2; i < cost.size(); i++) {
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
cout << dp[i] << endl;
}
// 注意最后一步可以理解为不用花费,所以取倒数第一步,第二步的最少值
return min(dp[cost.size() - 1], dp[cost.size() - 2]);
}
};