动态规划 (01背包模型)
1、基本的背包模型
2、动态规划的理解方式
2.1状态表示部分(抽象点)
背包模型基本表示成二维的:
- 一个表示在哪几个物品里面选
- 一个表示背包的体积多少
动态规划需要用到的数据结构是数组,用来记忆之前的属性推出下一个的属性的过程
用数组的下标,记录当前的状态,一个状态表示的是当前状态所约束下的集合,
属性:
数组的值表示的是属性
属性有最大值最小值,和数量等,
属性是指当前状态下集合的属性,是基于当前状态的基础上的属性
正如上面所说,集合是受状态约束的,约束的变量是数组的两个下标,不同的题目“约束的条件”不一样
01背包的约束条件
- 只从前i个物品里面选
- 总体积<= j
约束条件就好像一个函数,是随着题目不同而确定下来的
- 状态:数组的下标就是自变量
- 状态改变 ——>集合改变。
- 集合元素改变——>属性改变!
状态是最根本的,状态的改变决定了集合的范围,以及属性的值
2.2状态计算部分(难点)
状态计算的过程,就是通过已知的部分集合的属性求出未知的集合的属性
2.2.1将集合进行划分:
状态为:
1-i
个不包含i
,容量为j
1-i
个包含i
的,容量为j
首先,集合的改变只受状态(自变量)改变的影响,所以当i
改变时,也就是可选择物品的数量增加时,要遍历一遍 j 的所有不同的取值。
表示的是,当可选择物品的数量改变的时候,对应j所有的不同情况,更新一遍集合的属性
所以定义双重循环遍历所有的状态:
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
{
/**
*占位符
**/
}
改变前的集合属性是已知的,但是改变后的集合属性是未知的,也正是我们要求的
将改变后的集合分为两个部分:
第一部分,所有选法都不包含新增物品
第二部分:所有选法都包含新增物品
如下图可知,当新增了一个物品i之后,选法就多了很多,有包含i的和没有包含i的
那么我们可以将新增的物品后的所有集合都将其分为两个部分,一个是包含i的选法,一个是不包含i的选法
由上可知,不包含i的选法的属性是已知的,但是包含i的选法是未知的
由上图可知,如果要求新增物品i之后的所有选法的最大值,我们可以曲线救国:
- 先求不包含i的选法的最大值(已知)
- 求包含i的选法的最大值(未知,可求)
- 取两个最大值的最大值(简单的对比)
2.2.2求包含i的选法的最大值
第一部分状态表示:从
1~i
不包含i
的物品里面选,背包容量为j-v[i]
完整背包容量扣掉i
物体的体积第二部分状态表示:只选择第
i
个物品,背包容量刚好为i
的体积
第一部分是将背包的容量拆分成一个刚好容下i的体积的容量
第二部分是剩下的容量存放其他物体的
入上图可知,我们将一个方案拆分成两个部分,所以所有的方案数,也就是状态所表示的集合数量应应用乘法原理得到:第一部分的的方案数,乘以 第二部分的方案数
这个集合中方案的最大值,应该应用加法原理:第一部分方案的最大值 + 第二部分方案的最大值;
这时候所有都求出来了
二维代码:
#include<iostream>
using namespace std;
const int N = 10010;
int v[N],w[N];
int f[N][N];
int main()
{
int n,m;
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 = 0; j <= m; j++)
{
//第一部分 不包含i 容量为j
f[i][j] = f[i-1][j];
//第二部分 只包含i的 容量为j
// 再拆分成两部分, 不包含i 容量为j-v[i]
// 只有i 容量为V[i]
if(j >= v[i])f[i][j] = max(f[i][j],f[i-1][j-v[i]] + w[i]);
}
}
cout<<f[n][m]<<endl;
return 0;
}
优化代码,二维变一维
思路
二维不断更新的过程
特点:
-
每次都只用到要更新的上一层的值,之前的值都不会用到
-
具体用到上一层的那个格子是不固定的,由上面的式子可以知道,是由新增物品的体积所决定的。
但是,不管哪个格子用到的体积肯定比当前要更新的容积要小
f[j-v[i]] + w[i]
每次用到的都是当前要求的背包容量减去新增物品的体积
总结:
- 特点1可得:每次都只用到上一层,那么我们可以用滚动数组来做
滚动数组:
举例: 要求b要用到a,那么当用a求出b的时候,就可以把当前b的值当成是a用来再求b+1的值
由此可以将原本的二维数组变成一维数组
- 特点2可得:不管哪个格子用到的体积肯定是比当前要更新的容积要小
要将j
原本从小到大的遍历更新顺序变成从大到小
那么更新的时候就要保证两点:
第一点:用到的数据比当前要更新的数据要小
第二点:用到的数据是上一层的
解决第一点:因为这个部分是必须要拥有新增物品i的,所以背包容量至少要大于等于v[i]
才行,所以遍历j的时候从v[i]
开始,到m
结束,这样就保证,j - v[i] <= j
并且j - v[i] >=0
解决第二点:如果按照上一种情况的话就会遇到用到的数据不是上一层的,如下图
问题: 因为j
是从小到大遍历的,而更新的时候要用到小的,就会使用到已经更新过的值
所以修改一下方案,将j
的遍历从大到小,这样既解决了第一点,又解决了第二点,如下图
一维代码:
#include<iostream>
using namespace std;
const int N = 10010;
int v[N],w[N];
int f[N];
int main()
{
int n,m;
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--)
f[j] = max(f[j],f[j-v[i]] + w[i]);
cout<<f[m]<<endl;
return 0;
}