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.mzph.cn/news/66917.shtml

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

相关文章

大数据可视化大屏实战项目(10)无线网络大数据平台—HTML+CSS+JS【源码在文末】(可用于比赛项目或者作业参考中)

大数据可视化大屏实战项目&#xff08;10&#xff09;无线网络大数据平台—HTMLCSSJS【源码在文末】&#xff08;可用于比赛项目或者作业参考中&#x1f415;&#x1f415;&#x1f415;&#xff09; 一&#xff0c;项目概览 ☞☞☞☞☞☞项目演示链接&#xff1a;http://59.…

Langchain使用介绍之outparser 和memory

上一篇博客中对Langchain中prompt进行了详细的介绍&#xff0c;此篇博客将介绍Langchain中的outparser和memory。当调用大模型生成内容时&#xff0c;返回的内容默认是string类型&#xff0c;这对于我们获取response中的某些内容信息可能会带来障碍&#xff0c;例如返回的内容本…

MyBatis中至关重要的关系映射----全方面介绍

目录 一 对于映射的概念 1.1 三种关系映射 1.2 resultType与resultMap的区别 resultType&#xff1a; resultMap&#xff1a; 二&#xff0c;一对一关联查询 2.1 嵌套结果集编写 2.2 案例演示 三&#xff0c;一对多关联查询 3.1 嵌套结果集编写 3.3 案例演示 四&…

统计教程|PASS实现两计量指标的的Pearson相关分析时的样本量估计

在临床研究中我们经常会研究两个观察指标间是否有联系&#xff0c;如人的身高与体重、体温与脉搏次数、药物剂量与反应等&#xff0c;相关分析就是研究观察指标间相关关系的统计方法&#xff0c;由于不同资料类型其分析方法有所差异&#xff0c;故本节主要讲解的是定量资料的线…

综合实训-------成绩管理系统 V1.1

综合实训-------成绩管理系统 V1.1 1、一维数组数据double 2、我们用元素的位置来当学号。 1、录入数据 【5个数据】或【通过文件的方式取数据】 2、显示数据 3、添加一条记录 4、修改一条记录 5、删除一条记录 6、查找一条记录。【输入学号&#xff0c;显示成绩】 7、统计。【…

使用Python进行Base64编码和解码

假设您有一个想要通过网络传输的二进制图像文件。您很惊讶对方没有正确接收该文件 - 该文件只是包含奇怪的字符&#xff01; 嗯&#xff0c;您似乎试图以原始位和字节格式发送文件&#xff0c;而所使用的媒体是为流文本而设计的。 避免此类问题的解决方法是什么&#xff1f;答…

说说你了解的 Nginx

分析&回答 nginx性能数据 高并发连接: 官方称单节点支持5万并发连接数&#xff0c;实际生产环境能够承受2-3万并发。内存消耗少: 在3万并发连接下&#xff0c;开启10个nginx进程仅消耗150M内存 (15M10150M) 1. 正向、反向代理 所谓“代理”&#xff0c;是指在内网边缘 …

C++信息学奥赛1177:奇数单增序列

#include<bits/stdc.h> using namespace std; int main(){int n;cin>>n; // 输入整数 n&#xff0c;表示数组的大小int arr[n]; // 创建大小为 n 的整型数组for(int i0;i<n;i) cin>>arr[i]; // 输入数组元素for(int i0;i<n;i){ // 对数组进行冒泡排序f…

uniapp-秋云图表 ucharts echarts 对比与关系

科普&#xff1a; 秋云图表库&#xff0c;包含二种配置属性对应二种js配置文件。 一种是 &#xff1a;echarts.js,一种是 &#xff1a; ucharts。 二者的配置属性不一样&#xff01; ucharts和echarts对比 ucharts和echarts都是用于数据可视化的开源JavaScript库&#xff0c;它…

gRPC-Gateway 快速实战

今天来分享一波 gRPC-Gateway &#xff0c; 之前咱们有分享过什么是 gRPC 及其使用方式&#xff0c;可以看看这些关于 gRPC 的历史文章&#xff1a; gRPC介绍 gRPC 客户端调用服务端需要连接池吗&#xff1f; gRPC的拦截器 gRPC的认证 分享一下 gRPC- HTTP网关 I 今天主要是分…

Python学习教程:进程的调度

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

keepalived 主备都存在vip, keepalived主备跨网段配置;keepalived主备服务器不在同一个网段怎么配置

keepalived 主备都有vip问题&#xff1b;主备服务器不在同一个网段怎么配置 主机&#xff1a;128.192.10.10 备机&#xff1a;128.192.11.11 备机&#xff1a;128.192.22.22 # keepalived的配置文件增加如下配置即可实现 # 主机&#xff1a;128.192.10.10 vrrp_instance VI_1 {…

uni-app 之 安装uView,安装scss/sass编译

uni-app 之 安装uView&#xff0c;安装scss/sass编译 image.png image.png image.png 点击HBuilder X 顶部&#xff0c;工具&#xff0c;插件安装&#xff0c;安装新插件 image.png image.png 安装成功&#xff01; 注意&#xff0c;一定要先登录才可以安装 image.png 1. 引…

【python】—— 函数详解

前言&#xff1a; 本期&#xff0c;我们将要讲解的是有关python中函数的相关知识&#xff01;&#xff01;&#xff01; 目录 &#xff08;一&#xff09;函数是什么 &#xff08;二&#xff09;语法格式 &#xff08;三&#xff09;函数参数 &#xff08;四&#xff09;函…

webpack实战:某网站JS逆向分析

文章目录 1. 写在前面2. 抓包分析3. 扣加密代码 1. 写在前面 好的逆向能够帮助我们了解加密实现&#xff0c;然后根据加密方式&#xff08;md5,base64,res,des,rsa…)还原加密算法的过程。可以看看我之前的这篇文章&#xff1a;快速定位查找加密方式特征与技巧 目标站点&#…

什么是rem单位和em单位?它们有什么区别?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ rem 和 em 单位⭐ rem 单位&#xff08;Root Em&#xff09;⭐ em 单位⭐ 区别总结⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订阅本专栏哦 几何带你启航前端之旅 欢迎来到前端入…

java八股文面试[多线程]——newWorkStealingPool

newWorkStealingPool是什么&#xff1f; newWorkStealingPool简单翻译是任务窃取线程池。 newWorkStealingPool 是Java8添加的线程池。和别的4种不同&#xff0c;它用的是ForkJoinPool。 使用ForkJoinPool的好处是&#xff0c;把1个任务拆分成多个“小任务”&#xff0c;把这…

UDP 广播

一、UDP 通信图解 UDP通信、本地套接字_呵呵哒(&#xffe3;▽&#xffe3;)"的博客-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/132523536?spm1001.2014.3001.5501 #include <sys/types.h> #include <sys/socket > ssize_t sendto(in…

hadoop学习:mapreduce的wordcount时候,继承mapper没有对应的mapreduce的包

踩坑描述&#xff1a;在学习 hadoop 的时候使用hadoop 下的 mapreduce&#xff0c;却发现没有 mapreduce。 第一反应就是去看看 maven 的路径对不对 settings——》搜索框搜索 maven 检查一下 Maven 路径对不对 OK 这里是对的 那么是不是依赖下载失败导致 mapreduce 没下下…

MySQL总复习

目录 登录 显示数据库 创建数据库 删除数据库 使用数据库 创建表 添加数据表数据 查询表 添加数据表多条数据 查询表中某数据 增insert 删delete 改update 查select ​ where like ​编辑 范围查找 order by 聚合函数 count max min sum avg g…