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

来源:互联网 发布:shopee123软件怎么样 编辑:程序博客网 时间:2024/06/04 18:50

由于做题的时候老被这玩意儿卡住的我很不爽,决定写个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[i1][jkw[i]]+kv[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+6djF[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 0x3f3f3f3fusing namespace std;int b[N],c[N],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;}
原创粉丝点击