一文看懂3种背包问题

前言

前面在动态规划详解一文种讲解了几个动态规划的基本问题。最近在工作中碰到了背包问题,写了一下基于动态规划的算法,决定写一篇文章总结一下。

3种背包问题的比较

背包问题:有N种物品和一个容量为b的背包,第i种物品的重量为w_i,价值为v_i,如何选择放入的物品使得总价值最大。

根据每件物品的数量限制,就分为3种背包问题,定义如下:

  • 01背包:每种物品只有一件,要么选择放入,要么选择不放入
  • 完全背包:每种物品的数量有无穷多个
  • 多重背包:每种物品都有数量限制,第i种物品的最大数量为n_i

三种背包问题都有一个共同的限制,那就是背包容量,背包的容量是有限的,这便限制了物品的选择,而三种背包问题的共同目的,便是让背包中的物品价值最大。

不同的地方在于物品数量的限制,01背包问题中,每种物品只有一个,对于每种物品而言,便只有选和不选两个选择。完全背包问题中,每种物品有无限多个,所以可选的范围要大很多。在多重背包问题中,每种物品都有各自的数量限制。

因此问题归结为如下的线性规划:

                                                                  目标函数:max\sum_{0}^{n-1}v_ix_i

                                                                  约束条件:\sum_{0}^{n-1}w_ix_i\leq b, x_i\epsilon N

 

完全背包问题

对于完全背包问题,上述约束条件已经足够。

因此我们首先来分析完全背包问题的状态转移方程,也就是建立递推关系式。

首先,我们来界定子问题:由参数k和y来界定,其中

  • k表示对0,1,...k号物品的选择
  • y表示背包总重量不超过y

当k=n-1,y=b就是我们的原始问题。

优化函数F_k(y):装前k种物品,总重量不超过y,背包达到的最大价值。

那么考虑第k种物品是否装入时,有两种情况:

  1. 第k种物品不装入,那么F_k(y)=F_{k-1}(y)
  2. 第k种物品装入1个以上,那么F_k(y)=F_{k}(y-w_k)+v_k

因此,我们得到递推关系式:

                       F_k(y)=max\{F_{k-1}(y),F_{k}(y-w_k)+v_k\}

初始条件为:

                            F_0(y)=\left \lfloor \frac{y}{w_0} \right \rfloor\cdot v_0,0\leqslant y\leqslant b

                             F_k(0)=0

有了初始条件和递推关系,采用自上而下填表法,就可以很容易地写出代码了。

举个例子,输入如下:b=10,

v_0=1,v_1=3,v_2=5,v_3=9,\\ w_0=2,w_1=3,w_2=4,w_3=7

F_k(y)的计算表如下:

其中,第一行根据初始条件直接计算得到,然后利用递推公式,依次增大k和y,往下填表。

解的追踪

F_k(y)的表格(在代码中就是二维数组)计算出来后,F_{n-1}(b)就是要求解的最大价值。接下来,就需要确定达到最大价值时,xi的取值。根据递推公式,我们容易得到伪代码如下:

嗯,完美解决。时间复杂度即填表耗时O(N * b),这里用了一个二维数组来存储子问题的解,所以空间复杂度为O(N * b)。

下面给出一个Delphi实现的代码:

function Knapsack(W, V: TList<Integer>; Wmax: Integer; Xout: TList<Integer>): Integer;
var
  i, j, k: Integer;
  Vmax: Integer;
  Vky: array of array of Integer;//子问题最大价值记录表
begin
  if (W.Count <> V.Count) or (W.Count = 0) then
    raise Exception.Create('Error Input');
  //初始化
  SetLength(Vky, W.Count);
  for i := 0 to W.Count - 1 do
  begin
    SetLength(Vky[i], Wmax + 1);
    Vky[i][0] := 0;
  end;
  for i := 1 to Wmax do
    if i >= W[0] then
      Vky[0][i] := (i div W[0]) * V[0]; // 完全背包(物品数量无穷)

  //自下而上填表
  for i := 1 to W.Count - 1 do
  begin
    k := i - 1;
    for j := 1 to Wmax do
      if j < W[i] then//装不进去
        Vky[i][j] := Vky[k][j]
      else
      begin
        Vmax := Vky[i][j - W[i]] + V[i];//用i号物品>=1个时的最大价值
        Vky[i][j] := Max(Vky[k][j], Vmax);
      end;
  end;

  //追踪解
  Result := Vky[W.Count - 1][Wmax];
  Xout.Clear;
  Xout.Capacity := W.Count;
  for I := 0 to W.Count - 1 do
    Xout.Add(0);
  j := Wmax;
  for I := W.Count - 1 downto 1 do
    while Vky[i][j] > Vky[i - 1][j]  do
    begin //i号物品被使用
      j := j - W[i];
      Xout[i] := Xout[i] + 1;
    end;
  if j >= W[0] then
   Xout[0] := j div W[0];
end;

Tips:完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足w[i] <= w[j]且vl[i] >= v[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高的j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。
————————————————

01背包问题

对于01背包问题,还要加上一个约束条件,即每种物品只有一件: x_i\leqslant 1.

同理,考虑第k种物品是否装入时,有两种情况:

  1. 第k种物品不装入,那么F_k(y)=F_{k-1}(y)
  2. 第k种物品装入1个,那么F_k(y)=F_{k-1}(y-w_k)+v_k

注意第二种情况相对与完全背包发生了变化,因为物品最多只有一件,因此F_k(y-w_k)的下标由k变成了k-1。

因此,递推公式如下:

                       F_k(y)=max\{F_{k-1}(y),F_{k-1}(y-w_k)+v_k\}

初始条件为:

                            F_0(y)= 0, y< w_0\\ F_0(y)= v_0, y\geqslant w_0\\ F_k(0)=0

同样的,对于上面的例子,此时F_k(y)的计算表如下:

对于01背包解的追踪,比较简单:从表格的最优解处,开始逆向追踪,如果F_k(y)=F_{k-1}(y),表示使用了k号物品,否则k号物品未使用。

转化成代码如下:

function Knapsack01(W, V: TList<Integer>; Wmax: Integer; Xout: TList<Integer>): Integer;
var
  i, j, k: Integer;
  Vmax: Integer;
  Vky: array of array of Integer;//子问题最大价值记录表
begin
  if (W.Count <> V.Count) or (W.Count = 0) then
    raise Exception.Create('Error Input');
  //初始化
  SetLength(Vky, W.Count);
  for i := 0 to W.Count - 1 do
  begin
    SetLength(Vky[i], Wmax + 1);
    Vky[i][0] := 0;
  end;
  for i := 1 to Wmax do
    if i >= W[0] then
      Vky[0][i] := V[0];
    //Vky[0][i] := (i div W[0]) * V[0];  完全背包(物品数量无穷)

  //自下而上填表
  for i := 1 to W.Count - 1 do
  begin
    k := i - 1;
    for j := 1 to Wmax do
      if j < W[i] then//装不进去
        Vky[i][j] := Vky[k][j]
      else
      begin
        Vmax := Vky[k][j - W[i]] + V[i];//用i号物品时的最大价值
        Vky[i][j] := Max(Vky[k][j], Vmax);
      end;
  end;

  //追踪解
  Result := Vky[W.Count - 1][Wmax];
  Xout.Clear;
  Xout.Capacity := W.Count;
  for I := 0 to W.Count - 1 do
    Xout.Add(0);
  j := Wmax;
  for I := W.Count - 1 downto 1 do
    if Vky[i][j] <> Vky[i - 1][j]  then
    begin //i号物品被使用
      j := j - W[i];
      Xout[i] := 1;
    end;
  if j >= W[0] then
   Xout[0] := 1;
end;

                    

多重背包问题

由于01背包问题也有数量限制(每种物品限制一件),我们可以考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第i种物品最多选b/w_i件,于是可以把第i种物品转化为b/w_i件费用及价值均不变的物品,然后求解这个01背包问题。但这样做,如果物品重量较小,空间复杂度将很高!

更高效的转化方法是:把第i种物品拆成费用为w_i\cdot 2^k、价值为v_i\cdot 2^k的若干件物品,其中k满足w[i]*2^k<b。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。这样把每种物品拆成O(log(b/w_i))件物品,是一个很大的改进。

例如:7:1、2、4;

          9:1、2、2、4;

          10:1,2、3、4

唯一的问题是如何拆分物品。事实上,只需要列出数字2~10的拆分,那么任意数字的拆分都很容易做到了,只需将个位、十分位、百分位、千分位...分别拆分即可。

做这样的拆分后,时间复杂度就成了O(\sum_{0}^{n-1}log(\mathbf{b}/w_i)\cdot \mathbf{b})。问题完全转化为了01背包,相信读者完全可以写出代码,这里就不再给出了。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值