0/1 背包动态规划优化,从二维矩阵到一维矩阵
Posted
技术标签:
【中文标题】0/1 背包动态规划优化,从二维矩阵到一维矩阵【英文标题】:0/1 Knapsack Dynamic Programming Optimization, from 2D matrix to 1D matrix 【发布时间】:2013-06-19 06:13:45 【问题描述】:我需要***的澄清:Knapsack,部分
因此,此解决方案将在 O(nW) 时间和 O(nW) 空间中运行。此外,如果 我们只使用一维数组 m[W] 来存储当前的最优值 并传递这个数组 i+1 次,每次都从 m[W] 重写为 m[1],我们 仅对 O(W) 空间获得相同的结果。
我无法理解如何将 2D 矩阵转换为 1D 矩阵以节省空间。此外,rewriting from m[W] to m[1] every time
是什么意思(或它是如何工作的)。
请提供一些例子。假设我有 K = 9 的集合 V,W --> (5,4),(6,5),(3,2)。
一维数组会是什么样子?
【问题讨论】:
【参考方案1】:在许多动态规划问题中,您将逐行构建一个 2D 表,其中每行仅依赖于紧接在其前面的行。在 0/1 背包问题的情况下,递归(来自***)如下:
m[i, w] = m[i - 1, w] 如果 wi > w
m[i, w] = max(m[i - 1, w], m[i - 1, w - wi] + vi)否则
请注意,填充第 i 行时从表中读取的所有内容仅来自第 i - 1 行;表中较早的行实际上并未使用。因此,您可以通过仅存储两行来节省 2D 表中的空间 - 前一行和您正在填写的行。您可以通过更聪明地了解如何填写来将其进一步优化为仅一行表条目。这将空间使用量从 O(nW)(O(n) 行和 O(W) 列)减少到 O(W)(一或两行和 O(W) 列)。
不过,这是有代价的。许多 DP 算法不会在执行过程中明确计算解决方案,而是填写表格,然后在最后对表格进行第二次遍历以恢复最佳答案。如果您只存储一行,那么您将获得最佳答案的值,但您可能不知道最佳答案恰好是什么。在这种情况下,您可以读出您可以装入背包的最大值,但您不一定能够恢复为达到该值而应该做的事情。
希望这会有所帮助!
【讨论】:
对于我的情况,我需要记住选择了哪个条目,并且根据您的说法,我不一定能够恢复我是如何实现该值的;这是否意味着对于这个特定问题,我不能将 O(n*W) 转换为 O(W)? 或者换句话说,优化空间使用只适用于我们不需要记住哪些物品被挑选,而只想知道最大值的情况? @templatetypedef 你能帮忙解释一下为什么一维解需要从m[w]迭代到m[j],为什么不能从m[j]迭代到m[w]? @PeitiPeterLi 如果我们从左到右迭代,它会覆盖之前 i 的较小权重的值。【参考方案2】:我知道这是一个老问题。但是我不得不花一些时间来寻找这个,我只是在这里记录这些方法以供任何人将来参考。
方法一 使用 N 行的直接 2D 方法是:
int dp[MAXN][MAXW];
int solve()
memset(dp[0], 0, sizeof(dp[0]));
for(int i = 1; i <= N; i++)
for(int j = 0; j <= W; j++)
dp[i][j] = (w[i] > j) ? dp[i-1][j] : max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);
return dp[N][W];
这使用 O(NW) 空间。
方法二 您可能会注意到,在计算特定行的矩阵条目时,我们只查看前一行而不是之前的行。这可以被利用来仅维护 2 行,并不断将它们的角色交换为当前行和上一行。
int dp[2][MAXW];
int solve()
memset(dp[0], 0, sizeof(dp[0]));
for(int i = 1; i <= N; i++)
int *cur = dp[i&1], *prev = dp[!(i&1)];
for(int j = 0; j <= W; j++)
cur[j] = (w[i] > j) ? prev[j] : max(prev[j], prev[j-w[i]] + v[i]);
return dp[N&1][W];
这需要 O(2W) = O(W) 空间。 cur
是第 i 行,prev
是第 (i-1) 行。方法 3
如果你再看一遍,你会发现当我们连续写一个条目时,我们只查看前一行左边的项目。我们可以使用它来使用单行并从右到左处理它,这样当我们为一个条目计算新值时,它左边的条目具有它们的旧值。这是一维表格法。
int dp[MAXW];
int solve()
memset(dp, 0, sizeof(dp));
for(int i =1; i <= N; i++)
for(int j = W; j >= 0; j--)
dp[j] = (w[i] > j) ? dp[j]: max(dp[j], dp[j-w[i]] + v[i]);
return dp[W];
这也使用 O(W) 空间,但只使用单行。必须反转内循环的主要原因是因为当我们使用dp[j-w[i]]
时,我们需要外循环上一次迭代的值。为此,j
值必须以从大到小的方式进行处理。
测试用例(来自http://www.spoj.com/problems/PARTY/)
N = 10, W = 50
w[] = 0, 12, 15, 16, 16, 10, 21, 18, 12, 17, 18 // 1 based indexing
v[] = 0, 3, 8, 9, 6, 2, 9, 4, 4, 8, 9
答案 = 26
【讨论】:
对于方法3,我们只需要将j
递减到w[i]
而不是0
,那么我们将得到for(int j = W; j >= w[i]; --j) dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
"内循环必须反转,因为当我们使用 dp[j-w[i]] 时,我们需要外循环上一次迭代的值。"你能解释一下为什么这是真的吗?
@permian 您可以比较在 method1 和 method3 中如何计算复发。一维数组(方法1)中的dp[j]
应该对应于二维数组(方法3)中的dp[i-1][j]
而不是dp[i][j]
,即我们需要来自i-loop 的最后一次迭代而不是当前迭代的dp[j]
的值。另外,请注意,由于所有w[i]
都是+ve,j-w[i] < j
- 即我们只从我们正在写入的插槽左侧读取,从不从右侧读取。我们可以利用这一点将 2 行减少到 1 行,同时仍然能够通过反转 j-loop 来读取 i-loop 上一次迭代的值。【参考方案3】:
回答你的问题:如果我们对数组使用基于 0 的索引,那么编写递归关系的正确方法是:
dp[i][j] = (w[i-1] > j) ? dp[i-1][j] : max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1]);
由于i
表示第一个i
项目,所以例如如果i
是5,
那么第 5 项将分别位于 weights 和 values 数组中的第 4 位,因此 wt[i-1]
和 v[i-1]
。
【讨论】:
以上是关于0/1 背包动态规划优化,从二维矩阵到一维矩阵的主要内容,如果未能解决你的问题,请参考以下文章
经典动态规划——0/1背包问题(二维数组动规,一维数组动规实现)