背包九讲总结


前言

根据y总的视频,总结了一下背包九讲的内容,准备好好搞一下dp问题。


一、01背包(二维分析、一维优化)

1.背景

01背包

先给定背包容积大小V,再给定物品数目N,之后根据N的值输入每个物品的体积v[i]跟价值w[i],问背包所能容纳物品的最大总价值为多少。


2.分析

步骤一:确定状态
步骤二:确定状态转移方程
步骤三:确定边界情况和初始条件
步骤四:确定计算顺序

1.二维分析

我们选择f[i][j]来作为一个状态,意味着有i个物品,在背包容积为j的情况下的最优解。

那么我们不妨以f[i][j]为例来推一下状态转移方程
此时我们要讨论第i个物品是否要放入:
如果不放或者说放不下物品i,f[i][j]=f[i-1][j],即与i-1个物品所对应的结果一致;
如果选择放且能放下,f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]),在原来不放的情况下与选择放的情况下选出一个最优解,而且如果是选择了放,那么物品 i 势必要占用 v[i] 的空间,所以我们要借助在f[i-1][j-v[i]] 下的解加上w[i] 的和作为此时的解。

i 应该选择从第一个物品开始,即由1到N,而 j 从小到大还是从大到小于此时并无影响
我们不妨选择由小到大’。
那么在一个二维表上,可以更为直观地表明我们的推导方向是自上往下、自左向右

2.一维优化

通过对状态转移方程的观察,我们不难发现在推导第 i行时,我们参考的数据其实只有第 i-1 行,那么我们能否只借助一个一维数组达到原先的功能呢?
之前说过在第 i 行上的推导自左往右与自右往左并无影响,因为它们可以直接参考上一行的数据;如果我们仅使用一行,意味着这一行需要将原来的 i-1 行与变化后的第 i 行联系在一起。
我们的思路就是从右往左遍历这一行,并讨论能否放下第 i 个物品:
若不能放下,则不进行操作,保留原值,恰为上方的f[i-1][j];
若可放下,进行比较 f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);

物品嘛,进行一个个讨论;空间嘛,自右往左,一旦放不下就算啦,讨论下一个物品吧。


3.代码

//一维写法
#include<iostream>
using namespace std;
int v[1005];
int w[1005];
int t[1000005];
int T,m;
int main(){
	cin>>m>>T;
	for(int i=1;i<=m;i++){
		cin>>v[i]>>w[i];
	}
	for(int i=1;i<=m;i++){
		for(int j=T;j>=v[i];j--){
			t[j]=max(t[j],t[j-v[i]]+w[i]);
			}
		}
	cout<<t[T];
}

二、完全背包(与01背包比较)

1.背景

完全背包
背景与01背包类似,不过此时对物品的数量不做限制,意味着每个物品都可以取任意个。


2.分析

1.第一印象
我第一次见这道题时,以为是要用贪心来做,按价值体积比由大到小逐一选择,后来才晓得这是一道动态规划的题目。

2.与01背包联系
这道题与01背包的区别在于物品数目没有限制,(其实好像也可以转化为01背包来处理吧,不过有点类似多重背包了) 我们在这里用了一个很巧妙的思路。
在01背包中,我们是从右到左进行处理,避免我们的操作会影响循环内后续的操作,因为我们只有一个物品;
而在这里,我们选择却是自左往右处理,其余操作不进行改变;

3.解释区别
以 f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i])为例进行比较:

在01背包中,f[i][j-v[i]]里面是一定没有放入物品 i

在完全背包中,f[i][j-v[i]]里面却有可能已经放入了若干个物品 i,这就意味着我们其实是多次调用了物品 i


3.代码

#include<iostream>
using namespace std;
int v[1005];
int w[1005];
int t[1000005];
int T,m;
int main(){
	cin>>m>>T;
	for(int i=1;i<=m;i++){
		cin>>v[i]>>w[i];
	}
	for(int i=1;i<=m;i++){
		for(int j=v[i];j<=T;j++){
			t[j]=max(t[j],t[j-v[i]]+w[i]);
			}
		}
	cout<<t[T];
}

三、多重背包(朴素、二进制优化划分、单调队列优化)

1.背景

背景可以参考完全背包,不过每种物品的数量存在上限,所以我们此时对于每个物品还需要增加一个参数——数量 s[i]


2.分析

1.朴素写法
多重背包1
0<v[i],w[i],s[i]≤100

时间复杂度:O(n * n * n)
对于数据量较小的情况,我们完全可以把这道题看作是一道01背包的问题,把第 i 个物品拆分为 s[i] 个 体积价值均相同的物品。
那么代码与上方01背包类似,不过要修改一下状态转移方程,此时对于物品 i ,因为有 s[i] 个物品,就需要在里面进行逐一比较,选出最优解。

for(int k=1;k<=s[i]&&j>=k*v[i];k++){
				f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
			}

2.二进制优化划分
多重背包2
0<v[i],w[i],s[i]≤2000

时间复杂度:O(n * n * log n)

此时的数据量比较大,如果采取朴素写法是一定会超时的,我们仍然参考01背包,不过在朴素写法中是把第 i 个物品拆分为 s[i] 个 体积价值均相同的物品,这也是我们在朴素写法中的第三重循环,但我们完全可以对划分进行优化。
对于任意一个数字来说,都可以用一个二进制来表达,如7 ,二进制为“111”,可以被划分为个数分别为1、2和4的三堆物品,但我们此时并不是完全采用二进制来划分.

误区:
我一开始觉得用二进制优化,不如干脆来一个lowbit,这不就是一个二进制划分吗,还不用额外的讨论剩余的s。

	while(s!=0){			
			node tmp;
			tmp.V=v;
			tmp.W=w;
			tmp.S=lowbit(s);
			s-=lowbit(s);
			a.push_back(tmp);
		}

但这有问题!!!有问题!!!有问题!!!
9 为例,将会被直接划分为 18 ,之后在挨个取出讨论时,如果直接讨论8,很有可能我们是放不进去的!!!如果要得出正确结果,就需要对8,再进行一次划分,如果直接分为8份,那么恭喜你,又回去了,成为了苦逼O(n * n * n);如果继续以二进制划分,那还不如采取另一种二进制划分思路,也就是我们接下来的写法。

正途:
每个数都可以划分为2 ^ 0+ 2 ^ 1 + ······2 ^ k + s,s小于2 ^ (K+1),这就均匀的使用了每一位的二进制数。
同样以 9 为例,先划分出一个1,再划分出 2,再划分出 4,最后剩下了一个 2,2小于8,就需要单独划分为一堆。

	for(int k=1;s>=k;k<<=1){
			s-=k;
			node tmp;
			tmp.V=v;
			tmp.W=w;
			tmp.S=k;
			a.push_back(tmp);
		}
		if(s>0){
			node tmp;
			tmp.V=v;
			tmp.W=w;
			tmp.S=s;
			a.push_back(tmp);	
		}

3.单调队列优化
多重背包3
0<v[i],w[i],s[i]≤20000

时间复杂度:O(n * m )

此时的思路跟之前不同,之前是使用划分来优化,而此处是借助单调队列达成了我们比较的优化单调队列可见于)。

在外层循环中,我们仍然是依次输入n个数据,但在内层循环中:
1、我们先根据物品的体积v,将f[k]分为从0到v-1的v类,也就是对当前容量k进行模为v的取模操作;
2、对于模为j的一类来说,我们先回归最基本的式子,也就是我们的递推公式
f[k]=max(f[k],f[k-v[i]]+w[i]);
如果我们可以直接确定最优的f[k-v[i]]+w[i],那么我们就可以省下来很多的比较操作。
而所谓的最优,也就是在f[k]左侧 中 放入物品后价值最大的,而且要保证由此处可以通过增加物品 (所增加的物品的体积要小于等于s*v) 使得体积变为k (这也是我们可以使用单调队列窗口滑动的依据)
3、接下来依照这个思路我们就可以直接套用滑动窗口的模板只需在这个基础上做一些修改,比如修改此时的队尾出队条件,需要转化为放入物品后价值的比较,将破坏了单调性的元素从队尾输出。


3.代码

//朴素写法
#include<iostream>
using namespace std;
int v[105];
int w[105];
int s[105];
int t[1000005];
int T,m;
int main(){
	cin>>m>>T;
	for(int i=1;i<=m;i++){
		cin>>v[i]>>w[i]>>s[i];
	}
	for(int i=1;i<=m;i++){
		for(int j=T;j>=v[i];j--){
			for(int k=1;k<=s[i]&&j>=k*v[i];k++){
				t[j]=max(t[j],t[j-k*v[i]]+k*w[i]);
			}
		}
	}
	cout<<t[T];
}
//二进制优化划分
#include<bits/stdc++.h>
using namespace std;
int t[100005];
int v,w,s;
struct node{
	int V,W,S;
};
int lowbit(int x){
	return x & -x;
}
vector<node>a;
int T,m;
int main(){
	cin>>m>>T;
	for(int i=1;i<=m;i++){
		cin>>v>>w>>s;
	for(int k=1;s>=k;k<<=1){
			s-=k;
			node tmp;
			tmp.V=v;
			tmp.W=w;
			tmp.S=k;
			a.push_back(tmp);
		}
		if(s>0){
			node tmp;
			tmp.V=v;
			tmp.W=w;
			tmp.S=s;
			a.push_back(tmp);	
		}
	}
	for(int i=0;i<=a.size();i++){
		for(int j=T;j>=a[i].V;j--){
			t[j]=max(t[j],t[j-a[i].V*a[i].S]+a[i].S*a[i].W);
		}
	}
	cout<<t[T];
}
//单调队列优化
#include<bits/stdc++.h>
using namespace std;
const int N=20010;
int f[N],g[N],q[N],n,m;
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++){
		int v,w,s;
		cin>>v>>w>>s;
		memcpy(g,f,sizeof(f));	//g存f,作为备用 ,之后会在g中不断更新f ,注意在单次过程中,g没有变化 
		for(int j=0;j<v;j++){//枚举余数相同的一类情况 ,将n拆成v类 
			int h=0,t=-1;
			for(int k=j;k<=m;k+=v){//对每个类使用单调队列 ,窗口是在g数组上滑动的 
				if(h<=t&&q[h]<k-s*v)h++;//除去队首已经不在窗口内的元素 ,s*c为窗口大小(最大放置量),实际为s个元素 
				if(h<=t)f[k]=max(g[k],g[q[h]]+(k-q[h])/v*w);//使用队头最大值来更新f ,仍然是用max(原来值,更新值)作为结果 
				//而且在max中,是用了 g[k],f[q[h]]+(k-q[h])/v*w,原因可以参考01背包跟完全背包的递推顺序,避免重复计数 
				while(h<=t&&g[q[t]]-(q[t]-j)/v*w<=g[k]-(k-j)/v*w)t--;//当前值比队尾值更有价值时,队尾出队 
				q[++t]=k;//下标入队 
			}
		}
	}
	cout<<f[m]<<endl;
	return 0;
}

四、混合背包问题

1.背景

混合背包问题
混合背包问题也就是对之前三种情况的汇总,
此时我们有三种物品,
第一种物品,只有一个(01背包);
第二种物品,有无限个(完全背包);
第三种物品,有Si个(多重背包);
仍然求背包最大价值


2.分析

直接使用之前的代码,加一个分支判断就可以了,我为了省事,把01背包也当作了特殊的多重背包来处理,影响不大。
在数据量不大的情况下,当然也可以把多重背包二进制划分为01背包;


3.代码

#include<bits/stdc++.h>
using namespace std;
const int N=20010;
int f[N],g[N],q[N],n,m;
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++){
		int v,w,s;
		cin>>v>>w>>s;
		if(s==-1)s=1;
		if(s>0){
			memcpy(g,f,sizeof(f));	
			for(int j=0;j<v;j++){
			int h=0,t=-1;
				for(int k=j;k<=m;k+=v){
					if(h<=t&&q[h]<k-s*v)h++;
					if(h<=t)f[k]=max(g[k],g[q[h]]+(k-q[h])/v*w);
					while(h<=t&&g[q[t]]-(q[t]-j)/v*w<=g[k]-(k-j)/v*w)t--;
					q[++t]=k;
				}
			}
		}
		else {
			for(int j=v;j<=m;j++){
				f[j]=max(f[j],f[j-v]+w);
			}
		}
	}
	cout<<f[m]<<endl;
	return 0;
}

五、二维费用的背包问题

1.背景

二维费用的背包问题

每个物品都只能使用一次;
每个物品有三个参数,为体积,质量和价值;
我们需要找到将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。

数据范围
0<N≤1000
0<V,M≤100
0<vi,mi≤100
0<wi≤1000


2.分析

题目的数据量比较小,而且物品只能使用一次,所以我们直接参考01背包的思路,在里面加入一个对体积和质量的二重循环


3.代码

#include<bits/stdc++.h>
using namespace std;
const int SI=1010;
int f[SI][SI];
int N,V,M;
int v,m,w;
int main(){
	cin>>N>>V>>M;
	for(int i=0;i<N;i++){
		cin>>v>>m>>w;
		for(int j=V;j>=v;j--){
			for(int k=M;k>=m;k--){
				f[j][k]=max(f[j][k],f[j-v][k-m]+w);
			}
		}
	}
	cout<<f[V][M];
	return 0;
}

六、分组背包问题

1.背景

分组背包问题

有N组物品,每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。


2.分析

其实组与组之间没有联系,而且数据量很小,那么我们可以直接在每一组的讨论中采用01背包从后往前的思路,从而保证每组只会使用一个。
比较暴,幸好数据量小。
三重循环:
第一重循环组数;
第二重循环各种体积;
第三重循环在该体积的最优决策。

为了避免负值产生,我用了一个结构体和排序,当然也可以直接加一个判断。


3.代码

#include<bits/stdc++.h>
using namespace std;
const int SI=1010;
int f[SI];
int N,V;
int s;
struct node {
	int v,w;
};
node n[SI];
int cmp(node a,node b){
	return a.v<b.v;
}
int main(){
	cin>>N>>V;
	for(int i=0;i<N;i++){
		cin>>s;
		for(int j=0;j<s;j++)cin>>n[j].v>>n[j].w;
		sort(n,n+s,cmp);
		for(int j=V;j>=0;j--){
			for(int k=0;j-n[k].v>=0&&k<s;k++){
				f[j]=max(f[j],f[j-n[k].v]+n[k].w);
			}
		}
	}
	cout<<f[V];
	return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值