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

文章目录

      • 一、题目
      • 二、题解
        • 方法一:完全背包问题的变体(版本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.mzph.cn/news/76982.shtml

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

相关文章

NAT实验:构建复杂网络拓扑,实现互联网访问与FTP发布

文章目录 一、实验背景与目的二、实验拓扑三、实验需求四、实验解法1.配置链路上各个接口的IP地址。2.在私网中配置单臂路由3.在R1和R3上配置默认路由指向公网。4.私网A通过NAPT使vlan 20和vlan 10能够使用R1的公网访问互联网。5.私网B通过在R3上配置EASY IP访问互联网。6.私网…

Docker部署EMQX

1、简介 EMQ X (Erlang/Enterprise/Elastic MQTT Broker) 是基于 Erlang/OTP 平台开发的开源物联网 MQTT 消息服务器。 Erlang/OTP是出色的软实时 (Soft-Realtime)、低延时 (Low-Latency)、分布式 (Distributed)的语言平台。 MQTT 是轻量的 (Lightweight)、发布订阅模式 (Pu…

labelme2voc 标签重叠/覆盖问题

使用labelme自带的 labelme2voc.py转换voc数据集时可能标签重叠

Maven Helper mvn项目冲突如何解决

一般用这款插件来查看maven的依赖树。 一、安装&#xff1a; File-->setting--->Plugins--->在搜索框中填写Maven Helper然后搜索&#xff0c;单击Install按钮进行安装&#xff0c;装完重启IDE。 二、使用 当Maven Helper 插件安装成功后&#xff0c;打开项目中的p…

AMD R7 7840HS 核显 780M 性能怎么样

目录 1. 基本数据 2.性能对比 2.1对比一 2.2 对比二 3.综合 1. 基本数据 2.性能对比 2.1对比一 锐龙77840h相当于i几 答&#xff1a;类似于I7-12700H R7 7840H介于13500-13700之间。 R7 7840H是AMD锐龙旗下高性能的一款处理器&#xff0c;主要应用在主流的游戏本中。各…

GO语言篇之发布开源软件包

GO语言篇之发布开源软件包 文章目录 GO语言篇之发布开源软件包新建仓库拉取到本地初始化项目编写代码提交代码发布引用软件包 我们写GO语言程序的时候难免会引用第三方的软件包&#xff0c;那么你知道别人是怎么发布自己的软件包吗&#xff0c;别急&#xff0c;这篇博客教你怎么…

数据分享|R语言分析上海空气质量指数数据:kmean聚类、层次聚类、时间序列分析:arima模型、指数平滑法...

全文链接&#xff1a;http://tecdat.cn/?p30131 最近我们被客户要求撰写关于上海空气质量指数的研究报告。本文向大家介绍R语言对上海PM2.5等空气质量数据&#xff08;查看文末了解数据免费获取方式&#xff09;间的相关分析和预测分析&#xff0c;主要内容包括其使用实例&…

Vue通过ref修改 <el-input-number> 增减按钮的样式

Vue 为一个 <el-input-number> 设置了ref为‘inputNumberRef’, 通过这个ref获取<el-input-number>组件中的增、减按钮所在的<i>标签&#xff0c;并将它们的class分别改为el-icon-plus 和 el-icon-minus。 可以通过以下代码实现&#xff1a; <template&g…

如何实现CSDN评论区粉丝幸运抽选功能:一场代码与运气的碰撞

文章目录 前言抽选规则实现思路代码实现1.获取评论数据2.过滤符合抽选规则的评论者3.获取粉丝数据4.过滤符合抽选规则的粉丝5.增加公众号留言权重6.抽选粉丝 完整的代码效果展示结语 前言 为了回馈粉丝们一直以来的的关注和支持&#xff0c;我近期开启了赠书活动&#xff0c;活…

【网络编程·数据链路层】MAC帧/以太网协议/ARP协议/RARP协议

需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网&#xff0c;轻量型云服务器低至112元/年&#xff0c;新用户首次下单享超低折扣。 目录 一、MAC帧 1、IP地址和MAC地址的区别 2、MAC帧协议 3、MTU对IP协议的…

腾讯云2核4G服务器5M带宽 218元一年 优惠价格明细表

腾讯云2核4G服务器5M带宽可以选择轻量应用服务器或云服务器ECS&#xff0c;轻量2核4G5M带宽服务器218元一年&#xff1a; 腾讯云2核4G服务器5M带宽收费 腾讯云2核4G服务器可以选择轻量应用服务器或者ECS云服务器&#xff0c;云服务器ECS是专业级云服务器&#xff0c;大多数使用…

【K8S系列】深入解析k8s网络插件—Canal

序言 做一件事并不难&#xff0c;难的是在于坚持。坚持一下也不难&#xff0c;难的是坚持到底。 文章标记颜色说明&#xff1a; 黄色&#xff1a;重要标题红色&#xff1a;用来标记结论绿色&#xff1a;用来标记论点蓝色&#xff1a;用来标记论点 在现代容器化应用程序的世界中…

docker-compose安装redis

基于docker-compose快速安装redis 目录 一、目录结构 1、docker-compose.yml 2、redis.conf 二、连接使用 一、目录结构 1、docker-compose.yml version: 3 services:redis:image: registry.cn-hangzhou.aliyuncs.com/zhengqing/redis:6.0.8 # 镜像red…

【性能测试】Jmeter —— jmeter计数器

jmeter计数器 如果需要引用的数据量较大&#xff0c;且要求不能重复或者需要递增&#xff0c;那么可以使用计数器来实现 如&#xff1a;新增功能&#xff0c;要求名称不能重复 1&#xff0c;新增计数器 计数器&#xff1a;允许用户创建一个在线程组之内都可以被引用的计数器…

van-list 下拉刷新 触底分页 触底分页事件只加载一次

我是 头部是筛选的条件&#xff0c;&#xff0c;更换不同的状态&#xff0c;显示不同的列表数据&#xff0c;比如 审批中数据是 对的&#xff0c;触底分页也是对的&#xff0c;如果我切换一个状态的话&#xff0c;总共是 15条数据&#xff0c;但是 切换了状态只显示第一页的数据…

8个免费的AI和LLM游乐场

推荐&#xff1a;使用 NSDT场景编辑器 快速搭建3D应用场景 在本文中&#xff0c;我们的目标是通过引入八个用户友好的平台来弥合这一差距&#xff0c;这些平台使任何人都可以免费测试和比较开源AI模型。此外&#xff0c;它们还提供多种更新型号&#xff0c;确保您及时了解最新进…

无涯教程-JavaScript - SLN函数

描述 SLN函数返回资产在一个期间内的直线折旧。 语法 SLN (cost, salvage, life)争论 Argument描述Required/OptionalCostThe initial cost of the asset.RequiredSalvage The value at the end of the depreciation (sometimes called the salvage value of the asset).Re…

win11本地连接没了怎么办

很多用户在使用win11系统时发现自己的网络连接没有了&#xff0c;遇到这种情况的话&#xff0c;我们应该怎么处理呢&#xff1f;我们可以尝试打开网络图标&#xff0c;下面就是小编整理出的教程&#xff0c;大家一起看看吧。 win11本地连接没了怎么办 方法一&#xff1a; 1、…

数据分享|R语言武汉流动人口趋势预测:灰色模型GM(1,1)、ARIMA时间序列、logistic逻辑回归模型...

全文链接&#xff1a;http://tecdat.cn/?p32496 人口流动与迁移&#xff0c;作为人类产生以来就存在的一种社会现象&#xff0c;伴随着人类文明的不断进步从未间断&#xff08;点击文末“阅读原文”获取完整代码数据&#xff09;。 相关视频 人力资源是社会文明进步、人民富裕…

Redis I/O多路复用机制

一、基础回顾 1.1 多路复用要解决什么问题 并发多客户端连接场景&#xff0c;在多路复用之前最简单和典型的方案就是同步阻塞网络IO模型。 这种模式的特点就是用一个进程来处理一个网络连接(一个用户请求),比如一段典型的示例代码如下。 直接调用 recv 函数从一个 socket 上…