个人博客:The Blog Of WaiterXiaoYY 欢迎来互相交流学习。
今天,动态规划又来了,
动态规划问题无非就是找出状态和选择,
对状态的定义不同,定义的dp数组不同,对应的状态转移方程也是不一样的。
今天,我整理了零钱兑换系列的问题,分享一下。
零钱兑换
题目给出一组硬币的数额,然后给出一个amount,叫你求出凑成这个目标数所需要的最小硬币数,
当我一眼看完题目,我判断出这道题应该要用动态规划的思维来做,
主要有两个原因:
- 有可选择的东西(硬币)
- 求到达一个目标数(amount)的最小硬币数(也就是最优解)
其实这道题和背包问题是一样的,
背包问题应该可以说是动态规划最典型的案例了,
我在很多算法书里面看到对动态规划的讲解都先是用背包问题,
前一段时间,当我刚接触到动态规划的时候,对背包问题是真的一头雾水,
花了一整天的时间也没弄出个所以然,说多了都是泪。
背包问题是这样子的,
给出一组物品的价值和对应的重量,然后给出背包所能装载的最大重量,然后求背包所能装载的最大价值,
同样的,他也有两大特征,
- 有物品可供选择(包括重量和价值)
- 求到达目标数所能装载的最大价值
是不是很相似?
但今天主要还是来讲零钱兑换的问题,
解决动态规划问题,就是一个求最优解的问题,同时也是一个求最优子解的问题,
动态规划中的另一个典型问题——爬楼梯,求跳到这一级阶梯的方式有多少种,
就是看跳到这一级的上一级的位置在哪里,确定上一级的位置,就是确定子问题,
同样的,在零钱问题中,求到amount的最小硬币数,就是求到amount前子问题的最小硬币数,
前子问题是什么?
我们先举个例子,
输入: coins = [1, 2, 5], amount = 11
输出: 3
硬币的面额有1,2,5三种,目标数是11,那目标数的子问题是什么?
没错,就是 amount - coins
,在这道题中就是10, 9, 6
那题就转换成了去求到达目标数10,到达目标数9,到达目标数6的最小硬币数,
继续这样缩小范围,直到最基本的问题,也就是amount == 1
的时候。
前面我们说了,动态规划问题要找出状态和选择,
状态是什么?状态就是到达这个目标数的最小硬币数,
选择是什么?选择就是这个硬币我选还是不选?
依据是什么?依据是能不能让我硬币数达到最小,
比如到达目标数8,是让 5 + 1 + 1 + 1呢? 还是5 + 2 + 1呢?
当然就是后者,这就是选择。
dp数组记录的是什么?是状态。
那可以转换成我们的状态转移方程了,
// dp[i] 初始化为最大值
dp[i] = MAX_VALUE
dp[i] = min(dp[i - coins] + 1, dp[i])
// i 代表当前目标数
// coins代表硬币数额
// +1,表示加上一个硬币数
状态转移方程的问题解决了,这道题也就解决了一半了,
但还有一些细节问题需要考虑,
- 数组长度为多少。
数组长度应该为 amount + 1,因为我们的目标数是从 0 开始到 amount ,
dp[0] 当然就是 0 ,
- dp[amount] 最大可能为多少?
最大为amount,硬币数额不可能为0,最小数额为1,所以是amount,判断这个有什么用呢?
有用,因为这是一个临界条件(极端条件),
如果dp[i] > amount, 说明就没有一组硬币组合符合。
应该都比较清楚了,我们直接上代码:
代码
class Solution {
public int coinChange(int[] coins, int amount) {
// 初始一个比较大的值
int max = amount + 1;
int []dp = new int[amount + 1];
//将dp数组填充最大值,因为我们求的是最小硬币数
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);
}
}
//没有符合的情况就返回 - 1
return dp[amount] > amount ? -1 : dp[amount];
}
}
刚刚我们讨论的是零钱兑换系列的一个问题,是求最小硬币数,
还有一种问法,问你有多少种组合数,
就是到达这个目标数有多少种组合,
一样的,我们举个例子,
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
这时候我们再来问问,
状态是什么?状态是到达目标数的组合数。
选择是什么?一样的,是这个硬币我选不选呢?
依据是什么?选了能否到达我的目标数。
dp数组记录的就是组合数。
和上面一样,也是利用穷举的方法,穷举从 1 到 amount 每种情况的组合数,
因为要求amount的组合数,其实就是求子问题的组合数,
子问题怎么判断呢?
当然还是 目标数 - coins,
当前的组合数 = 上一种情况到这里的组合数 + 子问题的组合数。
这就是我们的状态转移方程,
dp[i] = dp[i] + dp[i - coins];
// i 表示目标数
列举一下,就很明白了:
首先我们要知道 dp[0] = 1
, 也就是说当目标数等于0 的时候,是1种组合。
- 没有使用硬币的情况
目标数 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
no coins | 1 | 0 | 0 | 0 | 0 | 0 |
- 使用数额为 1 的情况
数额为1,只有amount >= 1的时候才有考虑的意义,
目标数 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
no coins | 1 | 0 | 0 | 0 | 0 | 0 |
coins 1 | 1 | 1 | 1 | 1 | 1 | 1 |
- 使用数额为 2 的情况
目标数 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
no coins | 1 | 0 | 0 | 0 | 0 | 0 |
coins 1 | 1 | 1 | 1 | 1 | 1 | 1 |
coins 2 | 1 | 1 | 2 | 2 | 3 | 3 |
- 使用数额为 5 的情况
目标数 | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
no coins | 1 | 0 | 0 | 0 | 0 | 0 |
coins 1 | 1 | 1 | 1 | 1 | 1 | 1 |
coins 2 | 1 | 1 | 2 | 2 | 3 | 3 |
coins 5 | 1 | 1 | 2 | 2 | 3 | 4 |
所以最终结果是4,。
搞定!!
上代码!
代码
class Solution {
public int change(int amount, int[] coins) {
int []dp = new int[amount + 1];
//当目标数为 0 的时候,组合数为1
dp[0] = 1;
//对硬币种类进行穷举
for(int co : coins) {
//穷举每个目标数的情况,直至所有硬币种类都列举完,最后更新的就是最终的组合数
for(int i = co; i <= amount; i++) {
if(i >= co) {
//组合数等于当前的组合数加上子问题的组合数
dp[i] += dp[i - co];
}
}
}
return dp[amount];
}
}
整理于 2020.3.24