组成指定金额的最少硬币数量问题探讨

题目

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/gaM7Ch 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

硬币使用次数无限

其实这就是一个简单的背包问题,等同于塞满一个指定的背包所需要的最少物件。 例如要凑成总量为amount的金币。你可以先假想为放置的金币是有先后顺序的,那么你所放置的最后一枚金币,肯定存在于coins数组。因此,若能知晓填满amount-coins[i]所需的金币数量,你就可以递推出填满amount所需的金币数量。如果要取最少的数量,直接利用Math.max(dp[j],dp[j-coins[i]]+1)比较得出即可,又知题目要求金币可以无限用。

显然,在做动态规划的方程转移时。需要把金币循环放置在第二层。这样,才有反复使用的效果。

代码如下

class Solution {
    public int coinChange(int[] coins, int amount) {
        //金币的数量不可能超过amount+1
        int max = amount + 1;
        int[] dp = new int[amount + 1];
        //因为涉及最小值比较,初始值必须设置为amount以上的数值,本例选取了amount+1
        Arrays.fill(dp, max);
        //凑满0不需要金币,因此是0
        dp[0] = 0;
        for (int i = 1; i <= amount; i++) {
            for (int j = 0; j < coins.length; j++) {
                if (coins[j] <= i) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

硬币只可以使用1次

如果硬币每次只能使用1次,那么,再将金币的循环放置在第二层显然是不可取的。因此金币的循环需要在外层。保证每种金币只使用一次。

此外,dp数组的初始值需要大于coins.length或者amount

最关键的问题就在于第二层循环,必须是倒序,因为顺序的话,根据转移方程,金币还是会被多次使用, 而采用倒序的方式,即可避免此问题

class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = coins.length + 1;
        int[] dp = new int[amount+1];
        //因为涉及最小值比较,初始值必须设置为coins.length或amount以上的数值,本例选取了coins.length+1
        Arrays.fill(dp, max);
        //凑满0不需要金币,因此是0
        dp[0] = 0;
        for (int i = 0; i < coins.length; ++i) {
            for (int j = amount; j >= coins[i] ; --j) {
                    dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
            }
        }
        return dp[amount] > coins.length ? -1 : dp[amount];
    }
}

硬币使用的次数有限

若题目除了给出coins外,再给出一个count数组,表示每种金币的数量,那么这种情况下如何求出组成amount金额所需的最少金币数量呢。

与此前的一次和无限次不同,此种情况给定了每种金币所使用次数的限制条件,因此,在做状态方程转移时,我们必须要知道当前金币所使用的数量,假设有状态转移方程dp[j]=dp[j-coins[i]]+1;表示我们现在使用了一枚面额为coins[i]的金币,可是,我们如何知道dp[j-coins[i]]里已经用了多少枚面额为coins[i]的金币呢?额外使用一个dp2数组维护是否可以?其实试试就不难发现,金币额的使用是无法简单用一个一维数组维护的,

请看如下代码。

public static int coinChange(int[] coins, int[] count, int amount) {
        int max = coins.length + 1;
        int[] dp = new int[amount+1];
        int[] dp2=new int[amount+1];
        Arrays.fill(dp, max);
        dp[0] = 0;
        for (int i = 0; i < coins.length; ++i) {
            Arrays.fill(dp2,0);
            for (int j = coins[i]; j <= amount ; ++j) {
                if(dp[j-coins[i]]+1<dp[j]){
                    if(dp2[j-coins[i]]+1<=count[i]){
                        dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
                        dp2[j]=dp2[j-coins[i]]+1;
                    }
                }

            }
        }
        return dp[amount] > coins.length ? -1 : dp[amount];
    }

假设dp2表示当前循环所使用的金币的使用情况,例如coins={1,2,3},count={10,10,2},aount=9;在循环到coins[2]时,此时循环dp[6]肯定等于2,而dp2[6]=2,表示金额为三的金币已经使用两次,在循环到dp[9]时,我们发现就不能再添加金额为3的金币了,因为其使用量已经达到上限,因而dp[9]只能使用循环coins[1]时的数值,其值为5,可是9可以拆分为3,3,2,1,只需要4枚。因而出错,按正确答案回推,dp[6]应该等于3,dp2[6]应该等于1,即1,2,3。显然,1,2,3在coins[2]循环中会被3,3替代。此方法不可行。

如果我们将dp表示成一个二维数组,行为金币的种类,列为amount。

第一行表示,如果现在没有金币,组成amount所需的金币最少数量,第二行表示,如果只有一种金币,组成amount所需的金币的最少数量.....以此递推,求出dp[coins.length][amount]。即为正确答案。

直接上代码。

class Solution {
    public  int coinChange(int[] coins, int[] count, int amount) {
        //因为涉及最小值比较,初始值必须设置为amount以上的数值,本例选取了amount+1
        int max = amount + 1;
        int[][] dp = new int[coins.length+1][amount+1];
        //初始化数组
        for(int i = 0; i < coins.length+1; i++){
            Arrays.fill(dp[i],max);
            dp[i][0] = 0;
        }
        for (int i = 1; i <= coins.length; ++i) {
            for (int j = 1; j <= amount ; ++j) {
                if(j < coins[i-1]){
                    dp[i][j] = dp[i-1][j];
                }else{
                    int top = Math.min(count[i-1],j/coins[i-1]);
                    int min = amount +1;
                    for( int k = 0; k <= top; ++k){
                        min = Math.min(min,dp[i-1][j-k*coins[i-1]]+k);
                    }
                    dp[i][j] = min;
                }
            }
        }
        return dp[coins.length][amount] > amount ? -1 : dp[coins.length][amount];
    }
}
全部评论

相关推荐

大家收到面试预通知了吗?
投递中国邮政储蓄银行等公司8个岗位 >
点赞 评论 收藏
转发
头像
04-09 14:29
Java
点赞 评论 收藏
转发
1 1 评论
分享
牛客网
牛客企业服务