欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

322. 零钱兑换(Java)

程序员文章站 2022-04-04 09:03:08
...

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 方法一(递归;正向暴力;超出时间限制)

先写状态方程
322. 零钱兑换(Java)

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) {
        // 特殊情况1
        if(amount == 0) return 0;
        // 特殊情况2
        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 方法三(迭代;自底向上动态规划)

class Solution {
    public int coinChange(int[] coins, int amount) {
        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 (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 迭代内部包含状态方程