背包问题是DP动态规划算法中比较经典的一类模型,在NOIP考场上不定期地上位,令人琢磨不透,但是一旦学会了他,你就可以在短短十分钟的时间里,切掉他,达到节约时间,而且一次AC的目的.——某位大佬
01背包 | 完全背包 | 多重背包 | 分组背包 | 混合背包 |
---|---|---|---|---|
对于物品而言只能选择1个或者0个两种情况 | 对于物品而言可以无限制选取,也可以不选 | 对于物品而言最多能够选择从s[i]个,同样也可不选 | 一些物品捆绑在一起,每一组物品中只能选择其中的一个物品 | 有些物品可以选择1,有些物品可以选择无数个,有些物品只能选择是s[i]个.即:01背包+完全背包+多重背包. |
滚动数组 | 滚动数组 | 滚动数组 | 滚动数组 | 滚动数组 |
暂无 | 暂无 | 二进制优化或者单调队列优化 | 暂无 | 其中多重背包部分参考多重背包优化 |
以上就是本次的思维导图,即学习框架.
首先我们以01背包,所有背包的基础,开始进行本次的学习!
01背包
题目链接
有 $N$ 件物品和一个容量是 $V$ 的背包。每件物品只能使用一次。
第 $i$ 件物品的体积是 $v_i$,价值是 $w_i$。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,$N,V$,用空格隔开,分别表示物品数量和背包容积。
接下来有 $N$ 行,每行两个整数 $v_i, w_i$,用空格隔开,分别表示第 $i$ 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
$0 \lt N, V \le 1000$
$0\lt v_i, w_i \le 1000$
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
朴素思路
首先这道题目很明显的思路就是,对于一个物品而言,我们只有两种选择.
- 可以选择将他放入背包
- 不选择他,不放入背包中.
那么一个朴素思路就是,我们可以使用二进制枚举,枚举每一个物品,我们到底是,选择这个物品,还是不选择这个物品.
时间复杂度$O(2^N)$
DP思想 $O(n \times m)$
对于上面所说的复杂度,我们显然是不满意的,那么既然如此的话,我们如何去优化他呢?我们会想到动态规划(DP),因为这道题目有几个特殊的地方:
- 我们选择第i件物品,对于后面的物品并没有任何影响,即无后效.
- 它的决策非常明显,也就是我们只有,选择这个物品 或者 不选择这个物品这两种方案.
综上所述,我们可以先考虑考虑DP算法.
那么现在我们就要开始动态规划算法的思考题目流程.
- 状态数组如何定义
- 阶段是什么
- 决策是什么
- 边界是什么
- 状态转移方程
状态数组的定义
状态数组的定义:往往就是题目要我们求解的最终答案,所以我们大致可以这么定义状态数组.
对于$f[i][j]$,它表示为,在前i个物品选取若干个,已经花费的容量为j的最大利润.
举个例子,$f[3][5]$表示为在前三个物品中,我们选取0或者1或者2或者3个,然后已经花费了5点容量的最大利润.
如果觉得例子解释不好,可以不看,害怕误解
阶段
根据我们的状态数组,我们可以这么选择阶段,也就是$1 \le i \le n$,就是以物品为阶段.
决策
决策很明显,就是选取或者不选选取
边界条件
f数组全部清空就好了,因为我们要求的是最大利润.
状态转移方程
f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i];
$f[i-1][j]$就是前i-1个物品,花费j个容量的最大利润,在这里我们可以理解为,不选取第i个物品(即选取前i-1个)的最大利润.
$f[i][j-v[i]]+w[i]$就是选取第i个物品,花费为j个容量的最大利润,那么为什么要这么写?
因为我们要放入这个物品,那么至少我们得需要$v[i]$个容量,所以我们只能从$j-v[i]$这个容量推过来,不然我们放不下这个背包.
$+w[i]$很明显就是选取了这个物品,当然要将这个物品的价值加上去.
滚动数组优化
第一步优化:
首先,对于状态数组而言,我们只利用了$f[i][j]$和$f[i-1][j]$.如下面这个例子所示
- 当i为1的时候,我们利用的是$f[0][j]$和$f[1][j]$
- 当i为2的时候,我们利用的是$f[1][j]$和$f[2][j]$
- 当i为3的时候,我们利用的是$f[2][j]$和$f[3][j]$
所以其实我们只需要利用$f[0][j]$和$f[1][j]$就好了,0实际上表示i-1,1实际上表示i.
第二步优化:
实际上我们的j不一定要从$v[i],1+v[i],2+v[i]…m$,而是可以从$m,m-1.m-2…v[i]$因为这样的话,我们就可以从$f[2][n]$到$f[n]$了.因为刚开始没有处理第二种决策的时候,$f[1][j]$肯定等于$f[0][j]$,然后我们从m到$v[i]$,开始从上到下开始决策,就可以忽略$f[0]$这一维了.
这里讲的不太好,建议还是自行模拟一遍吧= ̄ω ̄=
具体代码实现
#include <bits/stdc++.h>
using namespace std;
const int N=1100;
const int M=2e5;
int n,m,i,j,k,a[N],f[M];
int main()
{
ios::sync_with_stdio(false);
cin>>m>>n;//读入最大容量和物品个数
for(int i=1;i<=n;i++)
cin>>a[i];
memset(f,0xcf,sizeof(f));//我这里是初始化为-INF,其实初始化为0就好了.
f[0]=0;
for(int i=1;i<=n;i++)//选取i个物品
for(int j=m;j>=a[i];j--)//容量设定
f[j]=max(f[j],f[j-a[i]]+a[i]);//转移
int ans=0;
for(int i=0;i<=m;i++)//f[n][i]表示选取n个物品,消耗容量为i的最有价值.
ans=max(ans,f[i]);
cout<<ans;//最优解
return 0;
}
大佬为何退役啊?
大佬,状态转移方程应该是f[i][j]=max(f[i-1][j],f[i - 1][j-v[i]]+w[i];
谢谢
当i为1的时候,我们利用的是f[1][j]f[1][j]和f[2][j]f[2][j]
当i为1的时候,我们利用的是f[2][j]f[2][j]和f[3][j]
大佬,这里写错了吧
这里打错了,修改已经完毕了.复制的时候忘记修改了.
再顶一下
顶一下
谢谢啦
顶一下😀
谢谢啦
顶一下
谢谢啦