动态规划刷题总结
个人心得
问题本质
动态规划名字看起来高大上,感觉是种很复杂的算法,令人“望文生畏”,其实一句话概括,就是 数学归纳法 。动态规划问题的每一个状态是由上一个状态通过 状态转移方程 推导得出(对于存在很多状态的问题,需要画状态图辅助推导出正确的状态转移方程,类似《编译原理》的自动状态机)。动态规划是一种 “聪明的穷举” ,所谓具备 “最优子结构” 和存在 “重叠子问题” 。本质上来说,动态规划是通过引入dp数组这种 “空间换时间” 的方法来降低暴力算法的时间复杂度。
解题步骤
参考Carl的方法,牢牢把握“五部曲”:
-
确定dp数组以及下标的含义
-
确定递推公式
-
dp数组如何初始化
-
确定遍历顺序
-
举例推导dp数组
解释说明:
- 根据题目选择二维数组还是滚动数组(一维),状态数决定二维dp数组的列数
- 2先于3是因为不同的状态转移方程需要不同的初始化方法
- 深刻理解1,才能初始化好3
- 遍历顺序主要集中于背包问题:用滚动数组解决01背包时,注意先物品再背包且背包倒序遍历(防止重复)。完全背包正序、倒序均可,若求排列问题只能先遍历背包,若求组合问题只能先遍历物品
- 如果存在错误,打印dp数组日志来验证每一步过程,下附“灵魂三问”,解决好这三个问题debug将不再困难:
- 这道题目我举例推导状态转移公式了么?
- 我打印dp数组的日志了么?
- 打印出来了dp数组和我想的一样么?
一、基础题目(就不贴外链了)
509.斐波那契数
70.爬楼梯
746.使用最小花费爬楼梯
62.不同路径
62.不同路径II
343.整数拆分(注意理解)
96.不同的二叉搜索树(注意理解)
二、背包问题(难点是遍历顺序,其次是递推公式)
引用自:代码随想录-背包总结篇
递推公式
问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:
问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:
问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:
问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:
遍历顺序
01背包
二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历
一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历,防止重复!
完全背包
纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
相关题目如下:
如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:
三、打家劫舍
198. 打家劫舍,从一维数组上打劫
213. 打家劫舍 II ,从环形一维数组上打劫
337. 打家劫舍 III ,从树的根节点上打劫
四、买卖股票
- 动态规划:121.买卖股票的最佳时机,只能买卖一次
- 动态规划:122.买卖股票的最佳时机II,可以买卖多次
- 动态规划:123.买卖股票的最佳时机III,最多买卖两次
- 动态规划:188.买卖股票的最佳时机IV,最多买卖K次
- 动态规划:309.最佳买卖股票时机含冷冻期,可以买卖多次,卖出后有一天冷冻期
- 动态规划:714.买卖股票的最佳时机含手续费,可以买卖多次,卖出时有手续费
根据题目中所包含的状态数确定二维dp数组的列数,如309.,包含了4种状态和7种转移方程,因此二维dp数组有4列
五、子序列
引用自:代码随想录-动态规划总结篇
子序列(不连续)
-
300. 最长递增子序列,基于数组
-
1143. 最长公共子序列,基于字符串
-
1035. 不相交的线,看似是给两个数组连线,本质上与1143题别无二致
子序列(连续)
-
674. 最长连续递增序列,基于数组
-
718. 最长重复子数组,返回两个数组中公共的 、长度最长的子数组的长度
-
53. 最大子数组和,求连续子数组最大和
编辑距离(由三道基础题目引出)
这里贴一个2022建行Fintech大赛第二道编程题:
// 文本相似度 = 1 - 最小编辑距离 / 最长的字符串长度
// 每次插入、删除、增加操作使得最小编辑距离 + 1
// 输出保留两位小数
#include<bits/stdc++.h>
using namespace std;
int editDistance(string s1, string s2) {
int len1 = s1.size(), len2 = s2.size();
vector<vector<int>> dp(len1 + 1, vector<int>(len2 + 1, 0));
for (int i = 1; i <= len1; i++) {
dp[i][0] = i;
}
for (int j = 1; j <= len2; j++) {
dp[0][j] = j;
}
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
if (s1[i - 1] == s2[j - 1])
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(dp[i - 1][j] + 1, min(dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1));
}
}
return dp[len1 - 1][len2 - 1];
}
int main(){
string s1 = "abcde", s2 = "ace";
int maxLen = max(s1.size(), s2.size());
// 两数相除,只有分子本身是double结果才能显示小数,否则强制转换都没用
double similarity = 1 - (double)editDistance(s1, s2) / maxLen;
// 使用fixed(不用则只保留有效数字)配合setprecision(2)来确保cout输出保留两位小数
cout << fixed << setprecision(2) <<similarity << endl;
return 0;
}
回文串
-
647. 回文子串,定义布尔dp
-
516. 最长回文子序列,不连续
-
5. 最长回文子串,定义布尔dp,贴一个自己根据“五部曲”来分析的题解:《代码随想录》动规“五部曲”题解