动态规划
- 确定dp数组及其下标含义
- 确定递推公式
- dp数组初始化
- 确定遍历顺序
- 举例推导dp数组
- 递归改动态规划,有几个可变参数决定你维护的dp数组是几维数组
- 状态表示 集合(所有选法、条件)
- 状态计算 集合划分
一些文章
分类
记忆化搜索(答案一定要是return值才记忆化搜索,不能参数是答案返回值在是void用)
int dp[N][N][N] ;
int dfs(int a, int b, int c){
if(越界/不符合条件) return ...;
if(dp[a][b][c]已经算过) return dp[a][b][c];
int ans = 0;
for(...){
ans += dfs(...) ;
}
//记录
dp[a][b][c] = ans;
return ans;
}
- 题目:
1. 背包dp
!以下问题是背包最大价值是多少,不一定装满
1-1 01背包(每件物品最多只用一次,0和1两种情况)
n个物品(体积vi,价值wi),容量是v的背包,每个物品只能用一次
总体积小于等于v,总价值最大
//二维
dp[n][m] 放入1-n件物品,重量不超过m的最大价值
//初始化(不需要)
dp[0][m]都是0,因为装入0件物品
//放入1-i件物品
for(int i = 1; i <= n; i++){
//背包容量为j
for(int j = 0; j <= m; j++){
dp[i][j] = dp[i-1][j]
if(j >= weight[i]) dp[i][j] = max(dp[i][j], dp[i-1][j-weight[i]) + value[i];
}
}
//一维,滚动数组
dp[n]
//放入1-i件物品
for(int i = 1; i <= n; i++){
//背包容量为j
//从后向前遍历,相当于之前的数还没改变,还是上一层物品的值
//为忽略判断条件,要满足j >= v[i]
for(int j = m; j >= v[i]; j--){
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
1-2 完全背包(每件物品有无限个)
//二维 朴素做法
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
//k为0,相当于不放物品第i件时的最大值,就是dp[i-1][j]
for(int k = 0; k * weight[i] <= j; k++){
//max第二项包括了dp[i-1][j] 的情况,故第一个为dp[i][j]
dp[i][j] = max(dp[i][j], dp[i-1][j-k * weight[i] + value[i] * k);
}
}
}
优化思路:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P3GTw40F-1640611084660)(https://note.youdao.com/yws/res/44/FFE5D90878BA474A85EE2195B4F61FE4)]
比较:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mtlsVzYq-1640611084661)(https://note.youdao.com/yws/res/46/D560C84D1A004D46A2DEB248DCE01BCF)]
//优化
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
dp[i][j] = dp[i-1][j];
if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i][j - weight[i]) + value[i] ;
}
}
}
//一维
for(int i = 1; i <= n; i++){
for(int j = v[i]; j <= m; j++){
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
01背包从大到小循环
完全背包从小到达循环
1-3 多重背包(每件物品有si个)
//二维 朴素做法
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
//k为0,相当于不放物品第i件时的最大值,就是dp[i-1][j]
for(int k = 0; k * weight[i] <= j && k <= s[i]; k++){
//max第二项包括了dp[i-1][j] 的情况,故第一个为dp[i][j]
dp[i][j] = max(dp[i][j], dp[i-1][j-k * weight[i]) + value[i] * k;
}
}
}
优化:
二进制->十进制的启发
s=200打包成1,2,4,8,16,32,64,73 = 127 + 73
所以0-127能凑出来,73-200能凑出来
拆分
for(int i = 1; i <= n; i++){
for(int j = m; j >= v[i]; j--){
f[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
1-4 分组背包(物品有N组,每一组物品有若干个,每一组只能选一个)
只从前i组物品各选一件,并且总体积不大于j
for(int i = 1; i <= n; i++){
for(int j = m; j >= 0; j--){
//s[i]是第i组有几个物品
for(int k = 0; k < s[i]; k++){
if(v[i][k] <= j){
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
}
2. 线性dp
单串:
- 最长上升子序列(LIS)
- 最大子数组和系列?
- 打家劫舍
- 股票问题
- 题目
- 300. 最长递增子序列(Ologn的算法同样重要,贪心二分)
双串
- 最长公共子序列(LCS)
- 编辑距离
- 题目
3. 区间dp
- 模板
for(int len = 2; len <= n; ++len){
for(int i = 0; i <= n - len; ++i){
int j = i + len - 1;
for(int k = i + 1; k < j; k++){
p[i][j] = ...;
}
}
}
4. 计数dp
5. 数位dp
- 计算1-n中某个数字出现的个数
- 如果计算a-b用前缀和
- 题目
6. 状压dp
- 用01序列表示状态
7. 树形dp
一些新的理解
- dp方程定义、状态转移、初始化、遍历顺序、举例验证
- 线性dp:从前到后
- 区间dp:从短到长
//仅与前一个有关
for(len){
for(i){
int l = i, r = i + len;
dp...
}
}
//与前n个有关
for(len){
for(i){
int l = i, r = i + len;
for(k:l~r){
dp...
}
}
}
- 状压dp:用一个32位二进制数表示(是否使用选取/使用的)状态