完整的硬币组合搜索算法

时间:2017-04-26 18:40:37

标签: python algorithm

问题类似于硬币变化问题,但有点不同。

问题表述为:你有一组硬币,你知道硬币的价值和每种硬币的数量。你想知道你可以从这些硬币的非空分组中得到多少不同的总和。

因此,例如coins = [1, 2, 3]和数量= [1, 2, 2],有11种可能的总和,基本上所有数字都是1 - 11。

阵列硬币的长度最多只能达到20,但数量[x]最多可以达到10 ^ 5.

什么是有效的算法解决方案。收集如此大量的所有可能组合将需要永远。有没有可以确定答案的数学公式?我不知道它会如何起作用,特别是它需要不同的总和。

我在考虑根据硬币及其数量生成阵列。基本上是它的倍数:

[ [1],
  [2, 4],
  [3, 6]]

然后必须从每个阵列中选择1或无。

1
1,2
1,4
1,3
...
1,4,6

我似乎无法想出一个很好的算法来执行它。嵌套循环可能太慢,因为可能有20个不同的硬币,每个硬币可能有很多。

另一种可能的解决方案是循环1到最大值。最大值是所有硬币的总和乘以其相关数量。但问题在于确定是否存在将等于该数字的子集。我知道有一个动态编程算法(子集和)来确定是否存在将加起来某个值的子集,但是数组是什么?

对于这个例子,它工作正常,列表为[1,2,4,3,6],目标总和为11,然后计算' True'在DP中将获得11.但是例如coins = [10,50,100]quantity = [1,2,1]。答案是9可能的总和,但如果使用子集和DP算法将得到21'真'。如果根据[[10],[50,100],[100]提供的清单是[10,50,100,100]或[10,50,100]]

首选python解决方案,但不是必需的。

下面是我目前的代码,[10,50,100]硬币示例得到21。

def possibleSums(coins, quantity):
    def subsetSum(arr,s):
        dp = [False] * (s + 1)  
        dp[0] = True

        for num in sorted(arr):  
            for i in range(1, len(dp)):  
                if num <= i:  
                    dp[i] = dp[i] or dp[i - num]  
        return sum(dp)


    maximum = sum((map(lambda t: t[0] * t[1], zip(coins, quantity))))

    combinations = [[]]*len(coins)
    for i,c in enumerate(coins):
        combinations[i] = [ j for j in range(c,(c*quantity[i])+1,c) ]

    array = []
    for item in combinations:
        array.extend(item)

    print(subsetSum(array,maximum) - 1)

保证约束:

1 ≤ coins.length ≤ 20,
1 ≤ coins[i] ≤ 10^4.

quantity.length = coins.length,
1 ≤ quantity[i] ≤ 10^5.

保证(数量[0] + 1)*(数量[1] + 1)* ... *(数量[数量。长度 - 1] + 1)&lt; = 10 ^ 6。

9 个答案:

答案 0 :(得分:10)

错误修复

您的原始解决方案很好,但您需要以相反的顺序进行迭代,以避免多次添加相同的硬币。

只需将内循环更改为:

    for num in sorted(arr):  
        for i in range(len(dp)-1,-1,-1):  
            if num <= i:  
                dp[i] = dp[i] or dp[i - num]

更有效的解决方案

您还可以通过依次扫描每个可能的剩余部分来利用具有相同值的多个硬币来降低复杂性:

def possibleSums2(coins, quantity):
    maximum = sum((map(lambda t: t[0] * t[1], zip(coins, quantity))))

    dp = [False] * (maximum + 1)
    dp[0] = True
    for coin,q in zip(coins,quantity):
        for b in range(coin):
            num = -1
            for i in range(b,maximum+1,coin):
                if dp[i]:
                    num = 0
                elif num>=0:
                    num += 1
                dp[i] = 0 <= num <= q

    print(sum(dp) - 1)

这将具有复杂度O(最大*硬币)而不是O(最大*硬币*数量)

答案 1 :(得分:9)

不要收集所有组合,只收集总和。

总和的以[0]开头。一次一个地循环通过硬币。对于每个硬币,迭代其数量,将该倍数添加到集合中的每个项目。设置 - 将这些总和中的每一个添加到集合中。例如,让我们采用原始案例:coins = [1,2,3],quant = [1,2,2]。走过这个......

sum_set = {0}
current_coin  = 1;  #  coin[0]
current_quant = 1;  # quant[0]
This step is trivial ... add 1 to each element of the set.  This gives you {1}.
Add that to the existing set.  You now have
sum_set = {0, 1}

下一枚硬币:

current_coin  = 2;  #  coin[0]
current_quant = 2;  # quant[0]
Now, you have two items to add to each set element: 1*2, giving you {2, 3}; and 2*2, giving you {4, 5}.  
Add these to the original set:
sum_set = {0, 1, 2, 3, 4, 5}

最终硬币:

current_coin  = 3;  #  coin[0]
current_quant = 2;  # quant[0]
You add 1*3 and 2*3 to each set element, giving you {3, 4, 5, 6, 7, 8} and {6, 7, 8, 9, 10, 11}.  
Adding these to the sum_set gives you the set of integers 0 through 11.

从集合中删除0(因为我们对该总和不感兴趣)并取出剩余集合的大小。 11是你的答案。

这足以让你把它变成算法吗?我将把各种效率留给你。

答案 2 :(得分:4)

我打算使用生成函数提出解决方案,但之后又添加了

  

保证(数量[0] + 1)*(数量1 + 1)* ... *(数量[quantity.length - 1] + 1)&lt; = 10 ^ 6 < / p>

在那种情况下,只是蛮力吧!浏览每一组可能的硬币,计算总和,并使用一组来查找您获得的多少独特金额。 10 ^ 6种可能性是微不足道的。

对于生成函数解,我们可以通过多项式用值V的硬币数量Q表示可能的总和

1 + x^V + x^(2V) + ... + x^(QV)

其中带指数N的项表示可以实现值N的总和。

如果我们再乘以两个多项式,例如

(1 + x^(V1) + x^(2*V1) + ... + x^(Q1*V1))(1 + x^(V2) + x^(2*V2) + ... + x^(Q2*V2))

产品中带有指数N的项的存在意味着可以通过组合对应于输入多项式的硬币来实现值N的和。

然后效率归结为我们如何乘以多项式。如果我们使用dict s或set s来有效地按指数查找项,我们可以通过组合类似的术语来消除暴力,以消除蛮力的一些冗余工作。我们可以丢弃系数,因为我们不需要它们。基于number-theoretic transform的高级多项式乘法算法在某些情况下可以进一步节省。

答案 3 :(得分:3)

这是一个简洁的蛮力解决方案(Python 3):

def numsums(values, counts):
    from itertools import product
    choices = [range(0, v*c+1, v) for v, c in zip(values, counts)]
    sums = {sum(p) for p in product(*choices)}
    return len(sums) - 1  # sum "0" isn't interesting

然后,例如,

print(numsums([10,50,100], [1, 2, 1])) # 9
print(numsums([1, 2, 3], [1, 2, 2])) # 11
print(numsums([1, 2, 4, 8, 16, 32], [1]*6)) # 63

沿途消除重复

这种变化在功能上等同于其他一些答案;它只是展示如何将其作为蛮力方式的变体:

def numsums(values, counts):
    sums = {0}
    for v, c in zip(values, counts):
        sums |= {i + choice
                 for choice in range(v, v*c+1, v)
                 for i in sums}
    return len(sums) - 1  # sum "0" isn't interesting

事实上,如果你斜视正确;-),你可以将其视为实现@ user2357112的多项式乘法思想的一种方法,其中“乘法”已被重新定义只是以跟踪“这个指数存在与否的术语?“ (当且仅当指数在sums集中时),“是”。然后外环将多项式“乘以”到目前为止与对应于当前(value, count)对的多项式,并且x**0项的乘法隐含在|=并集中。虽然,但是,如果你跳过那个“解释”,它会更容易理解; - )

答案 4 :(得分:1)

这是一个更加优化的

function possibleSums(coins, quantity) {
  // calculate running max sums
  var max = coins.reduce(function(s, c, i) {
    s += c * quantity[i];
    return s;
  }, 0);

  var sums = [0];
  var seen = new Map();

  for (var j = 0; j < coins.length; j++) {
    var coin = coins[j];
    var n = sums.length;
    for (var i = 0; i < n; i++) {
      var s = sums[i];
      for (var k = 0; k < quantity[j]; k++) {
        s += coin;
        if (max < s) break;
        if (!seen.has(s)) {
          seen.set(s, true);
          sums.push(s);
        }
      }
    }
  }
  return Array.from(seen.keys()).length;
}

答案 5 :(得分:0)

HMM。这是非常有趣的问题。 如果你想获得总和值,请使用possibleSums()。 要查看所有案例,请使用possibleCases()。

import itertools


coins = ['10', '50', '100']
quantity = [1, 2, 1]

# coins = ['A', 'B', 'C', 'D']
# quantity = [1, 2, 2, 1]


def possibleSums(coins, quantity):
    totalcnt=1
    for i in quantity:
        totalcnt = totalcnt * (i+1)
    return totalcnt-1    # empty case remove


def possibleCases(coins, quantity):
    coinlist = []
    for i in range(len(coins)):
        cset=[]
        for j in range(quantity[i]+1):
            val = [coins[i]] * j
            cset.append(val)
        coinlist.append(cset)
    print('coinlist=', coinlist)

    # combination the coinlist
    # cases=combcase(coinlist)
    # return cases
    alllist =  list(itertools.product(*coinlist))
    caselist = []
    for x in alllist:
        mergelist = list(itertools.chain(*x))
        if len(mergelist)==0 :  # skip empty select.
            continue
        caselist.append(mergelist)
    return caselist


sum = possibleSums(coins, quantity)
print( 'sum=', sum)

cases = possibleCases(coins, quantity)
cases.sort(key=len, reverse=True)
cases.reverse()

print('count=', len(cases))
for i, x in enumerate(cases):
    print('case',(i+1), x)

输出就是这个

sum= 11
coinlist= [[[], ['10']], [[], ['50'], ['50', '50']], [[], ['100']]]
count= 11
case 1 ['10']
case 2 ['50']
case 3 ['100']
case 4 ['10', '50']
case 5 ['10', '100']
case 6 ['50', '50']
case 7 ['50', '100']
case 8 ['10', '50', '50']
case 9 ['10', '50', '100']
case 10 ['50', '50', '100']
case 11 ['10', '50', '50', '100']

你可以测试其他病例。 coins = ['A','B','C','D'] 数量= [1,3,2,1]

sum= 47
coinlist= [[[], ['A']], [[], ['B'], ['B', 'B'], ['B', 'B', 'B']], [[], ['C'], ['C', 'C']], [[], ['D']]]
count= 47
case 1 ['A']
case 2 ['B']
case 3 ['C']
case 4 ['D']
case 5 ['A', 'B']
case 6 ['A', 'C']
case 7 ['A', 'D']
case 8 ['B', 'B']
case 9 ['B', 'C']
case 10 ['B', 'D']
case 11 ['C', 'C']
case 12 ['C', 'D']
case 13 ['A', 'B', 'B']
case 14 ['A', 'B', 'C']
case 15 ['A', 'B', 'D']
case 16 ['A', 'C', 'C']
case 17 ['A', 'C', 'D']
case 18 ['B', 'B', 'B']
case 19 ['B', 'B', 'C']
case 20 ['B', 'B', 'D']
case 21 ['B', 'C', 'C']
case 22 ['B', 'C', 'D']
case 23 ['C', 'C', 'D']
case 24 ['A', 'B', 'B', 'B']
case 25 ['A', 'B', 'B', 'C']
case 26 ['A', 'B', 'B', 'D']
case 27 ['A', 'B', 'C', 'C']
case 28 ['A', 'B', 'C', 'D']
case 29 ['A', 'C', 'C', 'D']
case 30 ['B', 'B', 'B', 'C']
case 31 ['B', 'B', 'B', 'D']
case 32 ['B', 'B', 'C', 'C']
case 33 ['B', 'B', 'C', 'D']
case 34 ['B', 'C', 'C', 'D']
case 35 ['A', 'B', 'B', 'B', 'C']
case 36 ['A', 'B', 'B', 'B', 'D']
case 37 ['A', 'B', 'B', 'C', 'C']
case 38 ['A', 'B', 'B', 'C', 'D']
case 39 ['A', 'B', 'C', 'C', 'D']
case 40 ['B', 'B', 'B', 'C', 'C']
case 41 ['B', 'B', 'B', 'C', 'D']
case 42 ['B', 'B', 'C', 'C', 'D']
case 43 ['A', 'B', 'B', 'B', 'C', 'C']
case 44 ['A', 'B', 'B', 'B', 'C', 'D']
case 45 ['A', 'B', 'B', 'C', 'C', 'D']
case 46 ['B', 'B', 'B', 'C', 'C', 'D']
case 47 ['A', 'B', 'B', 'B', 'C', 'C', 'D']

答案 6 :(得分:0)

这是Peter de Rives的javascript版本,但效率更高一点,因为它不必为每个硬币进行最大迭代来找到其剩余部分



function possibleSums(coins, quantity) {
    // calculate running max sums
  var prevmax = 0;
  var maxs = [];
  for (var i = 0; i < coins.length; i++) {
    maxs[i] = prevmax + coins[i] * quantity[i];
    prevmax = maxs[i];
  }

  var dp = [true];

  for (var i = 0; i < coins.length; i++) {
    var max = maxs[i];
    var coin = coins[i];
    var qty = quantity[i];
    for (var j = 0; j < coin; j++) {
      var num = -1;
      // only find remainders in range 0 to maxs[i];
      for (var k = j; k <= max; k += coin) {
        if (dp[k]) {
          num = 0;
        } 
        else if (num >= 0) {
          num++;
        }
        dp[k] = 0 <= num && num <= qty;    
      }
    }
  }

  return dp.filter(e => e).length - 1;
}

答案 7 :(得分:0)

简单的python解决方案,使用dp并找到所有总和可能会导致超过时间限制。

def possibleSums(coins, quantity):
    combinations = {0}
    for c,q in zip(coins, quantity):
        combinations = {j+i*c for j in combinations for i in range(q+1)}

    return len(combinations)-1

答案 8 :(得分:0)

def possibleSums(coins, quantity) -> int:
    from itertools import combinations

    flat_list = []
    for coin, q in zip(coins, quantity):
        flat_list += [coin]*q

    uniq_sums = set([])
    for i in range(1, len(flat_list)+1):
        for c in combinations(flat_list, i):
            uniq_sums.add(sum(c))

    return len(uniq_sums)