终于来到了动态规划,传说中的神奇算法,也是好多人闻声色变的一种难以被真正理解的算法。
同样的我们仍然采用循序渐进的由浅入深式的做题,来帮助我们更好的理解和接触动态规划。
首先动态规划算法,可以解决哪些类型的问题呢?
主要有买卖股票问题,子序列问题,打家劫舍问题,背包问题等问题,动态规划主要是用来解决某一问题可以由多个重叠的子问题组成时,优先选用动态规划,动态规划的每一个状态都是由上一个状态所推导出来的,是有理有据的。
解题套路
动态规划类题目都可以分解为五部曲
即:设置dp数组,找出递推公式,将dp数组部分初始化,明确遍历顺序,打表dp数组。
设置dp数组,要求我们明确dp数组在本题中的具体含义是什么,dp[i]是什么i又是什么。
递推公式通常是由题目已知量推导求得的,通常具有规律性的举例数字带入,可以帮助我们容易的确定递推公式。
将dp数组部分初始化,是根据题意的要求,将题目里已经给出的数据,初始化在dp数组中,这也和递推公式有关,一般要满足初始化的数据数量足够让递推公式推出下一个数据。
明确遍历顺序,就是要知道我们通过什么样的顺序,是从前到后还是从后向前的写一个循环,通过循环和递推公式在遍历顺序的作用下,填写dp数组。遍历顺序并不一定总是从前向后的。
最后一步,实际上是排错的,也就是题目无法ac时候,我们可以将dp数组最后的值全部打印出来,用来对比题中测试用例,用以排错。
了解了这些步骤后,相信大家才能够更好的学习动态递归的算法,即使是简单题也按照这一思路来思考,这样培养出感觉了之后,遇到困难题,才能有基本的思路。
509. 斐波那契数 - 力扣(LeetCode)https://leetcode.cn/problems/fibonacci-number/斐波那契数算是比较经典的题目了,相信大家在一开始学习递归的时候,就接触过这道题,递归思路也是十分简单,但是值得注意的是,当我们要求的第n个数字如果n非常大,那么递归是无法完成的,我们要选用更加高效的动态规划。
基本思路就是用数组dp来记载每个数的值,我们在填写第n个数字时,是前两个数字相加的和,这一个规律就是递推公式,这道题目已经将递推公式给出来了,所以我们不用找规律验证了。dp[i]代表了第i个斐波那契数,i就是代表第几个,我们初始化就是将第0个数字初始化为0第一个初始化为1,这都是题目里给出的,第0个由于没有数所以赋值0。遍历顺序很明显一定是从前向后,因为我们后一个数是前两个数和得到的。知道了这些,代码就不难写出来了。
class Solution {
public:
int fib(int n) {
if(n<=1)return n;
int dp[n+1];
dp[0]=0;dp[1]=1;
for(int i=2;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
};
我们还可以使代码更精简一些,由于我们只需要维护三个数字,第n个斐波那契数,第n-1个数和第n-2个数字,我们以这种思想来写题解的话,就可以不用数组来保存前面的数字了,使空间复杂度变成了O1。思路就是创立三个变量,来分别保存这些信息,最后返回,这里不给出代码了。
70. 爬楼梯 - 力扣(LeetCode)https://leetcode.cn/problems/climbing-stairs/这道题是一道很适合入门的动态递归题目,问到n阶台阶共有几种走法,这道题没有做过的话,应该没有什么好的思路,看着很懵,但实际上和上一道题斐波那契数列差不多。
为什么这么说呢?我们每次只能走一步或者两步台阶,而走到第n阶的情况实际上可以等价于走到n-1阶后再走一阶达到目的地,也可以是走到n-2阶后再走两阶达到,那第一种我们可以理解,第二种n-2阶时候,可以走一阶再走一阶达到目的地吗?其实这样走的话,实际上就是n-1阶了,n-2走一步到n-1。所以我们走到第n阶思路就明确了,就是n-1阶的种数加上n-2阶种数就是走到第n阶总数,这一点和斐波那契数是一样的求法,实际上代码也是差不多的,唯一不同的就是dp数组的初始化部分,第1层就是1,第一层只有一种解法,而第二层有两种解法。
那有的同学要问了,为什么没有第0层了?实际上这道题有没有第0层影响都不大,由于是类似斐波那契数列解法,我们直接给出两个初始化能够求得剩余的就可以了,至于斐波那契那道题,0这个数也可以不赋值,采用1,2下标位置赋值成1也是可以的。
class Solution {
public:
int climbStairs(int n) {
if(n<=2)return n;
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];//实际上是dp[i-1]再上一层楼梯和dp[i-2]再上两层楼梯,dp[i-2]如果只上一层楼梯那么和dp[i-1]就一样了,所以不算
return dp[n];
}
};
746. 使用最小花费爬楼梯 - 力扣(LeetCode)https://leetcode.cn/problems/min-cost-climbing-stairs/这道题就相当于爬楼梯的消费版,虽然是这样说,但实际上还是有一些不一样的地方。
值得注意的点:我们一开始可以选择从0这个位置走,或者从1这个位置走,而且这两个开始的地方不收费,换句话说是往上爬楼梯时候收取的费用是现在所处的位置的价格,也就是上楼梯才收费,而且要注意我们要爬到顶端,是数组最后一个位置的下一个位置,而不是数组里包含元素-1。这一点很重要。
思路仍然是创建dp数组,dp[i]代表了达到这一层时候至少要多少钱,我们要求的是最小的花费。数组创建上创立一个数组数据个数+1这么大的空间,因为我们要求的是到楼顶的花费。数组初始化dp[0]=0dp[1]=0为什么这么写,上面也说过了,因为起始位置不花钱,往上走才花钱。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size()+1);
dp[0]=0;dp[1]=0;
for(int i=2;i<=cost.size();i++){
dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
return dp[cost.size()];
}
};
代码也是很简短的,dp[i]采用比较从哪一阶梯上来花的钱比较少,就存储哪一个方案。最后返回的钱数就一定是最少的开销。
以上代码均可ac。