322. 零钱兑换(Java)(动归正向迭代,一维dp)(完全背包问题,二维dp)

1 题目

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

示例 1:

输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:

输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-change
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

2 Java

将该问题看做动态规划问题,即将目标金额 amount 拆分成 0~amount 的 amount + 1 种不同金额的小问题

2.1 方法一(递归;正向暴力;超出时间限制)

先写状态方程
在这里插入图片描述

class Solution {
    public int coinChange(int[] coins, int amount) {
        // 初始条件/特殊情况/出口函数
        if(amount == 0) return 0;

        int ans = Integer.MAX_VALUE;
        // 状态方程
        for(int coin: coins){
            if(coin > amount)   continue;   // 特殊情况
            int subProblem = coinChange(coins, amount - coin);
            if(subProblem == -1)    continue;   // 特殊情况

            ans = Math.min(ans, subProblem + 1);    // 核心方程
        }
        // 特殊情况
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }
}

其实,上图中少一个状态:-1,当 Ci > n,且 i ∈ [1, k](就是任意硬币面值 > 目标金额)
这样一来,代码就变成了:

class Solution {
    public int coinChange(int[] coins, int amount) {
        // 特殊情况1
        if(amount == 0) return 0;
        // 特殊情况2
        Arrays.sort(coins);
        if(coins[0] > amount) return -1;

        int ans = Integer.MAX_VALUE;
        // 状态方程
        for(int coin: coins){
            int subProblem = coinChange(coins, amount - coin);
            if(subProblem == -1)	continue;	// 被迫加上
            ans = Math.min(ans, subProblem + 1);    // 核心方程
        }
        return ans == Integer.MAX_VALUE ? -1 : ans;	// 被迫加
    }
}

还是觉得不太对,和斐波那契数列的那种递归有点不一样。
其实是因为特殊情况 2 返回 -1,导致比大小时(Math.min())必须将 -1 的情况单另拿出来判断,将其改为返回MAX_VALUE就能归化到最基本的递归,结构十分清晰
见代码:

class Solution {
    public int coinChange(int[] coins, int amount) {
        // 特殊情况1
        if(amount == 0) return 0;
        // 特殊情况2
        Arrays.sort(coins);
        if(coins[0] > amount) return Integer.MAX_VALUE;

        int ans = Integer.MAX_VALUE;
        // 状态方程
        for(int coin: coins){
            ans = Math.min(ans, coinChange(coins, amount - coin));    // 核心方程
        }
        return ans + 1;
    }
}

递归(正向暴力)结构:
1特殊情况 / 初始条件 / 出口条件
2状态方程

2.2 方法二(自顶向下的动态规划)

对应2.1中第一个代码

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] memory = new int[amount + 1];
        Arrays.fill(memory, -2);
        return coinChangeHelper(coins, amount, memory);
    }

    public int coinChangeHelper(int[] coins, int amount, int[] memory){
        // 查询备忘录
        if(memory[amount] != -2)    return memory[amount];

        // 初始条件/特殊情况/出口函数
        if(amount == 0) return 0;

        int ans = Integer.MAX_VALUE;
        // 状态方程
        for(int coin: coins){
            if(coin > amount)   continue;   // 特殊情况
            int subProblem = coinChangeHelper(coins, amount - coin, memory);
            if(subProblem == -1)    continue;   // 特殊情况

            ans = Math.min(ans, subProblem + 1);    // 核心方程
        }
        
        // 将每一步的结果都存储到备忘录中
        memory[amount] = (ans == Integer.MAX_VALUE ? -1 : ans);
        // 特殊情况-1
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }
}

对应2.1中第二个代码

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] memory = new int[amount + 1];
        Arrays.fill(memory, -2);
        return coinChangeHelper(coins, amount, memory);
    }

    public int coinChangeHelper(int[] coins, int amount, int[] memory){
        // 特殊情况1
        if(amount == 0) return 0;
        // 特殊情况2
        Arrays.sort(coins);
        if(coins[0] > amount) return -1;
        // 查询备忘录
        if(memory[amount] != -2)    return memory[amount];

        int ans = Integer.MAX_VALUE;
        // 状态方程
        for(int coin: coins){
            int subProblem = coinChangeHelper(coins, amount - coin, memory);
            if(subProblem == -1)	continue;	// 被迫加上
            ans = Math.min(ans, subProblem + 1);    // 核心方程
        }
        
        // 将每一步的结果都存储到备忘录中
        memory[amount] = (ans == Integer.MAX_VALUE ? -1 : ans);
        return ans == Integer.MAX_VALUE ? -1 : ans;	// 被迫加
    }
}

对应2.1第三段代码

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] memory = new int[amount + 1];
        Arrays.fill(memory, -2);
        return coinChangeHelper(coins, amount, memory);
    }

    public int coinChangeHelper(int[] coins, int amount, int[]memory) {
        // base case
        if(amount == 0) return 0;
        // 特殊情况:没法凑刚好
        Arrays.sort(coins);
        if(coins[0] > amount) return Integer.MAX_VALUE;
        // 查询备忘录
        if(memory[amount] != -2)    return memory[amount];

        int ans = Integer.MAX_VALUE;
        // 状态方程
        for(int coin: coins){
            ans = Math.min(ans, coinChangeHelper(coins, amount - coin, memory));    // 核心方程
        }
        // 将每一步的结果都存储到备忘录中
        memory[amount] = ans + 1;

        return ans + 1;
    }
}

逆向暴力(递归)——> 自顶向下动态规划的步骤:
1、将递归方法存入字方法XXXXHelper,并加入备忘录memory
2、递归方法中查询备忘录,一般放在递归方法开头
3、递归方法末尾将结果存入备忘录,一般在return前

2.3 方法三(动归正向迭代,一维dp)

class Solution {
    public int coinChange(int[] coins, int amount) {
        // 创建备忘录,初始化!!!只有dp[0]可达
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, amount + 1);        // 初始化一个足够大不可能出现的情况就行,不能Integer.MAX_VALUE,因为判断的时候有+1,越界
        dp[0] = 0;

        // 外层for向前步进
        for (int i = 1; i <= amount; i++){
            // 内层for多路择优(从哪来)
            for (int coin : coins)
                if (coin <= i)
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

自顶向下——>自底向上:
1 将递归转化为迭代(就是一个for循环)
2 其它的地方要看情况改变,变动比较大

自底向上结构:
1 初始条件
2 迭代(for循环)
3 迭代内部包含状态方程

2.4 方法四(完全背包问题变形,二维dp)

class Solution {
    /*
        N种硬币(物品),W总金额(背包容量)
        val[]个数(价值,这里都是1),wt[]面值(物品重量,就是coins[])
        求最少硬币数
    */
    public int coinChange(int[] coins, int amount) {
        int N = coins.length, W = amount;
        // 创建备忘录,初始化
        int[][] dp = new int[N + 1][W + 1];
        for(int i = 0; i < N + 1; i++){
            Arrays.fill(dp[i], Integer.MAX_VALUE);  // 状态不可达
            dp[i][0] = 0;       // 目标金额为0时,仅需0个硬币
        }
        
        
        // 外层for状态步进
        for(int i = 1; i <= N; i++){
            for(int j = 1; j <= W; j++){
                // 内层for多路择优
                for(int k = 0; j - k * coins[i - 1] >= 0; k++){
                    // 特殊情况
                    if(dp[i - 1][j - k * coins[i - 1]] == Integer.MAX_VALUE)    continue;   // 当前遍历的前一状态不可达,跳过
                    // 普通状态转移
                    else    dp[i][j] = Math.min(dp[i][j], dp[i - 1][j - k * coins[i - 1]] + k * 1);   // 从所有可能的前一状态择优
                }
            }
        }

        return dp[N][W] == Integer.MAX_VALUE ? -1 : dp[N][W];
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值