背包九讲

背包九讲

01背包

经典问题。

\(f[i][j]\) 表示前 \(i\) 种物品放入容量为 \(j\) 的背包能获得的最大价值,则:

\[f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]) \]

优化空间复杂度,把第一维滚动掉,因为不能重复选,所以状态 \(i\) 只能 \(i-1\) 转,压到一维里只能从后往前枚举保证不会用更新过的状态更新后面的。

01背包问题 - AcWing题库

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
//#define int long long
const int N=1e6+7;
using namespace std;

inline int read() 
{
	int x(0),f(0);
	char ch=getchar();
	while(isspace(ch))f|=(ch=='-'),ch=getchar();
	while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return f?-x:x;
}

int n,V,w[N],v[N],f[N];

signed main() 
{
	n=read(),V=read();
	for(int i=1;i<=n;i++)v[i]=read(),w[i]=read();
	for(int i=1;i<=n;i++)
		for(int j=V;j>=v[i];j--)
			f[j]=max(f[j],f[j-v[i]]+w[i]);
	printf("%d\n",f[V]);
	return 0;
}

完全背包

话说刚开始只知道和 01 背包的枚举顺序不一样,原理不知道。

其实完全背包的基本 DP 柿子应该是:

\[f[i][j]=max(f[i-1][v-k \times v[i]]+k\times w[i]),0\le k\times v[i]\le V \]

复杂度很高,所以要优化。

其实也可以不枚举 \(k\):

\[f[i][j]=f[i-1][j],j-v[i]\le 0 \]

\[f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]),j-v[i]\ge0 \]

可以说是继承上一维的状态拿到这一维继续更新。

然后滚动掉第一维,正序枚举即可。

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
//#define int long long
const int N=1e6+7;
using namespace std;

inline int read() 
{
	int x(0),f(0);
	char ch=getchar();
	while(isspace(ch))f|=(ch=='-'),ch=getchar();
	while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return f?-x:x;
}

int n,V,w[N],v[N],f[N];

signed main() 
{
	n=read(),V=read();
	for(int i=1;i<=n;i++)v[i]=read(),w[i]=read();
	for(int i=1;i<=n;i++)
		for(int j=v[i];j<=V;j++)
			f[j]=max(f[j],f[j-v[i]]+w[i]);
	printf("%d\n",f[V]);
	return 0;
}

多重背包

就是在完全背包的基础上限制了数量。

Ⅰ:

最简单的思路就是把物品拆开跑 01背包,复杂度 \(\mathcal{O(V\times \displaystyle\sum{s_i})}\)

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
//#define int long long
const int N=1e6+7;
using namespace std;

inline int read() 
{
	int x(0),f(0);
	char ch=getchar();
	while(isspace(ch))f|=(ch=='-'),ch=getchar();
	while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return f?-x:x;
}

int n,V,w[N],v[N],s[N],f[N];

signed main() 
{
	n=read(),V=read();
	for(int i=1;i<=n;i++)v[i]=read(),w[i]=read(),s[i]=read();
	for(int i=1;i<=n;i++)
		for(int j=V;j>=v[i];j--)
			for(int k=0;k<=s[i]&&k*v[i]<=j;k++)
			f[j]=max(f[j],f[j-k*v[i]]+k*w[i]);
	printf("%d\n",f[V]);
	return 0;
}

Ⅱ:

当然对于 Ⅰ 中的实现方式的复杂度很难接受,所以要进行优化,而二进制拆分可以很好的解决这个问题,因为每个 \(s[i]\) 都能用二进制表示出来,那么将 \(s[i]\) 进行二进制拆分,能组成 \(1\)\(s[i]\) 的所有数,也就间接的选了一些数量的物品。

复杂度 \(\mathcal{O(V\times \displaystyle \sum{\log_{s_i}})}\)

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
//#define int long long
const int N=1e6+7;
using namespace std;

inline int read() 
{
	int x(0),f(0);
	char ch=getchar();
	while(isspace(ch))f|=(ch=='-'),ch=getchar();
	while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return f?-x:x;
}

int n,V,cnt,w[N],v[N],s[N],f[N];

signed main() 
{
	n=read(),V=read();
	for(int i=1;i<=n;i++)
	{
		int vv=read(),ww=read(),ss=read();
		for(int j=1;j<=ss;j<<=1)
		{
			v[++cnt]=j*vv;
			w[cnt]=j*ww;
			ss-=j;
		}
		if(s) v[++cnt]=ss*vv,w[cnt]=ss*ww;
	}
	for(int i=1;i<=cnt;i++)
		for(int j=V;j>=v[i];j--)
			f[j]=max(f[j],f[j-v[i]]+w[i]);
	printf("%d\n",f[V]);
	return 0;
}

Ⅲ:

二进制优化依然不优怎么办?

继续思考优化。

回到朴素的 DP 柿子:\(f[j]=max(f[j],f[j-k\times v]+k\times w)\)

不难发现 \(j\) 的状态跟 \(j-k\times v\) 有关,突然发现它们模 \(v\) 之后的余数是一样的,也就是说 DP 数组是按类更新的,它跟前面 \(s\)\(j-k\times v\) 有关。

既然是一类一类的递推,考虑单调队列,因为单调队列是顺序更新的,所以先 copy 一遍 DP 数组,再用 copy 出来的数组更新就做到了顺序更新。

考虑空间带来价值的影响更新单调队列即可。

复杂度 \(\mathcal{O(nV)}\)

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
//#define int long long
const int N=1e6+7;
using namespace std;

inline int read() 
{
	int x(0),f(0);
	char ch=getchar();
	while(isspace(ch))f|=(ch=='-'),ch=getchar();
	while(isdigit(ch))x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return f?-x:x;
}

int n,V,w[N],v[N],s[N],f[N],g[N],q[N];

signed main() 
{
	n=read();V=read();
	for(int i=1;i<=n;i++) v[i]=read(),w[i]=read(),s[i]=read();
	for(int i=1;i<=n;i++)
	{
		memcpy(g,f,sizeof f);
		for(int j=0;j<v[i];j++)
		{
			int head=1,tail=0;
			for(int k=j;k<=V;k+=v[i])
		    //分组f[j-s*v[i]] -- f[j]
			{
				if(head<=tail&&q[head]+s[i]*v[i]<k)head++;
				if(head<=tail) f[k]=max(g[k],g[q[head]]+(k-q[head])/v[i]*w[i]);
				while(head<=tail&&g[k]>=g[q[tail]]+(k-q[tail])/v[i]*w[i]) tail--;
				q[++tail]=k;
			}
		}
	}
	printf("%d\n",f[V]);
	return 0;
}

我们比较一下三种方法的效率:

朴素:\(\mathcal{O(V\sum s_i)}\)

二进制:\(\mathcal{O(V\log{s_i})}\)

单调队列:\(\mathcal{O(nV)}\)

假若 \(n=1e3,s_i=1e3\)

那么二进制较朴素快了 \(100\) 倍,单调队列相较于二进制又提高 \(10\) 倍!当然数据越大差距越大。

混合背包

很简单,把多重背包二进制拆解成 01 背包,然后分 01 和完全两类类转移。

或者分别写三个函数,是哪个就套哪个。

不是很想写,就把之前代码粘过来了。

#include<cmath>
#include<queue>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

const int N=1e6+2049,M=2049;
const int INF=0x3f3f3f3f;
const int Mod=1e9+7;

int read() {
	int x=0,f=0;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
    for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return f?-x:x;
}

int n,w[N],v[N],C[N],sc,f[N];

signed main() {
   int n=read(),V=read();
   for(int i=1,v_,w_,s;i<=n;i++){
       v_=read();w_=read();s=read();
       if(!s)v[++sc]=v_,w[sc]=w_,C[sc]=INF;
       else{
           if(s==-1) s=1;
           for(int k=1;k<=s;k<<=1){
               v[++sc]=v_*k;w[sc]=w_*k;C[sc]=-INF,s-=k;
           }
           if(s) v[++sc]=v_*s,w[sc]=w_*s,C[sc]=-INF;
       }
   }
   for(int i=1;i<=sc;i++){
       if(C[i]==-INF)
       for(int j=V;j>=v[i];j--)f[j]=max(f[j],f[j-v[i]]+w[i]);
       else 
       for(int j=v[i];j<=V;j++)f[j]=max(f[j],f[j-v[i]]+w[i]);
   }
   printf("%d\n",f[V]);
   return 0;
}

二维费用背包

类比 01 背包,再加一维即可。

\(f[i][j]\) 表示使用 \(i\) 的容积,\(j\) 的承重能装的最大价值。

复杂度 \(\mathcal{O(nVM)}\)

代码也很好写,还是贴之前代码:

#include<cmath>
#include<queue>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;

const int N=2049;
const int INF=0x3f3f3f3f;
const int Mod=1e9+7;

int read() {
	int x=0,f=0;char ch=getchar();
	for(;!isdigit(ch);ch=getchar()) f|=(ch=='-');
    for(;isdigit(ch);ch=getchar()) x=(x<<1)+(x<<3)+(ch^48);
	return f?-x:x;
}

void print(int x) {
	if(x<0) putchar('-'),x=-x;
	if(x>9) print(x/10);
	putchar(x%10+48);
}

int f[N][N],n,V,M;
int v[N],m[N],w[N];

signed main() {
   n=read();V=read();M=read();
   for(int i=1;i<=n;i++)v[i]=read(),m[i]=read(),w[i]=read();
   for(int i=1;i<=n;i++){
       for(int j=V;j>=v[i];j--){
           for(int k=M;k>=m[i];k--){
               f[j][k]=max(f[j][k],f[j-v[i]][k-m[i]]+w[i]);
           }
       }
   }
   print(f[V][M]);
   return 0;
}

分组背包

\(f[i][j]\) 表示前 \(i\) 组物品,能放入容量为 \(j\) 的背包的最大价值。

朴素算法就是循环组,循环背包容量。

先寄了再说……

posted @ 2022-05-28 16:40  Gym_nastics  阅读(36)  评论(0编辑  收藏  举报