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

news2024/11/29 0:38:30

文章目录

  • 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.coloradmin.cn/o/967886.html

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

相关文章

绩效被打了 C 就要走人吗?

文章目录 前言一、什么是绩效&#xff1f;二、上级的评价是客观的吗&#xff1f;三、工作必须要和上级搞好关系吗&#xff1f;四、自我评价要写多少字&#xff1f;五、绩效低的话会被开除吗&#xff1f;六、低绩效钱会少吗?七、有关星球提问统一回复 前言 今天是英雄算法联盟九…

横向对比 npm、pnpm、tnpm、yarn 优缺点

前端工程化是现代Web开发中不可或缺的一环&#xff0c;它的出现极大地提升了前端开发的效率和质量。 在过去&#xff0c;前端开发依赖于手动管理文件和依赖&#xff0c;这导致了许多问题&#xff0c;如版本冲突、依赖混乱和构建繁琐等。而今&#xff0c;随着众多前端工程化工具…

荣耀10 关闭自动更新

1.电脑端下载安装手机助手 2.下载adb工具https://adbdownload.com/ 3.解压后&#xff0c;打开工具所在目录&#xff0c;在地址栏输入cmd然后回车 4.进入这黑麻麻的工具&#xff0c;输入adb devices然后回车&#xff0c;确认连接到手机 输入命令 adb shell pm disable-user com…

理解 std::thread::detach

C多线程并发编程入门&#xff08;目录&#xff09; detach 的作用 detach 的作用就是让线程独自执行。 为何需要 detach 在 理解 std::thread::join 中&#xff0c;我们看到了&#xff0c;如果所有线程都是一开始就在 main 函数中创建好的&#xff0c;那么只需要有一个 joi…

香橙派Orangepi Zero2 刷机步骤

目录 1.香橙派Orangepi Zero2简介 2.刷机 2.1物料准备 2.2 格式化SD卡 2.3 烧录镜像到SD卡 2.4 安装SD卡到Orangepi 2.5 连接Pi电源 2.6 MobaXterm 串口登陆Orangepi 2.6.1 连线示意图 2.6.2 MobaXterm 使用 2.6.3修改登陆密码 2.6.4 网络配置 2.7 SSH登陆开发版…

3D封装技术发展

长期以来&#xff0c;芯片制程微缩技术一直驱动着摩尔定律的延续。从1987年的1um制程到2015年的14nm制程&#xff0c;芯片制程迭代速度一直遵循摩尔定律的规律&#xff0c;即芯片上可以容纳的晶体管数目在大约每经过18个月到24个月便会增加一倍。但2015年以后&#xff0c;芯片制…

手把手教你申请腾讯云免费SSL证书

2023腾讯云免费SSL证书申请流程&#xff0c;一个腾讯云账号可以申请50张免费SSL证书&#xff0c;免费SSL证书为DV证书&#xff0c;仅支持单一域名&#xff0c;申请腾讯云免费SSL证书3分钟即可申请成功&#xff0c;免费SSL证书品牌为TrustAsia亚洲诚信&#xff0c;腾讯云百科分享…

[深度学习]大模型训练之框架篇--DeepSpeed使用

现在的模型越来越大&#xff0c;动辄几B甚至几百B。但是显卡显存大小根本无法支撑训练推理。例如&#xff0c;一块RTX2090的10G显存&#xff0c;光把模型加载上去&#xff0c;就会OOM&#xff0c;更别提后面的训练优化。 作为传统pytorch Dataparallel的一种替代&#xff0c;D…

技术干货 —— 手把手教你通过缓存提升 API 性能

许多开发者都希望能够彻底搞清楚 API 的工作方式&#xff0c;以及如何利用缓存 API 请求来提升业务&#xff0c;但是当这个需求进入实现阶段时&#xff0c;许多人就会发现手头并没有合适的工具和恰当的方法&#xff0c;所以我们今天就为大家做一个全面的讲解&#xff1a; ① 几…

Python的由来和基础语法(一)

目录 一、Python 背景知识 1.1Python 是咋来的? 1.2Python 都能干啥? 1.3Python 的优缺点 二、基础语法 2.1常量和表达式 2.2变量和类型 变量的语法 (1) 定义变量 (2) 使用变量 变量的类型 (1) 整数 (2) 浮点数(小数) (3) 字符串 (4) 布尔 (5) 其他 动态类型…

NAT与代理服务器

1.DNS Domain Name System 是一整套从域名映射到IP的系统&#xff08;把域名转化为IP地址&#xff09; 2.域名简介 3.周鸿祎 傅盛 4.ICMP协议 用来网络故障排查原因 草图理解“位置” ping ICMP 是绕过TCP UDP传输协议的&#xff0c;没有端口号 traceroute 5.NAT技术 N…

Shell脚本练习——系统应用相关

显示系统信息 [rootwenzi data]#cat systemInfo.sh #/bin/bash RED"\E[1;31m" GREEN"\E[1;32m" END"\E[0m" echo -e "$GREEN----------------------Host systeminfo--------------------$END" echo -e "HOSTNAME: $REDho…

论文阅读:Distortion-Free Wide-Angle Portraits on Camera Phones

论文阅读&#xff1a;Distortion-Free Wide-Angle Portraits on Camera Phones 今天介绍一篇谷歌 2019 年的论文&#xff0c;是关于广角畸变校正的。 Abstract 广角摄影&#xff0c;可以带来不一样的摄影体验&#xff0c;因为广角的 FOV 更大&#xff0c;所以能将更多的内容…

git ------ IDEA中建立本地/远程仓库及上传

目录 建立本地仓库 1. idea中选择创建本地仓库 选择目标文件 创建远程仓库 1.码云上进行库创建 将本地仓库数据提交到远程仓库 提交代码 推送到远程 建立本地仓库 1. idea中选择创建本地仓库 或 vsm中找下列2 即可 选择目标文件 成功后会出现以下标识 更新 提交 推…

YOLO目标检测——室内场景识别数据集下载分享

目标检测室内场景识别数据集可以广泛应用于各种需要对室内场景进行目标识别和跟踪的领域&#xff0c;包括安防监控、智能家居、物流仓储管理等 数据集点击下载&#xff1a; YOLO室内场景识别数据集&#xff08;一&#xff09;5950图片26类别.rar YOLO室内场景识别数据集&#…

基础算法--快速排序

快速排序 算法原理 1. 取一个元素p(第一个元素&#xff0c;最后一个元素&#xff0c;中间元素&#xff0c;随机 都可以)&#xff0c;使元素p归位。 2. 列表被p分成两部分&#xff0c;左边都比p小&#xff0c;右边都比p大。 3. 递归完成排序。 动态演示 python代码实现 import…

Code Snippet的使用

文章目录 前言Code Snippet:就是咱们在VS中敲的prop、propfull、ctol【构造器快捷键】、for等快捷键&#xff0c;然后按tab键自动生成代码1.VS自带的&#xff1a;prop、propfull、ctol【构造器快捷键】、for等快捷键&#xff0c;直接使用2.自定义Snippet&#xff1a; 巨人的肩膀…

深度学习入门教学——卷积神经网络CNN

目录 一、CNN简介 一、输入层 二、卷积层 三、池化层 四、全连接层 一、CNN简介 1、应用领域 检测任务 分类与检索 超分辨率重构 2、卷积网络与传统网咯的区别 传统神经网络和卷积神经网络都是用来提取特征的。神经网络&#xff1a; 可以将其看作是一个二维的。卷积神经…

力扣:随即指针138. 复制带随机指针的链表

复制带随机指针的链表 OJ链接 分析&#xff1a; 该题的大致题意就是有一个带随机指针的链表&#xff0c;复制这个链表但是不能指向原链表的节点&#xff0c;所以每一个节点都要复制一遍 大神思路&#xff1a; ps:我是学来的 上代码&#xff1a; struct Node* copyRandomList(s…

3D DRAM:突破内存瓶颈的新希望

DRAM&#xff0c;动态随机存储器&#xff0c;是一种在计算机、消费电子、通信等领域广泛应用的内存技术。它的主要特点是在同一周期内可以随机读取或写入单字节数据&#xff0c;使得其在各种计算应用中成为不可或缺的组件。 DRAM的发展历程充满了戏剧性和技术革新。最早的DRAM诞…