【问题标题】:Is Dynamic 0/1 Knapsack a Total Joke?动态 0/1 背包完全是个笑话吗?
【发布时间】:2018-01-03 15:02:13
【问题描述】:

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

断言:

  1. 用于解决背包问题的 bnb(分支定界)算法的大小并非与 K(背包容量)无关。
  2. bnb 树完整空间的大小总是为 O(NK),其中 N 是项目数,不是 O(2^N)
  3. bnb 算法在时间和空间上总是优于标准动态规划方法。

前置假设:bnb算法容易出现无效节点(如果剩余容量小于当前item的权重,我们不打算扩展它。另外,bnb算法已经完成以深度优先的方式。

草率证明:

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

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

但是如果 k

现在想象这个例子:

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

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

现在想象一下,不管这是否是一个好主意,我们只想通过记忆而不是表格使用递归公式来执行此操作,使用地图/字典或简单数组来存储访问的单元格。为了使用记忆化解决这个问题,我们应该解决表示的单元格:

现在这与我们使用 bnb 方法获得的树完全一样:

现在是草率的证明:

  1. Memoization 和 bnb 树的节点数相同
  2. 记忆节点取决于表的大小
  3. 表大小取决于 N 和 K
  4. 因此bnb不独立于K
  5. 记忆空间以 NK 为界,即 O(NK)
  6. 因此 bnb 树的完整空间(或者如果我们以广度优先方式进行 bnb 的空间)总是 O(NK) 而不是 O(N^2) 因为整棵树不会被构造出来,它就像妈妈化一样。
  7. Memoization 比标准动态编程有更好的空间。
  8. bnb 比动态规划有更好的空间(即使是在广度优先的情况下)
  9. 没有松弛的简单 bnb(仅消除不可行的节点)将比记忆化有更好的时间(记忆化必须在查找表中搜索,即使查找可以忽略不计,它们仍然是相同的。)李>
  10. 如果我们忽略记忆的查找搜索,它比动态更好。
  11. 因此bnb算法在时间和空间上总是优于动态。

问题:

如果无论如何我的证明是正确的,就会出现一些有趣的问题:

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

ps:很抱歉发了这么长的帖子!

编辑:

由于其中两个答案都集中在记忆上,我只想澄清一下,我根本不关注这个!我只是使用记忆作为一种技术来证明我的断言。我的主要关注点是分支定界技术与动态编程,这是另一个问题的完整示例,由 bnb + 松弛解决(来源:Coursera - 离散优化):

【问题讨论】:

  • 我投票结束这个问题,因为它更适合cs.stackexchange.com
  • 我怎样才能把它移到那里?
  • 您可能希望避免使用“a Total Joke”之类的短语;它引发了人们的疯狂警报。
  • 总结一下:我在解决修改后的变更问题和随后的调查中发现标准(“经典”)DP解决方案远不如同样使用内存的体面BnB解决方案- 受限的记忆缓存。我还没有找到任何个测试用例,其中 BnB+memoization 解决方案甚至与经典 DP 解决方案一样慢。这让我不解为什么它是经典的解决方案。

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


【解决方案1】:

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

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

【讨论】:

  • 我主要关心的是 bnb 与动态。提到记忆是为了证明我的观点。
  • 如果没有记忆,bnb 可能会慢得多,因为它会重新访问节点并且在最坏的情况下可能是 2^n 复杂度。我建议您尝试比较一些实际示例的实际执行时间:我个人更喜欢编写记忆递归算法,但通常需要转换为动态编程才能获得足够的速度。
  • 不! memoization 和 branch and bound 是完全不同的。枝桠是一棵树!并且在一棵树中,每个节点只有一个入边,因此不需要重新计算!
【解决方案2】:

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

主要区别在于计算顺序。计算整个矩阵的方式称为“自下而上 DP”,因为您从 (0,0) 开始并自己向上工作。您的方式(树方法)称为“自上而下的 DP”,因为您从目标开始并沿着树向下工作。但他们都在使用动态规划。

现在回答你的问题:

你高估了你真正节省了多少。 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 元素),列代表k 值(允许的权重)。 1 是您将在树方法中访问的 DP 矩阵中的位置。当然你会在矩阵的底部看到很多0s,但是你会访问上半部分的每个位置。矩阵中大约 68% 的位置被访问。在这种情况下,自下而上的 DP 解决方案会更快。递归调用速度较慢,因为您必须为每个递归调用分配一个新的堆栈帧。使用循环而不是递归调用的 2 倍加速并不是不典型的,这已经足以使自下而上的方法更快。我们甚至还没有谈到树方法的记忆成本。

请注意,我在这里没有使用实际的 bnb。我不太确定您将如何处理绑定部分,因为您实际上只有在通过访问其子节点来计算节点时才知道该节点的值。

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

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

【讨论】:

    【解决方案3】:

    我认为您有一个误解,即动态规划是背包问题的最先进解决方案。这个算法在大学里被教授,因为它是动态规划和伪多项式时间算法的一个简单而好的例子。

    我没有该领域的专业知识,也不知道现在的最新技术是什么,但是分支定界方法已经使用了相当长一段时间来解决背包问题:本书@ 987654321@ 已经很老了,但对分支定界的处理相当广泛。

    不过,从你的角度来看,这是一个很好的观察,分支和绑定方法可以用于背包 - 唉,你出生太晚了,不能成为第一个有这个想法的人:)

    您的证明中有一些我不理解的地方,我认为需要更多解释:

    1. 您需要记忆,否则您的树将有O(2^N) 节点(显然会有这种情况,否则背包不会是NP-hard)。我在你的证明中没有看到任何东西,这可以确保记忆记忆/计算步骤少于O(NK)
    2. 动态编程只需要 O(K) 内存空间,所以我不明白为什么您可以声称“bnb 算法在时间和空间上总是比动态算法更好”。

    也许您的说法是正确的,但我无法按照现在的证明方式看到它。

    另一个问题是“更好”的定义。如果分支定界方法更适合大多数问题或常见问题,或者它必须更​​适合最坏情况(在现实生活中不会发挥任何作用),那么分支定界方法是否更好?

    我链接到的书还对算法的运行时间进行了一些比较。基于动态规划的算法(显然比在学校教授的算法更复杂)对于某些类型的问题甚至更好——参见第 2.10.1 节。开个玩笑还不错!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-13
      • 2018-03-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-28
      • 2013-01-03
      • 1970-01-01
      相关资源
      最近更新 更多