用于查找列表中哪些数相加等于特定数字的算法

54
我有一列数字,还有一个特定的总和。这个总和由我的列表中的一些数字组成(我可能/可能不知道它是由多少个数字组成的)。有没有快速算法可以得到可能数字的列表?最好用Python编写,但伪代码也可以。(我现在只能读懂Python :P)
例子:
list = [1,2,3,10]
sum = 12
result = [2,10]

注:我了解从大小为 n 的列表中查找相加结果等于另一个数字的算法(但我不会读取 C# 代码,无法确定它是否适合我的需求。我在 Linux 上尝试使用 Mono,但出现错误,而且无法弄清楚如何使用 C# :(
并且我也了解对于所有组合的列表数字求和的算法(但它似乎非常低效。我不需要所有的组合。)


5
在Google上搜索“子集和”可能会得到一些有用的结果。 - Jerry Coffin
顺便提一句,如果你很熟悉Python的话,阅读类似C#之类的语言应该不难,至少可以了解代码正在做什么的大致情况。 - Sasha Chedygov
关于“我不需要所有组合”: 由于这个问题被认为是NP完全问题,最终您可能必须枚举所有可能性。 - phimuemue
@musicfreak:我还处于学习阶段。我尝试用Python重写它,但似乎在处理一组4个数字和1个总和时出了问题;所以我猜我没有正确地编写它。 - avacariu
5个回答

66

这个问题可以转化为0-1背包问题,其中您需要找到一个精确和的集合。解决方案取决于约束条件,在一般情况下,这个问题是NP完全的。

然而,如果最大搜索和(我们称之为S)不太高,则可以使用动态规划来解决该问题。我将使用递归函数和记忆化来解释它,这比自下而上的方法更容易理解。

让我们编写一个函数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)算法。现在,我们可以应用记忆化使其在时间复杂度为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。这是因为它假设第一个十与第二个十不同。可以修复算法以假设两个十相等(从而回答一个),但这有点更加复杂。

1
谢谢!这正是我在寻找的。我从未做过这么高级的东西,所以这太棒了! - avacariu
2
不用谢 =)。如果你喜欢动态规划,这里有一个很好的教程:http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=dynProg。 - jbernadas
我正在尝试将你的代码翻译成Ruby,但目前进展不太顺利。这是我的尝试:https://gist.github.com/webconsult/8710eede3f91d84d7860 有人能帮我找出问题在哪里吗?它报告了undefined method `+' for nil:NilClass(第5行),但调试显示只有在触发第6行的递归调用时才会发生。我有点困惑发生了什么? - zkwsk
我尝试使用一个长度为1M的列表。结果遇到了“超出最大递归深度”的运行时错误。 - Anas
大家好,有人知道如何使用稍作修改的上述代码获取所有加起来等于同一总数的不同解决方案吗?例如:给定 v = [1100, 1105, 11830, 14790, 2325, 2455, 2555, 2935, 3050, 3150, 3185, 3370, 3475, 350, 3530, 3590, 3680, 3745, 885, 9624] 和 sum = 43029,存在多个解决方案,我想获取所有的解决方案,请给予建议。 - ihightower
显示剩余2条评论

12

我知道你在10年前就提出了这个问题,但我真的需要知道如何做到这一点,而jbernadas的方法对我来说太难了,所以我用了一个小时在Google上搜索并找到了一个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)

希望这能帮到你!


谢谢回答。真的帮我省了很多时间 :) - Amresh Giri
对于序列[1, 2]和目标和3,它不起作用。 - fdermishin
1
从大O表示法的角度来看,这是O(n2)。我想知道是否有更高效的解决方案。 - user2596892

4
因此,逻辑是将数字进行逆排序,并假设数字列表为 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 是解的一部分或者不是。 所以,我们假设 i 是解的一部分,然后问题就变成了 l 变为 l[l.index(i+1):]s 变为 s-i。因此,如果我们的函数是 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只剩下一个元素,那么它要么是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毫秒,而且可以处理负数。

因此,我认为有些改进可以进行。


1
虽然这段代码可能运行良好,但一个好的答案应该包括它是如何工作以及为什么它是一个好的解决方案的解释。 - Blackwood

0

我已经找到了一个答案,它的运行时间复杂度为O(n),空间复杂度约为O(2n),其中n是列表的长度。

这个答案满足以下限制:

  1. 列表可以包含重复项,例如[1,1,1,2,3],你想要找到和为2的数对

  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)
  1. 创建一个空字典,遍历列表并将所有可能的键放入字典中,初始值为0。请注意,必须指定键(k-iter1),例如,如果列表包含1但不包含4,并且总和为5。然后当我们查看1时,我们想知道有多少个4,但如果字典中没有4,则会引发错误。
  2. 再次遍历列表,并计算每个整数出现的次数,并将结果存储到字典中。
  3. 遍历字典,这次是为了找到我们有多少对。我们需要考虑3种情况:

    3.1 键恰好是总和的一半,并且此键在列表中出现多次,例如,列表为[1,1,1],总和为2。我们将此特殊条件视为代码所做的操作。

    3.2 键恰好是总和的一半,并且此键在列表中仅出现一次,我们跳过此条件。

    3.3 对于其他情况,键不是总和的一半,只需将其值与另一个键的值相乘,其中这两个键的总和为给定值。例如,如果总和为6,则将temp [1]和temp [5],temp [2]和temp [4]等相乘...(我没有列出数字为负数的情况,但思路是相同的。)

最复杂的步骤是第三步,它涉及到搜索字典,但由于搜索字典通常很快,几乎是恒定复杂度。(尽管最坏情况是O(n),但不应该发生在整数键上。) 因此,假设搜索是恒定复杂度,总复杂度为O(n),因为我们只是单独多次迭代列表。
欢迎提供更好的解决方案 :)

0
#!/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

这很棒。如果没有找到结果,它会默默退出。 - spuder
1
如果你要找的总和是22,该怎么办? - Nello

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接