【问题标题】:Complete search algorithm for combinations of coins完整的硬币组合搜索算法
【发布时间】:2021-05-26 10:00:03
【问题描述】:

这个问题类似于硬币找零问题,但有一点不同。

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

例如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,然后计算 DP 中的“真”将得到 11。但例如 coins = [10,50,100] 和 @ 987654326@。答案是 9 个可能的总和,但如果使用子集总和 DP 算法将得到 21“真”。如果提供的列表是 [10,50,100,100] 或 [10,50,100] 基于 [[10], [50, 100], [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.

保证 (quantity[0] + 1) * (quantity[1] + 1) * ... * (quantity[quantity.length - 1] + 1)

【问题讨论】:

  • 这感觉可能是 NP 难的。我怀疑有一种方法可以将分区问题的某些变体减少为这个问题。
  • 币值有限制吗?
  • 我用限制/约束更新了问题。我忘了提到这是一个由大约 100 人解决了它的代码战造成的问题。所以肯定有一个解决方案可以在 4 秒(python)或 0.5(c++)内解决

标签: python algorithm


【解决方案1】:

错误修复

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

只需将内部循环更改为:

    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(最大 * 硬币 * 数量)

【讨论】:

  • 感谢您修复错误。这个算法是最快的。硬币 = [10,50,100],数量 = [100,2000,1000]。这些是我从这里得到的 3 个解决方案的执行时间:修剪:1.3836107661727957(根据他的解释授予我的代码库可能会被优化)。蒂姆·彼得斯:43.38692857611791。彼得·德·里瓦兹:0.08758417620116177
  • 好主意。通过跟踪我们需要多少硬币才能达到给定的总和并分别处理每个剩余部分,我们避免了对同一硬币类型的每个副本进行重复传递的需要。
  • @user1179317:您的测试数据违反了问题中规定的约束;该测试中有超过 10^8 种可能的硬币组合,超过了 10^6 的限制。
  • 什么跑得最快很大程度上取决于最大和与可能的硬币组合数量的比较,以及(对于一些可能的解决方案,包括多项式乘法思想的某些版本)有多少重复的和。此解决方案适用于低于可能的硬币组合数量的最大总和;如果最大值高于可能的硬币组合数量,其他解决方案可能会表现更好。
  • @ScottSkiles dp[i] 为真当且仅当值 i 可以从硬币中获得
【解决方案2】:

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

你的 set 和以 [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 是你的答案。

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

【讨论】:

  • 这似乎是一个简单而聪明的解决方案。让我试试看
  • 太棒了!当您达成解决方案时,请记住对有用的事情进行投票并接受您最喜欢的答案(即使您必须自己编写),以便 Stack Overflow 可以正确存档问题。
  • 必须选择 Tim Peters 的答案,这是最快的。不过,您的答案仍然很简单:P
  • @user1179317:我想你可能把你的 Peters 搞混了。
  • 哦,我的错,我做到了。我是说彼得·德·里瓦兹
【解决方案3】:

我打算使用生成函数提出一个解决方案,但后来你添加了

保证 (quantity[0] + 1) * (quantity1 + 1) * ... * (quantity[quantity.length - 1] + 1)

在那种情况下,就用蛮力吧!遍历所有可能的硬币组,计算总和,并使用一组来找出你得到多少个独特的总和。 10^6 种可能性是微不足道的。


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

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 的总和。

效率然后归结为我们如何乘以多项式。如果我们使用dicts 或sets 来有效地按指数查找项,我们可以通过组合相似项来消除蛮力所做的一些冗余工作,从而战胜蛮力。我们可以丢弃系数,因为我们不需要它们。在某些情况下,基于number-theoretic transform 的高级多项式乘法算法可能会进一步节省成本。

【讨论】:

  • 是的,当我从网站复制它时,我也注意到了这一点。它比我想象的要小很多。无论如何,您应该提出您正在考虑的解决方案,我仍然感兴趣:P
  • @user1179317:我已经添加了一些关于生成函数的详细说明。
  • 感谢您的解释。这对我来说似乎太复杂了嘿嘿。不过谢谢
【解决方案4】:

这是一个简洁的蛮力解决方案(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 项的乘法隐含在|= 联合中。虽然,是的,如果你跳过那个“解释”会更容易理解;-)

【讨论】:

  • 谢谢,这个解决方案有效,但我认为它会超过时间限制。 @prune 提到的解决方案要快得多
  • 正如@user2357112 所指出的,产品中的元素不超过一百万 - 按照现代标准,这太小了;-)
  • 是的。我测试了我得到的 3 个解决方案,并将发布执行时间以供将来任何人参考。
【解决方案5】:

这是一个更加优化的

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;
}

【讨论】:

    【解决方案6】:

    简单的python解决方案

    Note:using dynamic programming and finding all sums may result in time limit exceed.

    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
    

    【讨论】:

      【解决方案7】:

      嗯。这是一个非常有趣的问题。 如果您只想获得总和值,请使用 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']
      

      您可以测试其他情况。 硬币 = ['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']
      

      【讨论】:

        【解决方案8】:

        这是 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;
        }
        
        

        【讨论】:

          【解决方案9】:
          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)
          

          【讨论】:

            猜你喜欢
            • 2011-08-19
            • 1970-01-01
            • 2016-01-25
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-03-12
            • 1970-01-01
            相关资源
            最近更新 更多