有多少种硬币组合,更优解法

写在前面的自我反省 v2

上周我发布了一篇博文有多少种硬币组合——找出独特子数组之和,是关于有多少种硬币组合的算法题的解法。虽然算法自己可以给出一个正确答案,但是仔细想来,我却没办法给出一个简单直接的解释为何这样跑能够奏效。好的算法应该是可以以一种通俗易懂的方法教给别人的,若是连作出这道算法题的人都没法解释清楚,那即使这个算法可以跑起来,也不能算好吧。javascript

就这个问题,一位高人给出了一个更好的解决方式,通俗易懂。听完以后,我真是不得不服功力之高深呐~java

问题和复制粘贴的分析

若是你有一个硬币数组和一个表明其数量的数组,如何获得一共有多少种不一样组合所得的和?算法

好比:硬币数组[10, 50, 100],和表明其数量的数组[1, 2, 1]就表示这里一共有三种硬币,1枚10分,2枚50分和1枚100分硬币。那么其可能的排列组合则包括数组

  1. 10 = 10
  2. 50 = 50
  3. 100 = 100
  4. 10 + 50 = 60
  5. 10 + 100 = 110
  6. 50 + 50 = 100
  7. 50 + 100 = 150
  8. 10 + 50 + 50 = 110
  9. 10 + 50 + 100 = 160
  10. 50 + 50 + 100 = 200
  11. 10 + 50 + 50 + 100 = 210

则,不重复的值则包括加黑的部分,一共是9个。数据结构

接下来是正经分析

第一步:将全部硬币组成一个数组

首先,上一篇博文里的第一步和第二步是正确的,咱们确实须要将全部硬币组合起来,组成一个新的数组coinList,其中包含了全部的硬币。则这部分的代码仍是继续使用。测试

代码以下:code

/**
 * Count number of 
 * @param {Array<Number>} coins array contains coins with different values
 * @param {Array<Number>} counts array contains corresponding counts of different coins
 * @returns {Number} The count of distinct sums
 */
function countDistinctSum(coins, counts) {
    if(coins.length !== counts.length) {
        return -1;
    }

    const results = {};
    const coinList = [];

    for(let i = 0; i < coins.length; i++){
        for(let j = 0; j < counts[i]; j++) {
            coinList.push(coins[i]);
        }
    }

    // where the beauty of algorithm shows

    return Object.keys(results).length - 1; // Remove the empty 0 coin element
}

第二步:了解全部的组合是如何计算获得的

咱们一步一步来看,全部的组合是如何的出来的。假如,如今咱们只有一个硬币,1分。则可能性只有1种,那就是[1]ip

如今咱们增长一个硬币,2分。则可能性则变成了[1, 2, 1+2],3种。element

若是咱们再加一个硬币,3分呢?则可能性又变成了[1, 2, 3, 1+2, 1+3,2+3,1+2+3],7种。rem

仔细看,当硬币的数量一个一个地增长,可能的组合数量也相应地有规律地再增长。什么规律???好像看不是很清楚。那么咱们换一种方法来表示呢?

若是将硬币表示为A, B, C。

一枚硬币:A

一枚硬币的时候,是:
A

两枚硬币:A、B

两枚硬币的时候呢?那咱们直接在以前的A后面加上一枚新的B

A + B

除此以外,以前的A也应该在里面

A + B

A

再加上新增长的B,则变成了:

A + B

A

B

三枚硬币:A、B、C

这时候加第三枚了,咱们在以前的这三种状况下都加上C,则变成了

A + B + C

A + C

B + C

而以前的那三种组合是还存在的,因此整个数组变成了

A + B + C

A + C

B + C

A + B

A

B

最后加上新增长的C,最终获得了

A + B + C

A + C

B + C

A + B

A

B

C

这下是否更清楚了?每当一个新的硬币被加进数组之中时,组合的数量始终是以前的两倍多一个。好比:

  • 1枚硬币时,组合数量是1
  • 2枚硬币时,组合数量是1 * 2 + 1 = 3
  • 3枚硬币时,组合数量是3 * 2 + 1 = 7
  • 4枚硬币时,组合数量是7 * 2 + 1 = 15

......

以此类推

第三步:只储存非重复的值

在计算过程当中,不免会遇到许多重复的值。好比两枚硬币都是10分的时候,计算出来的组合是[10, 10, 20]。但其实咱们不须要两个10,而只须要[10, 20]就能够了。这个时候咱们须要用到Set这种数据结构来储存数据。由于set里面的元素都是非重复的。

好比,一组硬币[10, 50, 50]。处理第一枚硬币的时候,Set里有[10]

处理第二枚硬币时,对这个Set里的全部元素提取出来而且加上新的硬币值,10 + 50获得了60,并添加获得的和与新添加的这枚硬币值进入进入Set里,获得了[10, 50, 60].

处理第三枚硬币时,仍是继续提取出这个Set里的全部元素,而且加上新的硬币值。因而:

  • 10 + 50 = 60
  • 50 + 50 = 100
  • 60 + 50 = 110

将这三个新的和加入Set,去掉重复值以后获得了[10, 50, 60, 100, 110]。而后再把第三枚硬币自己的值,50,也添加进去。但由于50已经存在了,则Set仍是[10, 50, 60, 100, 110]

一直重复循环到最后,咱们将获得全部非重复的和。问题至此也被解决了~

大功告成

完整代码

/*
 * Count number of
 * @param {Array<Number>} coins array contains coins with different values
 * @param {Array<Number>} counts array contains corresponding counts of different coins
 * @returns {Number} The count of distinct sums
 */
function countDistinctSum(coins, counts) {
    if(coins.length !== counts.length) {
        return -1;
    }

    const coinList = [];
    for(let i = 0; i < coins.length; i++){
        for(let j = 0; j < counts[i]; j++) {
            coinList.push(coins[i]);
        }
    }
    // Create a new set
    const results = new Set();
    for(let i = 0; i < coinList.length; i++){
        // tempSum is used to store the temporary sum of every element in the set and new coin value
        const tempSum = [];
        for (let it = results.values(), val= null; val = it.next().value; ) {
            tempSum.push(val + coinList[i]);
        }

        // add every sum in tempSum to the set and the set will do automatic duplication removal.
        tempSum.forEach(n => {
            results.add(n);
        });

        // add the new coin value into the set
        results.add(coinList[i]);
    }

    return results.size;
}

测试:

const a = [10, 50, 100];
const b = [1, 2, 1];

console.log('Result:', countDistinctSum(a, b)) // Result: 9
相关文章
相关标签/搜索