LeetCode学习笔记——零钱兑换(动态规划)

个人博客: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种组合。

  • 没有使用硬币的情况
目标数012345
no coins100000
  • 使用数额为 1 的情况

数额为1,只有amount >= 1的时候才有考虑的意义,

目标数012345
no coins100000
coins 1111111
  • 使用数额为 2 的情况
目标数012345
no coins100000
coins 1111111
coins 2112233
  • 使用数额为 5 的情况
目标数012345
no coins100000
coins 1111111
coins 2112233
coins 5112234

所以最终结果是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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值