LeetCode518. 零钱兑换 II 以及 动态规划相关的排列组合问题

这篇具有很好参考价值的文章主要介绍了LeetCode518. 零钱兑换 II 以及 动态规划相关的排列组合问题。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


一、题目

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

示例 1:

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:

输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。

示例 3:

输入:amount = 10, coins = [10] 
输出:1

提示:

  • 1 <= coins.length <= 300
  • 1 <= coins[i] <= 5000
  • coins 中的所有值 互不相同
  • 0 <= amount <= 5000

二、题解

方法一:完全背包问题的变体(版本1)

题目理解

这道题目是一个动态规划问题,需要计算凑成总金额的硬币组合数。给定一组硬币的面额数组 coins 和一个总金额 amount,要求计算有多少种不同的组合方式来凑成总金额。每个硬币的面额都可以被使用无限次。

动态规划思路

我们可以将硬币问题与完全背包问题联系起来:

  • 将硬币的面额视为物品的重量。
  • 将总金额视为背包的容量。
  • 将计算硬币组合数的问题视为在完全背包问题中计算组合数量的变种。
  1. 定义状态

我们需要定义一个状态来表示问题的子问题和最优解。在这个问题中,我们可以使用二维数组 dp[i][j] 来表示前 i 种硬币组成总金额 j 的组合数。其中,i 表示考虑的硬币种类数量,j 表示总金额。

  1. 初始化状态

我们需要初始化状态数组 dp,确保其初始值是正确的。在这里,可以看到 dp[i][0] 应该初始化为0,因为没有硬币可供选择。当 i % coins[0] == 0dp[0][i] 应该初始化为1,因为i可以由整数个第一个硬币组成。

  1. 状态转移方程

接下来,我们需要找到状态之间的转移关系,即如何从子问题的最优解推导出原问题的最优解。在这个问题中,状态转移方程如下:

  • 如果当前总金额 j 小于硬币面额 coins[i],则无法将硬币 i 加入组合,所以 dp[i][j] = dp[i-1][j],表示不使用硬币 i
  • 如果 j 大于等于硬币面额 coins[i],我们可以选择使用硬币 i 或者不使用。因此,dp[i][j] 等于两者之和:
    • 不使用硬币 i,即 dp[i-1][j]
    • 使用硬币 i,即 dp[i][j - coins[i]],这里的 dp[i][j - coins[i]] 表示在考虑硬币 i 时,总金额减去硬币 i 的面额后的组合数。
  1. 填充状态表格

通过上述状态转移方程,我们可以通过双重循环遍历所有的子问题,从而填充状态表格 dp。外层循环遍历硬币种类 i,内层循环遍历总金额 j,根据状态转移方程更新 dp[i][j]

  1. 获取最终答案

最后,我们可以通过 dp[coins.size() - 1][amount] 来获取问题的最终答案,即考虑了所有硬币种类并且总金额为 amount 时的组合数。

代码解析

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<vector<int>> dp(coins.size(), vector<int>(amount + 1, 0));
        for (int i = 0; i <= amount; i++) {
            if (i % coins[0] == 0) {
                dp[0][i] = 1;
            }
        }
        for (int i = 1; i < coins.size(); i++) {
            for (int j = 0; j <= amount; j++) {
                if (j < coins[i]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];
                }
            }
        }
        return dp[coins.size() - 1][amount];
    }
};
  1. 创建一个二维数组 dp,其中 dp[i][j] 表示前 i 种硬币组成总金额 j 的组合数。

  2. 初始化 dp[0][j],即考虑只有一种硬币时,对应总金额 j 的组合数。如果 j 可以被第一种硬币整除,那么 dp[0][j] 初始化为1,表示有一种组合方式,即只使用第一种硬币。

  3. 通过嵌套的循环遍历硬币种类 i 和总金额 j,根据状态转移方程更新 dp[i][j]。如果 j 小于硬币面额 coins[i],则 dp[i][j] 等于 dp[i-1][j],否则 dp[i][j] 等于 dp[i-1][j] + dp[i][j - coins[i]]

  4. 最后返回 dp[coins.size() - 1][amount],即考虑了所有硬币种类并且总金额为 amount 时的组合数。

方法二:完全背包问题变体(版本2)
  1. 定义状态

首先,我们需要定义一个状态,来表示问题的子问题和最优解。在这个问题中,我们可以使用一维数组 dp,其中 dp[i] 表示总金额 i 的组合方式数量。

  1. 初始化状态

接下来,我们需要初始化状态数组 dp,确保其初始值是正确的。在这里,可以看到 dp[0] 应该初始化为1,因为总金额为0时,只有一种组合方式,那就是什么硬币都不选。

  1. 状态转移方程

然后,我们需要找到状态之间的转移关系,即如何从子问题的最优解推导出原问题的最优解。状态转移方程如下:

  • 对于每个硬币面额 coins[i],我们可以选择使用该硬币或不使用。
  • 如果我们选择使用硬币 coins[i],那么 dp[j] 应该等于 dp[j] + dp[j - coins[i]],表示在考虑硬币 coins[i] 时,总金额 j 的组合方式数量应该加上总金额 j - coins[i] 的组合方式数量。
  • 如果我们选择不使用硬币 coins[i],那么 dp[j] 保持不变。
  1. 填充状态数组

通过上述状态转移方程,我们可以通过循环遍历所有的子问题,从而填充状态数组 dp。外层循环遍历硬币的面额 i,内层循环遍历总金额 j,根据状态转移方程更新 dp[j]

  1. 获取最终答案

最后,我们可以通过 dp[amount] 来获取问题的最终答案,即总金额为 amount 时的组合方式数量。

代码解析

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1,0);
        dp[0] = 1;
        for(int i = 0; i < coins.size(); i++){
            for(int j = coins[i]; j <= amount; j++){
                dp[j] += dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
};
  1. 创建一个一维数组 dp,其中 dp[i] 表示总金额 i 的组合方式数量。

  2. 初始化 dp[0] 为1,因为总金额为0时,只有一种组合方式,即不选硬币。

  3. 通过嵌套的循环遍历硬币面额 coins[i] 和总金额 amount,根据状态转移方程 dp[j] += dp[j - coins[i]] 来更新 dp[j]。这表示在考虑硬币 coins[i] 时,总金额 j 的组合方式数量应该加上总金额 j - coins[i] 的组合方式数量。

  4. 最后返回 dp[amount],即总金额为 amount 时的组合方式数量。

二维数组版本

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<vector<int>> dp(coins.size(), vector<int>(amount + 1, 0));
        
        // 初始化第一行,当金额为0时,有1种方式,即不选择任何硬币
        for (int i = 0; i < coins.size(); i++) {
            dp[i][0] = 1;
        }
        
        for (int i = 0; i < coins.size(); i++) {
            for (int j = 1; j <= amount; j++) {
                // 如果当前硬币面值大于金额j,则不能选择当前硬币,直接继承上一种方式的数量
                if (coins[i] > j) {
                    if (i > 0) {
                        dp[i][j] = dp[i - 1][j];
                    }
                } else {
                    // 否则,可以选择当前硬币或不选择当前硬币
                    if (i > 0) {
                        dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];
                    } else {
                        dp[i][j] = dp[i][j - coins[i]];
                    }
                }
            }
        }
        
        return dp[coins.size() - 1][amount];
    }
};

三、拓展:先遍历物品后遍历背包vs先遍历背包后遍历物品

先遍历物品后遍历背包(组合问题)

如果我们选择先遍历物品后遍历背包,那么我们的状态 dp[j] 表示的是总金额为 j 时的硬币组合数量。在这种情况下,我们考虑了每个硬币,并决定是否将其放入组合。这导致了我们计算的是硬币的组合数量,而不考虑硬币的排列顺序。

#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1, 0); // 初始化动态规划数组,dp[i] 表示凑成金额 i 的方法数
        dp[0] = 1; // 凑成金额为 0 的方法数为 1,因为什么都不选也是一种方法

        for (int i = 0; i < coins.size(); i++) {
            int coin = coins[i];
            for (int j = coin; j <= amount; j++) {
                // 如果当前硬币面额小于等于当前金额 j,可以考虑将该硬币加入方案
                // 当前 dp[j] 的值应该加上 dp[j-coin],表示不使用这个硬币时的方法数
                dp[j] += dp[j - coin];
            }

            // 输出当前填写后的 dp 数组
            cout << "Coins: " << coin << " | DP Array: ";
            for (int k = 0; k <= amount; k++) {
                cout << dp[k] << " ";
            }
            cout << endl;
        }

        return dp[amount]; // 返回凑成目标金额的方法数
    }
};

int main() {
    Solution s;
    vector<int> coins = { 1, 2, 5 };
    int amount = 5;
    int result = s.change(amount, coins);
    cout << "Result: " << result << endl;
    return 0;
}

LeetCode518. 零钱兑换 II 以及 动态规划相关的排列组合问题,LeetCode刷题,LeetCode,动态规划,算法

[注意]大多数格子由三行组成,第一行是推导出结果的公式,第三行是推导出的结果,第二行是组合方式。

LeetCode518. 零钱兑换 II 以及 动态规划相关的排列组合问题,LeetCode刷题,LeetCode,动态规划,算法

先遍历背包后遍历物品(排列问题)

如果我们选择先遍历背包后遍历物品,那么我们的状态 dp[j] 表示的是总金额为 j 时的硬币排列数量。在这种情况下,我们考虑了每个背包容量,然后决定放入哪些硬币。这导致了我们计算的是硬币的排列数量,考虑了硬币的顺序。

注意:我写了两个版本,一个是一维数组,一个是二维数组,最直观的就是展现成二维数组。

一维数组

#include <iostream>
using namespace std;
#include <vector>

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1, 0);
        dp[0] = 1;
        
        for (int j = 0 ; j <= amount; j++) {
            for (int i = 0; i < coins.size(); i++) {
               if(j >= coins[i]) dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
};

int main()
{
    Solution s;
    vector<int> coins;
    coins.push_back(1); coins.push_back(2); coins.push_back(5);
    int result;
    result = s.change(5, coins);
    cout << "result:" << result << endl;
}

二维数组
下面这段代码中dp[j][i]的意义是背包为j大小时,最后一个放入价值为coins[i]硬币加上不放入该硬币(最后一个投入的硬币是从coins[0]coins[i]之间的硬币)的方法种类总数。

#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<vector<int>> dp(amount+1,vector<int>(coins.size(),0)); 
        dp[0][0] = 1; 
        int j = 0;
        for (j = 0; j <=amount ; j++) {
            for (int i = 0; i <coins.size(); i++) {
                if( i > 0 ) dp[j][i] = dp[j][i - 1];
                if (j >= coins[i]) {
                    dp[j][i]+=dp[j - coins[i]][coins.size() - 1];
                }    
            }

            // 输出当前填写后的 dp 数组
            cout << "j(容量) " << j << " | DP Array: ";
            for (int k = 0; k < coins.size(); k++) {
                cout << dp[j][k] << " ";
            }
            cout << endl;
        }

        return dp[amount][coins.size()-1]; // 返回凑成目标金额的方法数
    }
};

int main() {
    Solution s;
    vector<int> coins = { 1, 2, 5 };
    int amount = 5;
    int result = s.change(amount, coins);
    cout << "Result: " << result << endl;
    return 0;
}

LeetCode518. 零钱兑换 II 以及 动态规划相关的排列组合问题,LeetCode刷题,LeetCode,动态规划,算法

[注意]大多数格子由三行组成,第一行是推导出结果的公式,第三行是推导出的结果,第二行是排列方式。

LeetCode518. 零钱兑换 II 以及 动态规划相关的排列组合问题,LeetCode刷题,LeetCode,动态规划,算法
转置这个矩阵(如果看上面矩阵不顺眼)

#include <iostream>
#include <vector>
using namespace std;

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<vector<int>> dp(coins.size(), vector<int>(amount + 1, 0));
        dp[0][0] = 1;
        int i = 0;
        for (i = 0; i <= amount; i++) {
            for (int j = 0; j < coins.size(); j++) {
                if (j > 0) dp[j][i] = dp[j-1][i];
                if (i >= coins[j]) {
                    dp[j][i] += dp[coins.size()-1][i-coins[j]];
                }
            }
        }
        for (int i = 0; i < coins.size(); i++) {
            for (int j = 0; j <= amount; j++) {
                cout << dp[i][j] << " ";
            }
            cout << endl;
        }
        return dp[coins.size() - 1][amount]; // 返回凑成目标金额的方法数
    }
};

int main() {
    Solution s;
    vector<int> coins = { 1, 2, 5 };
    int amount = 5;
    int result = s.change(amount, coins);
    cout << "Result: " << result << endl;
    return 0;
}

LeetCode518. 零钱兑换 II 以及 动态规划相关的排列组合问题,LeetCode刷题,LeetCode,动态规划,算法文章来源地址https://www.toymoban.com/news/detail-699981.html

到了这里,关于LeetCode518. 零钱兑换 II 以及 动态规划相关的排列组合问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 算法学习——LeetCode力扣动态规划篇3(494. 目标和、474. 一和零、518. 零钱兑换 II)

    494. 目标和 - 力扣(LeetCode) 描述 给你一个非负整数数组 nums 和一个整数 target 。 向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 : 例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “

    2024年04月14日
    浏览(44)
  • 【十九】【动态规划】518. 零钱兑换 II、279. 完全平方数、474. 一和零,三道题目深度解析

    动态规划就像是解决问题的一种策略,它可以帮助我们更高效地找到问题的解决方案。这个策略的核心思想就是将问题分解为一系列的小问题,并将每个小问题的解保存起来。这样,当我们需要解决原始问题的时候,我们就可以直接利用已经计算好的小问题的解,而不需要重

    2024年02月03日
    浏览(27)
  • 代码随想录第44天|动态规划:完全背包理论基础 518.零钱兑换II 377. 组合总和 Ⅳ

    代码随想录 (programmercarl.com) 动态规划之完全背包,装满背包有多少种方法?组合与排列有讲究!| LeetCode:518.零钱兑换II_哔哩哔哩_bilibili 完全背包和01背包问题唯一不同的地方就是,每种物品有无限件 。 完全背包中的物品可以添加多次,所以要从小到大遍历: 518. 零钱兑换

    2024年04月25日
    浏览(35)
  • 【算法与数据结构】518、LeetCode零钱兑换 II

    所有的LeetCode题解索引,可以看这篇文章——【算法和数据结构】LeetCode题解。    思路分析 :本题的硬币是无数的,因此本题可以抽象成一个完全背包问题。完全背包和01背包的不同之处在于完全背包式从前往后遍历的。在本题的完全背包问题中,amount代表背包的最大重量

    2024年01月23日
    浏览(34)
  • leetcode动态规划(零钱兑换II、组合总和 Ⅳ)

    给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 示例 1: 输入: amount = 5, coins = [1, 2, 5] 输出: 4 解释: 有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1 示例 2: 输入: amount = 3, coins = [2] 输出: 0 解释: 只用面

    2024年02月01日
    浏览(33)
  • 【LeetCode动态规划#08】完全背包问题实战与分析(零钱兑换II)

    力扣题目链接(opens new window) 给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 示例 1: 输入: amount = 5, coins = [1, 2, 5] 输出: 4 解释: 有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1 示例 2: 输入: amount = 3

    2023年04月19日
    浏览(37)
  • 【LeetCode动态规划#08】完全背包问题实战与分析(零钱兑换II--求组合、组合总和IV--求排列)

    力扣题目链接(opens new window) 给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 示例 1: 输入: amount = 5, coins = [1, 2, 5] 输出: 4 解释: 有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1 示例 2: 输入: amount = 3

    2023年04月19日
    浏览(27)
  • 力扣 518. 零钱兑换 II

    题目来源:https://leetcode.cn/problems/coin-change-ii/description/ C++题解(来源代码随想录): 这是一道典型的背包问题,一看到钱币数量不限,就知道这是一个完全背包。但本题和纯完全背包不一样, 纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数

    2024年02月12日
    浏览(25)
  • 力扣518. 零钱兑换 II

    思路: 假设 dp[i] 为金额 i 使用零钱的组合数,其可以由其中的一种零钱 coin 和 i - coin 组合;  遍历零钱数组,对每一种零钱 coin 进行如下操作: 从 coin 到 amount 金额进行遍历,dp[j] = dp[j] + dp[j - coin] 初始值,dp[0] = 1 上述做法不会重复计算不同的排列。因为外层循环是遍历数

    2024年01月24日
    浏览(33)
  • 518. 零钱兑换 II -- 完全背包

    518. 零钱兑换 II 这道题其实就是一个 完全背包 问题,关于背包问题的相关文章见: 01背包问题 – 动态规划 完全背包问题

    2024年02月09日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包