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

题目

给定不同面额的硬币 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];
    }
}
全部评论

相关推荐

02-25 09:55
已编辑
门头沟学院 Java
2.4&nbsp;一面2.6&nbsp;二面2.9&nbsp;三面(hr面)2.13&nbsp;oc1.15号收到面试电话那会就开始准备,因为一开始没底所以选择推迟一段时间面试,之后开始准备八股,准备实习可能会问的东西,这期间hot100过了有六七遍,真的是做吐了快,八股也是背了忘,忘了背,面经也看了很多,虽然最后用上的只有几道题,可是谁知道会问什么呢自从大二上开始学java以来,一开始做外卖,点评,学微服务,大二下五六月时,开始投简历,哎,投了一千份了无音讯,开始怀疑自己(虽然能力确实很一般),后来去到一家小小厂,但是并不能学到什么东西,而且很多东西都很不规范,没待多久便离开,大二暑假基本上摆烂很怀疑自己,大三上因为某些原因开始继续学,期间也受到一俩个中小厂的offer,不过学校不知道为啥又不允许中小厂实习只允许大厂加上待遇不太好所以也没去,感觉自己后端能力很一般,于是便打算转战测开,学习了一些比较简单的测试理论(没有很深入的学),然后十二月又开始继续投,java和测开都投,不过好像并没有几个面试,有点打击不过并没有放弃心里还是想争一口气,一月初因为学校事比较多加上考试便有几天没有继续投,10号放假后便继续,想着放假应该很多人辞职可能机会大一点,直到接到字节的面试,心里挺激动的,总算有大厂面试了,虽然很开心,但同时压力也很大,心里真的很想很想很想进,一面前几天晚上都睡不好觉,基本上都是二三点睡六七点醒了,一面三十几分钟结束,问的都不太难,而且面试官人挺好但是有些问题问的很刁钻问到了测试的一些思想并不是理论,我不太了解这方面,但是也会给我讲一讲他的理解,但是面完很伤心觉得自己要挂了。但是幸运的是一面过了(感谢面试官),两天后二面,问的同样不算难,手撕也比较简单,但也有一两个没答出来,面试官人很好并没有追问,因为是周五进行的二面,没有立即出结果,等到周一才通知到过了,很煎熬的两天,根本睡不好,好在下周一终于通知二面过了(感谢面试官),然后约第二天三面,听别的字节同学说hr面基本上是谈薪资了,但是我的并不是,hr还问了业务相关的问题,不过问的比较浅,hr还问我好像比较紧张,而且hr明确说了还要比较一下,我说我有几家的面试都拒了就在等字节的面试,三面完后就开始等结果,这几天干啥都没什么劲,等的好煎熬,终于13号下午接到了电话通知oc了,正式邮件也同时发了,接到以后真的不敢信,很激动但更重要的是可以松一口气了,可以安心的休息一下了终于可以带着个好消息过年了,找实习也可以稍微告一段落了,虽然本人很菜,但是感谢字节收留,成为忠诚的节孝子了因为问的比较简单,面经就挑几个记得的写一下一面:1.实习项目的难点说一下2.实习中用到了哪些测试方法3.针对抖音评论设计一下测试用例4.手撕:合并两个有序数组二面:1.为什么转测开2.线程进程区别,什么场景适合用哪个3.发送一个朋友圈,从发出到别人看到,从数据流转的角度说一下会经历哪些过程4.针对抖音刷到广告视频设计测试用例5.手撕:无重复字符的最长字串
厂办龚彪:锲而不舍 金石可镂
查看8道真题和解析
点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务