您的位置:首页 > 其它

多重背包的优化 二进制/单调队列解析

2017-10-25 22:32 417 查看
由于做题的时候老被这玩意儿卡住的我很不爽,决定写个blog来加深自己的印象以及不用到处找资料回忆。

多重背包的问题的具体描述如下:

给出一个体积为v的背包,有n个物品,每个物品可以选c[i]次,问最多能得到多大的代价。

直接做DP的复杂度是n*v*max(c[i]),这显然无法承受正常难度的数据范围。

然后我们考虑优化。

首先来看稍微简单一点的二进制优化。

很久以前打的就不要纠结是不是c艹了。

uses math;
const mo=1000000007;
var
i,j,k,n,m:longint;
dp,c,w,num:array[0..100000]of longint;
procedure zpack(cost,weight,n:longint);
var
i:longint;
begin
for i:=n downto cost do
dp[i]:=max(dp[i],dp[i-cost]+weight);
end;
procedure cpack(cost,weight,n:longint);
var
i:longint;
begin
for i:=cost to n do
dp[i]:=max(dp[i],dp[i-cost]+weight);
end;
function multipack(n,m:longint):longint;
var
i,j,k:longint;
begin
fillchar(dp,sizeof(dp),0);
for i:=1 to n do
begin
if num[i]*c[i]>m then cpack(c[i],w[i],m)
else
begin
k:=1;
while k<num[i] do
begin
zpack(k*c[i],k*w[i],m);
num[i]:=num[i]-k;
k:=k*2;
end;
zpack(num[i]*c[i],num[i]*w[i],m);
end;
end;
exit(dp[m]);
end;
begin
readln(n,m);
for i:=1 to n do readln(c[i],w[i],num[i]);
writeln(multipack(n,m));
end.


具体的思想就是,如果能选的总和超过体积就做完全背包,否则就对其进行二进制拆分做01背包。然后说说二进制拆分是怎么一回事。

比如一个次数为7=111的物品,他可以被分解001 010 100三种数字。

这三种数字任意组合可以组合成不重复的,小于等于7的数字。

那么我们就是相当于对这三个数做01背包,只不过代价和价值都被累加在一起而已。

简而言之就是相当于把一个能选c[i]次的物品拆成log个只能选一次的,价值体积累加在一起的物品,所以时间复杂度是O(N*V*sigma log(c[i]))

在大部分情况下二进制拆分可以应对绝大多数的多重背包,但是也有一些题目会卡常,比如N,V<=5000就会GG,这个时候就要用到线性但是较难理解的单调队列优化。

具体来说就是:

f[i][j]=(f[i−1][j−k∗w[i]]+k∗v[i])

设m[i]为合法的,i能选的最多次数。

(接下来一段比较复杂,这里直接粘贴了)

若用F[i][j]表示对容量为j的背包,处理完前i种物品后,背包内物品可达到的最大总价值,并记m[i] = min(n[i], j / v[i])。放入背包的第i种物品的数目可以是:0、1、2……,可得:
F[i][j] = max { F[i - 1] [j – k * v[i] ] + k * w[i] }  (0 <= k <= m[i])       ㈠

如何在O(1)时间内求出F[i][j]呢?
先看一个例子:取m[i] = 2, v[i] = v, w[i] = w, V > 9 * v,
并假设 f(j) = F[i - 1][j],观察公式右边要求最大值的几项:
j = 6*v:   f(6*v)、f(5*v)+w、f(4*v)+2*w 这三个中的最大值
j = 5*v:   f(5*v)、f(4*v)+w、f(3*v)+2*w 这三个中的最大值
j = 4*v:   f(4*v)、f(3*v)+w、f(2*v)+2*w 这三个中的最大值
显然,公式㈠右边求最大值的几项随j值改变而改变,但如果将j = 6*v时,每项减去6*w,j=5*v时,每项减去5*w,j=4*v时,每项减去4*w,就得到:
j = 6*v:   f(6*v)-6*w、f(5*v)-5*w、f(4*v)-4*w 这三个中的最大值
j = 5*v:   f(5*v)-5*w、f(4*v)-4*w、f(3*v)-3*w 这三个中的最大值
j = 4*v:   f(4*v)-4*w、f(3*v)-3*w、f(2*v)-2*w 这三个中的最大值
很明显,要求最大值的那些项,有很多重复。

根据这个思路,可以对原来的公式进行如下调整:
假设d = v[i],a = j / d,b = j % d,即 j = a * d + b,代入公式㈠,并用k替换a - k得:
F[i][j] = max { F[i - 1] [b + k * d] - k * w[i] } + a * w[i]   (a – m[i] <= k <= a)    ㈡

对F[i - 1][y] (y= b  b+d  b+2d  b+3d  b+4d  b+5d  b+6d  …  j)
F[i][j]就是求j的前面m[i] + 1个数对应的F[i - 1] [b + k * d] - k * w[i]的最大值,加上a * w[i],如果将F[i][j]前面所有的F[i - 1][b + k * d] – k * w放入到一个队列,那么,F[i][j]就是求这个队列最大长度为m[i] + 1时,队列中元素的最大值,加上a * w[i]。因而原问题可以转化为:O(1)时间内求一个队列的最大值。


代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#define N 205
#define M 20005
#define inf 0x3f3f3f3f
using namespace std;

int b
,c
,f[M],q[M],w[M];

int main()
{
int n,m;
scanf("%d",&n);
for (int i=1;i<=n;i++)
scanf("%d",&b[i]);
for (int i=1;i<=n;i++)
scanf("%d",&c[i]);
scanf("%d",&m);
memset(f,inf,sizeof(f));
f[0]=0;
for (int i=1;i<=n;i++)
{
for (int j=0;j<b[i];j++)
{
int head=1,tail=0;
for (int k=j;k<=m;k+=b[i])
{
while (head<=tail&&w[head]<k-c[i]*b[i]) head++;
while (head<=tail&&f[k]-(k-w[head])/b[i]<=q[tail]) tail--;
q[++tail]=f[k];
w[tail]=k;
f[k]=min(f[k],q[head]+(k-w[head])/b[i]);
}
}
}
printf("%d",f[m]);
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: