使用动态编程从 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 上的子集总和问题中获取所有子集的主要内容,如果未能解决你的问题,请参考以下文章