- 母题清单
- 509. 斐波那契数(第一次浅浅尝试使用动态规划)
- 70. 爬楼梯(第一次将动态规划用于实战)
- 746. 使用最小花费爬楼梯(递归公式进一步增加了难度,注意理解)
- 62.不同路径(第一次使用二维动态规划,注意初始化的时候是初始几个一维数值,这道题目还可以用数论来解决,可以尝试一下)
- 63. 不同路径 II(放上障碍之后会影响初始化以及后续
dp
数组的赋值,注意增加对应的判断条件,题解中有对起点和终点障碍物的判断,其实这部分可以不写) - 343. 整数拆分(题目中的递归式推导更加复杂了)
- 96.不同的二叉搜索树(递推式的获取更加隐晦,难度进一步增加)
- 01背包理论基础(这里仅仅是理论部分,注意一般采用的是一维数组,这样简单一些)
- 416. 分割等和子集(将背包体积定义为总量的1/2,检查背包是否能被装满)
- 1049. 最后一块石头的重量 II(尽量将石头一分为二,看看当背包的容量为总石头重量的一半时,最多能装多少重量的石头,然后将剩余的石头与这堆石头作差,便是结果)。
- 494.目标和(将数组中的元素分成符号为“+”的和符号为“-”的,两者相减即为目标值。通过简单的计算求出符号为"+"的数的总和,求能得到该总和的情况数。)
- 474. 一和零(这道题其实是01背包典型问题的变体,背包的重量其实就是目标01的个数m和n,而石头的重量就是子串中01的个数,石头的价值就是1。难点是如果把本是一维的背包问题转换为二维的)
- 完全背包问题(和01背包不同的点是:物品可以取无限次。背包问题又可以分为经典的背包问题、排列问题和组合问题(前一种dp[0]初始化为0,后两种dp[0]初始化为1),每中问题其实都是有对应的套路的,即便有套路,也要记得勤加思考。)
- 518.零钱兑换II(这道题是组合问题,而非排列问题,所以需要先对背包进行遍历然后再遍历物品)
- 377. 组合总和 Ⅳ(这道题是排列问题而非组合问题,所以需要先遍历物品再对背包进行遍历)
- 322. 零钱兑换(这道题是求凑成总金额最少硬币的个数,所以这不是一道排列组合题,不用在乎先遍历背包还是物品。其次这道题求的是最小值,所以需要初始化背包的元素为INT_MAX,然后取min,具体的递推公式比较典型,这里不作介绍)
- 279.完全平方数(和母题一样的解题思路)
- 139.单词拆分(这道题也属于完全背包问题,但自我感觉没有消化透,一定要再多练习一下。)
- 股票问题(类似于状态机)
- 121. 买卖股票的最佳时机(这道题可以使用贪心算法,也可以使用动态规划算法,都尝试一下好了)
- 122.买卖股票的最佳时机II(可以买卖多次)
- 123.买卖股票的最佳时机III(只允许买卖2次)
- 188.买卖股票的最佳时机IV(允许买买k次)
- 309.最佳买卖股票时机含冷冻期(状态转换非常复杂,总过有4中不同的类型的转换)
- 714.买卖股票的最佳时机含手续费(前面的题都会了,这道题就非常简单了)
- 子序列问题
- 300.最长递增子序列(注意
dp[i]
的定义是以nums[i]
为结尾的最长递增子序列的长度) - 674. 最长连续递增序列(这道题比上一道题简单)
- 718. 最长重复子数组(为了简化初始化等操作,定义dp[i][j]为以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j])
- 1143.最长公共子序列(没什么好说的,这些题目还是有点抽象的,一定要转变原有的惯性思维)
- 1035.不相交的线(和前题一样)
- 53. 最大子数组和(拿着笔多划一划,这道题就出来了)
- 300.最长递增子序列(注意
1、动态规划
- 题目链接:509. 斐波那契数
- 题解:这道题目可以用递归的方法简单地实现,当然这里主要是要介绍动态规划,下面介绍动规五部曲。
- 确定
dp
数组以及下标的含义
dp[i]
的定义为:第i
个数的斐波那契数值是dp[i]
- 确定递推公式
状态转移方程dp[i] = dp[i - 1] + dp[i - 2];
- dp数组如何初始化
dp[0] = 0; dp[1] = 1;
- 确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];
中可以看出,dp[i]
是依赖dp[i - 1]
和dp[i - 2]
,那么遍历的顺序一定是从前到后遍历的。 - 举例推导
dp
数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2]
,我们来推导一下,当N
为10
的时候,dp
数组应该是如下的数列:0 1 1 2 3 5 8 13 21 34 55
。如果代码写出来,发现结果不对,就把dp
数组打印出来看看和我们推导的数列是不是一致的。
- 确定
- 代码如下:
class Solution { public: int fib(int N) { /* 如果不加这行,假定N=0或者N=1,在初始化或者遍历的时候会出现数组越界问题。*/ if (N <= 1) return N; vector<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]; } };
2、爬楼梯
- 题目链接:70. 爬楼梯
- 题解:使用动态规划五部曲
- 确定
dp
数组以及下标的含义
dp[i]
: 爬到第i层楼梯,有dp[i]种方法 - 确定递推公式
dp[i] = dp[i - 1] + dp[i - 2]
。首先是dp[i - 1]
,上i-1
层楼梯,有dp[i - 1]
种方法,那么再一步跳一个台阶不就是dp[i]
了么。还有就是dp[i - 2]
,上i-2
层楼梯,有dp[i - 2]
种方法,那么再一步跳两个台阶不就是dp[i]
了么。 dp
数组如何初始化
dp[1] = 1,dp[2] = 2
- 确定遍历顺序
从递推公式dp[i] = dp[i - 1] + dp[i - 2]
;中可以看出,遍历顺序一定是从前向后遍历的 - 举例推导
dp
数组
举例当n
为5
的时候,dp table
(dp
数组)应该是这样的。
- 确定
- 代码如下:
class Solution { public: int climbStairs(int n) { if (n <= 1) return n; // 因为下面直接对dp[2]操作了,防止空指针 vector<int> dp(n + 1); dp[1] = 1; dp[2] = 2; for (int i = 3; i <= n; i++) { // 注意i是从3开始的 dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; } };
3、动态规划:01背包理论基础(滚动数组)
-
注意:该部分仅介绍01背包问题的一维版本。
-
模型原型
有n
件物品和一个最多能背重量为w
的背包。第i
件物品的重量是weight[i]
,得到的价值是value[i]
。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
- 本例中背包最大重量为4,物品为:
- 本例中背包最大重量为4,物品为:
-
直接使用动规五部曲分析:
-
确定
dp
数组的定义
在一维dp
数组中,dp[j]
表示:容量为j
的背包,所背的物品价值可以最大为dp[j]
。 -
一维
dp
数组的递推公式dp[j]
可以通过dp[j - weight[i]]
推导出来,dp[j - weight[i]]
表示容量为j - weight[i]
的背包所背的最大价值。dp[j - weight[i]] + value[i]
表示 容量为j - 物品i重量
的背包 加上物品i的价值
。(也就是容量为j
的背包,放入物品i
了之后的价值即:dp[j]
)- 此时
dp[j]
有两个选择,一个是取自己dp[j]
相当于不放物品i
,一个是取dp[j - weight[i]] + value[i]
,即放物品i
,指定是取最大的,毕竟是求最大价值。 - 最后得到递推公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
-
一维dp数组如何初始化
关于初始化,一定要和dp
数组的定义吻合,这里全部初始化为0即可。 -
一维dp数组遍历顺序
代码如下:for(int i = 0; i < weight.size(); i++) { // 遍历物品 for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } }
一维dp遍历的时候,背包是从大到小。倒序遍历是为了保证物品i只被放入一次!但如果一旦正序遍历了,那么物品
0
就会被重复加入多次!- 举一个例子:物品
0
的重量weight[0] = 1
,价值value[0] = 15
- 如果正序遍历
此时dp[1] = dp[1 - weight[0]] + value[0] = 15 dp[2] = dp[2 - weight[0]] + value[0] = 30
dp[2]
就已经是30
了,意味着物品0
,被放入了两次,所以不能正序遍历 - 如果倒序遍历
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0) dp[1] = dp[1 - weight[0]] + value[0] = 15
- 如果正序遍历
- 举一个例子:物品
-
举例推导
dp
数组- 一维
dp
,分别用物品0
,物品1
,物品2
来遍历背包,最终得到结果如下:
- 一维
-
-
最后得出最终源代码
void test_1_wei_bag_problem() { vector<int> weight = {1, 3, 4}; vector<int> value = {15, 20, 30}; int bagWeight = 4; // 初始化 vector<int> dp(bagWeight + 1, 0); for(int i = 0; i < weight.size(); i++) { // 遍历物品 for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } cout << dp[bagWeight] << endl; } int main() { test_1_wei_bag_problem(); }
4、买卖股票的最佳时机
-
题目链接:121. 买卖股票的最佳时机
-
题解:使用动规五部曲
- 确定
dp
数组(dp table)以及下标的含义dp[i][0]
表示第i天持有股票所得最多现金,很显然这个值只能是负数,该值越大证明买入的成本越低。dp[i][1]
表示第i
天不持有股票所得最多现金
- 确定递推公式
- 第
i
天持有股票:dp[i][0] = max(dp[i - 1][0], -prices[i]);
- 第
i-1
天就持有股票,那么就保持现状,即dp[i - 1][0]
- 第
i
天买入股票,财产为-prices[i]
- 第
- 第
i
天不持有股票:dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);
- 第
i-1
天就不持有股票,那么就保持现状,即dp[i - 1][1]
- 第
i
天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,即prices[i] + dp[i - 1][0]
- 第
- 第
- dp数组如何初始化
- 第
0
天持有股票,肯定是第0
天买入股票,即dp[0][0] -= prices[0]
- 第
0
天不持有股票,那么dp[0][1] = 0
- 第
- 确定遍历顺序
- 从递推公式可以看出
dp[i]
都是由dp[i - 1]
推导出来的,那么一定是从前向后遍历。
- 从递推公式可以看出
- 举例推导dp数组
以输入[7,1,5,3,6,4]为例,dp数组状态如下:
- 确定
-
最后得到代码:
class Solution { public: int maxProfit(vector<int>& prices) { int len = prices.size(); if (len == 0) return 0; vector<vector<int>> dp(len, vector<int>(2)); dp[0][0] -= prices[0]; dp[0][1] = 0; for (int i = 1; i < len; i++) { dp[i][0] = max(dp[i - 1][0], -prices[i]); dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]); } return dp[len - 1][1]; } };