鸣谢:@程墨竹
背包问题分析
1 背包问题的概念
给定几件(组)物品,每种物品都有自己的代价和收益,在最大可承受的代价内,我们如何选择,才能使得获得的总收益最高。
它可以看作是一种动态规划问题
2 背包问题的分类和解答
2.1 01背包
2.1.1 分析
01就是指物品只有拿和不拿两种选择,
正因如此
所以在Bag[i][j] 表示在有 i 种物品,容量为 j 的背包 中的状态只有
选择不拿
Bag[i-1][j]
/*
忽略第i个物品->不拿
所得价值即为前i-1个物品的最大的价值
*/
或者拿
Bag[i-1][j-v[i]]+c[i]
/*
Bag[以前几个物品][剩余空间]加上当前物品的价值->拿
*/
则状态转移方程就是以上两者的最大值
Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i]);
//拿与不拿的价值哪个大?
2.1.2 主要代码
上面说到,物品只有拿与不拿两种状态,但在拿的时候有一个需要注意的点就是确保空间足够
for(int i=1;i<=n;i++)
for(int j=V;j>=0;j--)
//可不可以拿
if(j>=v[i])//拿得了
Bag[i][j]=max(Bag[i-1][j]/*不拿*/,Bag[i-1][j-v[i]]+c[i]/*拿*/);
else//拿不了
Bag[i][j]=Bag[i-1][j];
2.1.3 完整代码
题目描述:
给定背包的容量和物品数量,以及每个物品的体积和价值,求可得的最大价值
样例输入:
10 4
2 1
3 3
4 5
7 9
样例输出:
12
数据范围:
所有数据和输出都在1000范围之内
参考代码:
#include <iostream>
using namespace std;
int main() {
int V,n;
cin>>V>>n;
int v[n+1],c[n+1];
int Bag[n+1][V+1];
for(int i=1;i<=n;i++)
cin>>v[i]>>c[i];
for(int i=0;i<=V;i++)
Bag[0][i]=0;
for(int i=1;i<=n;i++)
for(int j=V;j>=0;j--)
if(j>=v[i])
Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i]);
else
Bag[i][j]=Bag[i-1][j];
cout<<Bag[n][V];
return 0;
}
2.1.4 优化
生活中呢,有很多
很多时候,二维数组实在是占空间。
怎么办呢?
我们回顾一下01背包的状态转移方程:
Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i])
发现什么了吗?
i除了指向当前物品仿佛并没有作用,
那么,我们可以将Bag[n][V]改成Bag[V],
大概修改一下就是:
Bag[j]=max(Bag[j],Bag[j-v[i]+c[i]])
那么直接上完整代码:
#include <iostream>
using namespace std;
int main() {
int V,n;
cin>>V>>n;
int v[n+1],c[n+1];
int Bag[V+1];
for (int i=1;i<=n;i++)
cin>>v[i]>>c[i];
for (int i=0;i<=V;i++)
Bag[i]=0;
for (int i=1;i<=n;i++)
for (int j=V;j>=v[i];j--)
//之所以这里不加if,是因为如果空间不够就更新不了,因此再往下也没有意义
Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);
cout<<Bag[V];
return 0;
}
2.1.5 总结
01背包是所有背包问题的基础,请大家一定要掌握
也一定要掌握状态转移方程:
Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i])
2.2 完全背包
2.2.1 分析
完全背包问题中的每种物品都有无数件,可以无限取拿
我们这时就可以开 新循环k( 0 ~ j ( 1 ~ V ) / v[ 0 ~ n ] ) 来遍历这个物品可取拿的数量
根据01背包的状态转移方程:
Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i])
//原方程
来推出完全背包的状态转移方程:
Bag[i][j]=max(Bag[i][j],Bag[i-1][j-v[i]*k]+c[i]*k)
//k是当前物品拿取的数量
2.2.2 主要代码
因为要算出物品可取的数量,j只有从1到V循环答案才正确
for(int i=1;i<=n;i++)
for(int j=1;j<=V;j++)
for(int k=0;k<=j/v[i];k++)//若k从1开始,状态转移方程中的第一项应改为Bag[i-1][j]
// j/v[i] 就是物品可取数量
if(j>=v[i]*k)
//可以装不?
Bag[i][j]=max(Bag[i][j],Bag[i-1][j-v[i]*k]+c[i]*k);
/*
因为若装不了多的就装少一点(不管k怎么变化,都是赋值给Bag[i][j])
总之要尽量往大了装
*/
2.2.3 完整代码
题目描述:
给定背包的容量和物品数量,以及每种物品的体积和价值,每种物品有无数个,求可得的最大价值
样例输入:
10 4
2 1
3 3
4 5
7 9
样例输出:
12
数据范围:
所有数据和输出都在1000范围之内
参考代码:
#include <iostream>
using namespace std;
int main() {
//背包容量和物品总数
int V,n;
cin>>V>>n;
//物品体积与价值
int v[n+1],c[n+1];
int Bag[n+1][V+1];
for(int i=1;i<=n;i++)
cin>>v[i]>>c[i];
for(int i=0;i<=V;i++)
Bag[0][i]=0;
for(int i=0;i<=n;i++)
Bag[i][0]=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=V;j++)
for(int k=0;k<=j/v[i];k++)
if(j>=v[i]*k)
Bag[i][j]=max(Bag[i][j],Bag[i-1][j-v[i]*k]+c[i]*k);
cout<<Bag[n][V];
return 0;
}
2.2.4 简化
在01背包问题状态转移方程中第二项的意义为前i-1个物品所能得到的最大价值,而在完全背包中,每件物品可以无限获取,所以,完全背包的状态转移方程第二项应该为前i项的可获取的最大价值,即Bag[i][j-v[i]]。
代码如下:
for(int i=1;i<=n;i++)
for(int j=1;j<=V;j++)
if(j<=v[i]) Bag[i][j]=max(Bag[i-1][j],Bag[i][j-v[i]]+c[i]);
else Bag[i][j]=Bag[i-1][j];
2.3 多重背包
2.3.1 分析
多重背包问题中的物品有指定数量多个,还是开新循环k(0 ~ n[i])来遍历这个物品拿取的数量,即:
Bag[j]=max(Bag[j],Bag[j-v[i]*k]+c[i]*k);
2.3.2 基础代码
我们按上述方法,即开新循环遍历拿取数量来做
#include <iostream>
using namespace std;
int main()
{
int V,N;
cin>>V>>N;
int v[N+1],c[N+1],n[N+1];
int Bag[V+1];
for (int i=1;i<=N;i++)
cin>>v[i]>>c[i]>>n[i];
for (int i=0;i<=V;i++)
Bag[i]=0;
for (int i=1;i<=N;i++)
for (int j=V;j>=v[i];j--)
//不用算k循环的次数,所以还是从V到v[i]
for(int k=0;k<=n[i];k++)
//遍历每种物品的数量所可得的最大价值
if(j>=k*v[i])
Bag[j]=max(Bag[j],Bag[j-k*v[i]]+k*c[i]);
cout<<Bag[V];
return 0;
}
2.3.3 简化思路
以上思路的时间复杂度是:O(V×∑n[i])
但是还有更快的方法求解,它的时间复杂度是:O(V×∑㏒ n[i])
我们把 第i种物品换成n[i]件 01背包中的物品
那么,怎么 取0…n[i]件 才能等价于取若干件代换以后的物品并且取超过n[i]件的数量的策略不能出现
方法是:将第i(1~N)种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数
使这些系数分别为1,2,4,…,2(k-1),n[i]-2k+1, *1
例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。
数量 | 组合方式 |
---|---|
1 | 1 |
2 | 2 |
3 | 1+2 |
4 | 4 |
5 | 1+4 |
6 | 6 |
7 | 1+6 |
8 | 2+6 |
9 | 1+2+6 |
10 | 4+6 |
11 | 1+4+6 |
12 | 2+4+6 |
13 | 1+2+4+6 |
≥14 | 无法 |
综上所述,伪代码如下:
N<-0
△在输入时就调整:
for i <-1 to tmpn i++ do
tmp <- 1
while n>=tmp do
N<-N+1
v[N] <- x*tmp
c[N] <- y*tmp
n <- n-tmp
△如上所述的原理
tmp <- tmp*2
end
N <- N+1
v[N] <- x*n
c[N] <- y*n
end
2.3.4 完整代码
题目描述:
给定背包的容量和物品数量,以及每种物品的体积和价值以及数量,求可得的最大价值
样例输入:
8 2
2 1 4
4 1 2
样例输出:
4
数据范围:
所有数据和输出都在1000范围之内
参考代码:
#include <iostream>
#define maxn 12345
using namespace std;
int main()
{
int V,tmpn,N=0;
cin>>V>>tmpn;
int x,y,n,tmp;
int v[maxn],c[maxn];
int Bag[V+1];
for (int i=1;i<=tmpn;i++) {
cin>>x>>y>>n;
tmp=1;
while(n>=tmp) {
v[++N]=x*tmp;
c[N]=y*tmp;
n-=tmp;
tmp*=2;
}
v[++N]=x*n;
c[N]=y*n;
}
for (int i=0;i<=V;i++)
Bag[i]=0;
for (int i=1;i<=N;i++)
for (int j=V;j>=v[i];j--)
Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);
cout<<Bag[V];
return 0;
}
2.4 三种背包的混合情况
2.4.1 分析
这个题顾名思义,是将01,完全,多重背包三种问题混合起来,
主要代码的伪代码如下:
for i <- 1 to N i++
if n[i]==0 then
for j <- v[i] to V j++
△完全背包,j正着跑
Bag[j] <- max(Bag[j],Bag[j-v[i]]+c[i])
end
else
for j <- V to v[i] j--
△01和多重背包,j反着跑
for k <- 0 to n[i] k++
if j>=k*v[i] then
Bag[j] <- max(Bag[j],Bag[j-k*v[i]]+k*c[i])
end
end
end
2.4.2 完整代码
题目描述:
给定背包的容量和物品数量,以及每种物品的体积和价值以及个数,0为无数个,求可得的最大价值
样例输入:
10 3
2 1 0
3 3 1
4 5 4
样例输出:
11
数据范围:
所有数据和输出都在1000范围之内
参考代码:
#include <iostream>
using namespace std;
int main()
{
int V,N;
cin>>V>>N;
int v[N+1],c[N+1],n[N+1];
int Bag[V+1];
for (int i=1;i<=N;i++)
cin>>v[i]>>c[i]>>n[i];
for (int i=0;i<=V;i++)
Bag[i]=0;
for (int i=1;i<=N;i++)
if(n[i]==0) {
for (int j=v[i];j<=V;j++)
Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);
}
else {
for (int j=V;j>=v[i];j--)
for(int k=0;k<=n[i];k++)
if(j>=k*v[i])
Bag[j]=max(Bag[j],Bag[j-k*v[i]]+k*c[i]);
}
cout<<Bag[V];
return 0;
}
2.4.3 简化
因为多重背包的输入可以简化,我也可以在此题的输入中做类似的操作来简化时间复杂度
如下:
#include <iostream>
#define maxn 1000
using namespace std;
int main() {
int N,V;
cin>>V>>N;
int tmpv,tmpc,tmpk,v[N+1],c[N+1],n[N+1],cnt;
for(int i=1;i<=N;i++) {
cin>>tmpv>>tmpc>>tmpk;
if(tmpk==0) {
v[++cnt]=tmpv;
c[cnt]=tmpc;
n[cnt]=0;
}
else {
for(int j=1;j<=tmpk;j<<=1) {
tmpk-=j;
v[++cnt]=j*tmpv;
c[cnt]=j*tmpc;
n[cnt]=-1;
}
if(tmpk>0) {
v[++cnt]=tmpk*tmpv;
c[cnt]=tmpk*tmpc;
n[cnt]=-1;
}
}
}
int Bag[V+1]={0};
for(int i=1;i<=cnt;i++) {
if(n[i]==0) {
for(int j=v[i];j<=V;j++) {
Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);
}
}
else {
for(int j=V;j>=v[i];j--) {
Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);
}
}
}
cout<<Bag[V];
return 0;
}
2.5 小结
现在,01,完全,多重这三种基础的背包问题,以及他们的混合的问题已经介绍完了,希望大家能充分理解,谢谢
那么还有什么建议或者问题可以在评论区留言,另外,再次谢谢大家的阅读
另外,之后的问题只是略讲一番,
因为背包问题的思路已经全部介绍完了
2.6 二位费用背包
2.6.1 分析
那么继续介绍背包问题,二位费用,顾名思义就是指物品有两种代价,且两种代价不相关,那么,还是根据01的方程*2:
Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i]);
推出
Bag[i][v][u]=max(Bag[i-1][v][u],Bag[i-1][v-a[i]][u-b[i]]+c[i]);
其中v,u是第一二维的最大可承受代价,a是该物品第一维的代价,b是该物品的第二维代价,c是该物品的收益
2.6.2 主要代码
如上所说,我们直接开三层循环
伪代码如下:
for i 1 to N i++
for v V to a[i] v--
△逆序循环
for u U to b[i] u--
△逆序循环
Bag[v][u] <- max(Bag[v][u],Bag[v-a[i]][u-b[i]]+c[i]);
△Bag[第一维的费用-当前物品的第一维费用][第二维的费用减去-当前物品的第二维费用]+物品的价值
2.6.3 完整代码
题目描述:
给定背包的容量,最大可承重和物品数量,以及每种物品的体积,重量和价值,求可得的最大价值
样例输入:
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
样例输出:
8
数据范围:
所有数据和输出都在1000范围之内
参考代码:
#include<iostream>
using namespace std;
int main()
{
int n,V,U;
cin>>n>>V>>U;
int a[n+1],b[n+1],c[n+1],Bag[V+1][U+1]={0};
for (int i=1;i<=n;i++) cin>>a[i]>>b[i]>>c[i];
for (int i=1;i<=n;i++)
for (int v=V;v>=a[i];v--)
for (int u=U;u>=b[i];u--)
Bag[v][u]=max(Bag[v][u],Bag[v-a[i]][u-b[i]]+c[i]);
cout<<Bag[V][U];
}
2.7 分组的背包问题
2.7.1 分析
顾名思义,这个问题中的物品被分成数个组,但是明显不可能让你挑一个组去求,而是从各组中选一个出来,求所获得的最大价值
那很明显,第一层循环i应该遍历组数,第二层j遍历V,第三层k遍历i组中的物品
伪代码如下:
for i 1 to 分组总数 i++
for j V to 0 j++
for 遍历第i组的第k件物品
Bag[j] <- max(Bag[j],Bag[j-v[i][k]]+c[i][k])
这里,k是遍历物品的循环,但是它和01不同的是,k在第三层,这样才能保证每个组只拿一个物品,赋值给Bag[j]
2.7.2 完整代码
题目描述:
给定背包的容量,物品组数,以及每组物品的个数,及组内物品的体积,价值,求可得的最大价值
样例输入:
3 10
3
3 2
2 3
2 2
2
5 4
6 4
3
1 2
2 1
4 3
样例输出:
9
数据范围:
所有数据和输出都在1000之内
参考代码:
#include <iostream>
#define maxn 1005
using namespace std;
int main() {
int V,n;
cin>>n>>V;
int v[n+1][maxn],c[n+1][maxn],num[maxn];
int Bag[V+1]={0};
for(int i=1;i<=n;i++) {
cin>>num[i];
for(int j=1;j<=num[i];j++) {
cin>>v[i][j]>>c[i][j];
}
}
for(int i=1;i<=n;i++) {
for(int j=V;j>=0;j--) {
for (int k=1;k<=num[i];k++) {
if (v[i][k]<=j) {
Bag[j]=max(Bag[j],Bag[j-v[i][k]]+c[i][k]);
}
}
}
}
cout<<Bag[V];
return 0;
}
2.8 小结
这里,背包的二次进阶玩法就完了
都只要骚微的改一下就完了
后面的问题,就涉及到树形dp,函数了
我尽量把思路讲清,代码有点废肝*3
2.9 有依赖的背包问题
依赖的背包问题比较复杂,我简单说说,首先,假设物品b依赖a,那么只有选了a,才能选b,当然所有物体也可能有依赖关系,这里讲一下一个物品只能有一个被依赖的物品或者依赖的物品*4
那么,我们从推01的方程的过程:
推出这里简单的方程:
第一个状态:
Bag[j]
//不拿主件
第二个状态:
Bag[j-v1[i]]+c1[i]
//只拿主件
第三种状态:
Bag[j-v1[i]-v2[i]]+c1[i]+c2[i]
//主副件同时拿
伪代码如下:
for i 遍历主件
for j 遍历空间
if 有附件 & j>=v1[i]+v2[i] △可以拿附件
then Bag[j] <- max(Bag[j],Bag[j-v1[i]]+c1[i],Bag[j-v1[i]-v2[i]]+c1[i]+c2[i])
else if j>=v1[i] △只能拿主件
then Bag[j] <- max(Bag[j-v1[i]]+c1[i],Bag[j-v1[i]-v2[i]]+c1[i]+c2[i])
如果是将条件改成一个主件可以有多个附件,但附件不能有该物品的附件
则我们可以对当前物品的附件做一次01背包,设Ben[0~V-v[i]]为附件的dp结果,令k为遍历Ben的循环,那么v[i]+k为体积,Ben[k]+c[i]是价值,再一般一点就是树形dp了
套娃是不可能的,代码就算了
2.10 泛化背包
这里的物品全是一个个函数,你给它多少空间(v),他就给你 (fun(v))的价值
还是从01的方程:
Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i]);
推出:
Bag[j]=max(Bag[j],Bag[j-k]+c[i][k]);
其中k是用来遍历代价的循环,c[i][k]则是物品i所给代价k后所得的价值
2.11 01背包的方案总数
因为要求方案总数,那么我们可以额外定义Ste[i][j]来遍历Bag[i][j]问题的方案总数
伪代码如下:
Ste(0,0) <- 1 △重要一步
for i 1 to n i++
for j 0 to V j++
if j>=v[i] then Ste(i,j) <- Ste(i-1,j)+Ste(i-1,j-v(i))
else then Ste(i,j) <- Ste(i-1,j)
怎么样?是不是惊人的相似,因为计算方案总数的性质和计算答案差不多,区别在于一个求数据,一个求方案总数
3 总结
现在,你已经初步了解了背包问题,但更多的是练习,希望大家更了解背包,谢谢