1.0/1背包
var v,p:array[0..1000] of longint; f:array[0..100000] of longint; n,m,i,j:longint; function max(a,b:longint):longint; begin if a>b then exit(a); exit(b); end; begin readln(n,m); for i:=1 to n do readln(v[i],p[i]); for i:=1 to n do for j:=m downto v[i] do f[j]:=max(f[j],f[j-v[i]]+p[i]); writeln(f[m]); end.
2.完全背包
var v,p:array[0..1000] of longint; f:array[0..100000] of longint; n,m,i,j:longint; function max(a,b:longint):longint; begin if a>b then exit(a); exit(b); end; begin readln(n,m); for i:=1 to n do readln(v[i],p[i]); for i:=1 to n do for j:=v[i] to m do f[j]:=max(f[j],f[j-v[i]]+p[i]); writeln(f[m]); end.
note:0/1背包和完全背包中有时要求背包必须装满,其实等价于只能从F[0]转移过来.
3.多重背包
先贴上朴素代码:
var v,p,c:array[0..1000] of longint; f:array[0..100000] of longint; n,m,i,j,k:longint; function max(a,b:longint):longint; begin if a>b then exit(a); exit(b); end; begin readln(n,m); for i:=1 to n do readln(v[i],p[i],c[i]); for i:=1 to n do for k:=1 to c[i] do for j:=m downto v[i] do f[j]:=max(f[j],f[j-v[i]]+p[i]); writeln(f[m]); end.
复杂度O(NMC),联赛不太可能考这么裸的.
以下是几种优化的算法.
1.二进制拆包:
var v,p:array[0..1000,0..20] of longint; f:array[0..100000] of longint; c:array[0..1000] of longint; n,m,i,j,k,l,vv,pp,cc:longint; function max(a,b:longint):longint; begin if a>b then exit(a); exit(b); end; begin readln(n,m); for i:=1 to n do begin readln(vv,pp,cc); l:=trunc(ln(cc)/ln(2)); for j:=0 to l-1 do begin v[i,j]:=vv*(1<<j); p[i,j]:=pp*(1<<j); end; c[i]:=l-1; if cc-1<<l+1>0 then begin inc(c[i]); v[i,l]:=vv*(cc-1<<l+1); p[i,l]:=pp*(cc-1<<l+1); end; end; for i:=1 to n do for k:=0 to c[i] do for j:=m downto v[i,k] do f[j]:=max(f[j],f[j-v[i,k]]+p[i,k]); writeln(f[m]); end.
note:
设一个物品的个数为C.
设k是满足C-2^(k+1)+1>0的最大整数.
则将物品拆分为系数分别为1,2,4,...,2^k,C-2^(k+1)+1的物品,当然,要乘上体积、价值.
为什么可以这样拆?
因为上面的系数可以组成0~C的所有数.简单证明如下:
首先,1,2,4,...,2^k可以组合出0~2^(k+1)-1的所有整数.
而C-2^(k+1)+1<=2^(k+1)-1,否则将不满足"k是满足C-2^(k+1)+1>0的最大整数"
所以2^(k+1)~C的整数也全部可以被表示.
复杂度O(NM∑logCi),可以对付一般的题目.
2.单调队列优化
type rec=record v,p:longint; end; var f:array[0..10001] of longint; q:array[0..100000] of rec; n,m,i,j,h,t,v,p,c,mc,r:longint; function min(a,b:longint):longint; begin if a>b then exit(b); exit(a); end; procedure insert(pos,val:longint); begin while (h<=t)and(q[t].v<=val) do dec(t); inc(t); q[t].p:=pos; q[t].v:=val; end; begin readln(n,m); for i:=1 to n do begin readln(v,p,c); mc:=min(c,m div v); for r:=0 to v-1 do begin h:=1; t:=0; for j:=0 to (m-r) div v do begin insert(j,f[j*v+r]-j*p); if j-q[h].p>mc then inc(h); f[j*v+r]:=q[h].v+j*p; end; end; end; writeln(f[m]); end.
note:
我们先来观察普通的方程(对于一个物品):
f[j]=max{f[j-k*v]+k*p} (1<=k<=c,v<=j<=M)
复杂度为O(NMC).
怎么优化?
考虑决策点j,将其按模v的余数分类(分类转移并不影响决策的正确性).
对于每一类j,设p=[j/v],r=j-p*v (即j mod v),则j=p*v+r
那么j可以从l=q*v+r转移过来,其中q>=0 & p-c<=q<=p.
随着j的增加,p也增加,q的下界也在增加(单调性!).
将方程改写一下:
f[p*v+r]=max{f[q*v+r]+(p-q)*v} (q>=0 & p-c<=q<=p)
即f[p*v+r]=max{f[q*v+r]-q*v}+p*v
(这里r是人工枚举的,视为常量)
算法已经出来了:
for 每一个物品 do
for r=0 to v-1 do
{
清空队列.
for j=0 to lim do //lim为j的上界,lim*v+r<=M
{
将上一个阶段的值插入队列;
删除失效决策点;
取队头元素更新;
}
}
复杂度为O(NM),应该不会被卡掉..
多重背包的优化告一段落.
再写一种叫做数组标记的算法(建议先看POJ1742).
对于POJ1742这样的题目,数组标记具有代码短,易编写,易调试,效率高等等优点.
核心思想是用一个count数组记录体积为j时,当前物品使用了多少个.具体见代码:
var f:array[0..100001] of boolean; count:array[0..100001] of longint; a,c:array[0..101] of longint; n,m,i,j,ans:longint; begin while not seekeof do begin readln(n,m); if n+m=0 then break; fillchar(f,sizeof(f),0); f[0]:=true; ans:=0; for i:=1 to n do read(a[i]); for i:=1 to n do read(c[i]); readln; for i:=1 to n do begin fillchar(count,sizeof(count),0); for j:=a[i] to m do if(not f[j])and(f[j-a[i]])and(count[j-a[i]]<c[i]) then begin f[j]:=true; count[j]:=count[j-a[i]]+1; end; end; for i:=1 to m do if f[i] then inc(ans); writeln(ans); end; end.
复杂度O(NM)
这种算法相当优秀,但是有使用的局限性(不然要单调队列干什么).
好了,基础的背包问题就整理到这了.
参考资料:
http://www.cppblog.com/MatoNo1/archive/2011/07/05/150231.aspx?opt=admin
http://hi.baidu.com/sy2006ppkdc/blog/item/301e451f3169178686d6b621.html
http://www.cppblog.com/Onway/articles/122042.html