C++算法 —— 动态规划(1)斐波那契数列模型(包含动规思路总介绍)

文章目录

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


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]。但这个方法有一些注意的点。一个是要保证字符串中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];}

结束。

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

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

相关文章

Linux常用命令——cupsdisable命令

在线Linux命令查询工具 cupsdisable 停止指定的打印机 补充说明 cupsdisable命令用于停止指定的打印机。 语法 cupsdisable(选项)(参数)选项 -E&#xff1a;当连接到服务器时强制使用加密&#xff1b; -U&#xff1a;指定连接服务器时使用的用户名&#xff1b; -u&#…

git的常用命令

初始化git&#xff0c;以及如何提交代码 1、配置用户信息 git config --global user.name zhangsan # 设置用户签名 git config --global user.email zhangsanqq.com # 设置用户邮箱&#xff08;不会验证&#xff0c;可以不存在&#xff09;1.1、查看是否已经添加用户配置 在…

长城网络靶场,第一题笔记

黑客使用了哪款扫描工具对论坛进行了扫描&#xff1f;&#xff08;小写简称&#xff09; 第一关&#xff0c;第三小题的答案是awvs 思路是先统计查询 然后过滤ip检查流量 过滤语句&#xff1a;tcp and ip.addr ip 114.240179.133没有 第二个101.36.79.67 之后找到了一个…

可扩展的Blender插件开发汇总

成熟的 Blender 3D 插件是令人惊奇的事情。作为 Python 和 Blender 的新手,我经常发现自己被社区中的人们创造的强大的东西弄得目瞪口呆。坦率地说,其中一些包看起来有点神奇,当自我怀疑或冒名顶替综合症的唠叨声音被打破时,很容易想到“如果有人能做出可以做xxx的东西就好…

AI:06-基于OpenCV的二维码识别技术的研究

二维码作为一种广泛应用于信息传递和识别的技术,具有识别速度快、容错率高等优点。本文探讨如何利用OpenCV库实现二维码的快速、准确识别,通过多处代码实例展示技术深度。 二维码作为一种矩阵型的条码,广泛应用于各个领域,如商品追溯、移动支付、活动签到等。二维码的快速…

初识Kafka

kafka 第一章、初识Kafka 原先&#xff1a; kafka&#xff0c;由LinkedIn公司采用Scala语言开发的一个多分区&#xff0c;多副本&#xff0c;基于Zookeeper协调的分布式消息系统&#xff0c;被捐献给Apache基金会。 现在 分布式流式处理平台。 高吞吐 可持久化 可水平扩展 …

[SWPUCTF 2022]——Web方向 详细Writeup

SWPUCTF 2022 ez_ez_php 打开环境得到源码 <?php error_reporting(0); if (isset($_GET[file])) {if ( substr($_GET["file"], 0, 3) "php" ) {echo "Nice!!!";include($_GET["file"]);} else {echo "Hacker!!";} }e…

01-为什么阿里巴巴强制要求使用包装类型定义属性?【Java面试题总结】

1.为什么阿里巴巴强制要求使用包装类型定义属性&#xff1f; 我认为主要有以下几个方面的原因&#xff1a; 默认值问题&#xff1a;使用基本数据类型定义属性时&#xff0c;如果没有给属性赋初始值&#xff0c;会使用默认值&#xff08;如 int 的默认值为 0&#xff09;&…

linux安装minio以及springboot整合使用

文章目录 1.linux安装minio2.springboot整合minio使用 1.linux安装minio 1.新建文件夹 mkdir /home/minio # 数据文件夹 mkdir /home/minio/data # 创建日志文件夹 mkdir /home/minio/log2.进入文件夹 cd /home/minio3.下载minio&#xff0c;链接可从官网获取 wget https://…

【Terraform学习】使用 Terraform创建DynamoDB添加项目(Terraform-AWS最佳实战学习)

本站以分享各种运维经验和运维所需要的技能为主 《python》&#xff1a;python零基础入门学习 《shell》&#xff1a;shell学习 《terraform》持续更新中&#xff1a;terraform_Aws学习零基础入门到最佳实战 《k8》暂未更新 《docker学习》暂未更新 《ceph学习》ceph日常问题解…

4.1 链式栈StackT

C关键词&#xff1a;内部类/模板类/头插 C自学精简教程 目录(必读) C数据结构与算法实现&#xff08;目录&#xff09; 栈的内存结构 空栈&#xff1a; 有一个元素的栈&#xff1a; 多个元素的栈&#xff1a; 成员函数说明 0 clear 清空栈 clear 函数负责将栈的对内存释放…

CCF HPC China2023|澎峰科技:使能先进计算,赋能行业应用

CCF HPC China2023圆满落幕&#xff01; 桂秋八月&#xff0c;为期三天的中国高性能计算领域最高规格盛会——2023CCF全球高性能计算学术年会&#xff08;HPC China&#xff09;在青岛红岛国际展览中心圆满落幕。行业超算大咖、顶级学界精英、先锋企业领袖参会者齐聚山东青岛&a…

联邦学习FedAvg-基于去中心化数据的深度网络高效通信学习

随着计算机算力的提升&#xff0c;机器学习作为海量数据的分析处理技术&#xff0c;已经广泛服务于人类社会。 然而&#xff0c;机器学习技术的发展过程中面临两大挑战&#xff1a;一是数据安全难以得到保障&#xff0c;隐私泄露问题亟待解决&#xff1b;二是网络安全隔离和行业…

uniapp 配置网络请求并使用请求轮播图

由于平台的限制&#xff0c;小程序项目中不支持 axios&#xff0c;而且原生的 wx.request() API 功能较为简单&#xff0c;不支持拦截器等全局定制的功能。因此&#xff0c;建议在 uni-app 项目中使用 escook/request-miniprogram 第三方包发起网络数据请求。 官方文档&#xf…

【C++入门】命名空间、缺省参数、函数重载、引用、内联函数

​&#x1f47b;内容专栏&#xff1a; C/C编程 &#x1f428;本文概括&#xff1a; C入门学习必备语法 &#x1f43c;本文作者&#xff1a; 阿四啊 &#x1f438;发布时间&#xff1a;2023.9.3 前言 C是在C的基础之上&#xff0c;容纳进去了面向对象编程思想&#xff0c;并增加…

大数据-玩转数据-Flink窗口函数

一、Flink窗口函数 前面指定了窗口的分配器, 接着我们需要来指定如何计算, 这事由window function来负责. 一旦窗口关闭, window function 去计算处理窗口中的每个元素. window function 可以是ReduceFunction,AggregateFunction,or ProcessWindowFunction中的任意一种. Reduc…

打包个七夕exe玩玩

前段时间七夕 当别的哥们都在酒店不要不要的时候 身为程序员的我 还在单位群收到收到 正好后来看到大佬些的这个 https://www.52pojie.cn/thread-1823963-1-1.html 这个贱 我必须要犯&#xff0c;可是我也不能直接给他装个python吧 多麻烦 就这几个弹窗 好low 加上bgm 再打包成…

Nexus仓库介绍以及maven deploy配置

一 、Nexus仓库介绍 首先介绍一下Nexus的四个仓库的结构&#xff1a; maven-central 代理仓库&#xff0c;代理了maven的中央仓库&#xff1a;https://repo1.maven.org/maven2/&#xff1b; maven-public 仓库组&#xff0c;另外三个仓库都归属于这个组&#xff0c;所以我们的…

贝叶斯神经网络 - 捕捉现实世界的不确定性

贝叶斯神经网络 - 捕捉现实世界的不确定性 Bayesian Neural Networks 生活本质上是不确定性和概率性的&#xff0c;贝叶斯神经网络 (BNN) 旨在捕获和量化这种不确定性 在许多现实世界的应用中&#xff0c;仅仅做出预测是不够的&#xff1b;您还想知道您对该预测的信心有多大。例…

第2章 Linux多进程开发 2.18 内存映射

内存映射&#xff1a;可以进行进程间的通信 1.如果对mmap的返回值(ptr)做操作(ptr), munmap是否能够成功? void * ptr mmap(…); ptr; 可以对其进行操作 munmap(ptr, len); // 错误,要保存地址 2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样? 错…