C++算法 —— 动态规划(1)斐波那契数列模型

文章目录

  • 1、动规思路简介
  • 2、第N个泰波那契数列
  • 3、三步问题
  • 4、使用最小花费爬楼梯
  • 5、解码方法
  • 6、动规分析总结


1、动规思路简介

动规的思路有五个步骤,且最好画图来理解细节,不要怕麻烦。当你开始画图,仔细阅读题时,学习中的沉浸感就体验到了。

状态表示
状态转移方程
初始化
填表顺序
返回值

动规一般会先创建一个数组,名字为dp,这个数组也叫dp表。通过一些操作,把dp表填满,其中一个值就是答案。dp数组的每一个元素都表明一种状态,我们的第一步就是先确定状态。

状态的确定可能通过题目要求来得知,可能通过经验 + 题目要求来得知,可能在分析过程中,发现的重复子问题来确定状态。还有别的方法来确定状态,但都大同小异,明白了动规,这些思路也会随之产生。状态的确定就是打算让dp[i]表示什么,这是最重要的一步。

状态转移方程,就是dp[i]等于什么,状态转移方程就是什么。像斐波那契数列,dp[i] = dp[i - 1] + dp[i - 2]。这是最难的一步。一开始,可能状态表示不正确,但不要紧,大胆制定状态,如果没法推出转移方程,没法得到结果,那这个状态表示就是错误的。所以状态表示和状态转移方程是相辅相成的,可以帮你检查自己的思路。

初始化,就是要填表,保证其不越界。像第一段所说,动规就是要填表。比如斐波那契数列,如果要填dp[1],那么我们可能需要dp[0]和dp[-1],这就出现越界了,所以为了防止越界,一开始就固定好前两个值,那么第三个值就是前两个值之和,也不会出现越界。

填表顺序。填当前状态的时候,所需要的状态应当已经计算过了。还是斐波那契数列,填dp[4]的时候,dp[3]和dp[2]应当都已经计算好了,那么dp[4]也就出来了,此时的顺序就是从左到右。

返回值,要看题目要求。

2、第N个泰波那契数列

1137. 第 N 个泰波那契数

在这里插入图片描述

泰波那契数列从T0开始,而不是从1开始,这也是和斐波那契数列不同的点,但本质上思路都很相似。接下来要用动态规划来解决问题。

在这个题目中,我们让dp表的每一个元素都存储一个泰波那契数列,0下标对应T0,1下标对应T1。为什么要确定成这样的状态?题目要求拿到Tn的值,并且也存在T0,和数组下标一致,那么我们最好就把所有的数都填上,然后n作为下标,dp[n]一下子就能拿到结果。

根据上面的解析,我们这样写

    int tribonacci(int n) {//1. 状态表示: dp[i]就是第i个泰波那契数//2. 状态转移方程: 题目给了,Tn+3 = Tn + Tn+1 + Tn+2//处理边界情况,n如果小于3,那么只能取0, 1, 2,也可以走下面循环,但不如创建dp表之前就返回对应的值,减少空间消耗if(n == 0) return 0;if(n == 1 || n == 2) return 1;vector<int> dp(n + 1);dp[0] = 0, dp[1] = dp[2] = 1;//3. 初始化for(int i = 3; i <= n; i++){dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];//4. 填表顺序,也体现了状态转移方程}return dp[n];//5. 返回值}

这道题还可以优化一下空间。动规里要优化空间通常用滚动数组。当一个状态仅需要前面若干个状态来确定时,就可以用滚动数组。N^2的空间复杂度可以优化成N。当dp[3]确定后,我们让前四个值设为abcd,起初a是dp[0],b是dp[1],c是dp[2],d是dp[3],要算dp[4]的时候,就让abcd往后挪一位,也就是a是dp[1],d是dp[4],然后d = a + b + c,求出dp[4],算dp[5]的时候,还是一样的操作,让a来到dp[2]的位置,d则是dp[5]。这几个变量我们可以创建一个小数组来存储,也可以就创建四个变量。当开始滚动时,我们让a = b, b = c, c = d这样就能滚动了,但不能反向赋值,也就是c = d, b = c, a = b因为b要的是c之前的值,而c已经被赋值成d了,所以不行。使用变量来求值后,我们就可以不需要dp表了,只用四个变量来求出结果

    int tribonacci(int n) {//1. 状态表示: dp[i]就是第i个泰波那契数//2. 状态转移方程: 题目给了,Tn+3 = Tn + Tn+1 + Tn+2//处理边界情况,n如果小于3,那么只能取0, 1, 2,也可以走下面循环,但不如创建dp表之前就返回对应的值,减少空间消耗if(n == 0) return 0;if(n == 1 || n == 2) return 1;int a = 0, b = 1, c = 1, d = 0;//3. 初始化for(int i = 3; i <= n; i++){d = a + b + c;//4. 顺序a = b; b = c; c = d;//循环结束后,d就是最终的值}return d;//5. 返回值}

3、三步问题

面试题 08.01. 三步问题

在这里插入图片描述

可1可2还可3,这个状态转移方程应当如何算?先不要着急,一步步看题。根据题目,我们可以知道状态是到达i位置时,总共有几种方式,dp[i]记录着方式的个数。接下来就是找状态转移方程。虽然题目有三种计算,但我们不妨算几个值来看看,n = 1,2,3,4时,分别是1,2,4,7,如果仔细去加每一个n值的方法个数,会发现每一个n值就是前面3个n值的和,比如1 +2 + 4 = 7,所以这个题是有规律的,那么它的状态转移方程就是dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]。从1开始,dp[1],dp[2],dp[3]就分别初始化为1,2,4。填表顺序就是从左到右。返回值就是dp[n]。

由于本题中数目会过大,要取模,如果加完3个数再取模会不通过,要加完2个数就取模,然后再整体取模。

    int waysToStep(int n) {if(n == 1 || n == 2) return n;if(n == 3) return 4;const int MOD = 1e9 + 7;vector<int> dp(n + 1);dp[1] = 1, dp[2] = 2, dp[3] = 4;for(int i = 4; i <= n; i++){dp[i] = ((dp[i - 1] + dp[i - 2]) % MOD + dp[i - 3]) % MOD;}return dp[n];}

当然这个题也可以空间优化,和上一个题一样。

    int waysToStep(int n) {if(n == 1 || n == 2) return n;if(n == 3) return 4;const int MOD = 1e9 + 7;int a = 1, b = 2, c = 4, d = 0;for(int i = 4; i <= n; i++){d = ((a + b) % MOD + c) % MOD;a = b; b = c; c = d;}return d;}

4、使用最小花费爬楼梯

746. 使用最小花费爬楼梯

在这里插入图片描述

10,15,20,当你在10的位置,你会花费10块钱,往后走一次或者两次,当你在15的位置,你会花费15块钱,向后走一次或两次。但给的数组中最右边的元素不是楼顶,所有元素都是楼梯,楼顶在最后一个元素的下一个位置,比如示例1,在15的位置,花费15块钱,一次性跨2个楼梯,就到达了楼顶。

这道题的状态表示是什么样的?像一维dp数组,根据以上两道题来看,都是以i位置结尾来做状态表示,这道题也是一样,计算到i位置处最少需要的钱,所以dp[1],dp[2]就是到达1,2位置时最少的花费。

状态转移方程如何确定?看到现在,我们可以总结出一个规律,利用之前或者之后的状态,推导出dp[i]的值,比如dp[i]由dp[i - 1],dp[i - 2]等或者dp[i + 1], dp[i + 2]来确定。这道题来看,dp[i]要么从i - 1处走1步到达i,要么i - 2处走2步到达i,两种情况比较大小来得到dp[i]的值,而dp[i - 1],dp[i - 2]又是之前推导过来的。所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])。

初始化的时候,要不越界,需要初始化最前面的2个位置,根据题目,我们可以从0位置或者1位置开始,那么这两个位置就初始化成0即可。填表顺序是从左到右。返回值是dp[i]。

根据以上分析,写出代码

    int minCostClimbingStairs(vector<int>& cost) {int n = cost.size();vector<int> dp(n + 1);for(int i = 2; i <= n; i++){dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);}return dp[n];}

这道题还可以用另一种状态表示来写。上面是yii位置为结尾,这次是以i位置为起点。此时的dp[i]是从i位置出发,到达楼顶的最小花费,这个i从0开始。

这次的状态转移方程如何确定?从i位置出发,可以走一步,走两步,所以也分成2种情况,走一步,从i + 1位置到终点;走两步,从i + 2位置到终点,然后再算i + 1或者i + 2处算最小花费。第一种情况是dp[i + 1] + cost[i],第二种情况是dp[i + 2] + cost[i],去两者之小。

这次的初始化应该如何做?上一次是取dp[i - 1]和dp[i - 2],我们初始化最左边的两个值,现在是取dp[i + 1]和dp[i + 2],那最容易确定的就是最右边的两个值,dp[n - 1]和dp[n - 2],它们俩就分别是花费当前位置的钱即可。这次的填表顺序就是从右到左。返回值则是dp[0]和dp[1]的最小值,楼顶是n位置处。

思路其实相似,不过就是反过来了。这次开辟的数组就不用n + 1了,之前我们需要算到dp[n],而现在n - 1处是起始点。

    int minCostClimbingStairs(vector<int>& cost) {int n = cost.size();/*vector<int> dp(n + 1);for(int i = 2; i <= n; i++){dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);}return dp[n];*/vector<int> dp(n);dp[n - 1] = cost[n - 1], dp[n - 2] = cost[n - 2];for(int i = n - 3; i >= 0; i--){dp[i] = min(dp[i + 1], dp[i + 2]) + cost[i];//因为都要加一个cost[i],所以就提出来}return min(dp[0], dp[1]);}

5、解码方法

91. 解码方法

在这里插入图片描述
在这里插入图片描述

这道题还是可以用上面的分析。状态表示我们先表示为以i位置结尾。字符串有i个字符,i位置的数字应当是从第一个字符开始到i位置的字符总共能编码的个数,所以dp[i]是以i位置为结尾时,解码方法的总数。

状态转移方程如何确定?根据上面那些题的分析,我们知道要根据最近的一步,来划分问题,i位置处自己可能可以编码,i - 1和i位置处也可能可以编码,而由于是以i位置为结尾,所以i和i + 1就不管,不能i - 2位置组合起来编码是因为字母所代表的数字最多是两位数,所以这点就可以帮助我们确定方程。

现在是两种情况,dp[i]单独解码,dp[i- 1]和dp[i]组合解码。每个情况都有成功失败,如果dp[i]可以解码成功,那就说明从0到i - 1位置的所有编码方案后面加上一个i位置的字符就可以了,所以此时dp[i]的方案数就是dp[i - 1]的方案数。如果dp[i]单独编码失败,那么前面所有的可解码方案就全失败了,那么就是0了。

dp[i - 1]和dp[i]解码,也有成功失败。如果成功,那么i - 1处字符对应的数字乘10 + i处字符对应的数字应当>= 10 && <= 26,因为题目中说了不可能出现06这种情况,所以只能是一个正常的两位数,10及以上。把i - 1和i看作一个整体,这时候就相当于dp[i - 2]的所有方案后面都加上两个字符即可,所以就是dp[i - 2]的方案数。如果失败,也是一样,前面的全失败了,为0。

根据以上的分析,dp[i] = dp[i - 1] + dp[i - 2],但这两个并不是一定都加得上,可能为0。

初始化应当如何做?有两个方法。dp[i]既然是由前两个位置决定的,那么初始化时就得考虑一下dp[0]和dp[1],dp[0]要么是1,要么是0,它只有一个字符,dp[1]代表2种字符,2个字符都能单独解码是1种情况,2个字符组合才能解码是另一种情况,满足其中一个就是1,两个都满足就是2,都不满足就是0,所以dp[1]是0,1,2三个情况。

填表顺序是从左到右。而返回值,我们要解码到最后一个位置,所以应当是dp[n - 1]。

    int numDecodings(string s) {int n = s.size();vector<int> dp(n);dp[0] = s[0] != '0' ? 1 : 0;//判断dp[0]能否单独解码if(n == 1) return dp[0];//处理边界情况if(s[0] != '0' && s[1] != '0') dp[1] += 1;//判断dp[0]dp[1]能否都单独解码int t = (s[0] - '0') * 10 + s[1] - '0';if(t >= 10 && t <= 26) dp[1] += 1;//判断dp[0]dp[1]组合能否解码for(int i = 2; i < n; i++){if(s[i] != '0') dp[i] += dp[i - 1];//判断能否单独解码int t = (s[i - 1] - '0') * 10 + s[i] - '0';//判断能否组合解码if(t >= 10 && t <= 26) dp[i] += dp[i - 2];}return dp[n - 1];}

现在写一下另一种初始化方法。上面的代码可以看出,有段代码是重复的

        if(s[0] != '0' && s[1] != '0') dp[1] += 1;//判断dp[0]dp[1]能否都单独解码int t = (s[0] - '0') * 10 + s[1] - '0';if(t >= 10 && t <= 26) dp[1] += 1;//判断dp[0]dp[1]组合能否解码if(s[i] != '0') dp[i] += dp[i - 1];//判断能否单独解码int t = (s[i - 1] - '0') * 10 + s[i] - '0';//判断能否组合解码if(t >= 10 && t <= 26) dp[i] += dp[i - 2];

之前的dp表示[0, n - 1],现在我们给它扩充一个元素,变成[0, n],那么之前的dp[1],就相当于新表的dp[2],之前的dp[0]就是现在的dp[1],之前的dp[n - 1]就是新的dp[n],新表的dp[0]就是一个虚拟节点,用来更方便的初始化,下面能看到它起作用的地方。这个方法有一些注意的点。一个是要保证字符串中1位置处的字符能对应到dp[2],也就是保证映射关系;另一个就是新表中的dp[0],如何初始化它来保证结果正确。

我们要让循环从i = 2开始,使用相同的判断方法,dp[1]不是问题,而dp[0],对于dp[2]来说,就是dp[i - 2],那么如果原字符串中0和1位置,也就是新表的1和2位置处的字符能够组合编码,那就应该+dp[i - 2],也就是+1,所以dp[0]应当初始化为1。

字符串从0位置开始走,判断0位置的字符,对应的新表的1位置,i是在新表中走的,也就是此时i = 1,那应当判断s[1 - 1]的位置,也就是s[i - 1]的位置,就可以保证映射关系了。

   int numDecodings(string s) {int n = s.size();//优化vector<int> dp(n + 1);dp[0] = 1;dp[1] = s[1 - 1] != '0' ? 1 : 0;//s[0]for(int i = 2; i <= n; i++){if(s[i - 1] != '0') dp[i] += dp[i - 1];//判断能否单独解码int t = (s[i - 2] - '0') * 10 + s[i - 1] - '0';//判断能否组合解码if(t >= 10 && t <= 26) dp[i] += dp[i - 2];}return  dp[n];}

6、动规分析总结

状态的表示通常是以某个位置为结尾或者起点
状态转移方程的确定需要分析最近的一步
初始化的第二种方法是设立虚拟节点,注意事项就是如何初始化虚拟节点的数值来保证填表的结果是正确的,以及新表和旧表的映射关系的维护
填表顺序要看分析而决定

结束。

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

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

相关文章

uni-app之android离线打包

一 AndroidStudio创建项目 1.1&#xff0c;上一节演示了uni-app云打包&#xff0c;下面演示怎样androidStudio离线打包。在AndroidStudio里面新建空项目 1.2&#xff0c;下载uni-app离线SDK&#xff0c;离线SDK主要用于App本地离线打包及扩展原生能力&#xff0c;SDK下载链接h…

k8s使用ECK(2.4)形式部署elasticsearch+kibana-http协议

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、准备elasticsearch-cluster.yaml二、部署并测试总结 前言 之前写了eck2.4部署eskibana&#xff0c;默认的话是https协议的&#xff0c;这里写一个使用http…

开源照片管理服务LibrePhotos

本文是为了解决网友 赵云遇到的问题&#xff0c;顺便折腾的。虽然软件跑起来了&#xff0c;但是他遇到的问题&#xff0c;超出了老苏的认知。当然最终问题还是得到了解决&#xff0c;不过与 LibrePhotos 无关&#xff1b; 什么是 LibrePhotos ? LibrePhotos 是一个自托管的开源…

微服务设计和高并发实践

文章目录 1、微服务的设计原则1.1、服务拆分方法1.2、微服务的设计原则1.3、微服务架构 2、高并发系统的一些优化经验2.1、提高性能2.1.1、数据库优化2.1.2、使用缓存2.1.3、服务调用优化2.1.4、动静分离2.1.5、数据库读写分离 2.2、服务高可用2.2.1、限流和服务降级2.2.2、隔离…

美团 Flink 资源调度优化实践

摘要&#xff1a;本文整理自美团数据平台计算引擎组工程师冯斐&#xff0c;在 Flink Forward Asia 2022 生产实践专场的分享。本篇内容主要分为四个部分&#xff1a; 相关背景和问题解决思路分析资源调度优化实践后续规划 点击查看原文视频 & 演讲PPT 一、相关背景和问题 在…

企业数字化转型的关键技术有哪些?_光点科技

随着科技的不断进步和信息技术的快速发展&#xff0c;企业数字化转型已经成为保持竞争力和适应市场变化的关键举措。在这个数字化时代&#xff0c;企业需要借助先进的技术来优化业务流程、提升效率&#xff0c;以及更好地满足客户需求。以下是企业数字化转型过程中的关键技术。…

list根据对象中某个字段属性去重Java流实现

list根据对象中某个字段属性去重Java流实现&#xff1f; 在Java的流(Stream)中&#xff0c;你可以使用distinct方法来实现根据对象中某个字段属性去重的功能。要实现这个功能&#xff0c;你需要重写对象的hashCode和equals方法&#xff0c;以确保相同字段属性的对象被认为是相…

因为axios请求后端,接收不到token的问引出的问题

vue axios请求后端接受不到token的问题。 相关概念 什么是跨域&#xff1f; 跨域指的是在浏览器环境下&#xff0c;当发起请求的域&#xff08;或者网站&#xff09;与请求的资源所在的域之间存在协议、主机或端口中的任何一个条件不同的情况。换句话说&#xff0c;只要协议、…

服务器数据恢复- Ext4文件系统分区挂载报错的数据恢复案例

Ext4文件系统相关概念&#xff1a; 块组&#xff1a;Ext4文件系统的空间被划分为若干个块组&#xff0c;每个块组内的结构大致相同。 块组描述符表&#xff1a;每个块组都对应一个块组描述符&#xff0c;这些块组描述符统一放在文件系统的前部&#xff0c;称为块组描述符表。每…

ssm+vue在线购书商城系统源码和论文

ssmvue在线购书商城系统源码和论文119 开发工具&#xff1a;idea 数据库mysql5.7 数据库链接工具&#xff1a;navcat,小海豚等 技术&#xff1a;ssm 摘 要 本课题是根据用户的需要以及网络的优势建立的一个在线购书商城系统&#xff0c;来满足用户网络查看、购买所需图书…

设计模式-9--迭代器模式(Iterator Pattern)

一、什么是迭代器模式 迭代器模式&#xff08;Iterator Pattern&#xff09;是一种行为型设计模式&#xff0c;用于提供一种统一的方式来访问一个聚合对象中的各个元素&#xff0c;而不需要暴露该聚合对象的内部结构。迭代器模式将遍历集合的责任从集合对象中分离出来&#xf…

目标检测YOLO算法,先从yolov1开始

学习资源 有一套配套的学习资料&#xff0c;才能让我们的学习事半功倍。 yolov1论文原址&#xff1a;You Only Look Once: Unified, Real-Time Object Detection 代码地址&#xff1a;darknet: Convolutional Neural Networks (github.com) 深度学习经典检测方法 one-stag…

比较opencv,pillow,matplotlib,skimage读取图像的速度比

上面这些库都被广泛用于图像处理和计算机视觉任务&#xff1b; 不同的图像读取库&#xff08;OpenCV&#xff0c;Pillow&#xff0c;matplotlib和skimage&#xff09;的读取速度&#xff0c;是怎么样的一个情况&#xff1f; 下面分别从读取速度&#xff0c;以及转换到RGB通道…

Linux环境基础开发工具

xshellssh xshell--充当客户端&#xff0c;提供远程登录服务 yum 背景知识 在Linux下安装软件, 一个通常的办法是下载到程序的源代码, 并进行编译, 得到可执行程序. 但是这样太麻烦了, 于是有些人把一些常用的软件提前编译好, 做成软件包(可以理解成windows上的安装程序)放…

Llama模型结构解析(源码阅读)

目录 1. LlamaModel整体结构流程图2. LlamaRMSNorm3. LlamaMLP4. LlamaRotaryEmbedding 参考资料&#xff1a; https://zhuanlan.zhihu.com/p/636784644 https://spaces.ac.cn/archives/8265 ——《Transformer升级之路&#xff1a;2、博采众长的旋转式位置编码》 前言&#x…

设计模式入门(二)观察者模式

设计模式入门 本系列所有内容参考自《HeadFirst设计模式》。因为书中的代码是采用java语言写的&#xff0c;博主这里用C语言改写。 这里采用讲故事的方式进行讲解。若有错误之处&#xff0c;非常欢迎大家指导。 设计模式&#xff1a;模式不是代码&#xff0c;而针对设计问题的…

深入理解作用域、作用域链和闭包

​ &#x1f3ac; 岸边的风&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! ​ 目录 &#x1f4da; 前言 &#x1f4d8; 1. 词法作用域 &#x1f4d6; 1.2 示例 &#x1f4d6; 1.3 词法作用域的…

Python学习教程:进程的调度

前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 要想多个进程交替运行&#xff0c;操作系统必须对这些进程进行调度&#xff0c; 这个调度也不是随即进行的&#xff0c;而是需要遵循一定的法则&#xff0c;由此就有了进程的调度算法。 python更多源码/资料/解答/教程等 …

iOS练手项目知识点汇总

基础理解篇 Objective-C是一种面向对象的编程语言&#xff0c;它支持元编程。元编程是指编写程序来生成或操纵其他程序的技术。 Objective-C中&#xff0c;元编程可以使用Objective-C的动态特性来实现。例如可以使用Objective-C的运行时函数来动态地创建类、添加属性和方法等等…

NPM 常用命令(二)

目录 1、npm bugs 1.1 配置 browser registry 2、npm cache 2.1 概要 2.2 详情 2.3 关于缓存设计的说明 2.4 配置 cache 3、 npm ci 3.1 描述 3.2 配置 install-strategy legacy-bundling global-style omit strict-peer-deps foreground-scripts ignore-s…