I and OI
Past...

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

 

posted on 2011-08-05 21:50  exponent  阅读(258)  评论(0编辑  收藏  举报