You are given coins of different denominations and a total amount of money amount. Write a function to compute 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.

Example 1:
coins = [1, 2, 5], amount = 11
return 3 (11 = 5 + 5 + 1)

Example 2:
coins = [2], amount = 3
return -1.

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

Credits:
Special thanks to @jianchao.li.fighter for adding this problem and creating all test cases.

这道题给我们了一些可用的硬币值,又给了一个钱数,问我们最小能用几个硬币来找零。根据题目中的例子可知,不是每次都会给全 1,2,5 的硬币,有时候没有1分硬币,那么有的钱数就没法找零,需要返回 -1。这道题跟 CareerCup 上的那道 9.8 Represent N Cents 美分的组成 有些类似,那道题给全了所有的美分, 25,10,5,1,然后给我们一个钱数,问所有能够找零的方法,而这道题只让求出最小的那种。没啥特别好的思路就首先来考虑 brute force 吧,暴力搜索如果也没思路肿么办 -.-|||。还是来看例子1,如果不考虑代码实现,你怎么手动找出答案。博主会先取出一个最大的数字5,比目标值 11 要小,由于这里的硬币是可以重复使用的,所以博主会再取个5出来,现在是 10,还是比 11 要小,这是再取5会超,那就往前取,取2,也会超出,于是就取1,刚好是 11。那么我们的暴力搜索法也是这种思路,首先要给数组排个序,因为想要从最大的开始取,递归函数需要一个变 量start,初始化为数组的最后一个位置,当前目标值 target,还有当前使用的硬币个数 cur,以及最终结 果res。在递归函数,首先判断如果 target 小于0了,直接返回。若 target 为0了,说明当前使用的硬币已经组成了目标值,用 cur 来更新结果 res。否则就从 start 开始往前遍历硬币,对每个硬币都调用递归函数,此时 target 应该减去当前的硬币值,cur 应该自增1,代码参见评论区七楼。但是暴力搜索 Brute Force 的方法会超时 TLE,所以我们考虑一下其他的方法吧。

如果大家刷题有一阵子了的,那么应该会知道,对于求极值问题,主要考虑动态规划 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 {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + , amount + );
dp[] = ;
for (int i = ; i <= amount; ++i) {
for (int j = ; j < coins.size(); ++j) {
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + );
}
}
}
return (dp[amount] > amount) ? - : dp[amount];
}
};

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

解法二:

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

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

解法三:

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

难道这题一定要 DP 来做吗,我们来看网友 hello_world00 提供的一种解法,这其实是对暴力搜索的解法做了很好的优化,不仅不会 TLE,而且击败率相当的高!对比 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 - , amount, , res);
return (res == INT_MAX) ? - : res;
}
void helper(vector<int>& coins, int start, int target, int cur, int& res) {
if (start < ) return;
if (target % coins[start] == ) {
res = min(res, cur + target / coins[start]);
return;
}
for (int i = target / coins[start]; i >= ; --i) {
if (cur + i >= res - ) break;
helper(coins, start - , target - i * coins[start], cur + i, res);
}
}
};

Github 同步地址:

https://github.com/grandyang/leetcode/issues/322

类似题目:

Coin Change 2

9.8 Represent N Cents 美分的组成

参考资料:

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

https://leetcode.com/problems/coin-change/discuss/77360/C%2B%2B-O(n*amount)-time-O(amount)-space-DP-solution

https://leetcode.com/problems/coin-change/discuss/77368/*Java*-Both-iterative-and-recursive-solutions-with-explanations

LeetCode All in One 题目讲解汇总(持续更新中...)

[LeetCode] Coin Change 硬币找零的更多相关文章

  1. [LeetCode] 322. Coin Change 硬币找零

    You are given coins of different denominations and a total amount of money amount. Write a function ...

  2. [LeetCode] 518. Coin Change 2 硬币找零 2

    You are given coins of different denominations and a total amount of money. Write a function to comp ...

  3. dp算法之硬币找零问题

    题目:硬币找零 题目介绍:现在有面值1.3.5元三种硬币无限个,问组成n元的硬币的最小数目? 分析:现在假设n=10,画出状态分布图: 硬币编号 硬币面值p 1 1 2 3 3 5 编号i/n总数j ...

  4. codevs 3961 硬币找零【完全背包DP/记忆化搜索】

    题目描述 Description 在现实生活中,我们经常遇到硬币找零的问题,例如,在发工资时,财务人员就需要计算最少的找零硬币数,以便他们能从银行拿回最少的硬币数,并保证能用这些硬币发工资. 我们应该 ...

  5. LeetCode:柠檬水找零【860】

    LeetCode:柠檬水找零[860] 题目描述 在柠檬水摊上,每一杯柠檬水的售价为 5 美元. 顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯. 每位顾客只买一杯柠檬水,然后向 ...

  6. NYOJ 995 硬币找零

    硬币找零 时间限制:1000 ms  |  内存限制:65535 KB 难度:3   描述 在现实生活中,我们经常遇到硬币找零的问题,例如,在发工资时,财务人员就需要计算最少的找零硬币数,以便他们能从 ...

  7. [LeetCode] Coin Change 2 硬币找零之二

    You are given coins of different denominations and a total amount of money. Write a function to comp ...

  8. [LeetCode] 518. Coin Change 2 硬币找零之二

    You are given coins of different denominations and a total amount of money. Write a function to comp ...

  9. LeetCode.860-卖柠檬水找零(Lemonade Change)

    这是悦乐书的第331次更新,第355篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第201题(顺位题号是860).在柠檬水摊上,每杯柠檬水的价格为5美元.客户站在队列中向 ...

随机推荐

  1. 读书笔记--SQL必知必会17--创建和操纵表

    17.1 创建表 使用CREATE TABLE语句创建表. 不同的DBMS中CREATE TABLE语句的语法可能不同. 17.1.1 表创建基础 利用CREATE TABLE创建表,必须具备如下信息 ...

  2. angularjs和ajax的结合使用 (二)

    今天我们来继续丰富上次的例子.我们来搞些 稍微复杂点的应用. 首先我们来加一个全选 的功能. 上一篇的例子里我们看到 分页时载入的是我们通过linq 查询自定义列 然后构建的匿名类 .使用这种EF框架 ...

  3. C#对.zip 存档读取和写入

    Framework4.5支持 引用: System.IO.Compression.dll,System.IO.Compression.FileSystem.dll 提取压缩文件 ZipFile.Ext ...

  4. Delphi_07_Delphi_Object_Pascal_基本语法_05_函数参数

    这里主要讨论Delphi中函数.方法的相关内容. 一.工程文件 { Delphi语法方法和函数 1.方法 2.函数 } program Routine; {$APPTYPE CONSOLE} uses ...

  5. 课堂Java小程序(加减乘除与验证码)

    一.编写一个程序,用户输入两个数,求出其加减乘除,并用消息框 显示计算结果. 1.设计思想:从键盘输入两个数字和运算符,然后计算.将输入的数字及运算符由字符型转换为整型,再用if判断输入的运算符,根据 ...

  6. java多线程解读一(基础篇)

    一.线程的定义 每个应用程序内部都是由一个或多个的进程组成,而每个进程内部都是由许多具体的线程执行,所以,线程是每个程序执行的最小单位. 二.线程的实现 1.通过继承java.lang.Thread类 ...

  7. java中如何实现多态

    复习基础知识 多态,就是重载和重写.重载发生在一个类中.重写发生在子类,意思就是子类重写父类相同名称的方法.刚学语言有的东西,不必搞得那么清楚,只有知道怎么用就行了,有的问题你要想真正把它搞得很懂,短 ...

  8. js对象和继承总结

    创建对象方式: [工厂模式]:无法解决对象识别问题 [构造函数模式]:每个方法都要在每个实例上创建一遍 [原型模式]:原型上属性为引用类型的问题,见例子 [组合模式]:解决上述问题 [动态原型模式]: ...

  9. handlebars.js的运用与整理

    最近在做部门的技术分享网站,主要是一些文章的列表和演讲信息展示,内容比较规律,复用性较高.同事推荐了 handlebars.js.用来看看. handlebars.js是一种静态JS模板,用起来还蛮简 ...

  10. LDAP注入与防御解析

    [目录] 0x1 LDAP介绍 0x2 LDAP注入攻击及防御 0x3 参考资料 0x1 LDAP介绍 1 LDAP出现的背景 LDAP(Lightweight Directory Access Pr ...