Bootstrap

背包基础问题讲解

 

三星Bada开发者聚会报名中
                                      背包基础问题讲解
                                                    ----sifx_xu
[算法分析]:
 
对于背包问题,通常的处理方法是搜索or DP
用递归来完成搜索,算法设计如下:
function make( i {处理到第i件物品} , j{剩余的空间为j}:integer) :integer;
初始时i=m , j=背包总容量
begin
if i:=0 then
make:=0;
if j>=wi then (背包剩余空间可以放下物品 i )
r1:=make(i-1,j-wi)+v[i]; (第i件物品放入所能得到的价值 )
r2:=make(i-1,j) (第i件物品不放所能得到的价值 )
make:=max{r1,r2}
end;
 
这个算法的时间复杂度是O(2^n),我们可以做一些简单的优化。
优化思路:由于本题中的所有物品的重量均为整数,经过几次的选择后背包的剩余空间可能会相等,在搜索中会重复计算这些结点,所以,如果我们把搜索过程中计算过的结点的值记录下来,以保证不重复计算的话,速度就会提高很多。这是简单的"以空间换时间"。
思路转向DP:我们发现,由于这些计算过程中会出现重叠的结点,符合动态规划中子问题重叠的性质
同时,可以看出如果通过第n次选择得到的是一个最优解的话,那么第n-1次选择的结果一定也是一个最优解。这符合动态规划中最优子问题的性质。
 
考虑用动态规划的方法来解决,这里的:
阶段是:在前n件物品中,选取若干件物品放入背包中;
状态是:在前n件物品中,选取若干件物品放入所剩空间为w的背包中的所能获得的最大价值;
决策是:n件物品放或者不放;
由此可以写出动态转移方程:
我们用f[i,j]表示在前 i 件物品中选择若干件放在所剩空间为 j 的背包里所能获得的最大价值
 
f[i,j]=max{f[i-1,j-wi]+pi (j>=wi), f[i-1,j]}
 
这样,我们可以自底向上地得出在前m件物品中取出若干件放进背包能获得的最大价值,也就是f[m,w]
 
算法设计如下:
procedure make;
begin
    for i:=0 to w do
        f[0,i]:=0;
    for i:=1 to m do
      for j:=0 to w do begin
        f[i,j]:=f[i-1,j];
        if (j>=w[i]) and (f[i-1,j-w[i]]+v[i]>f[i,j]) then
           f[i,j]:=f[i-1,j-w[i]]+v[i];
        end;
    writeln(f[m,wt]);
end;
 
DP VS 搜索:由于是用了一个二重循环,这个算法的时间复杂度是o(n*w)。而用搜索的时候,当出现最坏的情况,也就是所有的结点都没有重叠,那么它的时间复杂度是O(2^n)。看上去前者要快很多。但是,可以发现在搜索中计算过的结点在动态规划中也全都要计算,而且这里DP算得更多(有一些在最后没有派上用场的结点我们也必须计算),在这一点上好像是矛盾的。
事实上,由于我们定下的前提是:所有的结点都没有重叠。也就是说,任意n件物品的重量相加都不能相等,而所有物品的重量又都是整数,那么这个时候w的最小值是:1+2+2^2+2^3+……+2^n-1=2^n -1
此时n*w>2^n,动态规划比搜索还要慢~~|||||||所以,其实背包的总容量w和重叠的结点的个数是有关的
考虑能不能不计算那些多余的结点……
 
那么换一种状态的表示方式:
状态是:在前n件物品中,选取若干件物品放入所占空间为w的背包中的所能获得的最大价值;
 
阶段和决策:同上;
状态转移方程是:
f[i,j]=max{f[i-1,j-wi]+pi (j+wi<=背包总容量), f[i-1,j] }
这样,我们可以得出在m件物品中取出若干件放进背包在所占空间不同的状态下能获得的最大价值,在其中搜索出最大的一个就是题目要求的解。
 
算法设计如下:
procedure make;
begin
    f[0,wt]:=0;
    for i:=1 to n do
       for j:=0 to w (背包总容量) do
          if f[i-1,j]未被赋过值 then (这些结点与计算无关,忽略)
             continue
          else
             f[i,j]:=max{f[i-1,j+wi]+pi , f[i-1,j]};
      最大价值:=max{f[n,j]} (求最大值)
end;
 
由于事实上在计算的过程中每一个阶段的状态都只和上一个阶段有关,所以只需要来一个两层的数组循环使用就可以了,这是动态规划中较常使用的降低空间复杂度的方法。(提出滚动数组概念)
 
本题能够用动态规划的一个重要条件就是:所有的重量值均为整数 因为
1)这样我们才可以用数组的形式来储存状态;
2)这样出现子问题重叠的概率才比较大。(如果重量是实型的话,几个重量相加起来相等的概率会大大降低)
所以,当重量不是整数的时候本题不适合用动态规划。
 
[解的输出]:
 
在计算最大价值的时候我们得到了一张表格(f[i,j]),我们可以利用这张表格输出解。
可以知道,如果f[i-1,j+wi]+v[i]=f[i,j] (第二个算法),则选择物品i放入背包。
算法设计1:
进行反复的递归搜索,依次输出物品序号;
procedure out(i,j:integer);(初始时 i=n, j=获得最大价值的物品所占的空间)
begin
if i=0 then exit;
if f[i,j]=f[i-1,j+w[i]]+v[i] then begin
输出解
out(i-1,j+w[i]);
end
else
out(i-1,j); //第i个物品不选的情况
end;
 
算法设计2:
同样的思路我们可以用循环来完成;
procedure out2;
var
i,ws:integer;
begin
ws:=获得最大价值的物品所占的空间;
for i:=n downto 1 do begin
if (ws>=w[i]) and (f[i,ws]=f[i-1,ws-w[i]]+v[i]) then begin
输出解;
ws:=ws-w[i]; //减少背包的容量
end;
end;
writeln;
end;
用这两种算法的前提是我们必须存住 f[i,j] 这一整个二维数组,但是如果用循环数组的话怎样输出解呢?
显然,我们只需要存住一个布尔型的二维数组,记录每件物品在不同的状态下放或者不放就可以了。这样一来数组所占的空间就会大大降低。
 
[解题收获]:
1)在动态程序设计中,状态的表示是相当重要的,选择正确的状态表示方法会直接影响程序的效率。
2)针对题目的不同特点应该选择不同的解题策略,往往能够达到事半功倍的效果。像本题就应该把握住"所有的重量值均为整数"这个特点。
 
---------------------------------------------------------------------------------------------------华丽的分割线
 
背包问题全攻略
 
部分背包问题可有贪心法求解:计算Pi Wi
        数据结构:
        w[i]第i个背包的重量;
        p[i]第i个背包的价值;
 
(1)每个背包只能使用一次或有限次(可转化为一次):
        A.求最多可放入的重量。
        NOIP2001 装箱问题
        有一个箱子容量为v(正整数,o≤v≤20000),同时有n个物品(o≤n≤30),
每个物品有一个体积 (正整数)。
        要求从 n 个物品中,任取若千个装入箱内,使箱子的剩余空间为最小。
        l 搜索方法
      procedure search(k,v:integer); {搜索第k个物品,剩余空间为v}
      var i,jinteger;
      begin
        if v>best then best=v;
        if v-(s[n]-s[k-1]) =best then exit;
        {s[n]为前n个物品的重量和}
        if k<=n then begin
          if v>=w[k] then search(k+1,v-w[k]);
          search(k+1,v);
        end;
      end;
    
      0/1背包模型
        F[I,j]为前i个物品中选择若干个放入使其体积正好为j的标志,为布尔型(如果是要求最大值或最小值等最优值时就变成数值integer类型)。
        实现将最优化问题转化为判定性问题
        F[I,j]=f[i-1,j-w[i]] (w[I] =j =v) 边界:f[0,0]=true.
        For I=1 to n do
        For j=w[I] to v do F[I,j]=f[I-1,j-w[I]];
        优化:当前状态只与前一阶段状态有关,可降至一维。(降维十分重要的思路)
        F[0]=true;
        For I=1 to n do begin
          f1=f; 滚动数组实现方法)
          For j=w[I] to v do
          If f[j-w[I]] then f1[j]=true;
          f=f1;
        End;
        
        扩展:
        B.求可以放入的最大价值或最小值等等。
        C.求恰好装满的情况数。
    
(2)每个背包可使用任意次(完全背包模型):
        A.求最多可放入的重量。
        状态转移方程为
        f[I,j]=max{f[I,j],f[i,j-w[j]+v[i]}
        B.求可以放入的最大价值。
        USACO 3.1.2 Score Inflation
        进行一次竞赛,总时间T固定,有若干种可选择的题目,每种题目可选入的
数量不限,每种题目有一个ti(解答此题所需的时间)和一个si(解答此题所得
的分数),现要选择若干题目,使解这些题的总时间在T以内的前提下,所得的
总分最大,求最大的得分。
        易想到:
        F[I,j]=max(f[i-1,j],f[I,j-w[i]]+v[i]);
        由于状态只是跟i-1的状态和已经计算出来的第i层状态有关,所以可以直接用一维实现
        for i:=1 to n do begin
           read(v[i],w[i]);
           for j:=w[i] to v do
              f[j]=max(f[j],f[j-w[i]]+v[i]);
        end;
        C.求恰好装满的情况数。
        Ahoi2001 Problem2
        题目:质数和分解
任何大于1 的自然数n都可以写成若干个大于等于2且小于等于n的质
数之和表达式(包括只有一个数构成的和表达式的情况),并且可能有不止一
种质数和的形式。例如,9 的质数和表达式就有四种本质不同的形式:
9=2+5+2=2+3+2+2=3+3+3=2+7。
这里所谓两个本质相同的表达式是指可以通过交换其中一个表达式中
参加和运算的各个数的位置而直接得到另一个表达式。
试编程求解自然数n可以写成多少种本质不同的质数和表达式。
输入:文件中的每一行存放一个自然数n(2<=n<=200)。
输出:依次输出每一个自然数n的本质不同的质数和表达式的数目。
例如:
输入
2
9
输出
1
4
     
      思路一,递归搜索效率不高(可能是我写的递归效率低吧)
      A[]数组是素数表 rest是总数
      procedure try(pre,rest:integer)
var i,tmp:integer;
begin
        if(rest=0) inc(tot)
        else for i:=pre to len do begin
           tmp:=rest;
           while(tmp>=a[i]) begin
              dec(tmp,a[i]);
              try(i+1,tmp);
           end;
        end;
end;
    
     思路二:可使用动态规划求解
     将1到v之间的素数打表放在数组中,将素数看成物品,v看成是背包的容量,用物品填背包,这样就将问题转换成了完全背包模型了。
        伪代码:
        F[0]=1;
        For i:=1 to n do
           For j=a[i] to v do
              f[j]=f[j]+f[j-a[i]];
      
USACO 2.3.4 money system(完全背包)
V个物品,背包容量为n,求放法总数。
转移方程:
      for i:=1 to n do read(a[i]);
        f[0]:=1;
for i:=1 to v do f[i]:=0;
        for i:=1 to n do
          for j:=a[i] to v do
              f[j]:=f[j]+f[j-a[i]];
 
 
 
提供一个oi学习网站:
http://mail.bashu.cn:8080/BS41Online/problemlist?volume=1
上面的许多题目都可以在巴蜀中学的oi题库网上找到。
 
;