LintCode 740: Coin Change 2 (DP 完全背包类似题)

本文详细探讨了硬币找零问题的四种动态规划解法,包括完全背包DP、滚动数组优化、时间优化及综合优化,每种解法都附有代码实现,旨在帮助读者深入理解并掌握硬币找零问题的高效解决策略。

740. Coin Change 2

You are given coins of different denominations and a total amount of money. Write a function to compute the number of combinations that make up that amount. You may assume that you have infinite number of each kind of coin.

Example

Example1

Input: amount = 10 and coins = [10] 
Output: 1

Example2

Input: amount = 8 and coins = [2, 3, 8]
Output: 3
Explanation:
there are three ways to make up the amount:
8 = 8
8 = 3 + 3 + 2
8 = 2 + 2 + 2 + 2

Notice

You can assume below:

  • 0 <= amount <= 5000
  • 1 <= coin <= 5000
  • the number of coins is less than 500
  • the answer is guaranteed to fit into signed 32-bit integer

Input test data (one parameter per line)How to understand a testcase?

解法1:DP,类似完全背包
dp[i][j] the # of combinations that first i group coins make up the amount j
注意:
1) dp[i][0]应该初始化为1。以input = 
8
[2,3,8]
为例
如果dp[i][0]不初始化,那么dp打印结果为

0 0 0 0 0 0 0 0 0 
0 0 1 0 1 0 1 0 1 
0 0 1 0 1 1 1 1 2 
0 0 1 0 1 1 1 1 2 

可见dp[3][8] = 2,因为k=0时,dp[3][8]+=dp[2][8]=2,而k=1时,dp[3][8]+=dp[2][0]。这里如果dp[2][0]为0,那么dp[3][8]还是2。

加上初始化后,dp打印结果为

1 0 0 0 0 0 0 0 0 
1 0 1 0 1 0 1 0 1 
1 0 1 1 1 1 2 1 2 
1 0 1 1 1 1 2 1 3 

可见dp[3][8] = 3,结果就对了。

2) dp[1][i  * coins[0]] 应该初始化为1。
3) j和k这2个循环可调换位置。

class Solution {
public:
    /**
     * @param amount: a total amount of money amount
     * @param coins: the denomination of each coin
     * @return: the number of combinations that make up the amount
     */
    int change(int amount, vector<int> &coins) {
        int n = coins.size();
        //dp[i][j] the # of combinations that first i group coins make up the amount j
        vector<vector<int>> dp(n + 1, vector<int>(amount + 1, 0));
        
        for (int i = 0; i <= n; ++i) {
            dp[i][0] = 1;
        }
        
        for (int i = 1; i <= amount / coins[0]; ++i) {
            dp[1][i * coins[0]] = 1;
        }
        
        for (int i = 2; i <= n; ++i) {
            for (int j = 1; j <= amount; ++j) {
                //dp[i][j] += dp[i - 1][j]; //we do not pick any coin in group i
                for (int k = 0; k <= amount / coins[i - 1]; ++k) {
                    if (j >= k * coins[i - 1]) {
                        dp[i][j] += dp[i - 1][j - k * coins[i - 1]];
                    }
                }
            }
        }
        
        return dp[n][amount];
    }
};

上面可以简化为
 

class Solution {
public:
    /**
     * @param amount: a total amount of money amount
     * @param coins: the denomination of each coin
     * @return: the number of combinations that make up the amount
     */
    int change(int amount, vector<int> &coins) {
        int n = coins.size();
        //dp[i][j] the # of combinations that first i group coins make up the amount j
        vector<vector<int>> dp(n + 1, vector<int>(amount + 1, 0));
        
        for (int i = 0; i <= n; ++i) {
            dp[i][0] = 1;
        }

        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= amount; ++j) {
                //dp[i][j] += dp[i - 1][j]; //we do not pick any coin in group i
                for (int k = 0; k <= amount / coins[i - 1]; ++k) {
                    if (j >= k * coins[i - 1]) {
                        dp[i][j] += dp[i - 1][j - k * coins[i - 1]];
                    }
                }
            }
        }
        
        return dp[n][amount];
    }
};

时间复杂度O(n*m*m),空间复杂度O(n*m)。m就是amount。

解法2:解法1+滚动数组优化。

注意:
1) 这里的DP是累加。不能简单的就把解法1的i变成i%2和i-1变成(i-1)%2,那样前面后面的加到一起会搞混。应该对每个i,先把 dp[i % 2][j] = dp[(i - 1) % 2][j] 先赋值,这样就不会搞混了。
2) k循环从1开始,因为k=0的情形就是dp[i % 2][j] = dp[(i - 1) % 2][j]。

class Solution {
public:
    /**
     * @param amount: a total amount of money amount
     * @param coins: the denomination of each coin
     * @return: the number of combinations that make up the amount
     */
    int change(int amount, vector<int> &coins) {
        int n = coins.size();
        //dp[i][j] the # of combinations that first i group coins make up the amount j
        vector<vector<int>> dp(2, vector<int>(amount + 1, 0));
        
        for (int i = 0; i <= 1; ++i) {   //not i <= n
            dp[i][0] = 1;
        }

        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= amount; ++j) {
                dp[i % 2][j] = dp[(i - 1) % 2][j]; //we do not pick any coin in group i
                for (int k = 1; k * coins[i - 1] <= amount; ++k) {
                    if (j >= k * coins[i - 1]) {
                        dp[i % 2][j] += dp[(i - 1) % 2][j - k * coins[i - 1]];
                    }
                }
            }
        }

        return dp[n % 2][amount];
    }
};

时间复杂度O(n*m*m),空间复杂度O(m)。

解法3:解法1+时间优化。参考九章。
根据dp[i][j] += dp[i - 1][j - k * coins[i - 1]]可得:
dp[i][j] = dp[i - 1][j - 0 * coins[i - 1]]  //当前硬币不选的方案数
            +dp[i - 1][j - 1 * coins[i - 1]]  //当前硬币选1个的方案数
            +dp[i - 1][j - 2 * coins[i - 1]]  //当前硬币选2个的方案数
            +...
            +dp[i - 1][j - k * coins[i - 1]]  
//当前硬币选k个的方案数
可以看出上面的黑体部分可以理解为当前硬币至少选1个的方案数。
而根据上面的公式我们又有
dp[i][j - coins[i - 1]] = dp[i - 1][j - coins[i - 1] - 0 * coins[i - 1]]
                            += dp[i - 1][j - coins[i - 1] - 1 * coins[i - 1]]
                            ... 
                            += dp[i - 1][j - coins[i - 1] -  (k - 1) * coins[i - 1]]
而这就是上面的黑色部分,即前i个硬币,当前硬币至少选1个的方案数。所以我们有
dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]]
即前i个硬币凑出总额j的方案数就是前i-1个硬币凑出总额j的方案数 加上 前i个硬币里面当前硬币至少选1个的方案数。
代码如下:

class Solution {
public:
    /**
     * @param amount: a total amount of money amount
     * @param coins: the denomination of each coin
     * @return: the number of combinations that make up the amount
     */
    int change(int amount, vector<int> &coins) {
        int n = coins.size();
        //dp[i][j] the # of combinations that first i group coins make up the amount j
        vector<vector<int>> dp(n + 1, vector<int>(amount + 1, 0));
        
        for (int i = 0; i <= n; ++i) {
            dp[i][0] = 1;
        }
        
        for (int i = 1; i <= n; ++i) {
            for (int j = 0; j <= amount; ++j) {
                dp[i][j] = dp[i - 1][j];
                if (j >= coins[i - 1]) dp[i][j] += dp[i][j - coins[i - 1]];
        }
        
        return dp[n][amount];
    }
};

时间复杂度O(mn),空间复杂度O(mn)。m就是amount。

解法4:解法3+滚动数组优化
 

class Solution {
public:
    /**
     * @param amount: a total amount of money amount
     * @param coins: the denomination of each coin
     * @return: the number of combinations that make up the amount
     */
    int change(int amount, vector<int> &coins) {
        int n = coins.size();
        //dp[i][j] the # of combinations that first i group coins make up the amount j
        vector<vector<int>> dp(2, vector<int>(amount + 1, 0));
        
        for (int i = 0; i <= 1; ++i) {
            dp[i][0] = 1;
        }
        
        for (int i = 1; i <= n; ++i) {
            for (int j = 0; j <= amount; ++j) {
                dp[i % 2][j] = dp[(i - 1) % 2][j];
                if (j >= coins[i - 1]) dp[i % 2][j] += dp[i % 2][j - coins[i - 1]];
            }
        }
        
        return dp[n % 2][amount];
    }
};

时间复杂度O(mn),空间复杂度O(m)。

### 功能 `int coinChange(vector<int>& coins, int amount)` 函数的主要功能是解决零钱兑换问,即给定不同面额的硬币 `coins` 和一个总金额 `amount`,计算出凑成总金额所需的最少硬币个数。若无法凑出总金额,则返回 -1 [^1][^2][^4]。 ### 实现 #### 动态规划实现 ```cpp class Solution { public: int coinChange(vector<int>& coins, int amount) { vector<int> dp(amount + 1, amount + 1); dp[0] = 0; for (int i = 1; i <= amount; i++) { for (int j = 0; j < coins.size(); j++) { if (coins[j] <= i) dp[i] = min(dp[i], dp[i - coins[j]] + 1); } } return dp.back() > amount ? -1 : dp.back(); } }; ``` 上述代码使用动态规划的思想,创建一个长度为 `amount + 1` 的数组 `dp`,`dp[i]` 表示凑成金额 `i` 所需的最少硬币个数。初始化 `dp[0] = 0`,因为凑成金额 0 不需要任何硬币。然后通过两层循环,外层循环遍历金额从 1 到 `amount`,内层循环遍历所有硬币。对于每个硬币,如果其面值小于等于当前金额 `i`,则更新 `dp[i]` 为 `dp[i]` 和 `dp[i - coins[j]] + 1` 中的较小值 [^1][^4]。 #### 回溯 + 剪枝实现 ```cpp class Solution { public: int coinChange(vector<int>& coins, int amount) { if (amount == 0) return 0; int ret = INT_MAX; sort(coins.rbegin(), coins.rend()); coinChange(coins, amount, 0, ret, 0); return ret == INT_MAX ? -1 : ret; } void coinChange(vector<int>& coins, int amount, int count, int &ret, int index) { if (amount == 0) { ret = ret < count ? ret : count; return; } if (index == coins.size()) return; for (int k = amount / coins[index]; k >= 0 && k + count < ret; k--) { coinChange(coins, amount - k * coins[index], count + k, ret, index + 1); } } }; ``` 上述代码采用回溯 + 剪枝的方法,先将硬币从大到小排序,然后从最大面额的硬币开始,尽可能多地使用该硬币,若无法凑出金额则回溯。在回溯过程中,若当前使用的硬币数已经超过当前的最优解 `ret`,则不再向下搜索,进行剪枝 [^3]。 ### 优化方案 - **空间优化**:动态规划的实现中,由于每次状态转移只依赖于前一个状态,因此可以考虑使用滚动数组等方式进一步优化空间复杂度,但在该问中,由于主要是一维数组,空间优化效果不明显。 - **剪枝优化**:在回溯 + 剪枝的实现中,剪枝策略是关键。可以根据实际情况进一步优化剪枝条件,减少不必要的搜索。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值