多重背包(二进制优化+单调队列优化)

多重背包

弱化版退化成01背包做就可以,时间复杂度为 O ( ∑ 1 n c [ i ] × M ) O(\sum_1^n{c[i]}\times M) O(1nc[i]×M)

多重背包二进制优化:

原题链接

时间复杂度为 O ( ∑ 1 n log ⁡ c [ i ] × M ) O(\sum_{1}^n \log c[i]\times M) O(1nlogc[i]×M) ,基本上可以认为就是 N ≤ 1000 , M ≤ 2000 N\le 1000 ,M \le 2000 N1000,M2000 外加一个   l o g \ log  log

原理:通过二进制拆分,拆成 p + 2 p+2 p+2 个物品,使得,可以组成 0 0 0 c [ i ] c[i] c[i] 中所有数。

推导: 因为我们知道 2 0 , 2 1 . . . . 2 k 2^0,2^1....2^k 20,21....2k 从中选择数字,可以组合成 0 0 0 2 k + 1 − 1 2^{k+1}-1 2k+11 所有的数字。

我们找到一个 p p p 满足 2 0 + 2 1 . . . + 2 p ≤ C 2^0+2^1...+2^p\le C 20+21...+2pC ,其中 C C C 表示物品个数,那么我们设 R = C − ( 2 0 + 2 1 . . . + 2 p ) R=C-(2^0+2^1...+2^p) R=C(20+21...+2p)

由于 2 p + 1 > C 2^{p+1}>C 2p+1>C 2 0 + 2 1 . . . + 2 p + 2 p + 1 > C 2^0+2^1...+2^p+2^{p+1}>C 20+21...+2p+2p+1>C

移项得 2 p + 1 > C − ( 2 0 + 2 1 . . . + 2 p ) 2^{p+1}>C-(2^0+2^1...+2^p) 2p+1>C(20+21...+2p) 因为 R = C − ( 2 0 + 2 1 . . . + 2 p ) R=C-(2^0+2^1...+2^p) R=C(20+21...+2p)

所以得 2 p + 1 > R 2^{p+1}>R 2p+1>R

因为有推导给出的结论,所以 2 0 , 2 1 . . . 2 p 2^0,2^1...2^p 2021...2p 可以组成 0 0 0 R R R 的任意数。

我们再来证明 R R R C C C 中的数如何全部组合成功。

显然的 R + 2 0 + 2 1 . . . + 2 p = C R+2^0+2^1...+2^p=C R+20+21...+2p=C 2 p + 1 > C − R 2^{p+1}>C-R 2p+1>CR ,即 R R R C C C 部分。

所以可知 2 0 , 2 1 . . . 2 p 2^0,2^1...2^p 2021...2p 可以组成 C − R C-R CR 任意部分。

进推得的 R + 2 0 , 2 1 . . . 2 p R+2^0,2^1...2^p R+2021...2p 可以组成 R R R C C C 部分

所以得出 R , 2 0 , 2 1 . . . 2 p R,2^0,2^1...2^p R,2021...2p ,一共 p + 2 p+2 p+2 元素可以组成 C C C 的任意部分。

总的来说,其实就是先证明 R R R 部分可以组成,然后在证明 R R R C C C 的部分也可以组成。

模板如下:

注意点

  • 更新之后的体积和价值中不在包含以前的物品
#include<bits/stdc++.h>
#define int long long 
using namespace std;
int read(){int x;scanf("%lld",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
int T;
int f[B];
int n,m;
int v[B],w[B],c[B];
int a[B],b[B],cnt;
void work()
{
	n=read(),m=read();
	for (int i=1;i<=n;i++) 
	{
		w[i]=read();
		v[i]=read();
		c[i]=read(); 
	}
	for (int i=1;i<=n;i++)
	{
		for (int j=1;j<=c[i];j<<=1)
		{
			a[++cnt]=w[i]*j;
			b[cnt]=v[i]*j;
			c[i]-=j;
		}
		if (c[i]) {a[++cnt]=c[i]*w[i];b[cnt]=c[i]*v[i];}
	}
	for (int i=1;i<=cnt;i++)
		for (int j=m;j>=a[i];j--)
			f[j]=max(f[j],f[j-a[i]]+b[i]);
	cout<<f[m]; 
}
signed main()
{
	T=1;
	while (T--) work();
	return 0;
}

多重背包单调队列优化

时间复杂度将至到线型 O ( N M ) O(NM) O(NM) ,做多可以实现 1000 × 10000 1000\times 10000 1000×10000

原理:通过减少DP方程转移方向的数量来简化时间复杂度

实现:通过多重背包一般转移式可以发现:
f [ i ] [ j ] = max ⁡ { f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − k × w [ i ] ] + k × v [ i ] } f[i][j]=\max\{f[i-1][j],f[i-1][j-k\times w[i]]+k\times v[i]\} f[i][j]=max{f[i1][j],f[i1][jk×w[i]]+k×v[i]}
每次转移,体积差距都在 w [ i ] w[i] w[i] 的倍数,说明转移和被转移的体积对 w [ i ] w[i] w[i] 取模余数相同的。我们发现,余数不同的之间不会相互影响,因此我们可以考虑分组进行转移。

在这里插入图片描述

我再来对单调队列进行一下补充:为什么用单调队列,想必大家都已经明白了,这个算法的难点在于如何维护单调队列。单单这么讲,我们直观的看,就是滑动窗口模板,但是我们发现,从 j → j + w j\to j+w jj+w 的时候。
f [ i ] [ j + w ] = max ⁡ { f [ i − 1 ] [ j ] + v , f [ i − 1 ] [ j − w ] + 2 × w . . . . . . } f[i][j+w]=\max\{f[i-1][j]+v,f[i-1][j-w]+2\times w......\} f[i][j+w]=max{f[i1][j]+v,f[i1][jw]+2×w......}
而在转移 f [ i ] [ j ] f[i][j] f[i][j] 的时候是如下:
f [ i ] [ j ] = max ⁡ { f [ i − 1 ] [ j − w ] + v , f [ i − 1 ] [ j − 2 × w ] + 2 × w . . . . . . } f[i][j]=\max\{f[i-1][j-w]+v,f[i-1][j-2\times w]+2\times w......\} f[i][j]=max{f[i1][jw]+v,f[i1][j2×w]+2×w......}
我们发现在求解 f [ i ] [ j ] f[i][j] f[i][j] 的时候,我们的需要集合转移和 f [ i ] [ j + w ] f[i][j+w] f[i][j+w] 的集合转移是不一样的。我们所说的连续是指对于按照余数分组之后,体积是连续,但是我们发现,对于相同位置,需要的状态转移表达式是不一样的。

比如

  • f [ i ] [ j + w ] f[i][j+w] f[i][j+w] 中有 f [ i − 1 ] [ j − w ] + 2 × v f[i-1][j-w]+2\times v f[i1][jw]+2×v
  • f [ i ] [ j ] f[i][j] f[i][j] 中有 f [ i − 1 ] [ j − w ] + v f[i-1][j-w]+v f[i1][jw]+v

可是我们的单调队列维护的就是这个最大值。

不难发现,当体积发生变换的时候 j → j + w j\to j+w jj+w,相同位置上的值发生改变了,这该如何办。

所以我们发现,这题目的真正的难点在这是一个数字发生变化的移动窗口

那么我们如何解决这个问题?

我们发现当 j → j + w j\to j+w jj+w 的时候,队列中的每个状态都会 + v +v +v ,因为我们队列中存放的是每个转移的体积,维护的是 f [ i − 1 ] [ k ] + V f[i-1][k]+V f[i1][k]+V(随机数) 同时加是不会影响单调性的,相当于没加,我们可以当成不操作,但是,有一个新数会加入到队列 f [ i − 1 ] [ j ] + v f[i-1][j]+v f[i1][j]+v 此时,我们为保证大小,就必须更改队列在维护时的元素的 f + V f+V f+V 的值,使得他们都加 v v v

我们可以反过来看,我们需要比较的只是大小关系,我把 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j] 加入,省去 v v v,这样就不需要队列中的其他元素加了,只需要加上自己原有的 n u m × v num\times v num×v 就可以了。

所以得出了出队代码

int V=(j-q[tail])*v;
while (head<=tail && f[i-1][q[tail]]+V<=f[i-1][j]) tail--;//因为我们一直维护的是上一维度的f值,所以和 f[i-1][j] 比较,而不是 f[i][j]
q[++tail]=j;

模板

int f[3][B];
int q[B];
int n,m;
void work()
{
	cin>>n>>m;
	int x=0; 
	for (int i=1;i<=n;i++)
	{
		x^=1;
		int w=read(),v=read(),c=read();
		for (int j=0;j<w;j++)
		{
			int head=1,tail=0;
			for (int k=j;k<=m;k+=w)
			{
				f[x][k]=f[x^1][k];
				while (head<=tail && k-c*w>q[head]) head++;
				if (head<=tail) f[x][k]=max(f[x][k],f[x^1][q[head]]+(k-q[head])/w*v);
				while (head<=tail && f[x^1][q[tail]]+(k-q[tail])/w*v<=f[x^1][k]) tail--;//维护的是上一维度的f值 
				q[++tail]=k;
			}
		}
	}
	cout<<f[x][m];
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值