322.给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
- 动态规划:假设 dp[i] 为 amount 为 i 时的最少硬币数。那么我们首先可以知道的是 dp[0] = 0,并且 dp[coin] = 1,比如硬币面额 coin 为 2,那么 dp[2] 一定为 1,一个 2 元硬币就能使得总和为 2。那么 dp[i] 呢,最好的结果肯定是 dp[i-coin] + 1,比如你有 [1,2,5] 三种硬币, 当你要凑到 7 元时你会想,如果我们得到了凑成 6 元的最小硬币数,再加一个 1 元硬币就得到了凑成 7 元的一种可能,即 dp[7] = dp[7-1] + 1,或者我们之前已经得到了凑成 5 元的最小硬币数,再加一个 2 元硬币也能凑成 7 元,即 dp[7] = dp[7-2] + 1,我们取 min 即可
-
public int coinChange(int[] coins, int amount) {int[] dp = new int[amount+1];Arrays.fill(dp, -1);dp[0] = 0;for(int c:coins){// 硬币面值首先肯定得小于等于我所需的总数if(c<=amount)dp[c]=1;}for(int i=1;i<=amount;i++){// 得到过了就跳过if(dp[i]!=-1)continue;int min = Integer.MAX_VALUE;for(int c:coins){// 我们需要得到总共 i 元,硬币面额肯定首先得小于 i,等于的我们已经初始化过了// 并且我们在计算比如 dp[7] = dp[7-2] + 1 时,我们肯定首先得保证 dp[7-2] 存在// 不然怎么往后推if(c < i && dp[i-c]!=-1)min=Math.min(min,dp[i-c]);}if(min != Integer.MAX_VALUE)dp[i]=min+1;}return dp[amount];}
- 这里附上别人更简洁的写法,无法凑成的情况就用 int 最大值表示
-
public int coinChange(int[] coins, int amount) {if(coins.length == 0){return -1;}//dp[i]表示金额i最少需要的硬币数int[] dp = new int[amount+1];dp[0] = 0;for(int i=1;i<=amount;i++){int min = Integer.MAX_VALUE;for (final int coin : coins) {// 这里 dp[i - coin] < min 不仅能得出更少硬币数// 而且因为无法凑成的 dp[x] 为 int 最大值,所以 dp[x] 如果凑不成就不可能 < min// 所以它同时也排除了无法凑成的情况// 当然你也可以改写成 // if (i - coin >= 0) min = Math.min(min,dp[i - coin]);// 然后 min 不为 int 最大值才 + 1,否则上面就加 1 就超出 int 最大值了// dp[i] = min!=Integer.MAX_VALUE?min+1:Integer.MAX_VALUE;if (i - coin >= 0 && dp[i - coin] < min) {min = dp[i - coin] + 1;}}dp[i] = min;}return dp[amount]== Integer.MAX_VALUE ? -1 : dp[amount];}
- dfs(超时):我们很容易想到的就是像 n 叉树一样尝试每种组合,比如 [1,2] 我们从这两个硬币开始分叉
(1|2) -> (11,12 | 21,22) -> (111,112 | 121,122 | 211,212 | 221,222)......
-
int MAX = Integer.MAX_VALUE;int[] coins;int ans=MAX;public int coinChange(int[] coins, int amount) {this.coins = coins;dfs(amount,0);return ans==MAX?-1:ans;}// amount:所需总数// count:硬币数量public void dfs(int amount,int count){// 减到总数小于 0 肯定不行if(amount < 0)return;// 减到总数为 0 说明凑出一种可能了,记录下来if(amount==0){ans=Math.min(ans,count); }for(int coin:coins){dfs(amount-coin, count+1);}}
- 上面代码会超时,是因为其实组成比如12 和 21 时得到的总数是相同的都是总共 3 元,所以会包含大量重复的计算,那么我们很容易想到的就是用记忆化搜索改进,其实这和 dp 的原理很像
-
int MAX = Integer.MAX_VALUE;int[] coins;int[] memo;public int coinChange(int[] coins, int amount) {memo = new int[amount+1];this.coins = coins;return dfs(amount);}public int dfs(int amount){if(amount == 0)return 0;if(amount < 0)return -1;// 有了这个我们才减少了很多重复计算if(memo[amount] != 0)return memo[amount];int min = MAX;for(int coin:coins){int res = dfs(amount-coin);// res == -1 可以理解为 dp[amount-coin] 不存在,所以你会发现记忆化搜索都能改写成 dp// 所以 dp[amount-coin] 存在才能推出在此基础上加一个 coin 凑成 amount,推出 dp[amount]if(res!=-1)min = Math.min(min,res);}// 每个 coin 对应的 dp[amount-coin] 都不存在时 min 就保持不变还是 int 最大值// 这就表示我们无法用这些硬币得到总数 amount,所以为 -1memo[amount]=min==MAX?-1:min+1;return memo[amount];}