背包问题 总结&详解

就是感觉之前 dp 的 blog 太乱了整理一下。


0-1 背包

例题:P1048

朴素算法

思路

对于一个物品,我们可以选,也可以不选。

我们用w_i表示第 i 件物品的重量,v_i表示第 i 件物品的价值。

考虑dp_{i,j}表示前 i 件物品放入容量为j的背包中的最大价值。

如果我们放不下第 i 件物品(把它打入冷宫),那么dp_{i,j}=dp_{i-1,j};否则入宫

然后发生了一场宫斗:dp_{i,j}=max(dp_{i-1,j},dp_{i-1,j-w_i}+v_i)

这就是0-1背包的状态转移方程。

Code
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn];
//w[i]表示第i个物品的重量,v[i]是价值 
int dp[maxn][maxm];
//dp[i][j]表示前i个物品放入容量为j的背包里的最大值
int main(){
	int n,m;
	cin>>n>>m;
	//n是物品个数,m是背包大小
	for(int i=1;i<=n;i++)
		cin>>w[i]>>v[i];
	for(int i=1;i<=n;i++){
		for(int j=0;j<=m;j++){
			if(j<w[i])//放不下第i个物品 
				dp[i][j]=dp[i-1][j];//打入冷宫 
			else//入宫 
				dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);//然后是场宫斗
		}
	}
	cout<<dp[n][m]<<endl;
	return 0;
}

滚动数组优化

显然,朴素算法的二维数组太耗费空间了。我们可以进行优化。

观察一下二维数组的代码,注意到 i跟个摆设似的,没啥用途。(注意力惊人)

因为dp_{i,j}只跟dp_{i-1,j}有关系,所以我们没必要记录 i。

不妨我们来试一试优化(试试就逝世)

#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],dp[maxm];
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>w[i]>>v[i];
	for(int i=1;i<=n;i++){
		for(int j=w[i];j<=m;j++)
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	}
	cout<<dp[m]<<endl;
	return 0;
}

乍一看没什么问题,实则喜提 0 分好成绩。

为什么呢?           dp_{j-w_i}更新的比dp_j早!

因此倒过来循环即可。

Code
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],dp[maxm];//w是重量,v是价值
int main(){
	int n,m;//n是物品个数,m是背包容量
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>w[i]>>v[i];
	for(int i=1;i<=n;i++){
		for(int j=m;j>=w[i];i--)//这里注意了:由于如果正着循环,dp[j-w[i]]会比dp[i]更新的早,所以要倒过来循环 
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	}
	cout<<dp[m]<<endl;
	return 0;
}

完全背包

有N种物品和一个容量是M的背包,每种物品都有无限件可用。
第i种物品的体积是v[i],价值是w[i]。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

洛谷上找不到例题所以只能自己写了。

转 0-1 背包

完全背包和 0-1背包 的差别就是物品可以拿无限次。 所以考虑把完全背包转成 0-1背包。

我们可以想一下,物品有无限件,肯定是超过 M(背包容量)的

我们可以把每个物品拆分成\frac{M}{w_i}个小物品。这样就和 0-1 背包一样了。

拆分的代码:
int tot=0;//计算物品总数
for(int i=1;i<=n;i++){
	int a,b;//a是重量,b是价值
	cin>>a>>b;
	for(int j=1;j<=m/a;j++){
		w[++tot]=a;
		v[tot]=b;
	}
}
完整代码
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],dp[maxm];
int main(){
	int n,m;
	cin>>n>>m;
	int tot=0;//计算物品总数
	for(int i=1;i<=n;i++){
		int a,b;//a是重量,b是价值
		cin>>a>>b;
		for(int j=1;j<=m/a;j++){
			w[++tot]=a;
			v[tot]=b;
		}
	}
	for(int i=1;i<=tot;i++){
		for(int j=m;j>=w[i];j--)//倒着循环
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	}
	cout<<dp[m]<<endl;
	return 0;
}

完全背包自己的算法

这次我们不转 0-1背包。

由于至少放入一个,考虑最后一个放入的物品,其占用 w 的空间,价值是 v。

由于物品有无限多个,放入一个以后还是有无限多个,但是背包大小减少了 w。

问题转换为从前i个物品种选择一些物品放入j-w_i的背包中可以获得的最大价值。

由此推出状态转移方程:dp_{i,j}=max(dp_{i-1,j},dp_{i-1,j-k\times w_i}+k\times v_i)

我们用滚动数组,优化空间。和 0-1背包类似。

方程优化成这样:dp_j=max(dp_j,dp_{j-w_i}+v_i)

虽然它的状态转移方程和0-1背包的一样,但是它不用倒着循环,正着循环即可。

#include <bits/stdc++.h>
using namespace std;
//对于物品i,你可以把它打入冷宫(不选)或者让它入宫(选) 
//不过啊,这是无限背包,所以可以无限放 
//也就是说,你放完一个,还有无限个,但是呢背包的大小减少了w[i]
//因此问题转化为从前i个物品中选一些放入大小为j-w[i]的背包中可获得的最大价值
int w[maxn],v[maxn],dp[maxm];
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>w[i]>>v[i];
	for(int i=1;i<=n;i++){
		for(int j=w[i];j<=m;j++)//这个正着循环即可
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]); 
	}
	cout<<dp[m]<<endl;
	return 0; 
}

转多重背包

没错,完全背包还可以转多重背包。不过建议读者先阅读下面的多重背包再看这个算法。

其实很简单。我们把多重背包里的s_i定义成\frac{m}{w_i}来求解就可以了。

状态转移方程:dp_{i,j}=max(dp_{i-1,j},dp_{i-1,j-k\times w_i}+k\times v_i)

最后是代码部分:

#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],dp[maxm];
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>w[i]>>v[i];
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			for(int k=0;k<=j/w[i];k++)
				dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
		}
	}
	cout<<dp[n][m]<<endl;
	return 0;
}

当然,由于多重背包的朴素算法的复杂度是\Theta (N\times M\times K)的,所以必须优化(参见下面多重背包部分)。

多重背包

例题:P1776

朴素算法

多重背包 0-1背包 的差别就是 0-1背包 里一个物品只有一件,但多重背包里一个物品有s[i]件。

所以,我们分类讨论:

  1. s_i\times w_i\geq m 转完全背包
  2. 转0-1背包。拿一个 k 去枚举数量,于是问题变成了一个重量为k\times w,价值为k\times v的物品取不取

来人!上代码!

#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],s[maxn],dp[maxm];
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>w[i]>>v[i]>>s[i];
	for(int i=1;i<=n;i++){
		if(s[i]*w[i]>=m){//相当于完全背包(因为比背包容量m大) 
			for(int j=w[i];j<=m;j++)
				dp[j]=max(dp[j],dp[j-w[i]]+v[i]); 
		}
		else{//否则转0-1背包 
			for(int j=m;j>=w[i];j--){
				for(int k=s[i];k>=0;k--){//枚举数量 
					if(j>=k*w[i])
						dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);
				}
			}
		}
	}
	cout<<dp[m]<<endl;
	return 0;
}

显然,时间复杂度不是一般的高,是二般的高

所以优化是必须的。接下来我们来看多重背包的两种优化。

多重背包的二进制优化

思路

我们想一下,我们当时用 k 从s_i枚举到0是否必要。

我们其实可以合并一些物品。

所以我们可以只枚举一些物品,通过这些物品互相合并,产生新的物品。比如:10=1+2+4+3

那为什么不拆成1+2+7呢?因为这样你就无法合成出4了。

那怎么枚举呢?这里要用到一点倍增的思想了。

给大家看一下局部代码:

int a,b,s;//a是重量,b是价值,s是数量
cin>>a>>b>>s;
//如何合成这些多余的物体
int k=1;
while(k<=s){
	w[++cnt]=k*a;//合成的重量
	v[cnt]=k*b;//合成的价值
	s-=k;//物品数相应减少
	k*=2;//翻倍
}
if(s){//没办法翻倍了
	w[++cnt]=s*a;
	v[cnt]=s*b;
    //剩下的自动合成
}

这样就OK了。

Code
//由于直接枚举取多少个会TLE
//所以我们进行二进制优化
//思考一下,其实我们没必要枚举0-s
//我们可以不断倍增:1,2,4,8,16,32,64,128...
//取这些数量个物品
//这样,通过不断组合可以组合出所有种类
//比如:10=1+2+4+3
//为什么有个3呢?
//因为正好剩个3了
//靠1,2,4,3可以组成0-10的所有数
//5=1+4 6=2+4 7=3+4 8=3+4+1 9=2+3+4 10=1+2+3+4
//所以在枚举时,我们不断倍增就可以了
//这样可以大大减少枚举量
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],dp[maxm];
int main(){
	int n,m;
	cin>>n>>m;
	int cnt=0;//记录新物体数(相当于优化后的n)
	for(int i=1;i<=n;i++){
		int a,b,s;
		//a是重量,b是价值,s是数量
		cin>>a>>b>>s;
		//接下来是重中之重
		//如何合成这些多余的物体
		int k=1;
		while(k<=s){
			w[++cnt]=k*a;//合成的重量
			v[cnt]=k*b;//合成的价值
			s-=k;//物品数相应减少
			k*=2;//翻倍
		}
		if(s){//没办法翻倍了
			w[++cnt]=s*a;
			v[cnt]=s*b;
			//剩下的自动合成
		}
	}
	//0-1背包
	for(int i=1;i<=cnt;i++){
		for(int j=m;j>=w[i];j--)
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
	}
	cout<<dp[m]<<endl;
	return 0;
}

我个人认为这个方法好懂,好写,比下面的单调队列优化好一些。

单调队列优化多重背包

单调队列优化的思想就是给拿完后剩余体积分类,并且剩余体积一定不大于w,不然就能再拿。

我们可以枚举剩余体积,代码如下:

for(int i=1;i<=n;i++)          //->物品个数
    for(int j=0;j<w;j++)       //->剩余体积
        for(int k=j;k<=m;k+=w) //->不同个数

到这应该不难理解。

接下来,我们把这些状态表示一下:

dp_0dp_wdp_{2\times w} (剩余体积 0)
dp_1 dp_{w+1} dp_{2\times w+1} (剩余体积 1)
......
dp_jdp_{w+j}dp_{2*w+j}  (剩余体积 j)

于是,我们把问题分成 j 类,每类就是一个单调队列。

dp_i=dp_j
dp_{j+w}=max(dp_j+v,dp_{j+w})
......
dp_{j+k\times w}=max(dp_j+k\times v,dp_{j+w}+(k-1)\times v,......,dp_{j+k\times w})

我们把v提出来,就是:

dp_j=dp_j
dp_{j+w}=max(dp_j,dp_{j+w}-v)+v
dp_{j+2*w}=max(dp_j,dp_{j+w}-v,dp_{j+2\times w}-2\times v)+2\times v
......
dp_{j+k\times w}=max(dp_j,dp_{j+w}-v,dp_{j+2\times w}-2\times v, ... ,dp_{j+k\times w}-k\times v)+k\times v

所以,我们用一个队列维护最大值的下标,每次入队dp_{j+k\times w}-k\times w即可。

但是由于我们要进行比较,所以我们还需要一个 pre 数组维护上一轮的队列就OK啦。

//单调队列优化的思想就是给拿完后剩余体积分类
//剩余体积一定<w,不然就可以再装
//0,1,2,3,......,w-1
//怎么枚举呢?看代码:
//for(int i=1;i<=n;i++)          ->物品个数
//    for(int j=0;j<w;j++)       ->剩余体积
//        for(int k=j;k<=m;k+=w) ->不同个数
//我们把这些状态表示一下:
//dp[0] dp[w] dp[2*w] ...... (剩余体积0)
//dp[1] dp[w+1] dp[2*w+1] ...... (剩余体积1)
//......
//dp[j] dp[w+j] dp[2*w+j] ...... (剩余体积j)
//我们把问题分为j类,每类就是一个单调队列
//dp[j]=dp[j]
//dp[j+w]=max{dp[j]+v,dp[j+w]}
//......
//dp[j+k*w]=max{dp[j]+k*v,dp[j+w]+(k-1)*v,......,dp[j+k*w]}
//我们稍微变换一下:
//dp[j]=dp[j]
//dp[j+w]=max{dp[j],dp[j+w]-v}+v
//dp[j+2*w]=max{dp[j],dp[j+w]-v,dp[j+2*w]-2*v}+2*v
//......
//dp[j+k*w]=max{dp[j],dp[j+w]-v,dp[j+2*w]-2*v,......,dp[j+k*w]-k*v}+k*v
//相当于把价值v挪到max函数外了
//所以说,我们每次入队dp[j+k*w]-k*w,维护最大值即可
//que维护的是dp[j+k*w]-k*w最大值的下标
//pre维护的是上一次的que
//每次比较一下即可 
#include <bits/stdc++.h>
using namespace std;
const int maxm=40005;
int dp[maxm],pre[maxm],que[maxm];//que是队列 
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int w,v,s;
		cin>>w>>v>>s;
		memcpy(pre,dp,sizeof(dp));
		for(int j=0;j<w;j++){//j个单调队列,j枚举的是拿完后剩下的重量(最多剩w-1) 
			int head=0,tail=-1;//头尾指针
			for(int k=j;k<=m;k+=w){//k是取(k-j)/w件物品的重量 
				if(head<=tail && k-s*w>que[head])
					head++;
				while(head<=tail && pre[que[tail]]-(que[tail]-j)/w*v<=pre[k]-(k-j)/w*v)
					tail--;
				if(head<=tail)
					dp[k]=max(dp[k],pre[que[head]]+(k-que[head])/w*v);
				que[++tail]=k;
			}
		}
	}
	cout<<dp[m]<<endl;
	return 0;
}

复习(混合背包)

Review

混合背包就是 0-1背包、完全背包、多重背包 的结合。

内容状态转移方程注意事项
0-1背包物品选/不选dp_j=max(dp_j,dp_{j-w_i}+v_i)倒着循环
完全背包物品无数件dp_j=max(dp_j,dp_{j-w_i}+v_i)正着循环
多重背包物品s_idp_j=max(dp_j,dp_{j-k\times w_i}+k\times v_i)优化
思路

混合背包还是输入w_i\: v_i\: s_i,分别表示重量,价值,数量。然后进行分类讨论:

  • s_i=1时,是 0-1背包。
  • s_i=0时,是完全背包。
  • 其他为多重背包。

然后套用以上的状态转移方程即可。

代码
#include <bits/stdc++.h>
using namespace std;
int w[maxn],v[maxn],s[maxn],dp[maxm];
int main(){
	int n,m;
	cin>>m>>m;
	for(int i=1;i<=n;i++)
		cin>>w[i]>>v[i]>>s[i];
	for(int i=1;i<=n;i++){
		if(s[i]==1){//0-1背包 
			for(int j=m;j>=w[i];j--)
				dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
		else if(s[i]==0){//完全背包 
			for(int j=w[i];j<=m;j++)
				dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
		else{//多重背包(朴素),转0-1背包 
			for(int j=m;j>=w[i];j--){
				for(int k=1;k<s[i] && k*w[i]<=m;k++)
					dp[j]=max(dp[j],dp[j-k*w[i]]+k*v[i]);
			} 
		}
	}
	cout<<dp[m]<<endl;
	return 0;
}

分组背包

例题:P1757

思路

和 0-1背包 类似,只用一个二维数组存储每个组的解就行了。每组按 0-1 背包的解法做就ok了。

代码

#include <bits/stdc++.h>
using namespace std;
vector<int> w[maxn],v[maxn];
int dp[maxm];
int main(){
	for(int i=0;i<maxn;i++){
        w[i].push_back(0);
        v[i].push_back(0);
    }
	int m,n;
	cin>>m>>n;
	int kind=0;//种类数 
	for(int i=1;i<=n;i++){
		int a,b,c;//a是重量,b是价值 
		cin>>a>>b>>c;//c是种类 
		w[c].push_back(a);
		v[c].push_back(b);
		kind=max(kind,c);
	}
	for(int i=1;i<=kind;i++){
		for(int j=m;j>=0;j--){//倒着循环(0-1)背包 
			for(int k=1;k<(int)w[i].size();k++){
				if(j>=w[i][k])
					dp[j]=max(dp[j],dp[j-w[i][k]]+v[i][k]);
			} 
	    }
	}
	cout<<dp[m]<<endl;
	return 0;
}

结尾

这篇 blog 用了我整整 1 天的时间整理,主要因为之前的背包的 blog 太散了,之后还会整理线性 dp 和区间 dp 的blog。看在我写了 7k+ 字的份上,点个赞再走吧。

友情提醒:虽然模板都是正确的,但直接提交会让你喜提 0 分(里面的 maxn 我都没有定义)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值