用于查找列表中的哪个数字总和为特定数字的算法

时间:2010-08-06 04:09:51

标签: python algorithm math pseudocode

我有一个数字列表。我也有一定的金额。总和来自我的列表中的一些数字(我可能/可能不知道它的数量是多少)。是否有快速算法来获取可能的数字列表?用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上,我尝试使用单声道,但我得到错误,我无法弄清楚如何工作C#:(
AND 我知道algorithm to sum up a list of numbers for all combinations(但似乎效率很低。我不需要所有组合。)

5 个答案:

答案 0 :(得分:36)

此问题会缩减到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.这是因为它假设前十个与后十个不同。可以修复算法以假设两个十位相等(从而回答一个),但这有点复杂。

答案 1 :(得分:4)

我知道自您提出这个问题以来,十年后我就给出了答案,但是我真的很想知道如何用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)

希望有帮助!

答案 2 :(得分: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 是和的一部分。 因此,我们假设 i 是解决方案的一部分,然后问题减少为 l l[l.index(i+1):] s si 所以,如果我们的函数是(l,s),那么我们调用a(l[l.index(i+1):] ,s-i)。如果不是 s 的一部分,那么我们必须从l[l.index(i+1):]列表中形成 s 。 因此在两种情况下都是类似的,只有改变是如果我是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..but 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 :(得分:0)

Rx.Observable.prototype.finally(action)

这个python代码按照你的要求执行,它将打印唯一的数字,其总和等于目标变量。

if target number is 8, it will print: 
1 7
2 6
3 5
3 5
5 3
6 2
9 -1
5 3

答案 4 :(得分:0)

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

答案满足以下限制条件:

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

  2. 列表可以包含正整数和负整数

  3. 代码如下,然后是解释:

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

      3.1密钥只是总和的一半,并且该密钥在列表中出现不止一次,例如list是[1,1,1],sum是2.我们将这个特殊条件视为代码所做的。

      3.2密钥只是总和的一半,这个密钥只在列表中出现一次,我们跳过这个条件。

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

    4. 最复杂的步骤是步骤3,其中涉及搜索字典,但是搜索字典通常是快速的,几乎恒定的复杂性。 (虽然最坏的情况是O(n),但不应该发生整数键。)因此,假设搜索是恒定的复杂性,总复杂度是O(n),因为我们只分别多次迭代列表。

      欢迎提供更好的解决方案:)