複数のナップサック問題の詳細な分析(パート2)

この記事は、「新人クリエーションセレモニー」イベントに参加し、一緒にゴールドクリエーションの道を歩み始めました。

複数のナップサック問題の詳細な分析(パート2)

序文

前の3つの記事では、 01ナップサック問題完全なナップサック問題、および複数のナップサックの最初の部分について慎重に説明しました。この記事では、主に複数のナップサック問題の最適化方法を紹介します。マルチバックパックの最初の部分を読んでいない場合は、最初にマルチバックパックを読む必要があります

複数のナップサック問題の紹介

もつ N N アイテムと容量は のバックパック。最初 のアイテムはせいぜい s s_i ピース、各ピースのボリュームは v_i 、値は w_i アイテムの総量がバックパックの容量を超えず、合計値が最大になるように、バックパックに入れるアイテムを見つけます。

:上記で使用されている文字は、この記事全体で同じ意味を持っています。

マルチバックパック問題と01バックパックおよび完全バックパックの違いは、アイテムを使用できる回数です.01バックパックは1回だけ使用でき、マルチバックパックは数え切れないほど使用でき、マルチバックパックは複数回使用されました。

複数のナップザックのバイナリ最適化

バイナリ最適化の実装

複数のナップザックのバイナリ最適化を正式に分析する前に、複数のナップザックの時間計算量を分析しましょう N N アイテム、各アイテムの平均数は S S 、複数のナップザックの時間計算量は O (( N S ) O(ソ連) _ 複数のナップザックのバイナリ最適化により、この時間計算量を次のように減らすことができます。 O ( N V l o g ( S ) ) O(NVlog(S)) _

マルチナップザックの最初の部分では、マルチナップサックの動的伝達方程式について説明しました( T = m i n ( S , V v i ) T = min(S、\ frac {V} {v_i}) ,其中 S S 表示物品能够选择的次数, v i v_i 表示物品的体积, V V 表示当前背包的容量):

d p [ i ] [ j ] = m a x { d p [ i 1 ] [ j ] , d p [ i 1 ] [ j v [ i ] ] + w [ i ] , d p [ i 1 ] [ j v [ i ] 2 ] + w [ i ] 2 , . . . , d p [ i 1 ] [ j v [ i ] T ] + w [ i ] T } dp[i][j] = max\\ \{ \\ dp[i - 1][j], \\ dp[i - 1][j - v[i]] + w[i],\\ dp[i - 1][j - v[i] * 2] + w[i] * 2, \\ ..., \\ dp[i - 1][j - v[i] * T] + w[i] * T\\ \}

从上面的公式我们可以知道,对于某个有 S S 件的物品当中,我们要选择一个合适的数字使得我们的收益最大,这个数字可能是 1 , 2 , 3 , 4 , . . . , S 1, 2, 3, 4, ..., S 。我们在文章多重背包上篇提到我们可以将多重背包转化成01背包,我们将 S S 个物品在逻辑上分成体积和价值相同的 S S 个不同的物品,被分成 S S 个不同的物品在进行动态选择的时候与 S S 个相同的物品是一样的。比如说对于 S S 个相同的物品 A A ,我们在选择3个的时候收益可以达到最大,那么对于转化之后的01背包问题来说就选择3个与 A A 体积和价值相同的物品即可。

根据上面分析我们可以知道多重背包能够转化成01背包的原因就是多重背包在转化为01背包之后,01背包能够有多重背包选1个,选2个,选3个,...,选 S S 个的效果。

而我们的二进制优化也主要集中在这个地方。多重背包的二进制优化也是将多重背包问题转化成01背包问题,但是不是将 S S 个相同的物品转化成 S S 个体积和价值相同的不同的物品。根据上文的分析我们知道,我们在将多重背包转化成01背包之后是需要保证01背包能够实现多重背包选1个,选2个,选3个,...,选 S S 个的效果。那么我们如何实现这一点呢?下面代码主要显示二进制优化将多重背包转化成01背包该往装物品的价值和体积的集合里加入什么东西。

Scanner scanner = new Scanner(System.in);
int N = scanner.nextInt();
int V = scanner.nextInt();
ArrayList<Integer> v = new ArrayList<>();
ArrayList<Integer> w = new ArrayList<>();
for (int i = 0; i < N; i++) {
    // 这个表示第 i 个物品的体积
    int vi = scanner.nextInt();
    // 这个表示第 i 个物品的价值
    int wi = scanner.nextInt();
    // 这个表示第 i 个物品有多少个
    int si = scanner.nextInt();
    // 这段代码主要是实现多重背包能够选择1个
    // 选择2个,...,选择S个的效果
    for (int j = 1; j <= si; j *= 2) {
        si -= j ;
        v.add(vi * j);
        w.add(wi * j);
    }
    if (si > 0) {
        v.add(vi * si);
        w.add(wi * si);
    }
}

我们举一个例子来分析上面的代码,假设我们加入一个物品 A A ,它的个数为9,价值和体积分别为5和3。那么在集合 v v 和集合 w w 当中的数据分别为:

v = [ 3 , 6 , 12 , 6 ] v = [3, 6, 12, 6]
w = [ 5 , 10 , 20 , 10 ] w = [5, 10, 20, 10]

上面的例子将9个 A A 分成了 A 1 A_1 A 2 A_2 A 3 A_3 ,以及 A 4 A_4 A 1 A_1 A 4 A_4 的体积和价值分别相当于1个,2个,4个,2个的 A A 的体积和价值。我们在上文当中提到了,我们在将多重背包转化成01背包之后是需要保证01背包能够实现多重背包选1个,选2个,选3个,...,选 S S 个的效果,那么上面的转化是如何实现这个效果的呢?

  • 一个 A A :相当于 A 1 A_1
  • 两个 A A :相当于 A 2 A_2
  • 三个 A A :相当于 A 1 + A 2 A_1 + A_2 ,也就是在动态选择的时候选择了 A 1 A_1 A 2 A_2 两个物品。
  • 四个 A A :相当于 A 3 A_3
  • 五个 A A :相当于 A 1 + A 3 A_1 + A_3 ,也就是在动态选择的时候选择了 A 1 A_1 A 3 A_3 两个物品。
  • 六个 A A :相当于 A 2 + A 3 A_2 + A_3 ,也就是在动态选择的时候选择了 A 2 A_2 A 3 A_3 两个物品。
  • 七个 A A :相当于 A 1 + A 2 + A 3 A_1 + A_2 + A_3 ,也就是在动态选择的时候选择了 A 1 A_1 A 2 A_2 A 3 A_3 三个物品。
  • 八个 A A :相当于 A 2 + A 3 + A 4 A_2 + A_3 + A_4 ,也就是在动态选择的时候选择了 A 2 A_2 A 3 A_3 A 4 A_4 三个物品。
  • 九个 A A :相当于 A 1 + A 2 + A 3 + A 4 A_1 + A_2 + A_3 + A_4 ,也就是在动态选择的时候选择了 A 1 A_1 A 2 A_2 A 3 A_3 A 4 A_4 四个物品。

相信经过上面的例子之后你已经大致明白了二进制优化的大致实现过程,二进制优化也是将多重背包转化成01背包但是和之前的转化不同的是,我们不是将 S S 个物品 A A 划分成 S S 个体积和价值相同的物品,而是将其划分成体积和价值是原来的物品1倍、2倍、3倍,...., 2 n 2^n 倍的物品,即 1 + 2 + 4 + . . . + 2 n + a = S 1 + 2 + 4 + ... + 2^n + a = S ,其中 a a 是最后的剩下的余数( a < 2 n + 1 a \lt 2^{n + 1} ),比如上面最后一个2就是a = 9 - 1 - 2 - 4。这样的划分我们可以知道,我们划分之后的物品的数目会少非常多。如果物品的次数的最大值是int类型的最大值,如果我们一个一个的划分最多可以划分超过20亿个物品,而上面的划分方式,我们划分出来的物品不会超过32个,因此大大降低了时间复杂度。

之前一个一个的划分我们的时间复杂度为 O ( N S V ) O(NSV) ,而像上面那样划分我们最大的时间复杂度为 O ( N V l o g ( S ) ) O(NVlog(S)) ,其中 N N 表示物品的个数, S S 表示物品能够选择的平均次数, V V 表示背包的容量。

上面就是我们使用二进制优化方式将多重背包转化成01背包的方式,完整代码如下(下方代码使用了单行数组优化):

import java.util.ArrayList;
import java.util.Scanner;

public class Main {
  public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    int N = scanner.nextInt();
    int V = scanner.nextInt();
    ArrayList<Integer> v = new ArrayList<>();
    ArrayList<Integer> w = new ArrayList<>();
    for (int i = 0; i < N; i++) {
      int vi = scanner.nextInt();
      int wi = scanner.nextInt();
      int si = scanner.nextInt();
      for (int j = 1; j <= si; j *= 2) {
        si -= j ;
        v.add(vi * j);
        w.add(wi * j);
      }
      if (si > 0) {
        v.add(vi * si);
        w.add(wi * si);
      }
      System.out.println(v);
      System.out.println(w);
    }
    int[] f = new int[V + 1];
    for (int i = 0; i < v.size(); i++) {
      for (int j = V; j >= v.get(i); j--) {
        f[j] = Math.max(f[j], f[j - v.get(i)] + w.get(i));
      }
    }
    System.out.println(f[V]);
  }
}

二进制优化的本质

我们知道任何一个数都有他的二进制形式,任何一个数都可以由2的整数次幂相加得到:

假如我们有15个物品 A A ,那么我们会得到 A 1 A_1 A 2 A_2 A 3 A_3 A 4 A_4 ,他们的价值和体积分别是 A A 的1倍,2倍,4倍和8倍,这四个物品可以组成相当于任意整数倍的物品 A A 的价值和重量,在这个问题当中就是1, 2, 4, 8可以组成1~15之间任意一个数。

因为我们最终可能 S S 个物品当中全部选了,因此当我们将多重背包转化成01背包之后,所有转化之后的物品的价值和体积需要和 S S 个物品相同,而 S S 不一定恰好就是 n n 个整数幂的值相加,因此在上文当中还提到了 a a a a 就保证了我们最终可以取到1~ S S 之间任意一个数。

总结

本篇文章主要给大家介绍的多重背包问题的二进制优化,里面的逻辑还是稍微有点复杂的,可能需要大家仔细去体会,大家在看文字的时候可以参考代码仔细分析,可以理解的更好一点。

以上就是本篇文章的所有内容了,希望大家有所收获,我是LeHung,我们下期再见!!!(记得点赞收藏哦!)


更多精彩内容合集可访问项目:github.com/Chang-LeHun…

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

おすすめ

転載: juejin.im/post/7120926384822124575