许多人正确地评论说,多年前使用动态编程的以下答案错误地编码了允许数组元素多次出现在“子集”中的解决方案。幸运的是,基于 DP 的方法仍有希望。
如果输入数组的第一个 i 元素的大小为 k 子集,则让 dp[i][j][k] = true 总和为 j
我们的基本情况是dp[0][0][0] = true
现在,第一个i 元素的大小k 子集使用a[i + 1],或者不使用a[i + 1],从而重复出现
dp[i + 1][j][k] = dp[i][j - a[i + 1]][k - 1] OR dp[i][j][k]
把所有东西放在一起:
given A[1...N]
initialize dp[0...N][0...M][0...K] to false
dp[0][0][0] = true
for i = 0 to N - 1:
for j = 0 to M:
for k = 0 to K:
if dp[i][j][k]:
dp[i + 1][j][k] = true
if j >= A[i] and k >= 1 and dp[i][j - A[i + 1]][k - 1]:
dp[i + 1][j][k] = true
max_sum = 0
for j = 0 to M:
if dp[N][j][K]:
max_sum = j
return max_sum
给出O(NMK)时间和空间复杂度。
退一步,我们在这里隐含地做了一个假设,即A[1...i] 都是非负的。使用负数,初始化第二个维度0...M 是不正确的。考虑一个大小为K 的子集,该子集由一个大小为K - 1 的子集组成,其总和超过M 和另一个足够负的A[] 元素,使得总和不再超过M。类似地,我们的大小K - 1 子集可以求和到某个极负数,然后用足够正的元素A[] 求和到M。为了让我们的算法在这两种情况下仍然有效,我们需要将第二维从M 增加到A[] 中所有正元素的总和与所有负元素的总和(绝对值的总和)之间的差值。 A[] 中所有元素的值)。
至于是否存在非动态规划解,当然有朴素的指数时间蛮力解和优化指数中的常数因子的变体。
除此之外?好吧,您的问题与子集和密切相关,而且大名鼎鼎的 NP 完全问题的文献相当广泛。作为一般原则,算法可以有各种形状和大小——我不能想象说,随机化,近似,(只需选择足够小的误差参数!)对其他 NP 完全问题的简单旧约化(将您的问题转换为巨大的布尔电路并运行 SAT 求解器)。是的,这些是不同的算法。它们比动态编程解决方案更快吗?其中一些,可能。它们是否易于理解或实施,无需说超出标准介绍算法材料的培训?应该不会吧。
这是背包问题或子集问题的变体,在时间方面(以随着输入大小的增长而呈指数增长的空间需求为代价),动态规划是正确解决此问题的最有效方法问题。请参阅Is this variant of the subset sum problem easier to solve? 了解与您类似的问题。
但是,由于您的问题并不完全相同,我还是会提供一个解释。如果有长度为i 的子集等于j,则设dp[i][j] = true,如果没有,则设false。这个想法是dp[][] 将对每个可能长度的所有可能子集的总和进行编码。然后我们可以简单地找到最大的j <= M,使得dp[K][j] 是true。我们的基本情况 dp[0][0] = true 因为我们总是可以通过选择一个大小为 0 的子集来生成总和为 0 的子集。
重复也相当简单。假设我们使用数组的第一个 n 值计算了 dp[][] 的值。要查找数组的第一个 n+1 值的所有可能子集,我们可以简单地将 n+1_th 值添加到我们之前见过的所有子集中。更具体地说,我们有以下代码:
initialize dp[0..K][0..M] to false
dp[0][0] = true
for i = 0 to N:
for s = 0 to K - 1:
for j = M to 0:
if dp[s][j] && A[i] + j < M:
dp[s + 1][j + A[i]] = true
for j = M to 0:
if dp[K][j]:
print j
break