从零学算法322

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,所以为 -1
          memo[amount]=min==MAX?-1:min+1;
          return memo[amount];
      }
    
  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值