动态规划
方法论
1.确定dp数组以及下标的含义
2.确定递推公式
3.dp数组如何初始化
4.确定遍历顺序
5.举例推导dp数组
背包问题的模板
背包问题分类:
常见的背包类型主要有以下几种:
1、0/1背包问题:每个元素最多选取一次
2、完全背包问题:每个元素可以重复选择
3、组合背包问题:背包中的物品要考虑顺序
4、分组背包问题:不止一个背包,需要遍历每个背包
而每个背包问题要求的也是不同的,按照所求问题分类,又可以分为以下几种:
1、最值问题:要求最大值/最小值 -》dp[j] = max(dp[j], dp[j - nums[i]] + 1)
或者dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
2、存在问题:是否存在…………,满足…………-》dp[j] = dp[j] || dp[j - nums[i]]
3、组合问题:求所有满足……的排列组合-》dp[j] += dp[j - nums[i]]
分类解题模板
背包问题大体的解题模板是两层循环,分别遍历物品nums和背包容量target,然后写转移方程,
根据背包的分类我们确定物品和容量遍历的先后顺序,根据问题的分类我们确定状态转移方程的写法
首先是背包分类的模板:
1、0/1背包:外循环nums,内循环target,target倒序且target>=nums[i];
2、完全背包:外循环nums,内循环target,target正序且target>=nums[i];
3、组合背包:外循环target,内循环nums,target正序且target>=nums[i];
4、分组背包:这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板
然后是问题分类的模板:
1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);
2、存在问题(bool):dp[i]=dp[i]||dp[i-num];
3、组合问题:dp[i]+=dp[i-num];
背包问题
01背包
题目要求:有N件物品和一个最多能背W的背包,每一件物品的重量无w[i],价值为v[i]。每一件物品只能用一次,求解将哪些物品放入背包中可以使得背包中的物品价值总量最大。
第一种思路应该是暴力递归,只有N件物品,每一件物品只有一件,所以每一件物品只有两种状态:选或者不选。
(回溯)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, W;
int w[N], v[N];// 物体的重量和价值
int dp[N][N];
int ans;
void dfs(int index, int val)
{
if (W < 0) return ;// 如果背包不能够剩余的空间就直接返回
if (index == n + 1) {
if (val > ans) {
ans = val;
}
return ;
}
//不选第index件物品
dfs(index + 1, val);
// 选择第index件物品
val += v[index];// 将加上相应的价值
W -= w[index]; // 减去相应的重量
dfs(index + 1, val);
W += w[index]; // 撤销
val -= v[index];
}
int main()
{
cin >> n >> W;
for (int i = 1; i <= n; i ++) cin >> w[i] >> v[i];
int val = 0;
dfs(1, val);
cout << ans << endl;
return 0;
}
第二种思路就是动态规划,因为背包容量为W的背包可以拆解成更小的背包,w1 , w2,w3 …所以根据大问题化成小问题的思想就可以使用动态规划。并且01背包问题,相较前面的问题而言多了一个约束条件,就是不仅要将物品放入背包中,背包也是有容量的,由于这个约束条件所以才需要挑选,否则每次只要选择价值最大的物品即可。正因为这个有容量的考虑,所以这次就不可以只用一个一维的数组来保存前面的计算的结果,还要在多加一维数组来记录背包的容量。
1.dp数组的含义
dp[i][j]
表示前i个物品中任意选取,放入容量为j的背包,可以得到的最大价值总和为多少。
2.递推公式
将前i个物品这个大问题拆解成小问题,即只讨论第i个物品,前i - 1个默认已经递推计算完成了。
对于第i个物品来说只有两种状态:
1.背包中不放第i个物品,那么前i个物品中的价值总和前i - 1个物品中的价值总和相等。即dp[i][j] = dp[i - 1][j]
。
2.背包中方如第i个物品,那么前i个物品中价值总和等于第i个物品的价值加上前i - 1个物品中选择任意物品放入背包容量为W - w[i](背包的容量因为放入第i个物品而缩小了)。
在上面两者中选择最大值,即dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i])
。
小技巧:添加第一列空背包容量即没有背包已将满了这是必须要的,因为当背包容量为0的时候需要加dp[i][0] (等于0)
。但是如果加上上面的第一行(即空背包),可以简便计算。因为递推公式中:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
都需要用到上一层数组中的数字,但是如果不加第一行的话,因为第0行没有上一层,所以就需要先给第一行初始化,在使用递推公式。如果加上第一行的话,默认空背包的时候没有价值,这样就可以不用初始化了,因为也可以使用递推公式推出来了。
3.初始化dp数组
3.1
ps:如果不加第一行需要初始化第0行的代码:
for (int i = W; i >= w[0]; i --) {
// 逆序初始化
dp[0][i] = dp[0][i - w[0]] + v[0];
}
3.2
如果使用第一行的dp数组,第一行全部初始化为0即可。
4.遍历顺序
如果是最常规的遍历方法可以根据递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
可知需要用到上一行的计算过的数组,所有从上到下,从左到右的遍历即可。
如果要优化到一维数组的话就必须要从右往左的遍历(下面在优化dp的时候会有详细配图解释)。
5.举例dp
w[i] = {1, 2, 3, 4}
v[i] = {2, 4, 4, 5}
(朴素动规)
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, W;
int w[N], v[N];
int dp[N][N];
int main()
{
cin >> n >> W;
for (int i = 1; i <= n; i ++) cin >> w[i] >> v[i];
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= W; j ++) {
if (j < w[i]) {
dp