【算法 - 动态规划】找零钱问题Ⅰ

news2024/9/16 23:49:25

在前面的动态规划系列文章中,关于如何对递归进行分析的四种基本模型都介绍完了,再来回顾一下:

  1. 从左到右模型arr[index ...]index 之前的不用考虑,只考虑后面的该如何选择
  2. 范围尝试模型 :思考 [L ,R] 两端,即 开头和结尾 处分别该如何取舍。
  3. 样本对应模型 :以 结尾位置 为出发点,思考两个样本的结尾都会产生哪些可能性 。
  4. 业务限制模型 :不能够明确的知道一个参数的变化范围,通过业务的限制找到 最差情况 进行估计。

接下来的几篇文章我们继续深挖动态规划的一些 优化策略

通过前面文章的学习,相信小伙伴都能够根据不同模型的套路熟练的改出 严格表依赖 的动态规划版本了。

但有个问题?

记忆化搜索严格 dp 表依赖 的时间复杂度一样,记忆化搜索的代码写起来容易,为什么还非要找到动态规划的 状态转移方程 ,进而修改成为严格表依赖关系的呢。接下来我们通过一道题目 揭晓答案

找零钱问题Ⅰ

给定一个面值数组 arr ,其中的值均为无重复的正数,每一个值代表一种面值,张数无限。求能够组成 aim 的方法数是多少。

示例 1:

输入: arr = {1, 2} ,aim = 4 。

输出: 3

解释: 共三种组合方式

  • 1 + 1 + 1 + 1 = 4
  • 1 + 1 + 2 = 4
  • 2 + 2 = 4

示例 2:

输入: arr = {1, 2, 5} ,aim = 6 。

输出: 5

解释: 共五种组合方式

  • 1 + 1 + 1 + 1 + 1 + 1 = 6
  • 1 + 1 + 1 + 1 + 2 = 6
  • 1 + 1 + 2 + 2 = 6
  • 2 + 2 + 2 = 6
  • 1 + 5 = 6

首先我们依然采用最朴素的 暴力递归 来思考这道题目。

思路

这道题就是典型的 从左到右模型 ,因此,递归就可以按照 在 arr[index ...]数组中,index 之前的不用考虑,只考虑后面的该如何选择 的思路来划分情况:

  • 当前 index 下标对应的面值 参与 组合,选择任意张数,之后能有多少种情况;
  • 当前 index 下标对应的面值 不参与 组合,选择任意张数,之后能有多少种情况。

因为要求总的情况,因此要返回这两种情况的 总和

代码

public static int coinsWay(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    return process(arr, 0, aim);
}

public static int process(int[] arr, int index, int rest) {
    if (index == arr.length) { 
        return rest == 0 ? 1 : 0;
    }
    int ways = 0;
    for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
        ways += process(arr, index + 1, rest - (zhang * arr[index]));
    }
    return ways;
}

代码解释

递归中,base case 为下标来到最后时,如果剩余的钱数为 0 ,说明前面的组合刚好能够凑出 aim 值,记为有效的一种情况。

选和不选 体现在 zhang 从 0 开始,直到该张数的面值超过了剩余钱数 rest 为止。继续调用递归且下标 index + 1 ,剩余钱数也相应减少。最终返回总的方法数即可。


写出该暴力版的递归之后修改出动态规划版的就很容易了。

动态规划版

public static int dp(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    int N = arr.length;
    int[][] dp = new int[N + 1][aim + 1];
    dp[N][0] = 1;
    for (int index = N - 1; index >= 0; index--) {
        for (int rest = 0; rest <= aim; rest++) {
            int ways = 0;
            for (int zhang = 0; zhang * arr[index] <= rest; zhang++) {
                ways += dp[index + 1][rest - (zhang * arr[index])];
            }
            dp[index][rest] = ways;
        }
    }
    return dp[0][aim];
}

代码解释

可变的参数有两个:总的面值个数 N 和 剩余的目标钱数 rest 。因此,需要设置一个二维的 dp 表数组,由于 N, rest 的取值范围是 0~N 、0~aim ,因此数组大小设置为 dp[N + 1][aim + 1]

递归代码 index == arr.length 可知,初始只有 dp[N][0] 的值为 1 。

因为递归中只依赖 index + 1 的值,所以 dp 表倒着填写。

根据递归调用 process(arr, 0, aim) 可知最终返回 dp[0][aim]


之前的文章写到这里就结束了,但 这次不一样

观察递归的代码,发现竟然有 3 层 for 循环。为什么呢?

思考后发现, dp 表中的每个位置同样需要 枚举 后才能知道(之前的题目能够直接计算出来)。那有没有办法消掉这层枚举的 for 循环呢?答案是有的!

下面我们通过画 dp 表,探寻该动态规划应如何进一步优化。

假设此时剩余的总钱数 rest = 10,面值数 arr[i] = 3 。

一图胜千言~

通过枚举代码可知,arr[i][10] 的值,红色 = 黄色 + 3 个紫色。

黄色:不选面值为 3 的钱币时,rest 仍为 10,依赖下一格 i + 1。

紫色:分别选 1 张、2 张、3张…时,rest 对应每次减 3 ,且依赖下一格 i + 1 行。

稍加思考发现,蓝色的位置即 arr[i][10 - 3] 位置的值正是 3 个紫色的总和。那么,就可以将 红色 = 黄色 + 3 个紫色,改为 红色 = 黄色 + 蓝色,这样就不需要一直往前寻找了,减少一个 for 循环

最终优化版动态规划

public static int dp(int[] arr, int aim) {
    if (arr == null || arr.length == 0 || aim < 0) {
        return 0;
    }
    int N = arr.length;
    int[][] dp = new int[N + 1][aim + 1];
    dp[N][0] = 1;
    for (int index = N - 1; index >= 0; index--) {
        for (int rest = 0; rest <= aim; rest++) {
            // 先令 红色 等于 黄色部分
            dp[index][rest] = dp[index + 1][rest];
            // 如果没越界,说明前面有蓝色部分,再加上蓝色部分
            if (rest - arr[index] >= 0) {
                dp[index][rest] += dp[index][rest - arr[index]];
            }
        }
    }
    return dp[0][aim];
}

注意看越界的判断哦,黄色和蓝色分开加。这样就完成了最终版的动态规划~

回答最初的问题,为什么有了记忆化搜索还要写出严格的表依赖呢?

为了避免枚举行为多产生的 for 循环,有了 表依赖 才能找到如何 优化枚举

因此,前面学习的如何一步步的将暴力递归修改为严格表依赖动态规划的基础要打牢哦!还不会的赶快关注一下回顾前面的几篇文章吧!

~ 点赞 ~ 关注 ~ 不迷路 ~!!!

------------- 往期回顾 -------------
【算法 - 动态规划】原来写出动态规划如此简单!
【算法 - 动态规划】最长公共子序列问题
【算法 - 动态规划】最长回文子序列
【算法 - 动态规划】力扣 691. 贴纸拼词
【算法 - 动态规划】京东面试题 - 洗咖啡杯问题
【堆 - 专题】“加强堆” 解决 TopK 问题!
AC 此题,链表无敌!!!

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

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

相关文章

[面试]我们常说的负载均衡是什么东西?

什么是负载均衡 如果用户量很多, 服务器的流量也随之增大, 此时出现两个问题, 软件性能下降 容易出现单点故障 为了解决这些问题, 引入了集群化架构, 也就是把一个软件同时部署在多个服务器上 集群化架构出现的问题 架构改变后又出现了两个问题 如何将请求均匀的发送到多…

代码随想录day34--动态规划的应用2 | LeetCode343.整数拆分、LeetCode96.不同的二叉搜索树

LeetCode343.整数拆分 题目描述&#xff1a; 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。 示例 1: 输入: n 2 输出: 1 解释: 2 1 1, 1 1 1。…

mysql数据同步组件

# 要求&#xff1a; 支持实时同步&#xff0c; 支持增量同步&#xff0c; 不用写业务逻辑&#xff0c; 支持Mysql之间同步&#xff0c; 活跃度高。 # 可选组件&#xff1a;canal ,debezium, datax , Databus, Flinkx , Bifrost &#xff1a; Bifrost 特点&#xff1a; 1. …

Stable Diffusion 绘画入门教程(webui)-ControlNet(线稿约束)

上篇文章介绍了openpose&#xff0c;本篇文章介绍下线稿约束&#xff0c;关于线稿约束有好几个处理器都属于此类型&#xff0c;但是有一些区别。 包含&#xff1a; 1、Canny(硬边缘&#xff09;&#xff1a;识别线条比较多比较细&#xff0c;一般用于更大程度得还原照片 2、ML…

2024年西安市省级工业互联网平台申报条件材料、时间流程

一、申报条件 (一)申报单位须在西安市内登记注册,具有独立的法人资格和规范的财务管理制度,无不良信用记录,照章依法纳税。 (二)面向陕西省支持的35个工业重点产业方向内的中小企业提供服务,围绕各类型工业场景,加快企业内部信息化系统综合集成和云化改造,连接设备、软件、工…

【FPGA】线性反馈移位寄存器(LFSR)的Verilog实现

什么是移位寄存器 移位寄存器&#xff1a;是指多个寄存器并排相连&#xff0c;前一个寄存器的输出作为下一个寄存器的输入&#xff0c;寄存器中存放的数据在每个时钟周期向左或向右移动一位。 下面的右移移位寄存器因为左侧没有有效输入&#xff0c;所以在第4个时钟周期&…

转本考前如何调整心态

不少同学还在过年的氛围中还没走出来。 担忧自己成绩不进反退&#xff0c;又不知道该如何调整心态&#xff01;这个时候小编就有几点小建议给到各位考生。 *心态*情绪 良好的考试心态是没有固定的心态&#xff0c;对不同学习情况的学生来说&#xff0c;良好的考试心态是不一…

微信小程序--怎样在小程序中创建地图并渲染数据中的点标记

效果&#xff1a; 首先--创建地图 使用官方文档中的地图组件 map <map id"mapId" class"map" longitude"{{longitude}}" latitude"{{latitude}}" markers"{{markers}}"></map> 其中的属性值&#xff1a; lon…

爬虫入门四(抽屉半自动点赞、xpath使用、动作链、打码平台、scrapy框架介绍与安装及创建项目)

文章目录 一、抽屉半自动点赞二、xpath的使用三、动作链四、打码平台介绍超级鹰打码基本测试 五、自动登录超级鹰六、scrapy框架介绍安装创建爬虫项目 一、抽屉半自动点赞 登录抽屉账号保存cookiesimport timeimport jsonfrom selenium import webdriverfrom selenium.webdrive…

使用向量数据库pinecone构建应用04:混合搜索 Hybrid Search

Building Applications with Vector Databases 下面是这门课的学习笔记&#xff1a;https://www.deeplearning.ai/short-courses/building-applications-vector-databases/ Learn to create six exciting applications of vector databases and implement them using Pinecon…

啤酒:精酿啤酒与烧烤的热烈碰撞

在夏日的傍晚&#xff0c;烧烤与啤酒总是绝配。当Fendi Club啤酒遇上烧烤&#xff0c;它们将为我们带来一场热烈的美味碰撞。 Fendi Club啤酒&#xff0c;以其醇厚的口感和淡淡的麦芽香气而著称。这款啤酒在酿造过程中采用了特别的工艺&#xff0c;使得酒体呈现出诱人的金黄色&…

【JVM】线上一次fullGC排查思路

fullGC问题背景 监控告警发现&#xff0c;今天开始我们线上应用频繁出现fullGC&#xff0c;并且每次出现后磁盘都会被占满 查看监控 查看监控发现FULLGC的机器均为同一个机房的集器&#xff0c;并且该机房有线上error报错&#xff0c;数据库监控对应的时间点也有异常&#x…

如何在Linux搭建MinIO服务并实现无公网ip远程访问内网管理界面

文章目录 前言1. Docker 部署MinIO2. 本地访问MinIO3. Linux安装Cpolar4. 配置MinIO公网地址5. 远程访问MinIO管理界面6. 固定MinIO公网地址 前言 MinIO是一个开源的对象存储服务器&#xff0c;可以在各种环境中运行&#xff0c;例如本地、Docker容器、Kubernetes集群等。它兼…

Linux系统中前后端分离项目部署指南

目录 一.nginx安装以及字启动 解压nginx 一键安装4个依赖 安装nginx 启动 nginx 服务 开放端口号 并且在外部访问 设置nginx自启动 二.配置负载均衡 1.配置一个tomact 修改端口号 8081端口号 2.配置负载均衡 ​编辑 三.部署前后端分离项目 1.项目部署后端 ​编辑…

上海亚商投顾:沪指8连阳重新站上3000点 全市场逾百股涨停

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 沪指2月23日继续反弹&#xff0c;成功收复3000点大关&#xff0c;录得8连阳走势。AI概念持续活跃&#xff0c…

springboot219基于SpringBoot的网络海鲜市场系统的设计与实现

网络海鲜市场系统的设计与实现 摘 要 计算机网络发展到现在已经好几十年了&#xff0c;在理论上面已经有了很丰富的基础&#xff0c;并且在现实生活中也到处都在使用&#xff0c;可以说&#xff0c;经过几十年的发展&#xff0c;互联网技术已经把地域信息的隔阂给消除了&…

日志系统项目(2)项目实现(实用工具类、日志等级类、日志消息类、日志格式化输出类)

前面的文章中我们讲述了日志系统项目的前置知识点&#xff0c;再本文中我们将开始日志项目的细节实现。 日志系统框架设计 本项目实现的是一个多日志器日志系统&#xff0c;主要实现的功能是让程序员能够轻松的将程序运行日志信息落地到指定的位置&#xff0c;且支持同步与异…

20240223-2092.查找所有有秘密的人

题目要求 给你一个整数 n&#xff0c;表示有 n 个人&#xff0c;编号从 0 到 n - 1。你还给你一个 0 索引的二维整数数组 meetings&#xff0c;其中 meetings[i] [xi, yi, timei] 表示 xi 和 yi 在 timei 有一个会议。一个人可以同时参加多个会议。最后&#xff0c;给你一个整…

【Flutter/Android】运行到安卓手机上一直卡在 Running Gradle task ‘assembleDebug‘... 的终极解决办法

方法步骤简要 查看你的Flutter项目需要什么版本的 Gradle 插件&#xff1a; 下载这个插件&#xff1a; 方法一&#xff1a;浏览器输入&#xff1a;https://services.gradle.org/distributions/gradle-7.6.3-all.zip 方法二&#xff1a;去Gradle官网找对应的版本&#xff1a;h…

个人机器人课程中最棘手的问题

爱好 22年&#xff0c;观点。当然现在自动驾驶研究路径已经不是22年的模式了。 23年&#xff0c;观点&#xff1a; 一个热爱自动驾驶但妥妥外行之人的思考-2023-CSDN博客 前篇 不合格机器人工程讲师为何不分享成功的案例-CSDN博客 在这篇文章中&#xff0c;有一段&#x…