查找列表中哪个数字总和等于某个数字的算法
Posted
技术标签:
【中文标题】查找列表中哪个数字总和等于某个数字的算法【英文标题】:Algorithm to find which number in a list sum up to a certain number 【发布时间】:2011-03-26 03:46:27 【问题描述】:我有一个数字列表。我也有一定数额。总和是由我列表中的几个数字组成的(我可能/可能不知道它是由多少个数字组成的)。是否有一种快速算法来获取可能的数字列表?用 Python 编写会很棒,但伪代码也很好。 (除了 Python 之外,我还无法阅读任何内容:P)
例子
list = [1,2,3,10]
sum = 12
result = [2,10]
注意:我知道 Algorithm to find which numbers from a list of size n sum to another number(但我无法阅读 C#,我无法检查它是否满足我的需要。我在 Linux 上,我尝试使用 Mono,但是我遇到错误,我不知道如何使用 C# :(AND 我知道 algorithm to sum up a list of numbers for all combinations(但它似乎效率很低。我不需要所有组合.)
【问题讨论】:
谷歌搜索“子集总和”可能会得到一些有用的结果。 附带说明,如果您对 Python 非常了解,那么阅读 C# 等语言应该不会那么难,并且至少可以大致了解代码的作用。 关于 > 我不需要所有的组合:由于这个问题已知是 NP 完全的,最后你可能不得不列举所有的可能性。 @musicfreak:我还处于学习阶段。我试图用 Python 重写它,但它似乎不适用于一组 4 个数字和 1 个总和;所以我假设我没有写对。 【参考方案1】:这个问题归结为0-1 Knapsack Problem,你试图找到一个具有精确总和的集合。解决方案取决于约束条件,一般情况下这个问题是 NP-Complete。
但是,如果最大搜索和(我们称之为S
)不太高,那么您可以使用动态规划来解决问题。我将使用递归函数和memoization来解释它,这比自下而上的方法更容易理解。
让我们编写一个函数f(v, i, S)
,使其返回v[i:]
中的子集数,其和等于S
。要递归求解,首先我们要分析基数(即:v[i:]
为空):
S == 0:[]
的唯一子集总和为 0,因此它是有效子集。因此,该函数应返回 1。
S != 0:由于 []
的唯一子集总和为 0,因此不存在有效子集。因此,该函数应返回 0。
然后,我们来分析递归的情况(即:v[i:]
不为空)。有两种选择:在当前子集中包含数字v[i]
,或者不包含它。如果我们包含v[i]
,那么我们正在寻找总和为S - v[i]
的子集,否则,我们仍在寻找总和为S
的子集。函数f
可以通过以下方式实现:
def f(v, i, S):
if i >= len(v): return 1 if S == 0 else 0
count = f(v, i + 1, S)
count += f(v, i + 1, S - v[i])
return count
v = [1, 2, 3, 10]
sum = 12
print(f(v, 0, sum))
通过查看f(v, 0, S) > 0
,您可以知道您的问题是否有解决方案。但是,这段代码太慢了,每个递归调用都会产生两个新调用,这导致了 O(2^n) 算法。现在,我们可以应用memoization,让它运行时间为O(n*S),如果S
不太大的话会更快:
def f(v, i, S, memo):
if i >= len(v): return 1 if S == 0 else 0
if (i, S) not in memo: # <-- Check if value has not been calculated.
count = f(v, i + 1, S, memo)
count += f(v, i + 1, S - v[i], memo)
memo[(i, S)] = count # <-- Memoize calculated result.
return memo[(i, S)] # <-- Return memoized value.
v = [1, 2, 3, 10]
sum = 12
memo = dict()
print(f(v, 0, sum, memo))
现在,可以编写一个函数g
,它返回一个与S
相加的子集。为此,仅当至少有一个解决方案包含元素时添加元素就足够了:
def f(v, i, S, memo):
# ... same as before ...
def g(v, S, memo):
subset = []
for i, x in enumerate(v):
# Check if there is still a solution if we include v[i]
if f(v, i + 1, S - x, memo) > 0:
subset.append(x)
S -= x
return subset
v = [1, 2, 3, 10]
sum = 12
memo = dict()
if f(v, 0, sum, memo) == 0: print("There are no valid subsets.")
else: print(g(v, sum, memo))
免责声明:此解决方案表示 [10, 10] 的两个子集的总和为 10。这是因为它假设前十个与后十个不同。该算法可以固定为假设两个十位相等(因此回答一个),但这有点复杂。
【讨论】:
谢谢!这正是我一直在寻找的。我从来没有做过这么高级的东西,所以这太棒了! 不客气 =)。如果你喜欢动态编程,topcoder.com/tc?module=Static&d1=tutorials&d2=dynProg 有一个很好的教程。 我正在尝试将您的代码翻译成 ruby,但目前我运气不佳。这是我的尝试:gist.github.com/webconsult/8710eede3f91d84d7860 谁能帮我弄清楚我做错了什么?它报告 nil:NilClass 的未定义方法 `+'(第 5 行),但调试显示它仅在第 6 行的递归调用被触发时才会发生。我有点搞不懂这是怎么回事? 我用 1M 长的列表试过这个。我遇到了maximum recursion depth exceeded
RuntimeError
嗨,有谁知道如何获得所有不同的解决方案,这些解决方案加起来总和相同......使用上面稍微修改过的代码......例如:v = [1100, 1105 , 11830, 14790, 2325, 2455, 2555, 2935, 3050, 3150, 3185, 3370, 3475, 350, 3530, 3590, 3680, 3745, 885, 9624] sum = 43029... 有多种解。我想得到所有的解决方案..请指教。【参考方案2】:
所以,逻辑是对数字进行反向排序,假设数字列表是l,要形成的总和是s。
for i in b:
if(a(round(n-i,2),b[b.index(i)+1:])):
r.append(i)
return True
return False
然后,我们通过这个循环,从 l 中按顺序选择一个数字,假设它是 i 。
有两种可能的情况 i 是否是 sum 的一部分。
因此,我们假设 i 是解决方案的一部分,然后问题简化为 l 是 l[l.index(i+1):]
和 s 是 si 所以,如果我们的函数是 a(l,s) 那么我们调用a(l[l.index(i+1):] ,s-i)
。如果 i 不是 s 的一部分,那么我们必须从 l[l.index(i+1):]
列表中形成 s。
所以在这两种情况下都是相似的,唯一的变化是如果 i 是 s 的一部分,那么 s=s-i 否则只有 s=s。
现在要减少问题,如果 l 中的数字大于 s,我们会删除它们以降低复杂性,直到 l 为空,在这种情况下,选择的数字不是我们解决方案的一部分,我们返回 false .
if(len(b)==0):
return False
while(b[0]>n):
b.remove(b[0])
if(len(b)==0):
return False
如果 l 只剩下 1 个元素,那么它可以是 s 的一部分,那么我们返回 true 或者不是,那么我们返回 false 并且循环将遍历其他数字。
if(b[0]==n):
r.append(b[0])
return True
if(len(b)==1):
return False
注意循环中是否使用了 b..但 b 只是我们的列表。我已经尽可能地四舍五入,这样我们就不应该因为 python 中的浮点计算而得到错误的答案。
r=[]
list_of_numbers=[61.12,13.11,100.12,12.32,200,60.00,145.34,14.22,100.21,14.77,214.35,200.32,65.43,0.49,132.13,143.21,156.34,11.32,12.34,15.67,17.89,21.23,14.21,12,122,134]
list_of_numbers=sorted(list_of_numbers)
list_of_numbers.reverse()
sum_to_be_formed=401.54
def a(n,b):
global r
if(len(b)==0):
return False
while(b[0]>n):
b.remove(b[0])
if(len(b)==0):
return False
if(b[0]==n):
r.append(b[0])
return True
if(len(b)==1):
return False
for i in b:
if(a(round(n-i,2),b[b.index(i)+1:])):
r.append(i)
return True
return False
if(a(sum_to_be_formed,list_of_numbers)):
print(r)
这个解决方案工作得很快。比上面解释的更快。 但是,这仅适用于正数。 但是,如果只有解决方案,它也很有效,否则需要很长时间才能摆脱循环。
一个示例运行就像这样说
l=[1,6,7,8,10]
and s=22 i.e. s=1+6+7+8
so it goes through like this
1.) [10, 8, 7, 6, 1] 22
i.e. 10 is selected to be part of 22..so s=22-10=12 and l=l.remove(10)
2.) [8, 7, 6, 1] 12
i.e. 8 is selected to be part of 12..so s=12-8=4 and l=l.remove(8)
3.) [7, 6, 1] 4
now 7,6 are removed and 1!=4 so it will return false for this execution where 8 is selected.
4.)[6, 1] 5
i.e. 7 is selected to be part of 12..so s=12-7=5 and l=l.remove(7)
now 6 are removed and 1!=5 so it will return false for this execution where 7 is selected.
5.)[1] 6
i.e. 6 is selected to be part of 12..so s=12-6=6 and l=l.remove(6)
now 1!=6 so it will return false for this execution where 6 is selected.
6.)[] 11
i.e. 1 is selected to be part of 12..so s=12-1=1 and l=l.remove(1)
now l is empty so all the cases for which 10 was a part of s are false and so 10 is not a part of s and we now start with 8 and same cases follow.
7.)[7, 6, 1] 14
8.)[6, 1] 7
9.)[1] 1
只是为了比较一下我在我的电脑上运行的不太好。 使用
l=[61.12,13.11,100.12,12.32,200,60.00,145.34,14.22,100.21,14.77,214.35,145.21,123.56,11.90,200.32,65.43,0.49,132.13,143.21,156.34,11.32,12.34,15.67,17.89,21.23,14.21,12,122,134]
和
s=2000
我的循环运行了 1018 次和 31 毫秒。
之前的代码循环运行了 3415587 次,耗时近 16 秒。
但是,如果解决方案不存在,我的代码运行了几分钟,所以我停止了它,之前的代码只运行了大约 17 毫秒,之前的代码也适用于负数。
所以我觉得可以做一些改进。
【讨论】:
虽然此代码可能运行良好,但一个好的答案应该包括解释它是如何工作的以及为什么它是一个好的解决方案。【参考方案3】:#!/usr/bin/python2
ylist = [1, 2, 3, 4, 5, 6, 7, 9, 2, 5, 3, -1]
print ylist
target = int(raw_input("enter the target number"))
for i in xrange(len(ylist)):
sno = target-ylist[i]
for j in xrange(i+1, len(ylist)):
if ylist[j] == sno:
print ylist[i], ylist[j]
这个python代码按照你的要求做,它会打印出唯一的一对数字,其和等于目标变量。
如果目标数是 8,它将打印: 1 7 2 6 3 5 3 5 5 3 6 2 9 -1 5 3【讨论】:
这很棒。如果没有找到结果,它会静默退出。 如果您要查找的总和是 22 怎么办?【参考方案4】:我找到了一个运行时间复杂度为 O(n) 且空间复杂度约为 O(2n) 的答案,其中 n 是列表的长度。
答案满足以下约束:
列表可以包含重复项,例如[1,1,1,2,3] 并且您想找到对总和为 2
列表可以同时包含正整数和负整数
代码如下,后面是解释:
def countPairs(k, a):
# List a, sum is k
temp = dict()
count = 0
for iter1 in a:
temp[iter1] = 0
temp[k-iter1] = 0
for iter2 in a:
temp[iter2] += 1
for iter3 in list(temp.keys()):
if iter3 == k / 2 and temp[iter3] > 1:
count += temp[iter3] * (temp[k-iter3] - 1) / 2
elif iter3 == k / 2 and temp[iter3] <= 1:
continue
else:
count += temp[iter3] * temp[k-iter3] / 2
return int(count)
-
创建一个空字典,遍历列表并将所有可能的键放入初始值为 0 的字典中。
请注意,需要指定密钥 (k-iter1),例如如果列表包含 1 但不包含 4,并且总和为 5。那么当我们查看 1 时,我们想知道我们有多少个 4,但如果 4 不在 dict 中,则会引发错误.
再次遍历列表,计算每个整数出现的次数并将结果存储到字典中。
遍历dict,这一次是找出我们有多少对。我们需要考虑三个条件:
3.1 键只是总和的一半,并且该键在列表中出现多次,例如list 为 [1,1,1],sum 为 2。我们将这个特殊条件视为代码所做的事情。
3.2 key 只是总和的一半,并且这个 key 在列表中只出现一次,我们跳过这个条件。
3.3 对于其他情况,键不是总和的一半,只需将其值与另一个键的值相乘,这两个键的总和为给定值。例如。如果 sum 为 6,我们将 temp[1] 和 temp[5]、temp[2] 和 temp[4] 相乘,等等……(我没有列出数字为负的情况,但想法是一样的。)
最复杂的步骤是第 3 步,其中涉及搜索字典,但搜索字典通常很快,复杂度几乎不变。 (虽然最坏的情况是 O(n),但对于整数键不应该发生。)因此,假设搜索是恒定复杂度,总复杂度是 O(n),因为我们只分别迭代列表多次。
欢迎提出更好的解决方案的建议 :)
【讨论】:
【参考方案5】:我知道自从你问这个问题 10 年后我才给出答案,但我真的需要知道如何做到这一点,而 jbernadas 的方式对我来说太难了,所以我用谷歌搜索了一个小时,然后我找到了一个可以完成工作的 python 库itertools
!
我希望这对未来的新手程序员有所帮助。
您只需要导入库并使用.combinations()
方法,就是这么简单,它会按顺序返回集合中的所有子集,我的意思是:
对于集合 [1, 2, 3, 4]
和长度为 3 的子集,它不会返回 [1, 2, 3][1, 3, 2][2, 3, 1]
它只会返回 [1, 2, 3]
如果你想要一个集合的所有子集,你可以迭代它:
import itertools
sequence = [1, 2, 3, 4]
for i in range(len(sequence)):
for j in itertools.combinations(sequence, i):
print(j)
输出将是
() (1,) (2,) (3,) (4,) (1, 2) (1, 3) (1, 4) (2, 3) (2, 4) (3, 4) (1, 2, 3) (1, 2, 4) (1, 3, 4) (2, 3, 4)
希望对您有所帮助!
【讨论】:
感谢您的回答。从字面上节省了我很多时间:) 它不适用于序列 [1, 2] 和目标总和 3 从 big(O) 的角度来看,这是 o(n2)。我想知道是否有有效的解决方案以上是关于查找列表中哪个数字总和等于某个数字的算法的主要内容,如果未能解决你的问题,请参考以下文章