使用动态编程从 Python 上的子集总和问题中获取所有子集

Posted

技术标签:

【中文标题】使用动态编程从 Python 上的子集总和问题中获取所有子集【英文标题】:Getting all subsets from subset sum problem on Python using Dynamic Programming 【发布时间】:2021-12-28 03:54:39 【问题描述】:

我正在尝试从元素列表中提取所有子集,这些元素的总和为某个值。

例子-

列表 = [1,3,4,5,6] 总和 - 9 预期输出 = [[3,6],[5,4]]

尝试了不同的方法并获得了预期的输出,但在大量元素列表中需要花费大量时间。 这可以使用动态编程或任何其他技术进行优化吗?

方法一

def subset(array, num):
    result = []
    def find(arr, num, path=()):
        if not arr:
            return
        if arr[0] == num:
            result.append(path + (arr[0],))
        else:
            find(arr[1:], num - arr[0], path + (arr[0],))
            find(arr[1:], num, path)
    find(array, num)
    return result

numbers = [2, 2, 1, 12, 15, 2, 3]
x = 7
subset(numbers,x)

方法 2

def isSubsetSum(arr, subset, N, subsetSize, subsetSum, index , sum):
    global flag
    if (subsetSum == sum):
        flag = 1
        for i in range(0, subsetSize):
            print(subset[i], end = " ")
        print("")
    else:
        for i in range(index, N):
            subset[subsetSize] = arr[i]
            isSubsetSum(arr, subset, N, subsetSize + 1, 
                        subsetSum + arr[i], i + 1, sum)

【问题讨论】:

在最坏的情况下,当你有 N 零并且所需的总和也为零时,你需要输出所有 2^n - 1 子集 - 所以你不能比 O(2^n) 更好 【参考方案1】:

如果你想输出 所有 个子集,你不能做得比缓慢的 O(2^n) 复杂度更好,因为在最坏的情况下,这将是你的输出大小和时间复杂度是输出大小的下界(这是一个已知的 NP 完全问题)。但是,如果不是返回所有子集的列表,您只想返回一个布尔值,指示是否有可能实现目标总和,或者只是一个子集总和到目标(如果存在),您可以使用动态编程来实现伪-多项式O(nK)时间解,其中n是元素个数,K是目标整数。

DP 方法涉及填写 (n+1) x (K+1) 表,对应表项的子问题为:

DP[i][k] = subset(A[i:], k) for 0 <= i <= n, 0 <= k <= K

也就是说,subset(A[i:], k) 会问,“我可以使用从索引 i 开始的 A 的后缀求和到(小)k 吗?”填满整个表格后,整个问题的答案,subset(A[0:], K) 将在 DP[0][K]

基本情况是 i=n:它们表明如果您使用数组的空后缀,则除了 0 之外,您不能求和

subset(A[n:], k>0) = False, subset(A[n:], k=0) = True

要填表的递归情况有:

subset(A[i:], k) = subset(A[i+1:, k) OR (A[i] <= k AND subset(A[i+i:], k-A[i])) 

这只是将您可以使用当前数组后缀和 k 相加的想法,方法是跳过该后缀的第一个元素并使用您在前一行中已有的答案(当第一个元素不在你的数组后缀),或者在你的总和中使用A[i],并检查你是否可以在前一行中减少总和k-A[i]。当然,您只能在新元素本身不超过您的目标总和的情况下使用它。

例如:子集(A[i:] = [3,4,1,6],k = 8) 会检查:我是否已经用前一个后缀(A[i+1:] = [4,1,6])求和到 8?不,或者,我可以使用我现在可用的 3 来求和 8 吗?也就是说,我可以将 k = 8 - 3 = 5 与 [4,1,6] 相加吗?是的。因为至少有一个条件为真,所以我设置 DP[i][8] = True

因为所有基本情况都是针对 i=n 的,并且子集 (A[i:], k) 的递归关系依赖于较小子问题子集 (A[i+i:],. ..),您从表的底部开始,其中 i = n,为每一行填写从 0 到 K 的每个 k 值,然后一直到第 i = 0 行,确保您有较小的答案子问题,当你需要它们时。

def subsetSum(A: list[int], K: int) -> bool:
  N = len(A)
  DP = [[None] * (K+1) for x in range(N+1)]
  DP[N] = [True if x == 0 else False for x in range(K+1)]

  for i in range(N-1, -1, -1):
    Ai = A[i]
    DP[i] = [DP[i+1][k] or (Ai <=k and DP[i+1][k-Ai]) for k in range(0, K+1)]

  # print result
  print(f"A = A, K = K")
  print('Ai,k:', *range(0,K+1), sep='\t')
  for (i, row) in enumerate(DP): print(A[i] if i < N else None, *row, sep='\t')
  print(f"DP[0][K] = DP[0][K]")
  return DP[0][K]

subsetSum([1,4,3,5,6], 9)

如果你想在 bool 旁边返回一个实际可能的子集,指示是否可以创建一个,那么对于你的 DP 中的每个 True 标志,你还应该存储让你到达那里的前一行的 k 索引(它将是当前的 k 索引或 kA[i],具体取决于哪个表查找返回 True,这将指示是否使用了 A[i])。然后在表格填满后从 DP[0][K] 向后走以获得子集。这使得代码更混乱,但它绝对是可行的。但是,您无法以这种方式获得 所有 个子集(至少不会再次增加您的时间复杂度),因为 DP 表会压缩信息。

【讨论】:

以上是关于使用动态编程从 Python 上的子集总和问题中获取所有子集的主要内容,如果未能解决你的问题,请参考以下文章

打印总和等于 k ​​的集合的子集

为啥我的子集总和方法不正确?

为什么我的子集总和方法不正确?

在python中查找列表的子集的总和

总和小于 M 的大小为 K 的子集的最大总和

698. 划分为k个相等的子集(Python)