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

news2025/1/11 21:41:42

文章目录

      • 一、题目
      • 二、题解
        • 方法一:完全背包问题的变体(版本1)
        • 方法二:完全背包问题变体(版本2)
      • 三、拓展:先遍历物品后遍历背包vs先遍历背包后遍历物品
        • 先遍历物品后遍历背包(组合问题)
        • 先遍历背包后遍历物品(排列问题)


一、题目

给你一个整数数组 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;
}

在这里插入图片描述

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

如果我们选择先遍历背包后遍历物品,那么我们的状态 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;
}

在这里插入图片描述

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


转置这个矩阵(如果看上面矩阵不顺眼)

#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;
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/999188.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1905. 统计子岛屿

给你两个 m x n 的二进制矩阵 grid1 和 grid2 &#xff0c;它们只包含 0 &#xff08;表示水域&#xff09;和 1 &#xff08;表示陆地&#xff09;。一个 岛屿 是由 四个方向 &#xff08;水平或者竖直&#xff09;上相邻的 1 组成的区域。任何矩阵以外的区域都视为水域。 如…

文件夹怎么加密码?文件夹怎么设置密码?

文件夹设置密码可以有效地保护文件夹的安全&#xff0c;避免数据泄露。那么&#xff0c;文件夹怎么加密码呢&#xff1f;下面我们就来了解一下。 文件夹保护3000 文件夹保护3000提供了三种文件夹保护方式&#xff0c;文件夹加密码就是其中之一。我们可以通过三种简单快捷的方式…

【LeetCode-简单题】26. 删除有序数组中的重复项

文章目录 题目方法一&#xff1a;快慢指针 题目 方法一&#xff1a;快慢指针 class Solution { //快慢指针public int removeDuplicates(int[] nums) {int fast 1;int slow 0;while(fast < nums.length){if(nums[fast] nums[fast-1]) fast;//若当前元素和之前元素相同 则…

Java使用正则校验各种信息

一、前言 在实际项目中&#xff0c;我们有可能需要针对身份证、IP地址&#xff0c;手机号等相关信息做校验&#xff0c;这是可以通过正则匹配形式实现相关校验。 二、具体实现 1.先定义相关信息校验工具类。 public class RegexUtil {public static final String identity_re…

N9344C安捷伦Agilent N9344C频谱分析仪

181/2461/8938如果您在现场进行测量&#xff0c;安捷伦N9344C手持式频谱分析仪(HSA)会让您的工作更加轻松。符合MIL PRF 28800 Class 2标准&#xff0c;它具有在恶劣的野外环境中工作所需的特性&#xff0c;其测量性能让您对工作的正确完成充满信心。N9344C HSA可让您自动执行常…

FancyBox.js基于JQuery图集弹层插件用法

FancyBox是一款基于 jquery 开发的类 Lightbox 插件。支持对放大的图片添加阴影效果&#xff0c;对于一组相关的图片添加导航操作按纽&#xff0c;该 lightbox 除了能够展示图片之外&#xff0c;还可以展示 iframed 内容&#xff0c; 通过 css 自定义外观。 相对与 Lightbox 而…

CUDA小白 - NPP(6) 图像处理 Geometry Transforms (2)

cuda小白 原始API链接 NPP GPU架构近些年也有不少的变化&#xff0c;具体的可以参考别的博主的介绍&#xff0c;都比较详细。还有一些cuda中的专有名词的含义&#xff0c;可以参考《详解CUDA的Context、Stream、Warp、SM、SP、Kernel、Block、Grid》 常见的NppStatus&#xf…

LeetCode 1282. Group the People Given the Group Size They Belong To【哈希表】1267

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

Python入门学习15(面向对象)

一、多态 多态&#xff0c;指的是&#xff1a;多种状态&#xff0c;即完成某个行为时&#xff0c;使用不同的对象会得到不同的状态。 同样的行为&#xff08;函数&#xff09;&#xff0c;传入不同的对象&#xff0c;得到不同的状态 class Animal:def speak(self):passclass …

NTP8928(20W内置DSP双通道D类功放芯片)

由工采网代理的韩国NF(耐福)NTP8928是一款内置DSP双通道D类音频功放芯片&#xff0c;芯片集成了多功能数字音频信号处理功能&#xff0c;高性能&#xff0c;高保真全数字PWM调制器和两个大功率全桥MOSFET。 该芯片工作电压范围&#xff1a;5V&#xff5e;28V&#xff1b;2通道…

基于antd+vue2来实现一个简单的工作流程图功能

简单流程图的实现&#xff08;基于antdvue2的&#xff09;代码很多哦~ 实现页面如下 1.简单操作如下 2.弹框中使用组件&#xff1a; <vfdref"vfd"style"background-color: white;":needShow"true":fieldNames"fieldNames"openUse…

深入《C++ Core Guidelines解析》:提升C++编程实践的关键指南

目录 1、写在前面2、推荐理由3、内容介绍4、作者介绍5、赠书 or 购买 1、写在前面 C Core Guidelines是一个正在进行的开源项目&#xff0c;通过将广泛认可的现代C上佳实践集中在一个地方来解决这些问题。Core Guidelines依赖于几十年的经验和早期的编码规则。它们与C本身共享一…

2d关键点转bvh fbx

本文使用的方法是把关键点转换成3d关键点&#xff0c;然后再使用脚本转换成bvh。bvh转fbx可以直接使用blender转。 环境准备 MotionBERT(2D关键点转换到3D关键点)AlphaPose(提取2D关键点)。转换脚本&#xff0c;https://github.com/TnoobT/pose2bvh/tree/main 步骤 安装好A…

Python经典游戏04:用tkinter给老板写一封拒绝不了的辞职信

★★★★★博文原创不易&#xff0c;我的博文不需要打赏&#xff0c;也不需要知识付费&#xff0c;可以白嫖学习编程小技巧。**如果使用代码的过程&#xff0c;有疑问的地方&#xff0c;欢迎大家指正留言交流。**喜欢的老铁可以多多帮忙点赞&#xff0c;小红牛在此表示感谢。★…

Azure + React + ASP.NET Core 项目笔记一:项目环境搭建(三)

有意义的标题 连接Azure SQL 数据库添加swagger新建数据库编写APIAPI连接更新 简单的前端搭建结束后&#xff0c;就到了后端搭建了 连接Azure SQL 数据库 解决方案 > 发布中 服务依赖项添加&#xff0c;这步不难&#xff0c;不放具体步骤了 成功是这样的 添加swagger Nu…

Kotlin中函数的基本用法以及函数类型

函数的基本用法 1、函数的基本格式 2、函数的缺省值 可以为函数设置指定的初始值&#xff0c;而不必要传入值 private fun fix(name: String,age: Int 2){println(name age) }fun main(args: Array<String>) {fix("张三") }输出结果为&#xff1a;张三2 …

Kotlin变量与控制条件的基本用法

一、变量与控制条件 1、var与val var&#xff1a;可修改变量 val&#xff1a;只读变量&#xff0c;只读变量并非绝对只读。 编译时常量只能在函数之外定义&#xff0c;因为函数内常量是在运行时赋值&#xff0c;编译时常量要在变量赋值前存在。并且值是无法修改的。 const…

maven配置问题之命令提示符显示JAVA_HOME有问题

在配置maven的环境变量时&#xff0c;命令提示符mvn -v显示错误 显示JAVA_HOME配置有问题 使用java -version查看java的配置时没反应 说明java的环境配置确实有问题 打开环境变量看了一下 JAVA_HOME的路径后面多了一个分号&#xff0c;把它删掉又试了试 木的问题了

ABB机器人如何利用示教器中的筛选功能查找IO信号?

ABB机器人如何利用示教器中的筛选功能查找IO信号? 如下图所示,点击左上角进入主菜单,点击选择“输入输出”, 如下图所示,点击右下角的“视图”,这里以数字输出DO为例进行说明,点击选择“数字输出”, 如下图所示,此时显示的是所有的数字输出信号,但是逐个查找太麻烦,…

优思学院|质量改进的关键:降低变异性

Quality means fitness for use. 质量&#xff0c;就是适用性&#xff0c;适用性是一个关键概念&#xff0c;涵盖了产品和服务的两个重要方面&#xff1a;设计质量和合规质量。无论何种商品或服务&#xff0c;都存在不同等级或质量水平。这些差异是有意的&#xff0c;因此适用的…