最优子结构---零钱兑换

系列文章目录


动态规划

 


前言

最优子结构即:更大规模问题的最优解参考了规模更小的子问题的最优解。这个说法比较理论化,我们用具体的例子向大家解释。


一、题目描述

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

1.示例一:

输入: coins = [1, 2, 5], amount = 11

输出: 3

解释: 5 + 5 + 1 = 11

2.示例二: 

输入: coins = [2], amount = 3

输出: -1

解释:只有一枚 2 分钱的,无论是一个还是两个都不能刚好凑够 3 分

二、思路分析 

题目中问到所需要的最少硬币个数,也就是最优值,并没有提到最优解,一般这种情况就可以考虑动态规划

以示例一为例,做如下分析

输入: coins = [1, 2, 5], amount = 11

输出: 3

解释: 5 + 5 + 1 = 11

凑成面值为11的硬币数,有如下三种:

面值为 1 的这枚硬币 + 需要在凑出面值为 10 的硬币(可递归求解)

面值为 2 的这枚硬币 + 需要在凑出面值为 9 的硬币(可递归求解)

面值为 5 的这枚硬币 + 需要在凑出面值为 6 的硬币(可递归求解)

综上:

dp[11] = min(dp[10] + 1, dp[9] + 2, dp[6] + 5)

 1.树形图

 二、解决方案

 1.方法一:记忆化递归

public class Coins {
    //记忆化递归
    private int[] memo;
    public int coinChange(int[] coins, int amount){
        memo = new int[amount + 1];
        //进行数组填充,由于在这个题目描述中,说 -1 为未找到可凑出的硬币,那么我们以填充 -2 为主
        //memo: [-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2]
        Arrays.fill(memo,-2);
        //接下来我们将给定的硬币面值数组进行递增排序,这样做的目的是为了减少枝干,以便计算,
        //一个数减去较小的数都小于等于0,但是减去一个较大的数一定小于0
        Arrays.sort(coins);
        return dfs(coins,amount);
    }

    private int dfs(int[] coins, int amount) {

        //首先判断amount是否为0
        if(amount == 0){
            return 0;
        }
        // 因为计算过的值用 -2 填充,所以在此判断 这个值是否被计算,若没有,则返回
        if (memo[amount] != -2){
            return memo[amount];
        }
        int res = amount + 1;
        //去循环每一个硬币面额
        for (int coin : coins){
            //如果总面值减去硬币面值小于0的时候,就要退出循环,说明后面的硬币也不能满足了
            if (amount - coin < 0){
                break;
            }
            //递归循环,总金额变化为前面减去的硬币面值
            int subRes = dfs(coins,amount -coin);
            if(subRes == -1){
                continue;
            }
            res = Math.min(res,subRes + 1);
        }
        if (res == amount + 1){
            memo[amount] = -1;
            return -1;
        }
        memo[amount] = res;
        return res;
    }
}

读者可跟踪调试,会让你对这段代码有更深刻的印象

复杂度分析:

时间复杂度:O(amount * N),这里 N 是可选硬币的种类数,即数组 coins 的长度;
空间复杂度:O(amount)。

2.方法二:动态规划

第 1 步:定义「状态」

dp[i] :凑齐总价值 i 需要的最少硬币数。这里状态定义的形式就是题目问的问题。

第 2 步:推导「状态转移方程」 

dp[amount] = min(dp[amount - coin[i]] + 1) for i in [0, len - 1] if coin[i] <= amount 

说明:

首先,当前考虑的这一枚硬币的面值要 小于等于 当前要凑出来的面值;
其次,新状态的值要参考的值以前计算出来的 有效 状态值。即:剩余的面值要能够凑出来,状态才可以转移,例如:求 dp[11] 需要参考 dp[10],但是如果 dp[10] 不能凑出来,dp[10] 应该等于一个不可能的很大的值,可以设计为 11 + 1,也可以设计为 -1,它们的区别只在具体的代码编写细节上。

第 3 步:考虑初始值

所有 dp 数组的值在初始化的时候,应该先假设凑不出来。由于要找的是最小值,所以初始化的时候应该设置为一个不可能的较大的数。 

第 4 步:考虑输出值 

最后一个状态值就是输出值。

代码实现 

        public int coinChange(int[] coins, int amount) {
            // 给 0 占位
            int[] dp = new int[amount + 1];
            // 注意:因为要比较的是最小值,这个不可能的值就得赋值成为一个较大的值
            // 初始化
            Arrays.fill(dp, amount + 1);
            dp[0] = 0;
            // 递推开始
            for (int i = 1; i <= amount; i++) {
                for (int coin : coins) {
                    if (i - coin >= 0 && dp[i - coin] != amount + 1) {
                        dp[i] = Math.min(dp[i], 1 + dp[i - coin]);
                    }
                }
            }
            // 如果不能凑出,根据题意返回 -1
            if (dp[amount] == amount + 1) {
                dp[amount] = -1;
            }
            return dp[amount];
        }

总结

像这种题目要求我们找出最小值那么就可以使用动态规划这种算法。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值