【重点】【背包】322.零钱兑换

题目
本题思路参考
参考视频:灵茶山艾府
问:关于完全背包,有两种写法,一种是外层循环枚举物品,内层循环枚举体积;另一种是外层循环枚举体积,内层循环枚举物品。如何评价这两种写法的优劣?

答:两种写法都可以,但更推荐前者。外层循环枚举物品的写法,只会遍历物品数组一次;而内层循环枚举物品的写法,会遍历物品数组多次。从 cache 的角度分析,多次遍历数组会导致额外的 cache miss,带来额外的开销。所以虽然这两种写法的时间空间复杂度是一样的,但外层循环枚举物品的写法常数更小。

  • 各种背包问题区别:
    在这里插入图片描述

法1:动态规划

Python

# 写法1
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [amount+1] * (amount + 1) # dp[i]表示凑到i元用的最小数量
        dp[0] = 0
        for coin in coins:
            for i in range(1, amount+1):
                if i >= coin:
                    dp[i] = min(dp[i-coin] + 1, dp[i])
        
        return dp[amount] if dp[amount] < amount+1 else -1

Java

// 时间复杂度:O(kN)
class Solution {
    public int coinChange(int[] coins, int amount) {
        // dp[i]表示从coins[0, i]范围内组合成i的最小硬币数量
        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) {
                    // min{不取当前coin, 取当前coin}
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);         
                }
            }
        }

        return dp[amount] == amount + 1 ? -1 : dp[amount];
    }
}
<think>嗯,用户想了解零钱兑换II的动态规划解法。首先,我得回忆一下这个题目的具体要求。题目是说,给定不同面额的硬币和一个总金额,计算可以成总金额的组合数,每种硬币可以用无限次。这应该是一个完全背包问题,对吧? 根据引用的资料,比如引用[2]提到这是一个完全背包的方案数问题,需要装满背包。所以动态规划的状态定义应该是dp[i]表示出金额i的组合数。初始化的时候,dp[0]应该为1,因为出0元只有一种方式,就是不用任何硬币。这一点在引用[4]的代码里也看到,dp[0] = 1。 然后,状态转移方程需要考虑如何累加不同的硬币组合。这里要注意的是,题目要求的是组合数而不是排列数,也就是说,不同的顺序算同一种组合。比如用1和2成3,应该是1+2,而不是2+1算两种。所以遍历顺序很重要,必须先遍历硬币,再遍历金额,避免重复计算排列。引用[4]中的代码确实是先遍历硬币,再遍历背包容量,这样确保每个硬币被考虑的顺序固定,不会产生重复的组合。 举个例子,如果硬币是[1,2],金额是3。正确的组合应该是1+1+1,1+2。如果先遍历金额的话,可能会导致计算成1+2和2+1两种,但实际上应该算一种。因此,遍历顺序必须正确,先处理每个硬币,再处理每个可能的金额,这样每个硬币的使用顺序是固定的,不会出现排列的情况。 接下来,具体到状态转移方程,对于每个硬币coin,从coin到amount遍历,dp[j] += dp[j - coin]。这是因为对于当前金额j,可以通过加上当前硬币coin来构成,所以组合数等于之前j - coin的组合数加上当前的组合数。这个过程需要逐个硬币处理,确保组合的唯一性。 然后,代码实现方面,初始化数组dp的长度是amount +1,因为要考虑0到amount的所有金额。然后双重循环,外层是硬币,内层是金额,从当前硬币的面值开始,直到amount。每次更新dp[j]的值。 例如,coins = [1,2,5], amount = 5的情况,初始化dp[0]=1。处理硬币1时,每个金额j从1到5,dp[j] += dp[j-1],所以每个j都变成1,因为只有一种方式(全用1)。接着处理硬币2,从j=2开始,dp[2] += dp[0],即1,所以现在dp[2]=2(1+1和2)。继续到j=3,dp[3] += dp[1]=1,变成2,依此类推。这样一步步累加,最后得到正确的组合数。 用户可能会有的疑问包括为什么外层是硬币而不是金额,这时候需要解释组合和排列的区别,以及如何避免重复计算。此外,可能还需要考虑如果硬币有0面额或者amount为0的特殊情况处理,但题目中可能已经排除了这些情况。 另外,还需要注意时间复杂度,因为双重循环的时间是O(len(coins)*amount),这在题目给定的范围内应该是可行的。空间优化方面,这里使用了一维数组,每次覆盖更新,节省了空间。 总结起来,这个问题的核心在于理解完全背包的组合数问题,正确处理遍历顺序以避免排列,以及正确应用动态规划的状态转移方程。代码实现相对直接,但需要确保循环的顺序正确。</think>### 零钱兑换 II 动态规划详解 #### 问题描述 给定一组不同面额的硬币 `coins` 和一个总金额 `amount`,计算**组合方式的数量**,使得这些硬币的总金额等于 `amount`。每个硬币可以无限次使用。 #### 关键思路 1. **完全背包问题**:将硬币视为物品,金额视为背包容量,转化为“恰好装满背包的组合数”问题[^2]。 2. **动态规划定义**: - `dp[j]` 表示出金额 `j` 的组合数。 - 初始化 `dp[0] = 1`(出0元只有一种方式:不选任何硬币)[^4]。 3. **遍历顺序**: - **外层遍历硬币**:确保组合不考虑顺序(例如 `1+2` 和 `2+1` 视为同一种组合)。 - **内层遍历金额**:从当前硬币面值开始,逐步更新 `dp[j]`[^4]。 #### 状态转移方程 $$ dp[j] = \sum_{i=0}^{n-1} dp[j - \text{coins}[i]] \quad (\text{当 } j \geq \text{coins}[i]) $$ 实际通过循环累加实现: ```python for coin in coins: for j in range(coin, amount + 1): dp[j] += dp[j - coin] ``` #### 代码实现 ```python def change(amount: int, coins: list[int]) -> int: dp = [0] * (amount + 1) dp[0] = 1 # 初始化:金额0有1种组合方式(不选任何硬币) for coin in coins: for j in range(coin, amount + 1): dp[j] += dp[j - coin] return dp[amount] ``` #### 示例分析 假设 `coins = [1, 2, 5]`, `amount = 5`: 1. **初始化**:`dp = [1, 0, 0, 0, 0, 0]` 2. **处理硬币1**: - `dp[1] += dp[0]` → `dp[1] = 1` - `dp[2] += dp[1]` → `dp[2] = 1` - ... 最终所有金额均被更新为1(全用1元硬币) 3. **处理硬币2**: - `dp[2] += dp[0]` → `dp[2] = 2`(组合方式:`1+1` 和 `2`) - `dp[3] += dp[1]` → `dp[3] = 2`(`1+1+1` 和 `1+2`) 4. **处理硬币5**: - `dp[5] += dp[0]` → `dp[5] = 3`(新增组合 `5`) 最终结果 `dp[5] = 4`,即组合方式:`[1,1,1,1,1]`, `[1,1,1,2]`, `[1,2,2]`, `[5]`。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值