动态0/1背包是一个总笑话吗?

时间:2017-07-27 18:19:06

标签: algorithm tree dynamic-programming knapsack-problem branch-and-bound

我已经获得了一个证据,证明了关于0/1背包问题的普遍看法,我真的很难说服自己我是对的,因为我找不到任何东西任何地方支持我的主张,所以我首先陈述我的主张然后证明他们,我会感谢任何人试图进一步证实我的主张或取消他们。任何合作都表示赞赏。

断言:

  1. 用于解决背包问题的bnb(分支和边界)算法的大小,与K(背包的容量)无关。
  2. bnb树完整空间的大小总是 O(NK),其中N是项目数, O(2 ^ N)
  3. bnb算法始终优于标准动态编程方法,无论是时间还是空间。
  4. 预先假设:bnb算法倾向于无效节点(如果剩余容量小于当前项的权重,我们不会扩展它。此外,bnb算法已完成以深度优先的方式。

    邋Pro证明:

    以下是解决背包问题的递归公式:

    值(i,k)= max(值(i-1,k),值(n-1,k-weight(i))+ value(i)

    但是如果k <权重(i):值(i,k)=值(i-1,k)

    现在想象一下这个例子:

    K = 9
    N = 3   
    V W:   
    5 4   
    6 5   
    3 2
    

    现在这里是针对此问题的动态解决方案和表格:

    enter image description here

    现在想象一下,无论是否一个好主意,我们都希望通过memoization而不是表格使用递归公式,使用类似地图/字典或简单数组来存储访问过的单元格。为了使用memoization解决这个问题,我们应该解决所表示的单元格:

    enter image description here

    现在这就像我们使用bnb方法获得的树一样:

    enter image description here

    现在为了草率的证据:

    1. Memoization和bnb树具有相同数量的节点
    2. 记忆节点取决于表格大小
    3. 表格大小取决于N和K
    4. 因此 bnb与K
    5. 无关
    6. 记忆空间以NK为界,即O(NK)
    7. 因此 bnb树的完整空间(或者如果我们以广度优先的方式进行bnb的空间)总是O(NK)而不是O(N ^ 2),因为整个树是不会被构建,它就像妈妈一样。
    8. 记忆比标准动态编程有更好的空间。
    9. bnb比动态编程有更好的空间(即使首先在广度上完成)
    10. 没有放松的简单bnb(只是消除不可行的节点)比memoization有更好的时间(memoization必须在查找表中搜索,即使查找可忽略不计,它们仍然是相同的。)
    11. 如果我们忽略了备忘录的查找,那么它比动态更好。
    12. 因此 bnb算法在时间和空间上总是优于动态。
    13. 的问题:

      如果无论如何我的证据都是正确的,那么会出现一些有趣的问题:

      1. 为什么要烦扰动态编程?根据我的经验,你可以在dp背包中做的最好的事情就是拥有最后两列,你可以将它进一步改进到一列,如果你从下到上填充它,它将有O(K)空间但仍然不能(如果上述断言是正确的)击败bnb方法。
      2. 如果我们将它与松弛修剪(关于时间)相结合,我们还能说bnb更好吗?
      3. ps:很抱歉很长的帖子!

        编辑:

        由于其中两个答案都集中在记忆上,我只是想澄清一点,我并没有专注于根本就是!我只是用memoization作为一种技术来证明我的断言。我的主要关注点是分支定界技术与动态规划,这里是另一个问题的完整例子,由bnb + relax解决(来源:Coursera - 离散优化):

        enter image description here

3 个答案:

答案 0 :(得分:3)

首先,由于你正在应用记忆,你仍然在做DP。这基本上是DP的定义:递归+记忆。这也很好。没有记忆,你的计算成本就会爆炸。试想一下,如果两个项目同时具有权重2,并且第三个和第四个项目具有权重1.它们都会在树中的同一节点处结束,您将不得不多次进行计算,并且您最终会得到指数运行时间。

主要区别在于计算顺序。计算整个矩阵的方式被称为&#34;自下而上的DP&#34;,因为你从(0,0)开始并向上工作。你自己的方式(树方法)被称为&#34;自上而下的DP&#34;,因为你从目标开始并在树下工作。但他们都在使用动态编程。

现在回答你的问题:

你高估了你真正节省了多少钱。 N = 3是一个非常小的例子。我很快尝试了一个更大的例子,N = 20K=63(仍然非常小)和随机值和随机权重。这是我创造的第一张照片:

values: [4, 10, 9, 1, 1, 2, 1, 2, 6, 4, 8, 9, 8, 2, 8, 8, 4, 10, 2, 6]
weights: [6, 4, 1, 10, 1, 2, 9, 9, 1, 6, 2, 3, 10, 7, 2, 4, 10, 9, 8, 2]
111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111111111111
111111111111111111111111111111111111111111111111111111111111111
011111111111111111111111111111111111111111111111111111111111101
000001011111111111111111111111111111111111111111111111111111101
000000010111111111111111111111111111111111111111111111111111101
000000000010101011111111111111111111111111111111111111111010101
000000000000000000001010101111111111111111111111111111111010101
000000000000000000000000000101010101111111111111111111101010101
000000000000000000000000000001010101011111111111111111101010101
000000000000000000000000000000000101000001111100001111100000101
000000000000000000000000000000000000000000010100000111100000101
000000000000000000000000000000000000000000000000000010100000101
000000000000000000000000000000000000000000000000000000000000101
000000000000000000000000000000000000000000000000000000000000001

此图片是您显示的矩阵的转置版本。行代表i值(数组中的第一个i元素),cols代表k值(允许的权重)。 1是DP矩阵中的位置,您将在树木进近期间访问。当然,您会在矩阵的底部看到很多0,但您将访问上半部分的每个位置。访问矩阵中大约68%的位置。在这种情况下,自下而上的DP解决方案会更快。递归调用较慢,因为您必须为每个递归调用分配一个新的堆栈帧。使用循环而不是递归调用来加速2倍并不是非典型的,这已经足以使自下而上的方法更快。我们甚至还没有谈到树方法的记忆成本。

请注意,我在这里没有使用过实际的bnb。我不太确定你将如何处理绑定部分,因为一旦你通过访问它的孩子来计算它,你实际上只知道它的价值。

凭借我的输入数据,自下而上的方法显然是赢家。但这并不意味着你的做法很糟糕。恰恰相反。它实际上可能非常好。这一切都取决于输入数据。让我们想象K = 10^18和你所有的权重都是10^16。自下而上的方法甚至找不到足够的内存来分配矩阵,而你的方法很快就会成功。

但是,你可能通过执行A *而不是bnb来改善你的版本。您可以使用(i, k)估算每个节点int(k / max(weight[1..i]) * min(values[1..i])的最佳值,并使用此启发式修剪大量节点。

答案 1 :(得分:3)

我认为你的方面存在一种误解,即动态编程是背包问题的最先进解决方案。该算法在大学教授,因为它是动态编程和伪多项式时间算法的一个简单而好的例子。

我没有该领域的专业知识,也不知道现在最先进的是什么,但分支定界方法已经使用了相当长的一段时间来解决背包问题:本书Knapsak-Problems by Martello and Toth已经很老了,但是广泛地对待分支和界限。

尽管如此,这是一个很好的观察,你可以使用分支和绑定方法进行背包 - 唉,你出生太晚了,不能成为第一个有这个想法的人:)

您的证明中有一些我不理解的内容,在我看来需要更多解释:

  1. 你需要记忆,否则你的树会有O(2^N)个节点(显然会有这种情况,否则背包不会是NP难的)。我没有在您的证明中看到任何内容,这确保了记忆内存/计算步骤小于O(NK)
  2. 动态编程只需要O(K)内存空间,所以我不明白为什么你可以声称&b; bnb算法在时间和空间上总是优于动态&#34;。
  3. 也许你的说法属实,但我现在无法按照证据的方式来看待它。

    另一个问题是&#34;更好&#34;的定义。如果对于大多数问题或常见问题更好或者对于wost-case(在现实生活中不起任何作用)它是否更好,分支定界方法是否更好?

    我链接的书也对算法的运行时间进行了一些比较。基于动态编程的算法(显然比在学校教授的算法更复杂)对于某些问题甚至更好 - 见2.10.1节。一个完全开玩笑还不错!

答案 2 :(得分:2)

实际上,对于整数0/1背包,动态编程可能更好,因为:

  1. 没有递归意味着你永远不会遇到堆栈溢出
  2. 无需对每个节点进行查找搜索,因此通常更快
  3. 如您所知,存储最后两列意味着内存要求较低
  4. 代码更简单(不需要记忆表)