zoukankan      html  css  js  c++  java
  • LeetCode——322.零钱兑换难度

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

    示例 1:
    
    输入: coins = [1, 2, 5], amount = 11
    输出: 3 
    解释: 11 = 5 + 5 + 1
    
    示例 2:
    
    输入: coins = [2], amount = 3
    输出: -1
    

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

    https://leetcode-cn.com/problems/coin-change

    动态规划

    对于求极值问题,主要考虑动态规划 Dynamic Programming 来做,好处是保留了一些中间状态的计算值,可以避免大量的重复计算。

    我们维护一个一维动态数组 dp,其中 dp[i] 表示钱数为i时的最小硬币数的找零,注意由于数组是从0开始的,所以要多申请一位,数组大小为 amount+1,这样最终结果就可以保存在 dp[amount] 中了。

    初始化 dp[0] = 0,因为目标值若为0时,就不需要硬币了。其他值可以初始化是 amount+1,为啥呢?

    因为最小的硬币是1,所以 amount 最多需要 amount 个硬币,amount+1 也就相当于当前的最大值了,注意这里不能用整型最大值来初始化,因为在后面的状态转移方程有加1的操作,有可能会溢出,除非你先减个1。

    更新 dp[i] 的方法就是遍历每个硬币,如果遍历到的硬币值小于i值(比如不能用值为5的硬币去更新 dp[3])时,用 dp[i - coins[j]] + 1 来更新 dp[i],所以状态转移方程为:

    dp[i] = min(dp[i], dp[i - coins[j]] + 1);
    

    其中 coins[j] 为第j个硬币,而 i - coins[j] 为钱数i减去其中一个硬币的值,剩余的钱数在 dp 数组中找到值,然后加1和当前 dp 数组中的值做比较,取较小的那个更新 dp 数组。先来看迭代的写法如下所示:

    c++

    class Solution {
    public:
        int coinChange(vector<int>& coins, int amount) {
            vector<int> dp(amount + 1, amount + 1);
            dp[0] = 0;
            for (int i = 1; i <= amount; ++i) {
                for (int j = 0; j < coins.size(); ++j) {
                    if (coins[j] <= i) {
                        dp[i] = min(dp[i], dp[i - coins[j]] + 1);
                    }
                }
            }
            return (dp[amount] > amount) ? -1 : dp[amount];
        }
    }; 
    

    java

    public class Solution {
      public int coinChange(int[] coins, int amount) {
        int max = amount + 1;
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, max);
        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];
      }
    }
    

    python

    class Solution:
        def coinChange(self, coins: List[int], amount: int) -> int:
            dp = [float('inf')] * (amount + 1)
            dp[0] = 0
            
            for coin in coins:
                for x in range(coin, amount + 1):
                    dp[x] = min(dp[x], dp[x - coin] + 1)
            return dp[amount] if dp[amount] != float('inf') else -1
    

    递归加记忆数组

    c++

    class Solution {
    public:
        int coinChange(vector<int>& coins, int amount) {
            vector<int> memo(amount + 1, INT_MAX);
            memo[0] = 0;
            return coinChangeDFS(coins, amount, memo);
        }
        int coinChangeDFS(vector<int>& coins, int target, vector<int>& memo) {
            if (target < 0) return - 1;
            if (memo[target] != INT_MAX) return memo[target];
            for (int i = 0; i < coins.size(); ++i) {
                int tmp = coinChangeDFS(coins, target - coins[i], memo);
                if (tmp >= 0) memo[target] = min(memo[target], tmp + 1);
            }
            return memo[target] = (memo[target] == INT_MAX) ? -1 : memo[target];
        }
    }; 
    

    java

    class Solution {
        int[] memo;
        public int coinChange(int[] coins, int amount) {
            if(coins.length == 0){
                return -1;
            }
            memo = new int[amount];
    
            return findWay(coins,amount);
        }
        // memo[n] 表示钱币n可以被换取的最少的硬币数,不能换取就为-1
        // findWay函数的目的是为了找到 amount数量的零钱可以兑换的最少硬币数量,返回其值int
        public int findWay(int[] coins,int amount){
            if(amount < 0){
                return -1;
            }
            if(amount == 0){
                return 0;
            }
            // 记忆化的处理,memo[n]用赋予了值,就不用继续下面的循环
            // 直接的返回memo[n] 的最优值
            if(memo[amount-1] != 0){
                return memo[amount-1];
            }
            int min = Integer.MAX_VALUE;
            for(int i = 0;i < coins.length;i++){
                int res = findWay(coins,amount-coins[i]);
                if(res >= 0 && res < min){
                    min = res + 1; // 加1,是为了加上得到res结果的那个步骤中,兑换的一个硬币
                }
            }
            memo[amount-1] = (min == Integer.MAX_VALUE ? -1 : min);
            return memo[amount-1];
        }
    }
    

    python

    def coinChange(coins: List[int], amount: int):
        # 备忘录
        memo = dict()
        def coinChangeDFS(n):
            # 查备忘录,避免重复计算
            if n in memo: return memo[n]
    
            if n == 0: return 0
            if n < 0: return -1
            res = float('INF')
            for coin in coins:
                subproblem = coinChangeDFS(n - coin)
                if subproblem == -1: continue
                res = min(res, 1 + subproblem)
            
            # 记入备忘录
            memo[n] = res if res != float('INF') else -1
            return memo[n]
        
        return coinChangeDFS(amount)
    

    哈希

    再来看一种使用 HashMap 来当记忆数组的递归解法:

    c++

    class Solution {
    public:
        int coinChange(vector<int>& coins, int amount) {
            unordered_map<int, int> memo;
            memo[0] = 0;
            return coinChangeDFS(coins, amount, memo);
        }
        int coinChangeDFS(vector<int>& coins, int target, unordered_map<int, int>& memo) {
            if (target < 0) return - 1;
            if (memo.count(target)) return memo[target];
            int cur = INT_MAX;
            for (int i = 0; i < coins.size(); ++i) {
                int tmp = coinChangeDFS(coins, target - coins[i], memo);
                if (tmp >= 0) cur = min(cur, tmp + 1);
            }
            return memo[target] = (cur == INT_MAX) ? -1 : cur;
        }
    }; 
    

    递归改进

    首先是判断 start 是否小于0,因为需要从 coin 中取硬币,不能越界。

    下面就是优化的核心了,看 target 是否能整除 coins[start],比如假如目标值是 15,如果当前取出了大小为5的硬币,这里做除法,可以立马知道只用大小为5的硬币就可以组成目标值 target,那么用 cur + target/coins[start] 来更新结果 res。

    遍历 target/coins[start] 的次数,由于不能整除,只需要对余数调用递归函数,而且要把次数每次减1,并且再次求余数。

    举个例子,比如 coins=[1,2,3],amount=11,那么 11 除以3,得3余2,那么i从3开始遍历。

    那就是判断若 cur + i >= res - 1 成立,直接 break,不调用递归。

    这里解释一下,cur + i 自不必说,是当前硬币个数 cur 加上新加的i个硬币,这里 cur+i 如果大于等于 res 的话,那么 res 是不会被更新的,那么为啥这里是大于等于 res-1 呢?

    因为能运行到这一步,说明之前是无法整除的,那么余数一定存在,所以再次调用递归函数的 target 不为0,那么如果整除的话,cur 至少会加上1,所以又跟 res 相等了,还是不会使得 res 变得更小。

    c++

    class Solution {
    public:
        int coinChange(vector<int>& coins, int amount) {
            int res = INT_MAX, n = coins.size();
            sort(coins.begin(), coins.end());
            helper(coins, n - 1, amount, 0, res);
            return (res == INT_MAX) ? -1 : res;
        }
        void helper(vector<int>& coins, int start, int target, int cur, int& res) {
            if (start < 0) return;
            if (target % coins[start] == 0) {
                res = min(res, cur + target / coins[start]);
                return;
            }
            for (int i = target / coins[start]; i >= 0; --i) {
                if (cur + i >= res - 1) break;
                helper(coins, start - 1, target - i * coins[start], cur + i, res);
            }
        }
    };
    
  • 相关阅读:
    db2 SQL3055N 报错分析解决
    db2之load报错SQL3107W解决
    db2报错之ERRORCODE=-4220 SQLSTATE=NULL
    mybatis-generator自动生成工具配置
    db2 表空间backup pending
    DB2
    Redis
    Linux安装Redis
    vue2.0学习(五)-Vue-router
    JavaScript基础-数据类型
  • 原文地址:https://www.cnblogs.com/wwj99/p/12441167.html
Copyright © 2011-2022 走看看