LeetCode 322. Coin Change 硬币找零

208 阅读4分钟

leetcode.com/problems/co…

Discuss: www.cnblogs.com/grandyang/p…

You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money.

Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.

You may assume that you have an infinite number of each kind of coin.

 

Example 1:

Input: coins = [1,2,5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1

Example 2:

Input: coins = [2], amount = 3
Output: -1

Example 3:

Input: coins = [1], amount = 0
Output: 0

Example 4:

Input: coins = [1], amount = 1
Output: 1

Example 5:

Input: coins = [1], amount = 2
Output: 2

 

Constraints:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 104

解法一:

使用动态规划求解。如果大家刷题有一阵子了的,那么应该会知道,对于求极值问题,主要考虑动态规划 Dynamic Programming 来做,好处是保留了一些中间状态的计算值,可以避免大量的重复计算。我们维护一个一维动态数组 dp,其中 dp[i] 表示钱数为i时的最小硬币数的找零,注意由于数组是从 0 开始的,所以要多申请一位,数组大小为 amount+1,这样最终结果就可以保存在 dp[amount] 中了。初始化 dp[0] = 0,因为目标值若为 0 时,就不需要硬币了。其他值可以初始化是 amount+1,为啥呢?因为最小的硬币是1,所以 amount 最多需要 amount 个硬币,amount+1 也就相当于当前的最大值了,注意这里不能用整型最大值来初始化,因为在后面的状态转移方程有加1的操作,有可能会溢出,除非你先减个1,这样还不如直接用 amount+1 舒服呢。好,接下来就是要找状态转移方程了,没思路?不要紧!回归例子1,假设我取了一个值为5的硬币,那么由于目标值是 11,所以是不是假如我们知道 dp[6],那么就知道了组成 11 的 dp 值了?所以更新 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 数组。先来看迭代的写法如下所示:

class Solution {
    fun coinChange(coins: IntArray, amount: Int): Int {

        val dp = IntArray(size = amount + 1, init = { amount + 1 })
        dp[0] = 0
        for (i in 1..amount) {
            for (j in 0 until coins.size) {
                if (coins[j] <= i) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1)
                }
            }
        }

        return if (dp[amount] > amount) -1 else dp[amount]
    }
}

解法二:

迭代的 DP 解法有一个好基友,就是递归+记忆数组的解法,说其是递归形式的 DP 解法也没错,但博主比较喜欢说成是递归加记忆数组。其目的都是为了保存中间计算结果,避免大量的重复计算,从而提高运算效率,思路都一样,仅仅是写法有些区别:

class Solution {
    fun coinChange(coins: IntArray, amount: Int): Int {
        val memo = IntArray(size = amount + 1, init = { Int.MAX_VALUE })
        memo[0] = 0
        return coinChangeDFS(coins, amount, memo)
    }

    private fun coinChangeDFS(coins: IntArray, target: Int, memo: IntArray): Int {
        if (target < 0) {
            return -1
        }
        if (memo[target] != Int.MAX_VALUE) {
            return memo[target]
        }
        for (i in coins.indices) {
            if (coins[i] <= target) {
                val temp = coinChangeDFS(coins, target - coins[i], memo)
                if (temp >= 0) {
                    memo[target] = Math.min(memo[target], temp + 1)
                }
            }
        }
        memo[target] = if (memo[target] == Int.MAX_VALUE) -1 else memo[target]
        return memo[target]
    }
}

解法三:

暴力求解,会超时。这道题给我们了一些可用的硬币值,又给了一个钱数,问我们最小能用几个硬币来找零。根据题目中的例子可知,不是每次都会给全 1,2,5 的硬币,有时候没有 1 分硬币,那么有的钱数就没法找零,需要返回 -1。首先要给数组排个序,因为想要从最大的开始取,递归函数需要一个变量 start,初始化为数组的最后一个位置,当前目标值 target,还有当前使用的硬币个数 cur,以及最终结 果res。在递归函数,首先判断如果 target 小于 0 了,直接返回。若 target 为 0 了,说明当前使用的硬币已经组成了目标值,用 cur 来更新结果 res。否则就从 start 开始往前遍历硬币,对每个硬币都调用递归函数,此时 target 应该减去当前的硬币值,cur 应该自增 1。

// Brute Force (Time Limit Exceeded)
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 (target < 0) return;
        if (target == 0) {res = min(res, cur); return;}
        for (int i = start; i >= 0; --i) {
             helper(coins, i, target - coins[i], cur + 1, res);
        }
    }
};

解法四:

暴力求解优化。对比暴力求解 Brute Force 的方法,这里在递归函数中做了很好的优化。首先是判断 start 是否小于0,因为需要从 coin 中取硬币,不能越界。下面就是优化的核心了,看 target 是否能整除 coins[start],这是相当叼的一步,比如假如目标值是 15,如果当前取出了大小为5的硬币,这里做除法,可以立马知道只用大小为5的硬币就可以组成目标值 target,那么用 cur + target/coins[start] 来更新结果 res。之后的 for 循环也相当叼,不像暴力搜索中的那样从 start 位置开始往前遍历 coins 中的硬币,而是遍历 target/coins[start] 的次数,由于不能整除,只需要对余数调用递归函数,而且要把次数每次减1,并且再次求余数。举个例子,比如 coins=[1,2,3],amount=11,那么 11 除以3,得3余2,那么i从 3 开始遍历,这里有一步非常有用的剪枝操作,没有这一步,还是会 TLE,而加上了这一步,直接击败百分之九十九以上,可以说是天壤之别。那就是判断若 cur + i >= res - 1 成立,直接 break,不调用递归。这里解释一下,cur + i 自不必说,是当前硬币个数 cur 加上新加的i个硬币,这里 cur+i 如果大于等于 res 的话,那么 res 是不会被更新的,那么为啥这里是大于等于 res-1 呢?因为能运行到这一步,说明之前是无法整除的,那么余数一定存在,所以再次调用递归函数的 target 不为 0,那么如果整除的话,cur 至少会加上1,所以又跟 res 相等了,还是不会使得 res 变得更小。

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);
        }
    }
};