1. 动态规划
感谢代码随想录
解题思路
- 确定
dp数组及其下标的含义 - 确定
dp数组 - 确定如何初始化 ,和
dp数组的遍历顺序 - 举例推导
dp数组
验证dp数组的Bug时
- 打印程序日志
- 自行推导
dp数组 - 检查
dp数组与程序中哪里出现错误,对错误地方重点检查
1.1. 斐波拉契数列
dp是数列的值dp[i] = dp[i-1]+ dp[i-2];- 初始化
0,1即可 - 注意:题目中要求计算
n的值,对应f(n)的值,不是n-1
1.2. 跳台阶最省力方法
dp[i]是第i层台阶使用的力气,dp需要从dp[i-1]与dp[i-2]之间选择体力最小的值- 题目中,从第0,1阶开始跳,初始值可以为0;
- 从前向后遍历
1.3. 路径问题
1.3.1. 深度搜索
1.3.2. dp算法
dp[i][j]是到达i,j 的所有方法因为只能i, j只能左边
dp[i-1][j]和上面dp[i][j-1]共同决定,所以dp由两数之和决定第一行和第一列只能由一种方法到达,初始化为1;
从前向后遍历
因为dp是由上方和左边+1组成,可以使用一维累加数组完成dp数组的效果
1.3.3. 使用图论
移动需要走M+n-2中方法,且其中需要走m-1中向下的步数,结果为Cm + n − 2m − 1
由于阶乘数字较大,先求分子分母阶乘会超过long long 类型,所以边计算分子边计算分母
- 如果能够整除分母,且分母没有除完,选择除以分母
- 并且分子分母相加相减方向相同,尽快除以分母
int high = m-1;
int low = m+n-2;
int denominator = high;
long long numerator = 1;
// 求分母
for(int i = 1 ; i<= high ;i++) {
numerator*= low;
while(denominator>0 && numerator%denominator==0){
numerator/= denominator;
denominator--;
}
low--;
}
return numerator;
1.4. 有障碍的路径规划
dp[i][j]表示i, j 位置的路径选择方法, 所以当有障碍物时,无法到达这里,此时dp[i][j]=0,其余位置照常计算
牢牢记住dp的定义,如有异常情况,将dp值写出
1.5. 整数拆分
整数从i 拆分后分为i, n-i;
最大值取值有两个选择:
- i * (n − i),不进行下一轮的划分
- $ i* 对(n-i)划分后的最大乘积$,因为i会从0-i 都会分割一次,所以只用对(n-i)进行划分,因为在遍历过程中都会遍历到
- 其中2中的第二部分为问题1的子集,使用dp求解
dp[i] = max(dp[i],max(i*j, j*dp(i-j)))
因为dp[i] 被计算多次,最后需选择最大值,所以对dp[i]求最大值
1.6. 二叉搜索树数量
n个数组成二叉搜索树,以j为中间节点
1....j ....nj左边有
j-1-1+1= j -1个数字,构成二叉搜索树,数量为dp[j-1]j右边有n - j-1+1 =n-j个数字,构成二叉搜索树,数量为dp[n-j]左右子树为树的子问题,
dp[i] += dp[j-1]*dp[i-j]初始化dp[0] =1;
遍历顺序为从前向后,遍历
2. 背包问题
2.1. 0-1背包
- 每种物品只能选择一个
dp[物品i][重量w] 表示当前 物品i 与前面的物品[0-(i-1)] 在重量为w情况下的最大价值
dp,当重量j足够放下物品i计算途径有两种
- 放入,总重量为
j - w[i]时的最大价值,加上物品i的价值v - 不放入,总重量为上一个
物品i-1在重量j情况下选择结果的最大值
dp[i][j]从以上选择最大结果,dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]])+value[i]
- 初始化,对于第一个放入物品情况初始化
- 遍历,从前向后遍历,重量可以等于总重量
2.2. 数组分为两个相同的数组
- 数组分的数组和界限为
sum/2,等价为背包所能装的总的质量每一个数字=放入的物品,放入进去相当于有了重量- 放入进去后,物品的总价值累加,等于sum/2 时,背包正好被放满
2.3. 压石机
两两组合使得质量最小 = 将石头分为两堆, 第一堆减去第二堆尽可能小 = 两石堆的质量差距相同
回到问题2中
现在将二维dp数组使用一维滚动数组代替
- 先遍历物品,再遍历质量,因为dp[0-i]之前的数据是上一轮迭代结果,dp[i]需使用之前的数据,所有从后先前迭代
- 先遍历质量,再遍历物品,遍历得到当前质量j 下放下物品的最大价值,需计算出之前的数据,才能计算下一个质量下的最优值,所以必须从后向前迭代
第二种遍历方式错误,会造成物品选择两次
2.4. 目标和
- 将目标和转换为 数据求和等于某个值n
- 相当于将数值放入其中,可以得到值n
和为x, 差的和为sum -x ,所以 x- sum+x = target,求出x = (target+sum)/ 2, 所以目标和为(target+sum)/ 2
dp[i][j]是目标和为j 时选择i ,有多少组合方法dp的组合方式
- 选择i , 则方法总数为上一个数字总和为j - num[i], 即
·dp[i-1][j-nums[i]] - 不选择i,则方法总数为上一数字总和为
j,即dp[i-1][j]
总方法=
1+2- 选择i , 则方法总数为上一个数字总和为j - num[i], 即
初始化:
- 对于第一个数字,只有一种j 有一种方法,其余方法数为0;
- 对于剩余数字,当j = 0时,总数为0数字的2*n;
2.5. 0-1字串
一个字符串strs= ["10", "0001", "111001", "1", "0"],求出有m个0, n个1的最大子集元素个数
此时题目相当于m,n相当于背包的重量,总重量小于m、n两个维度,字符串相当于物品,字符串0、1个数相当于每个物品的质量。
此时可以使用0-1背包
推荐使用滚动数组解决0-1背包问题
- 初始化时将初始值置为0
- 优先物品遍历,质量遍历时使用倒序遍历
3. 完全背包问题
背包物品数量有无穷多个,可以重复选择
dp[i][j]公式含义: 第i个物品,在重量j条件下物品的最大价值dp[i][j]计算方式- 选择
i, 预留重量j- weight[i], 且背包中仍有i(区别0-1背包),dp[i][j- weight[i]] - 不选择
i, 不预留质量,不选择i,背包中只有i-1物品的最大值,dp[i-1][j]
dp[i][j] = max(dp[i][j- weight[i]], dp[i-1][j])- 选择
初始化
- 对
i = 0初始化 - 对
j = 0初始化
- 对
3.1. 零钱兑换
此时求得是总的组合数,不是最大价值,dp[i][j] 是由i 是否选择两种情况组合的和
dp[i][j] = dp[i][j- weight[i]] + dp[i-1][j]- 初始化
- 第一行初始化,如果可以整除,则有组合方式,初始化为1
- 第一列初始化,重量>1 ,则重量=0时,只有一种组合方式,初始为1
< int < unsigned int < long long < unsigned long long
int unsigned int long long unsigned long long int32_t uint32_t int64_t uint64_t 范围 2,147,483,647 4,294,967,295 9,223,372,036,854,775,807 18,446,744,073,709,551,615 十进制 2e9 4e10 9.2e18 1.8e19
3.1.1. 使用一维dp数组
- 使用滚动数组
dp[j] = dp[j] + dp[j- weight[i]]- 不选择时
nochoose = dp[j] - 选择时
choose = dp[j-weight[i]]
- 不选择时
- 初始化
- dp[0] 只有一种组合方式,初始化为1
- 遍历顺序
- 先遍历物品,在遍历质量,是组合
- 且遍历质量时,需顺序遍历,此时可以放置多个同一物品,需使用到之前的数据,
- 先遍历质量,再遍历物品,是排列数量
- 先遍历物品,在遍历质量,是组合
| 先遍历物品,再遍历质量 | 先遍历质量,再遍历物品 |
|---|---|
先放物品i ,再放入物品i+1,有放入顺序,排除顺序不同的情况,计算的是组合数 |
先计算出当前质量下所有物品的最大值,表示质量j所有组合情况,没有顺序问题,是排列数 |
3.2. 求出排列数量
求出排列数量有两种思路
完全背包的排列问题:
先遍历质量,再遍历物品, 可以得到排列数量
爬楼梯
爬到第
i层的值,等于之前能够爬到i的所有选择之和$ dp[i] = {dp[i- 能到i的跳跃次数]} 总和$
题目中用于跳跃到n的次数= 数组中用于相加等于n 的所有元素
3.3. 爬楼梯 = 完全背包排列问题
爬n阶台阶,每次能爬m 阶,两种思路解决
n阶台阶 = 背包总容量,m阶台阶是每次选择的物品质量,
价值 = 重量 = j。dp[j]是装满j层的方法总数dp[i] += dp[i-j]完全背包问题,优先遍历质量,再遍历物品
爬到
i层是之前i-m层所有爬楼方法的总和,dp[i] += dp[i-j]
3.4. 零钱兑换的最小方法数
dp[j]兑换j所需的金币数量dp[j]由其dp[i- coins[i]]兑换到当前值 的最小方法决定dp[j] = min(dp[j], dp[j- coins[i]])
- 初始化,需比较最小值,需将
dp[j]初始化为UINT64_MAXdp[0]方法数为0,初始化为0
- 此时不是求总的组合数 / 排列数,任意顺序遍历均可
3.5. 完全平方数
- dp与上一相同
3.6. 字串拆分
i 是需要达到的楼梯,
0-(i-1)是能够到达i之前所有的楼梯,对这些元素遍历,并检查 j - i 之间能否跳到i
dp[i] = dp[j] && (i-j)能否到达
切分
(i-j),切记i.j均是加1 后的结果, 那么j = j'+1,起始位置为j'+1,即为j, 总长度为i' - j+1=i -1-j+1=i-j切分范围为(j, i-j)
3.6.1. 回溯遍历,使用数组保存状态
使用memory保存状态,之后可以直接使用
4. 多重背包问题
多重背包问题 = 物品展开的0-1背包问题
| 重量 | 价值 | 数量 | |
|---|---|---|---|
| 物品0 | 1 | 15 | 2 |
| 物品1 | 3 | 20 | 3 |
| 物品2 | 4 | 30 | 2 |
| 重量 | 价值 | 数量 | |
|---|---|---|---|
| 物品0 | 1 | 15 | 1 |
| 物品0 | 1 | 15 | 1 |
| 物品1 | 3 | 20 | 1 |
| 物品1 | 3 | 20 | 1 |
| 物品1 | 3 | 20 | 1 |
| 物品2 | 4 | 30 | 1 |
| 物品2 | 4 | 30 | 1 |
相当于先遍历物品,再遍历物品的个数,再遍历背包(遍历物品个数在内部也一样)
物品
i, 质量为j, 最大数量为k
dp[i][j]对于总重量j有两种计算方式
- 不装:
dp[i-1][j]- 装k个:
dp[i-1][j-k*weight[i]] + k*value[i]- 初始化:
j = 0一列全部为0i =初始化,与取值都是错误的,二维数组很复杂
建议使用滚动数组
物品i , 质量为j, 最大数量为k
dp[j] 是物品i对于总重量j有两种计算方式
- 不装:
dp[j] - 装k个:
dp[j-k*weight[i]] + k*value[i]
如果先遍历物品再遍历质量时,此时每次放入一个,不再需要乘上系数k
5. 打家劫舍问题
dp[i]是第i家可以打劫到的最大金额dp[i]可以选择偷 / 不偷偷的话,需要偷
i-2之前房屋的金额,加上第i家的前dp[i-2]+ nums[i]不偷的话,第
i-1家可以被偷,所以第i的情况 = 第i-1家是否被偷的情况dp[i] = dp[i-1]
dp[i] = max(d[i-2]+ nums[i], dp[i-1])
初始化,需要初始化第0,1 家
- dp[0] = nums[0], dp[1] = max(dp[0] , dp[1]);
遍历顺序,从前向后
5.1. 成环的打家劫舍
成环后,将首尾分开讨论
不偷头,那么最后一间可以被偷
不偷尾,那么第一间房间可以被偷
求两次的能偷的最大价值,比较,返回最大值
5.2. 二叉树的打家劫舍
树的后序遍历,统计孩子们偷钱,再由中间节点统计
- 截至条件:
- 到NULL节点,偷的最大值为0
- 到叶子节点,偷的最大值是当前值
- 处理逻辑
- 根节点偷
- 跳过左右孩子,计算从左右孙子偷到的金额
- 根节点不偷
- 计算左右孩子偷盗的金额
- 选择哪一个值更大,选择偷拿个
- 使用记忆化存储,使用
map<root, val>,保存已访问节点的最大值,后续访问节点时,直接返回保存值
- 根节点偷
5.2.1. 树形的dp
dp[i] 取决于 i 的左右节点的dp[i->left], dp[i->right]
- 截至条件,所以函数需要返回孩子节点的选择状态<不偷,偷>
- 到NULL节点,返回<0,0>
- 处理逻辑:
- 当前节点偷,choose = val + 左右孩子不偷
- 当前节点不偷,可以考虑左右节点是否偷,nochoose = max(左孩子选择)+max(右孩子选择)
6. 股票问题
同一个
i有两种状态, 状态之间互相推导
第
i天有两种状态, 持有股票 / 不持有股票, 分别设置为dp[i][0]/ dp[i][1]第
i天持有股票,可由两种方式推导第
i-1持有股票第
i-1不持有股票,第i天购购入(因为之前没有购入股票,一切为0)dp[i][0] = max(dp[i-1][0], - price[i])如果之前卖出了股票,就成了由不持有股票状态
dp[i-1][1]-price[i]
第
i天不持有股票,由两种方式推导第
i-1不持有股票第
i-1天持有股票, 第i购入股票dp[i][1] = max(dp[i-1][1], dp[i-1][0] - price[i])
dp公式初始化,所有由第一天的状态组成,初始化
- 第一天持有股票
dp[0][0] = -price[0] - 第一天不持有股票
dp[0][1] = 0
- 第一天持有股票
遍历顺序,从前向后遍历
6.1. 有限次购买股票
问题:dp数组由多种不同的状态,且不同状态之间互相推导
解决方法: 找出所有可能的状态,并推导不同状态之间的公式
最多有n次购买股票,可以设置五种状态
| 0 | 没有操作 | dp[i][0] |
|---|---|---|
| 1 | 第一次持有股票 | dp[i][1] |
| 2 | 第一次不持有股票 | dp[i][2] |
| 3 | 第二次持有股票 | dp[i][3] |
| 4 | 第二次不持有股票 | dp[i][4] |
第一次持有股票
第
i-1第一次持有股票第
i天没有操作状态购入股票
第一次不持有股票
- 第
i-1天不持有股票 - 第
i-1天 持有股票后,第i天卖出股票
- 第
第二次持有股票
- 第二次持有股票
- 第
i-1天第一次不持有股票,第i天购入股票
第二次不持有股票
- 第二次不持有股票
- 第
i-1天持有股票, 第i天卖出股票
初始化:在第0天
- 第一次购入股票, 为
-price[0] - 第一次不持有股票,相当于第一天买了又卖了, 0
- 第二次购入股票,相当于第一天买入又卖出,再买入, 为
-price[0] - 略
dp结果: 只买一次股票包含在买两次股票结果中,所以第二次卖出股票为最终结果
6.2. 限制k次购买股票
k次购买有2k中持有与不持有状态,+1中首次误操作状态
dp[i][j+1]次状态有dp[i-1][j], 上一种状态dp[i-1][j]转换组成- 由于奇数次状态为持有股票状态,需要购入股票,由上一状态
- price[i]得到 - 偶数次状态为不持有股票状态,卖出股票, 总的价格增加,由上一状态
+ price[i]得到
- 由于奇数次状态为持有股票状态,需要购入股票,由上一状态
dp[i][j]公式- dp[i]][j] = max(dp[i − 1][j], dp[i − 1][j − 1] + ( − 1)j * prices[i])
- 方便计算,可以将奇偶公式分开枚举
6.3. 含有冷冻期的股票购买
尝试描述购买股票过程中有多少种状态,画出其状态转化图, 有四种状态,
- 持有股票状态 ,
dp[i][1] - 不持有股票状态,
dp[i][2] - 冷冻期,
dp[i][3] - 当天卖出股票状态(此时状态与2不同)
dp[i][4]
- 当前持有股票,
- 可由前一天持有股票
dp[i-1][1]得到, i-1天不持有股票,买入股票得到,dp[i-1][2] - price[i]i-1冷冻期中后一天,买入股票,dp[i-1][3] - price[i]
- 可由前一天持有股票
- 当前不持有股票
i-1是冷冻期,dp[i][3]- 当前是冷冻期
i-1当天售出股票,dp[i-1][4]
- 当天售出股票
i持有股票,卖出,dp[i-1][1] + price[i]
初始化:
- 持有股票时, 因买入股票, 初始为
-price[0] - 其余状态不持有股票,初始不买不入,初始为0
遍历顺序:从前向后遍历
6.4. 含手续费的股票售出
- 还是两种状态, 持有股票,不持有股票, 由持有股票售出股票-> 不持有股票,需缴纳手续费
- dp转换公式
dp[i][0] = max(dp[i-1][0] , dp[i-1][1] - price[i])dp[i][1] = max(dp[i-1][1], dp[i-1][0] + price[i] -fee),售出时缴纳手续费
- 初始化: 略
- 选择结果: 可以卖出,也可以不卖出,选择最大值情况
7. 子序列问题
字串问题操作数 = 不同结果的操作选择
- 找到所有可能的操作
- 当前情况可以由哪些操作得到,上一操作状态又是什么
- 使用
max, min选择最合适的操作
可以近似于 爬楼梯问题, 能爬到 num[i] 位置的最大长度
dp[i]表示为i之前包括i内以nums[i]为结尾的序列, 单调增序列的最大长度,for 0: j中,能够爬到if(nums[j] < nums[i])中, 长度最大值
if(nums[j] < nums[i] ) dp[i] = max(dp[i], dp[j] +1)
- 初始化,所有序列初始为1
7.1. 最长连续子序列
要求连续,所以只能从j-1跳到j的位置,所以只需要比较nums[j-1] < nums[j], 不需要从[0,j-1]全部与nums[j]比较
7.2. 最长重复子序列
dp[i][j]表示 以i-1结尾的A数组 与 以j-1结尾的B 数组 最长公共子序列因为
dp[i][j]时比较了了i-1与j-1的序列结果, 所以表示以i-1为结尾的子序列if(A[i-1] == B[j-1]) dp[i][j] = max(dp[i-1][j-1]+1), 比较的当前位置,因为i, j比实际i,j大1, 所以if中减去了1初始化
i=0 , j=0各种情况都是错误,初始化为0
遍历顺序: dp[i][j]需要从1 开始遍历
7.2.1. 一维dp数组
dp[j]表示与j-1结尾的相同的最大长度因为是从上一个
i-1复制下来得到,遍历过程中不能修改j-1的dp,否则影响后续计算·
if(A[i-1] == B[j-1]) dp[j] = max(dp[j-1]+1)遍历顺序:
i从头开始,j必须从后开始
7.3. 最长公共子序列
dp[i][j]表示以i-1结尾的序列与 以j-1为结尾的序列,最长的公共子序列dp[i][j]有两种计算方式A[i-1] == B[j-1],长度+1,dp[i-1][j-1]+1A[i-1] != B[j-1],需要看i-1和j/i与j-1是否有最长的公共子序列,从两者中选择最大值
- 初始化:
i=0, j=0情况,因为序号为0为空串, 与另一条序列的公共序列一定为0
- 遍历顺序: 从前向后
7.4. 最大连续子序和
连续子序和,可以由上一个序列延续获得, 也可以上一序列中断, 由当前序列继续计算
dp[i],以i为结尾的最长子序和dp[i] = max(dp[i-1]+ nums[i], nums[i])初始化,
dp[0]可以选择自身开始,或者从0开始
题目要求子序长度 >0 ,所以必须从自身开始
- 遍历顺序: 从前向后遍历
7.5. 判断子序列
子序列a 对应 序列b 的子序列长度
dp[i][j]是子序列a与序列b相同子序列长度
相同时 ,
dp[i][j] = dp[i-1][j-1] + 1;不相同时,删除
j节点,观察dp[i][j-1]能到达的最大长度此时,只能删除
j节点,删除i节点后,就不是原来的序列了
7.6. 不同的子序列
完整的序列t在s的子序列中出现的次数
dp[i][j], 以i-1结尾的序列t ,在以j-1为结尾的序列s中子序列出现次数有两种计算情况
s[i-1] == t[j-1],看i-1, j-1时匹配的结果, 同时还可以删除i-1看匹配结果(i-1可能由重复情况)相同情况时,选择用
s[i]与t[j]比较,因为s[i]前一位可能等于后一位,所有也需要向前移动一位比较不相同时,删除
i-1看匹配结果,dp[i-1][j]
初始化,
i = 0 , j=0都是异常情况i =0, s为空串,j与s相同结果为0j=0时,j是空串,s中删除到最后,一定有一串与j相同,初始化为1
遍历顺序: 从前向后遍历
7.7. 删除操作
7.7.1. 最长相同子序列长度
- 求出两字串最长公共子序列,长度n;
- 字串a,b删除除了公共子序列外的其他元素,删除后结果相同,删除长度 = 删除操作此处
7.7.2. 删除操作DP
dp[i][j]是以i-1为结尾的串a,以j-1为结尾的字串b需要删除的最小次数dp[i][j]有两种情况,相同结尾,不同结尾a[i-1] == b[j-1], 不需要删除操作,操作次数 =dp[i-1][j-1]a[i-1] != b[j-1],需要删除 a,b的最后一个,或者两个都删除删除
i-1,dp[i-1][j]+1删除
j-1,dp[i][j-1]+1两个都删除,
dp[i-1][j-1]+2
使用
max对三种方案进行选择, 3 包含在1, 2中,可以省略3初始化,
i=0, j=0异常情况,需初始化i=0, 字串a = NULL, 字串b[j]需删除j次- j = 0 ,同上
遍历顺序: 从前向后
7.8. 编辑距离
字串编辑有三种操作,字串a[i], b[j], 最后一位不同
删除,删除a 的最后一位
a[i],b没有改变增加,在
b[j-1]后增加一位a[i]与a相同, 增加一位a[i]与删除一位a[i]的操作力度相同替换,
dp[i][j]表示以i-1为结尾的串a,以j-1为结尾的字串b需要操作的最小次数dp[i][j]有两种情况,相同结尾,不同结尾a[i-1] == b[j-1], 不操作,记录之前的操作次数=dp[i-1][j-1]a[i-1] != b[j-1], 进行以上三种操作- 删除,
dp[i-1][j]+1, dp[i][j-1]+1 - 增加,
dp[i-1][j]+1, dp[i][j-1]+1 - 替换,
dp[i-1][j-1]+1
使用
max对操作进行选择- 删除,
初始化,
i= 0, j =0的情况,处理同上遍历顺序,从后向前遍历
8. 回文
8.1. 回文子串
8.1.1. DP算法
dp[i][j]表示[i,j]之间的字符是否为回文字串, 然后统计数组dp中有多少个true,, = 有多个回文字串两种情况
a[i] = a[j],[i, j]之间相同 / 相邻,一定是回文子串[i,j]之间不相邻,dp[i+1][j-1]是回文串,则true
- 不相同,不是回文串,跳过
初始化
dp[i][j]全初始化为false
遍历顺序
从左下角开始遍历, 即 下-> 上,左-> 右遍历
8.1.2. 双指针 中心扩散
回文串 由 中间1个 / 2个向左右扩散,统计扩散数量,不能扩散时,返回得到扩散的最大数量
- 中间 1 个向左右 扩散
- 中间 2 个向左右扩散
- 相加得到结果
8.2. 最长回文子串
字串要求 是连续的
8.3. 最长回文子序列
回文序列可以不连续
dp[i][j],表示[i,j]之间最大的回文序列长度dp[i][j]两种情况a[i] == a[j],子序列长度加2 ,dp[i+1][j-1]+2a[i] != a[j],那就是前一个区间的最大长度,可以缩短i/ j,max(dp[i-1][j], dp[i][j-1])初始化
由性质
i == j时候,dp[i+1][j-1]+2因为
i+1 > j-1无意义,所以i==j需要单独初始化其余初始化为0
遍历顺序: 下- > 上, 左-> 右
遍历时,
i == j时,已经初始化,且公式计算不到,所以j只需要从i+1开始遍历dp[i+1][j-1]+2,所以 `i <= s.size()-2, i >=0