前言:
动态规划的本质,是对问题状态的定义和状态转移方程。
动态规划具备三个特点:
1.将原来的问题分解成几个相似的子问题;
2.所有的子问题都只需要解决一次
3.每个状态存储子问题的解
一般从三个角度考虑动态规划:
1.状态表示:
2.状态计算 - > 集合的划分
3.状态初始化
集合的划分依据,需要满足两个条件
1. 以最后一个改变的元素为依据。
2. 集合中应包含所有的方案。
而动态规划问题一般可以分为线性DP,背包问题,区间DP,计数类DP,数位统计DP,状态压缩DP,树形DP,背包问题是大头,也是我们这章的重点。
全文共12499字
目录:
一 .四个基础背包问题
①01背包
②完全背包
③多重背包
④分组背包
二 .背包问题的扩展
①二维背包
②混合背包
③背包求方案数
④背包存路径
⑤背包问题求方案数的至多、至少、恰好问题的总结
正文:
一.背包问题都以01背包为基础
我们以01背包问题为引子:
有 N 件物品和一个容量是 V 的背包。问最多能装入背包的总价值是多大?
状态表示:所有只从前i个物品中选, 总体积不超过j的方案集合;
集合划分:
初始化:第0行和第0列都为0,表示没有装物品时的价值都为0:F(0,j) = F(i,0) = 0
;
对于集合的两种划分方式:
第一种方案:如果不选择第i个物品,价值就与前一维相同: f[i][ j] = f[i - 1][j]
第二种方案:不选择第i个物品: f[i][j] = f[i - 1][j - v[i]] + w[i]
从而得到状态转移方程,即max(f[i][j], f[i - 1][j - v[i]] + w[i] )
问题一:为什么方案二在这里第二维表示是j - v[i] ?
答:因为要选择的第i个物品体积为v[i] ,而总体积为j,所以要空出v[i]的体积来填充
二维c++代码如下:时间复杂度 :O(n * m )
注:具体题目可以在洛谷、力扣等平台找
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];
int main() {
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> v[i] >> w[i]; //注意i要大于0,因为后面要用到下标i - 1;
for(int i = 1; i <= n; i++) {
for(int j = 0; j <= m; j++) {
if(j < v[i])f[i][j] = f[i-1][j];
else f[i][j] = max(f[i - 1][j], f[i-1][j-v[i]]+w[i]);
//最后即比较加上该物品时的价值与不加该物品时的价值哪个大,
//不加该物品时f[i][j]是指不包含该物品时其它相加的价值总和
//而加上此物品后,得到的是 f[i][j - v[i]] 的价值最大值加上当前物品价值w[i]
}
}
cout << f[n][m] << endl;
return 0;
}
为什么答案是f[n][m] ?
答:第一维是因为要枚举n个物品, 而第二维,即我们的背包体积,f[n][m] 是从f[1][0]一路推过来的。
一维优化: 时间复杂度 :O(n * m )
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int n, m;//n是物品数量,m是背包最大容量
int v[N], w[N];//存储物品体积和价值
int f[N];//存储最优解答
int main()
{
cin >> n >> m;
//输入对应的 体积和价值
for(int i = 1;i <= n;i ++) cin >> v[i] >>w[i];
for(int i =1;i <= n;i ++)
for(int j = m;j >= v[i];j --)//如果正序枚举,j循环中的小体积的更新更新会 影响后面的更新
f[j] = max(f[j], f[j - v[i]] + w[i]);//因为f[j - v[i]] + w[i]这里用的 f[j - w[i]
//应该是上一层i的,即i - 1而不能在这层之前被更新,否则会影响到后面更新
cout << f[m] << endl;
return 0;
}
这里存在两个问题:
一. 为什么可以这样优化
我们可以注意到二维数组的更新方式为
f[i][j] = f[i - 1][j] // 不含i的所有选法的最大价值
if j >= v[i] //判断含i的选法是否成立
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i])
可以看出,f[i][] 只与f[i - 1]相关,所以根本没必要保留f[i - 2]即后面的状态值。
将空间从O(n*m)优化为 O(m), n, m分别为物品个数和背包体积。
二. 为什么第二行循环需要倒叙枚举 (与之相对的是完全背包的正序枚举)
当我们更新索引值较大的dp值时,需要用到索引值较小的上一层dp值dp[j - v[i]];
也就是说,在更新索引值较大的dp值之前,索引值较小的上一层dp值必须还在,还没被更新;
所以只能索引从大到小更新。
就是说 在状态转移方程f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i])中f[i - 1][j - v[i]] 这里使用的是i - 1层的dp,而如果我们正向枚举时 我们的 f[ j - v[i] ]会在 f[j] 之前更新,也就是在更新到f[j - v[i]]这一层时,我们使用的将是被更新过的 f[i][j - v[i]] ,这与原方程f[i - 1][j - v[i]] 不同,所以我们需要倒叙枚举,使得我们将使用的f[j - v[i]] 是未被更新过的
二.有了以上的基础,我们就可以展开对 完全背包,多重背包, 分组背包, 这三个基础背包模型的学习
完全背包 :有 N种物品和一个容量是 V 的背包,每种物品都有无限件可用。
状态表示: 所有只从前i个物品中选, 总体积不超过j的方案集合
集合的划分方式:第i个物品选几个