如何计算硬币问题的可能组合

Posted

技术标签:

【中文标题】如何计算硬币问题的可能组合【英文标题】:How to count possible combination for coin problem 【发布时间】:2011-05-13 17:32:26 【问题描述】:

我正在尝试实现一个硬币问题,问题规范是这样的

创建一个函数来计算可用于给定数量的所有可能的硬币组合。

All possible combinations for given amount=15, coin types=1 6 7 
1) 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
2) 1,1,1,1,1,1,1,1,1,6,
3) 1,1,1,1,1,1,1,1,7,
4) 1,1,1,6,6,
5) 1,1,6,7,
6) 1,7,7,

函数原型:

int findCombinationsCount(int amount, int coins[])

假设硬币数组已排序。对于上面的例子,这个函数应该返回 6。

有人指导我如何实现这个吗??

【问题讨论】:

这是一个很好的解决方案,例如:http://www.geeksforgeeks.org/dynamic-programming-set-7-coin-change/ 【参考方案1】:

使用递归。

int findCombinationsCount(int amount, int coins[]) 
    return findCombinationsCount(amount, coins, 0);


int findCombinationsCount(int amount, int coins[], int checkFromIndex) 
    if (amount == 0)
        return 1;
    else if (amount < 0 || coins.length == checkFromIndex)
        return 0;
    else 
        int withFirstCoin = findCombinationsCount(amount-coins[checkFromIndex], coins, checkFromIndex);
        int withoutFirstCoin = findCombinationsCount(amount, coins, checkFromIndex+1);
        return withFirstCoin + withoutFirstCoin;
    

你应该检查这个实现。我这里没有Java IDE,有点生疏,所以可能会有一些错误。

【讨论】:

不应该是amount-coins[checkFromIndex]吗?【参考方案2】:

虽然递归可以工作并且通常是在一些大学水平的算法和数据结构课程中实施的任务,但我相信“动态编程”实施更有效。

public static int findCombinationsCount(int sum, int vals[]) 
        if (sum < 0) 
            return 0;
        
        if (vals == null || vals.length == 0) 
            return 0;
        

        int dp[] = new int[sum + 1];
        dp[0] = 1;
        for (int i = 0; i < vals.length; ++i) 
            for (int j = vals[i]; j <= sum; ++j) 
                dp[j] += dp[j - vals[i]];
            
        
        return dp[sum];
    

【讨论】:

我认为该方法按应有的方式工作。它告诉你如何用硬币 vals[] 求和的不同可能性。例如:51 from [1,25] 这是一个非常优雅的解决方案。你能添加一点描述这里的直觉是什么吗?我知道您将问题分解为几个子问题:每个面额一个,而总和保持不变(我正在考虑使用总和来分解问题,例如:找到 1 然后 2 然后 3 的组合,依此类推,同时重用以前的总和)跨度> 那么我们如何将其扩展到计算机排列而不是组合?【参考方案3】:

您可以使用生成函数方法来给出使用复数的快速算法。

给定硬币值 c1, c2, .., ck,要得到求和 n 的方式数,你需要的是 x^n in 的系数

(1 + x^c1 + x^(2c1) + x^(3c1) + ...)(1+x^c2 + x^(2c2) + x^(3c2) + ...)....(1+x^ck + x^(2ck) + x^(3ck) + ...)

这和求x^n的系数是一样的

1/(1-x^c1) * 1/(1-x^c2) * ... * (1-x^ck)

现在使用复数,x^a - 1 = (x-w1)(x-w2)...(x-wa) 其中 w1、w2 等是单位的复数根。

所以

1/(1-x^c1) * 1/(1-x^c2) * ... * (1-x^ck)

可以写成

1/(x-a1)(x-a2)....(x-am)

可以用部分分数改写为

A1/(x-a1) + A2/(x-a2) + ... + Am/(x-am)

这里面x^n的系数很容易找到:

A1/(a1)^(n+1) + A2/(a2)^(n+1) + ...+ Am/(am)^(n+1).

计算机程序应该能够轻松找到 Ai 和 ai(可能是复数)。当然,这可能涉及浮点计算。

对于较大的 n,这可能比枚举所有可能的组合要快。

希望对您有所帮助。

【讨论】:

请参阅my answer 了解有关此方法的更实用的方法,以及对未指定算法的建议。 找到这个旧线程。当面额是相对的时候会有一些调整,在这种情况下,部分分数分解会给出Ak/(x-ak)^bk这样的术语,x^n的系数会变得更复杂。【参考方案4】:

递归非常简单:

 def countChange(money: Int, coins: List[Int]): Int = 
    def reduce(money: Int, coins: List[Int], accCounter: Int): Int = 
        if(money == 0) accCounter + 1
        else if(money < 0 || coins.isEmpty) accCounter
        else reduce(money - coins.head, coins, accCounter) + reduce(money, coins.tail, accCounter)
   

   if(money <= 0 || coins.isEmpty) 0
   else reduce(money, coins, 0)

这是 SCALA 中的示例

【讨论】:

提问者需要用 Java 编写的答案【参考方案5】:

Aryabhatta’s answer 为 计算用固定硬币找零的方法的数量 面额非常可爱,但实施起来也不切实际 描述。我们将使用模数,而不是使用复数 算术,类似于数论变换如何替换 整数多项式相乘的傅里叶变换。

D 成为硬币面额的最小公倍数。经过 狄利克雷关于算术级数的定理,无限存在 许多素数p 使得D 除以p - 1。 (运气好的话, 它们甚至会以我们可以找到它们的方式分发 有效。)我们将计算模数p 满足这个条件。通过以某种方式获得粗略的界限(例如, n + k - 1 选择 k - 1 其中n 是总数,k 是数字 面额),重复这个过程与几个不同的 乘积超过该界限的素数,并应用中文 余数定理,我们可以恢复确切的数字。

测试候选人 1 + k*D 的整数 k &gt; 0 直到我们找到一个素数 p。让g 是一个原始根模p(在 随机并应用标准测试)。对于每个面额d,表示 多项式 x**d - 1p 作为因子的乘积:

x**d - 1 = product from i=0 to d-1 of (x - g**((p-1)*i/d)) [modulo p].

注意d 除以D 除以p-1,所以指数确实是 整数。

m 是面额的总和。收集所有常量 g**((p-1)*i/d)a(0), ..., a(m-1)。下一步是找到一个 部分分数分解A(0), ..., A(m-1) 这样

sign / product from j=0 to m-1 of (a(j) - x) =
    sum from j=0 to m-1 of A(j)/(a(j) - x) [modulo p],

其中sign1 如果有偶数个面额并且 -1 如果有奇数个面额。推导出系统 A(j) 的线性方程,通过计算给定的两边 x的不同值的方程,然后用高斯求解 消除。如果有重复,生活就会变得复杂;选择另一个素数可能是最简单的。

给定这个设置,我们可以计算出路数(模p, 当然)进行更改,金额为n as

sum from j=0 to m-1 of A(j) * (1/a(j))**(n+1).

【讨论】:

感谢您提供使用模运算的有趣方法。在阅读它时,我了解了原始根及其在此类方程中的用途。我有一个问题是关于收集原始根g 的力量,即g^(p-1)*i/d,将其称为g_d^i 为所有i &lt; d。我可以理解为什么其中一些可能会重复,为此您将获得1/(g_d^i - x)^r 形式的因子。你也考虑这些吗?我试图弄清楚这些多重性如何影响系数A(j) @Diego 当然会弄得一团糟。最容易选择另一个素数,因为在概率上,这应该不是问题。 我认为这发生在你选择的任何素数上。在求解部分分数之前。 @Diego 你是对的。我不应该在一大早做数学题。 我看到你发布这个已经有一段时间了......我相信我在纸上找到了它并且正在测试计算机。在考虑重复根时,您会得到低于总和的每个幂的项,例如 1/(x - g_k)^r_k,然后求解 A(k,r)/(x-g_k)^r 形式的部分分数,其中 r &lt;= r_k。带有r_k 的系数“更容易”,因为您可以在乘以分母后替换x = g_k,一切都会抵消。其他项通过代入其他不同的值x= x_i 给出线性方程组(模 p)。【参考方案6】:
package algorithms;

import java.util.Random;

/**`enter code here`
 * Owner : Ghodrat Naderi
 * E-Mail: Naderi.ghodrat@gmail.com
 * Date  : 10/12/12
 * Time  : 4:50 PM
 * IDE   : IntelliJ IDEA 11
 */
public class CoinProblem
 
  public static void main(String[] args)
   
    int[] coins = 1, 3, 5, 10, 20, 50, 100, 200, 500;

    int amount = new Random().nextInt(10000);
    int coinsCount = 0;
    System.out.println("amount = " + amount);
    int[] numberOfCoins = findNumberOfCoins(coins, amount);
    for (int i = 0; i < numberOfCoins.length; i++)
     
      if (numberOfCoins[i] > 0)
       
        System.out.println("coins= " + coins[i] + " Count=" + numberOfCoins[i] + "\n");
        coinsCount += numberOfCoins[i];
       

     
    System.out.println("numberOfCoins = " + coinsCount);
   

  private static int[] findNumberOfCoins(int[] coins, int amount)
   
    int c = coins.length;
    int[] numberOfCoins = new int[coins.length];
    while (amount > 0)
     
      c--;
      if (amount >= coins[c])
       
        int quotient = amount / coins[c];
        amount = amount - coins[c] * quotient;
        numberOfCoins[c] = quotient;
       

     
    return numberOfCoins;
   
 

【讨论】:

【参考方案7】:

递归解决方案可能是正确的答案:

int findCombinationsCount(int amount, int coins[])

    // I am assuming amount >= 0, coins.length > 0 and all elements of coins > 0.
    if (coins.length == 1)
    
        return amount % coins[0] == 0 ? 1 : 0;
    
    else
    
        int total = 0;
        int[] subCoins = arrayOfCoinsExceptTheFirstOne(coins);
        for (int i = 0 ; i * coins[0] <= amount ; ++i)
        
            total += findCombinationsCount(amount - i * coins[0], subCoins);
        
        return total;
    

警告:我没有测试甚至编译上述内容。

【讨论】:

【参考方案8】:

提到的递归解决方案会起作用,但如果您添加更多硬币面额和/或显着增加目标值,它们将非常缓慢。

您需要加速它的是实现一个动态编程解决方案。看看knapsack problem。您可以调整此处提到的 DP 解决方案来解决您的问题,方法是计算可以达到总数的方式数量,而不是所需的最小硬币数量。

【讨论】:

正如我所说。在背包解决方案中,每个州都保留用于到达那里的最少硬币数量。对于这个问题,您可以执行类似 dp[current_total] += dp[current_total - current_denomination] 的操作。【参考方案9】:

@Jordi 提供的解决方案不错,但运行速度极慢。您可以尝试在该解决方案中输入 600,看看它有多慢。

我的想法是使用自下而上的动态规划。

请注意,一般来说,money=m 和 coina,b,c 的可能组合等于

m-c 和硬币a,b,c(带硬币 c) m 和硬币a,b 的组合(不含硬币 c)。

如果没有可用的硬币或可用的硬币不能满足所需的金额,则应在块中相应地填写0。如果金额为0,则填写1。

public static void main(String[] args)
    int[] coins = new int[]1,2,3,4,5;
    int money = 600;
    int[][] recorder = new int[money+1][coins.length];
    for(int k=0;k<coins.length;k++)
        recorder[0][k] = 1;
    
    for(int i=1;i<=money;i++)
        //System.out.println("working on money="+i);
        int with = 0;
        int without = 0;

        for(int coin_index=0;coin_index<coins.length;coin_index++)
            //System.out.println("working on coin until "+coins[coin_index]);
            if(i-coins[coin_index]<0)
                with = 0;
            else
                with = recorder[i-coins[coin_index]][coin_index];
            
            //System.out.println("with="+with);
            if(coin_index-1<0)
                without = 0;
            else
                without = recorder[i][coin_index-1];
            
            //System.out.println("without="+without);
            //System.out.println("result="+(without+with));
            recorder[i][coin_index] =  with+without;
        
    
    System.out.print(recorder[money][coins.length-1]);


【讨论】:

【参考方案10】:

此代码基于 JeremyP 提供的解决方案,该解决方案运行良好,我只是通过使用动态编程对其进行了增强以优化性能。我无法对 JeremyP 帖子发表评论,因为我没有足够的声誉 :)

public static long makeChange(int[] coins, int money) 
    Long[][] resultMap = new Long[coins.length][money+1];
    return getChange(coins,money,0,resultMap);


public static long getChange(int[] coins, int money, int index,Long[][] resultMap) 
    if (index == coins.length -1) // if we are at the end      
        return money%coins[index]==0? 1:0;
    else
        //System.out.printf("Checking index %d and money %d ",index,money);
        Long storedResult =resultMap[index][money];
        if(storedResult != null)
            return storedResult;
        long total=0;
        for(int coff=0; coff * coins[index] <=money; coff ++)

             total += getChange(coins, money - coff*coins[index],index +1,resultMap);
        
        resultMap[index][money] = total;
        return total;

    

【讨论】:

【参考方案11】:

第一个想法:

int combinations = 0;
for (int i = 0; i * 7 <=15; i++) 
    for (int j = 0; j * 6 + i * 7 <= 15; j++) 
      combinations++;
    

(在这种情况下,'

【讨论】:

【参考方案12】:

以下是带有记忆化Java解决方案的递归。对于低于 1,我们将 1、2、3、5 作为硬币,将 200 作为目标金额。

countCombinations(200,new int[]5,2,3,1 , 0, 0,new Integer[6][200+5]);

static int countCombinations(Integer targetAmount, int[] V,int currentAmount, int coin, Integer[][] memory)

    //Comment below if block if you want to see the perf difference
    if(memory[coin][currentAmount] != null)
        return memory[coin][currentAmount];
    

    if(currentAmount > targetAmount)
        memory[coin][currentAmount] = 0;
        return 0;
    
    if(currentAmount == targetAmount)
        return 1;
          
    int count = 0;
    for(int selectedCoin : V)
        if(selectedCoin >= coin)                
            count += countCombinations(targetAmount, V, currentAmount+selectedCoin, selectedCoin,memory);
        
            
    memory[coin][currentAmount] = count;        
    return count;

【讨论】:

【参考方案13】:
#include<iostream>
using namespace std;

int solns = 0;

void countComb(int* arr, int low, int high, int Val)

    bool b = false;
    for (size_t i = low; i <= high; i++)
    
        if (Val - arr[i] == 0)
        
            solns++;
            break;
        
        else if (Val - arr[i] > 0)
            countComb(arr, i, high, Val - arr[i]);
        
    


int main()

    int coins[] =  1,2,5 ;
    int value = 7;
    int arrSize = sizeof(coins) / sizeof(int);
    countComb(coins,0, arrSize,value);
    cout << solns << endl;
    return 0;

【讨论】:

【参考方案14】:

再次使用递归测试解决方案,虽然可能不是最优雅的代码。 (请注意,它返回要使用的每个硬币的数量,而不是重复实际硬币数量 n 次)。

public class CoinPerm 

    
    @Test
    public void QuickTest() throws Exception
    
        int ammount = 15;
        int coins[] = 1,6,7;
        
        ArrayList<solution> solutionList = SolvePerms(ammount, coins);
        
        for (solution sol : solutionList)
        
            System.out.println(sol);
        
        
        assertTrue("Wrong number of solutions " + solutionList.size(),solutionList.size()  == 6);
    

    
    
    public ArrayList<solution>  SolvePerms(int ammount, int coins[]) throws Exception
    
        ArrayList<solution> solutionList = new ArrayList<solution>();
        ArrayList<Integer> emptyList = new ArrayList<Integer>();
        solution CurrentSolution = new solution(emptyList);
        GetPerms(ammount, coins, CurrentSolution, solutionList);
        
        return solutionList;
    
    
    
    private void GetPerms(int ammount, int coins[], solution CurrentSolution,   ArrayList<solution> mSolutions) throws Exception
    
        int currentCoin = coins[0];
        
        if (currentCoin <= 0)
        
            throw new Exception("Cant cope with negative or zero ammounts");
        
        
        if (coins.length == 1)
        
            if (ammount % currentCoin == 0)
            
                CurrentSolution.add(ammount/currentCoin);
                mSolutions.add(CurrentSolution);
            
            return;
        
        
        // work out list with one less coin.
        int coinsDepth = coins.length;
        int reducedCoins[] = new int[(coinsDepth -1 )];
        for (int j = 0; j < coinsDepth - 1;j++)
        
            reducedCoins[j] = coins[j+1];
        
        
        
        // integer rounding okay;
        int numberOfPerms = ammount / currentCoin;
        
        for (int j = 0; j <= numberOfPerms; j++)
        
            solution newSolution =  CurrentSolution.clone();
            newSolution.add(j);
            GetPerms(ammount - j * currentCoin,reducedCoins, newSolution, mSolutions ); 
        
    
    
    
    private class solution 
    
        ArrayList<Integer> mNumberOfCoins;

        solution(ArrayList<Integer> anumberOfCoins)
        
            mNumberOfCoins = anumberOfCoins;
        
        
        @Override
        public String toString() 
            if (mNumberOfCoins != null && mNumberOfCoins.size() > 0)
            
                String retval = mNumberOfCoins.get(0).toString();
                for (int i = 1; i< mNumberOfCoins.size();i++)
                
                    retval += ","+mNumberOfCoins.get(i).toString();
                
                return retval;
            
            else
            
                return "";
            
        
        
        @Override
        protected solution clone() 
        
            return new solution((ArrayList<Integer>) mNumberOfCoins.clone());
        

        public void add(int i) 
            mNumberOfCoins.add(i);
        
    


【讨论】:

【参考方案15】:

下面是我创建的递归回溯解决方案,它列出并计算所有可能的面额(硬币)组合,加起来达到给定数量。

面额和金额都可以是动态的

public class CoinComboGenerate   
      public static final int[] DENO = 1,6,7;
      public static final int AMOUNT = 15;
      public static int count = 0;
    
      public static void change(int amount) 
        change(amount, new ArrayList<>(),0);  
      
    
      private static void change(int rem, List<Integer> coins, int pos)     
        if (rem == 0) 
          count++;
          System.out.println(count+")"+coins);
          return;
        
        
        while(pos<DENO.length)            
          if (rem >= DENO[pos]) 
            coins.add(DENO[pos]);
            change(rem - DENO[pos], coins,pos);
            coins.remove(coins.size() - 1);  //backtrack
          
          pos++;
          
      
    
      public static void main(String[] args) 
            change(AMOUNT);
         
    

输出: 1)[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 2)[1, 1, 1, 1, 1, 1, 1, 1, 1, 6] 3)[1, 1, 1, 1, 1, 1, 1, 1, 7] 4)[1, 1, 1, 6, 6] 5)[1,1,6,7] 6)[1,7,7]

【讨论】:

正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center。【参考方案16】:
public static void main(String[] args) 

    int b,c,total = 15;
    int combos =1;
        for(int d=0;d<total/7;d++)
           
             b = total - d * 7;
            for (int n = 0; n <= b /6; n++)
        
                    combos++;

        

        

      System.out.print("TOTAL COMBINATIONS  = "+combos);

【讨论】:

可能想提供一些描述【参考方案17】:

硬币(1,5,10,25,50)的相同问题具有以下解决方案之一。 解应满足以下方程: 1*a + 5*b + 10*c + 25*d + 50*e == cents

public static void countWaysToProduceGivenAmountOfMoney(int cents) 
        
        for(int a = 0;a<=cents;a++)
            for(int b = 0;b<=cents/5;b++)
                for(int c = 0;c<=cents/10;c++)
                    for(int d = 0;d<=cents/25;d++)
                        for(int e = 0;e<=cents/50;e++)
                            if(1*a + 5*b + 10*c + 25*d + 50*e == cents)
                                System.out.println("1 cents :"+a+", 5 cents:"+b+", 10 cents:"+c);
                            
                        
                    
                
            
        
    

这可以针对任何通用解决方案进行修改。

【讨论】:

以上是关于如何计算硬币问题的可能组合的主要内容,如果未能解决你的问题,请参考以下文章

确定硬币组合的算法

统计硬币

硬币概率和统计问题

如何(廉价)计算 n 个可能元素的所有可能的长度-r 组合

A1048 Find Coins (25 分)

如何使用递归在最小零钱问题中输出实际的硬币组合