【每日一题】力扣322. 零钱兑换

文章目录

    • 题目
    • 解题思路
    • 代码(C++)
      • 递归
      • 优化
      • 动态规划
    • 总结


题目

题目链接:力扣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

提示:

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

解题思路

看到这题应该可以立马想到是一道 dp 题,如果没怎么接触过 dp ,估计一时半会做不出来。那就先从会的思路开始想。

暴力递归(会超时)

没思路的最好办法就是暴力,先暴力出来再想优化。

这题我的第一思路是,先把硬币排序,然后再从大到小开始遍历,对总金额 amount 取余再整除,得到硬币数。但发现样例是:coins = [2, 3], amount = 4 的时候就出错了。所以必须遍历所有情况。

如何遍历所有情况?

递归,把硬币从大到小,取 [0, amount / coin[i]] 范围的个数,计算出最少的硬币个数。把范围内的硬币个数全都遍历一遍即可。递归的深度是硬币数组的长度(如果出现可以直接被总金额整除的硬币,那么可以直接返回,因为遍历是从硬币面值大开始的),递归的宽度是总金额能被当前硬币面额整除的最大硬币数。

递归的深度:从递归开始到第一次 return 的长度

递归的宽度:递归函数里的循环的长度

具体做法:

  • 先把硬币数组排序,再进入递归

  • 递归函数的返回条件:

    • 递归是从硬币面值大的到小的,所以就是遍历硬币数组,所以数组下标不能小于 0
    • 如果递归到当前层次时,硬币的总个数大于之前已经存储的个数,那么这里的递归一定不是最少的硬币个数,可以直接返回
    • 如果发现当前硬币面额可以整除当前总金额,判断和已存储的个数的大小,取小的那个。然后返回
  • 递归函数的参数:

    • 硬币数组
    • 总金额
    • 硬币总个数
    • 硬币数组的下标
  • 递归函数的实现功能

    循环遍历当前面额的硬币可以取的个数([0, amount/coins[i]]

优化

上面的方法超时了,原因在于有太多的重复的计算,比如:coins = [1, 2, 3], amount = 7 ,当遍历 3 时,后面会遍历 12 。遍历 2 时,会遍历 1 。这样就出现了重复的情况,可以当 amount 减少一次,就把其对应的所需硬币数存储起来,下次遇到时可以直接使用。

这需要把上面的方法稍微改一下。递归的深度是总金额的大小,递归的宽度是硬币数组的长度。

  • 递归函数的参数

    • 硬币数组
    • 总金额
  • 递归函数的返回条件

    • 当总金额小于 0 时,返回 -1
    • 当总金额等于 0 时,返回 0
    • 当存在当前情况存储的硬币数时,返回存储的硬币数
  • 递归函数的实现功能

    循环遍历硬币数组,将总金额减去硬币面值,放入递归中,取所有的返回值中最小的那个。然后存入数组中

这种方法解决了重复计算所以就没有超时,但不是最优的。

动态规划

动态规划感觉就是对上面优化的优化吧。

先给出转移方程吧:F[i] = min(F[i - coins[j]]) + 1,其中 j 的取值范围是 coins 数组的全范围。

因为动态规划都是先从下标小的到下标大的,下标大的需要使用下标小的数组值。所以需要先从开始计算,直到最终。

F[0] = 0F[i] 的取值与 coins 数组和 amount 值有关,所以直接给出核心代码吧:dp[i] = min(dp[i], dp[i - coins[j]] + 1);

知道了这行代码这题就结束了

代码(C++)

递归

class Solution {
public:
    int f = INT_MAX;
    void dfs(vector<int> coins, int amount, int n, int m) {
        if (m < 0 || n + amount / coins[m] >= f)
            return ;
        if (amount % coins[m] == 0) {
            f = min(f, n + amount / coins[m]);
            return ;
        }
        for (int i = amount / coins[m]; i >= 0; -- i) {
            dfs(coins, amount - i * coins[m], n + i, m - 1);
        }
    }
    int coinChange(vector<int>& coins, int amount) {
        sort(coins.begin(), coins.end());
        int n = 0;
        dfs(coins, amount, 0, coins.size() - 1);
        if (f == INT_MAX)
            f = -1;
        return f;
    }
};

优化

class Solution {
public:
    vector<int> f;
    int dp(vector<int> coins, int amount) {
        if (amount < 0)
            return -1;
        if (amount == 0)
            return 0;
        if (f[amount - 1] != 0)
            return f[amount - 1];
        int t = INT_MAX;
        for (int i = 0; i < coins.size(); ++ i) {
            int m = dp(coins, amount - coins[i]);
            if (m >= 0 && m < t) {
                t = m + 1;
            }
        }
        if (t == INT_MAX) {
            f[amount - 1] = -1;
            return -1;
        }
        else {
            f[amount - 1] = t;
            return t;
        }
    }
    int coinChange(vector<int>& coins, int amount) {
        if (amount < 1)
            return 0;
        f.resize(amount);
        return dp(coins, amount);
    }
};

动态规划

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

总结

动态规划的题不太好讲,一道题几乎就是一个转移方程,只要找到转移方程题目就解决了。对于找转移方程还需要多刷题,多练才比较容易找到。这道题还算是动态规划里的简单的题,题目算是非常直接的告诉你了。动态规划是一个比较难的算法,大家可以去看看y总的背包九讲,就在b站里。难的东西当然是要多练才行,而且动态规划是非常喜欢考的算法,一定要掌握。

你可能感兴趣的:(每日一题,leetcode,算法,每日一题,动态规划)