动态规划 (01背包模型)
1、基本的背包模型
2、动态规划的理解方式
通常可以将 动态规划(DP) 问题分为两个部分进行理解:1. 状态表示。2. 状态计算
2.1状态表示(抽象点)
背包模型,可以表示成二维的
- 一个表示在哪几个物品里面选
- 一个表示背包的体积多少
如: dp[i][j]
其中的 i
表示为从i
个物品中选择。j
表示选择的物品的题解不超过j
动态规划需要用到的数据结构是数组,用来记忆之前的属性从而推出下一个的属性的过程。用数组的下标,记录当前的状态,一个状态表示的是当前状态所约束下的集合,
属性:数组的值表示的是属性。例如:最大值最小值,和数量等,属性是指当前状态下集合的属性,是基于当前状态的基础上的属性。
如:dp[i][j]
表示的状态是从前 i
个物品中选择,总体积小于等于 j
的所有选择方案的集合。而 dp[i][j]
的值本身表示的是这个状态下集合中的属性
正如上面所说,集合是受状态约束的,约束的变量是数组的两个下标,不同的题目“约束的条件”不一样
01背包的约束条件
- 只从前i个物品里面选
- 总体积<= j
约束条件就好像一个函数,是随着题目不同而确定下来的
- 状态:数组的下标就是自变量
- 状态改变 ——>集合元素改变——>属性改变
状态是最根本的,状态的改变决定了集合的范围,以及属性的值
2.2状态计算部分(难点)
状态计算的过程:通过已知的部分集合的属性求出未知的集合的属性
2.2.1将集合进行划分:
首先,集合的改变只受状态(自变量)改变的影响,所以当i
改变时,也就是可选择物品的数量增加时,要遍历一遍 j
的所有不同的取值。
表示的是,当可选择物品的数量改变的时候。相同的背包体积,可存储的物品选择也会发生改变。从而引起集合个数的改变,因为集合的状态改变了,对应的属性也可能会发生相应的改变。因此要更新状态。
所以定义双重循环遍历所有的状态:
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
{
/**
*占位符
**/
}
可选择物品的数量改变前的集合属性是已知的,但是改变后的集合属性是未知的。而我们要做的就是通过计算,根据已知的集合属性求出改变后的集合属性。
将改变后的集合分为两个部分:
第一部分,未新增物品前的集合
第二部分:新增物品后,新增出来的集合
如下图可知,当新增了一个物品i
之后,就会新增很多种选择,新增的选择有一个特点,就是包含新增的物品 i
可以将集合都分为两个部分,一个是包含i
的选法即新增的部分,一个是不包含i
的选法即原本的部分
由上可知,不包含i
的选法的属性是已知的 dp[i-1][j]
,但是包含i
的选法是未知的
由上图可知,如果要求新增物品i之后的所有选法的最大值,我们可以曲线救国:
2.2.2求包含i的选法(新增集合)的最大值
新增集合即包含 i 物品的集合有一个特点,就是每一种组合都固定包含物品 i
可以将集合中的每个组合拆分成两个部分:
第一部分:物品 i
第二部分:除了物品 i 的其他物品
如上图可知,
我们将一个方案拆分成两个部分,所以所有的方案数,也就是状态所表示的集合数量:
应用乘法原理得到:第一部分的的方案数,乘以 第二部分的方案数
这个集合中方案的最大值:
应用加法原理:第一部分方案的最大值 + 第二部分方案的最大值;
即可以推出:dp[i][j] = max(dp[i-1][j],dp[i-1][j-v[i]] + w[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;
}
优化代码,二维变一维
思路
二维不断更新的过程
特点:需要用到的状态值是dp[i-1][j-v[i]]
其中 i 值用到了 i - 1,而 j 值用到了 j-v[i]
总结:
注意点 1:因为i
只用到了i-1
那么可以用滚动数组来做
滚动数组:
举例: 要求b要用到a,那么当用a求出b的时候,就可以把当前b的值当成是a用来再求b+1的值
注意点二:j
只用到了j-v[i]
当要更新 j 的时候只会用到体积j-v[i]
的状态。
加上上面已经将二维数组转换成一维的,要将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;
}
大佬TQL