动态规划算法的欢乐密码(一):斐波那契数模型

news2025/5/10 18:13:03

专栏:算法的魔法世界

个人主页:手握风云

目录

一、动态规划

二、例题讲解

2.1. 第 N 个泰波那契数

2.2. 三步问题

2.3. 使用最小花费爬楼梯

2.4. 解码方法


一、动态规划

        动态规划是一种将复杂问题分解为更小的子问题,并利用子问题的解来构建原问题的解的方法。它主要用于解决具有重叠子问题最优子结构特性的问题,通过存储子问题的解(避免重复计算)来提高效率。

        动态规划的解题思路通常可以分为以下几个步骤:(1)定义状态表示;(2)推导状态转移方程;(3)初始化和边界条件;(4)填表顺序;(5)返回值

二、例题讲解

2.1. 第 N 个泰波那契数

        本题可以看作是斐波那契数的加强版,并且首项是从第0项开始计算的。我们先来创建一个一位数组dp[],这个dp表里面的值就是我们的状态表示。题目要求我们求出第N个泰波那契数,那么dp[i]就表示第i个泰波那契数。dp[i]的表示就是状态转移方程,题目其实给出状态转移方程,dp[i]依赖于前三个状态,dp[i]=dp[i-1]+dp[i-2]+dp[i-3]。接下来就是初始化,保证填表的时候不越界,如果我们使用上面的状态表示方程,dp[0]、dp[1]、dp[2]都会发生越界。而题目也已经将前三个状态给了出来,dp[0]=0、dp[1]=1、dp[2]=1。下面我们填表的时候,为了填写当前状态,所需的状态必须是已经计算出来的,所以填表顺序从左到右。

        完整代码实现:

class Solution {
    public int tribonacci(int n) {
        if (n == 0)
            return 0;
        if (n == 1 || n == 2)
            return 1;

        int[] dp = new int[n + 1];
        dp[0] = 0;
        dp[1] = 1;
        dp[2] = 1;
        for (int i = 3; i <= n; i++)
            dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
        return dp[n];
    }
}

        时间复杂度为O(n),因为额外创建了数组,空间复杂度也为O(n)

        空间优化:一般情况下,动态规划都可以利用滚动数组来解决。在这道题中,从第三个数开始,我们仅需前三个数就可以求出当前状态的结果,其他的空间就会产生浪费。使用滚动数组,我们只需要存储最近的三个泰波那契数。如下图,先初始化a、b、c的值,第N个泰波那契数d=a+b+c,当我们要求下一个泰波那契数时,将d的值更新为c,c的值更新为b,b的值更新为a。因为这种求法不用创建数组,空间复杂度可以优化为O(n)

2.2. 三步问题

        当n=1时,只有1种方式。当n=2时,从0直接到2,1种方式;我们不管怎么从0到1,直接考虑从1到2,1种方式,所以0到2一共是1+1=2种方式。当n=3时,从0直接到3,1种方式;从1开始直接到3,1种方式,前面从0到1,1种方式,1*1=1种;从2开始直接到3,1种方式,从0到2,2种方式,2*1=2种方式,所以0到3一共是1+1+2=4种方式;当n=4时,按照上面的推导过程,从0到4一共是1+2+4=7种方式。所以递推公式为:当n>3时,F(n)=F(n-1)+F(n-2)+F(n-3)。

        我们根据题目要求可以得出状态表示为到达n号台阶时,共有多少种方式。这个小孩一次可以上1阶、2阶、3阶台阶,我们以第n阶台阶最近的一步划分,那么状态转移方程为:dp[n]=dp[n-1]+dp[n-2]+dp[n-3]。当n<4时,这个状态转移方程就没有意义了,初始化操作,dp[1]=1,dp[2]=2,dp[3]=4。根据题目要求,填表顺序为从左到右。

        完整代码实现:

class Solution {
    public int waysToStep(int n) {
        // 定义模数,用于防止结果溢出
        int mod = (int) 1e9 + 7;

        if (n == 1 || n == 2) return n;
        if (n == 4) return 4;

        int[] dp = new int[n + 1];
        // 初始化前三个台阶的方法数
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 4;

        // 从第4个台阶开始,计算每个台阶的方法数
        for (int i = 4; i <= n; i++) {
            // 到达第i个台阶的方法数为前三个台阶的方法数之和,并取模防止溢出
            dp[i] = ((dp[i - 1] + dp[i - 2]) % mod + dp[i - 3]) % mod;
        }
        return dp[n];
    }
}

2.3. 使用最小花费爬楼梯

        题目给出了一个长度为n的数组cost,其中cost[i]表示爬到第i个台阶所需要的花费。目标是计算出从底部爬到顶部所需的最小花费。我们首先要明白楼梯顶部在哪里,这里不是数组的最后一个元素,而是越过数组末尾的下一个位置。

  • 第一种解法

        根据题目分析可以得出,本题的状态表示为到达n阶台阶时,最小花费的数目。接着利用之前的状态来推导dp[n]的值,根据最近的一步或者两步来划分问题:先到达dp[n]的前一个位置dp[n-1],然后支付cost[n-1]走一步,到达dp[n]花费为dp[n-1]+cost[n-1];先到达dp[n-2]的前两个位置dp[n-2],然后支付cost[n-2]走两步,花费为dp[n-2]+cost[n-2]。那么递推公式dp[n]=Math.min(dp[n-1]+cost[n-1],dp[n-2]+cost[n-2])。初始化dp[0]=dp[1]=0。填表顺序从左到右。

        完整代码实现:

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int[] dp = new int[n + 1];
        for (int i = 2; i <= n; i++) {
            dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp[n];
    }
}
  • 第二种解法 

        第一种解法是利用结尾作为状态表示,第二种我们以起点作为状态表示。这里dp[i]表示从i位置出发到达楼顶的最小花费。状态转移方程:当我们支付cost[i]向后走一步到达i+1位置时,再从i+1位置到达终点,花费为dp[i+1]+cost[n];当我们支付cost[i]向后走两步到达i+2位置时,再从i+2位置到达终点,花费为dp[i+2]+cost[n],所以递推公式为dp[i]=Math.min(dp[i+1],dp[i+2])+cost[i]。接着将dp表最后两个元素初始化,仅需支付这一层台阶的花费到达终点即可,所以dp[n-1]=cost[n-1],dp[n-2]=cost[n-2]。填表顺序从左到右。起始位置要从0下标或者1下标开始爬楼梯,最后要返回dp[0]与dp[1]的最小值。

        完整代码实现:

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int n = cost.length;
        int[] dp = new int[n];
        // 初始化dp数组的最后一个元素,即到达最后一阶楼梯的花费
        dp[n - 1] = cost[n - 1];
        // 初始化dp数组的倒数第二个元素,即到达倒数第二阶楼梯的花费
        dp[n - 2] = cost[n - 2];
        for (int i = n - 3; i >= 0; i--) {
            // 到达当前阶楼梯的最小花费等于到达下一阶或下两阶楼梯的最小花费加上当前阶的花费
            dp[i] = Math.min(dp[i + 1],dp[i + 2]) + cost[i];
        }
        return Math.min(dp[0],dp[1]);
    }
}

    2.4. 解码方法

            题目给定一个只包含数字的非空字符串,要求计算解码方法的总数。如果没有合法的方式解码整个字符串,返回0。

            状态表示:以i位置为结尾,从0到i计算字符串有多种解码方式,题目当中要求计算总字符串的解码方式,所以dp[i]表示以i位置为结尾,解码方法的总数。状态转移方程:根据最近的一步,让i位置单独解码,或者以i-1和i结合进行解码(我们在算i+1位置时,会考虑i与i+1结合,所以在计算i位置时,不必考虑i与i+1结合)。当i位置单独解码时,1<=i<=9时成功解码,反之失败。如果0到i-1之间的解码数dp[i-1],再加上i位置解码,也是dp[i-1]种,如果失败,则整体解码也失败。当i-1与i位置结合时,10<=10*(i-1)+i<=99才能解码成功,整体解码数为dp[i-2],失败则没有。dp[i]=dp[i-1]+dp[i-2]。初始化:如果0位置解码成功,dp[0]=1,反之dp[1]=1;dp[1]结合上面的推导,可以出现0、1、2三种情况。填表顺序从左到右。结果直接返回dp[i]。

            完整代码实现:

    class Solution {
        public int numDecodings(String s) {
            int n = s.length();
            char[] ch = s.toCharArray();
            int[] dp = new int[n]; // 创建一个动态规划数组,用于存储每个位置的解码方式数量
    
            //初始化第一个位置
            if (ch[0] != '0') dp[0] = 1;
            if (n == 1) return dp[0];
    
            //初始化第二个位置
            if (ch[1] != '0' && ch[0] != '0') dp[1] += 1;
            int t = (ch[0] - '0') * 10 + ch[1] - '0';
            if (t >= 10 && t <= 26) dp[1] += 1;
    
            for (int i = 2; i < n; i++) {
                //单独解码
                if (ch[i] != '0') dp[i] += dp[i - 1];
    
                //结合解码
                int tt = (ch[i - 1] - '0') * 10 + ch[i] - '0';
                if (tt >= 10 && tt <= 26) dp[i] += dp[i - 2];
            }
            return dp[n - 1]; // 返回最后一个位置的解码方式数量,即为字符串的总解码方式数量
        }
    }
    • 边界处理和初始化的优化

            前面三道题的初始化代码非常简单,但这道题的初始化却有很多,并且还存在相似的代码。而接下来就是创建虚拟头结点来对初始化的代码进行优化。我们先将旧的dp表新增一个结点,并将旧dp表里的元素统一后移一位。这里需要注意,1.虚拟结点的值要保证后面填表的正确性,2.下标的映射关系。dp[2]的状态取决于dp[0]和dp[1],根据前面的状态转移方程,需要+dp[0]时,说明0和1位置可以解码成功,所以dp[0]初始化为1,解码不成功直接忽略。当我们在新dp表里面初始化dp[0]时,我需要看字符串0位置的解码情况。所以只需在新dp表里面-1就可以。

            优化后的代码:

    class Solution {
        public int numDecodings(String s) {
            int n = s.length();
            char[] ch = s.toCharArray();
            int[] dp = new int[n + 1]; // 创建一个动态规划数组,用于存储每个位置的解码方式数量
    
            //初始化第一个位置
            dp[0] = 1;
    
            //初始化第二个位置
            if(ch[1 - 1] != '0') 
                dp[1] = 1;
    
            for (int i = 2; i <= n; i++) {
                //单独解码
                if (ch[i - 1] != '0')
                    dp[i] += dp[i - 1];
    
                //结合解码
                int tt = (ch[i - 2] - '0') * 10 + ch[i - 1] - '0';
                if (tt >= 10 && tt <= 26)
                    dp[i] += dp[i - 2];
            }
            return dp[n]; // 返回最后一个位置的解码方式数量,即为字符串的总解码方式数量
        }
    }

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

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

    相关文章

    Echarts柱状图斜线环纹(图形的贴花图案)

    单独设置 <!--此示例下载自 https://echarts.apache.org/examples/zh/editor.html?cbar-stack&codePYBwLglsB2AEC8sDeAoWszGAG0iAXMmuhgE4QDmFApqYQOQCGAHhAM70A0x6L7ACsAjQwtQqhIkwATxDUGbABaMAJsADu9HrAC-xHd3TZqNaCvEHiFcuaKTjAMzAMAzAFIu28hUXPY9ABYPQxIAI2AwTABbV…

    2025.04.19【Spider】| 蜘蛛图绘制技巧精解

    Basic multi-group radar chart Start with a basic version, learn how to format your input dataset Radar chart with ggradar A Spider chart made using the ggradar package and a lot of customization.A work by Tuo Wang 文章目录 Basic multi-group radar chartRa…

    【Linux】深入理解Linux文件系统:从C接口到内核设计哲学

    文章目录 前言一、C语言中的文件接口1. 文件指针&#xff08;句柄&#xff09;FILE*以写方式打开文件&#xff0c;若文件不存在会新建一个文件W写入方式&#xff0c;在打开文件之前都会将文件内容全部清空追加写方式&#xff0c;其用法与写方法一致&#xff0c;不同在于a方法可…

    基于尚硅谷FreeRTOS视频笔记——15—系统配制文件说明与数据规范

    目录 配置函数 INCLUDE函数 config函数 数据类型 命名规范 函数与宏 配置函数 官网上可以查找 最核心的就是 config和INCLUDE INCLUDE函数 这些就是裁剪的函数 它们使用一个ifndef。如果定义了&#xff0c;就如果定义了这个宏定义&#xff0c;那么代码就生效。 通过ifn…

    Linux网络编程 深入解析TFTP协议:基于UDP的文件传输实战

    知识点1【TFTP的概述】 学习通信的基本&#xff1a;通信协议&#xff08;具体发送上面样的报文&#xff09;、通信流程&#xff08;按照什么步骤发送&#xff09; 1、TFTP的概述 tftp&#xff1a;简单文件传输协议&#xff0c;**基于UDP&#xff0c;**不进行用户有效性验证 …

    c# MES生产进度看板,报警看板 热流道行业可用实时看生产进度

    MES生产进度看板&#xff0c;报警看板 热流道行业可用实时看生产进度 背景 本软件是给宁波热流道行业客户开发的生产电子看板软件系统 功能 1.录入工艺流程图&#xff08;途程图&#xff09;由多个站别组成。可以手动设置每个工艺站点完成百分比。 2.可以看生成到哪个工…

    初识Redis · C++客户端string

    目录 前言&#xff1a; string的API使用 set get&#xff1a; expire: NX XX: mset,mget&#xff1a; getrange setrange: incr decr 前言&#xff1a; 在前文&#xff0c;我们已经学习了Redis的定制化客户端怎么来的&#xff0c;以及如何配置好Redis定制化客户端&…

    华硕原厂系统枪神9/9p超竟版-WIN11原装开箱出厂系统安装

    华硕原厂系统枪神9/9p超竟版-WIN11-24H2-专业工作站版本安装可带F12-ASUSRecovery恢复功能 适用机型&#xff1a; G635LX、G635LW、G835LX、G835LW、G615LW、G615LP、G615LM、G615LH G815LW、G815LP、G815LM、G815LH、G635LR、G835LR、G615LR、G815LR 远程恢复安装&#xff…

    CF1016赛后总结

    文章目录 前言T1:Ideal GeneratorT2&#xff1a;Expensive NumberT3:Simple RepetitionT4&#xff1a;Skibidi TableT5:Min Max MEXT6:Hackers and Neural NetworksT7:Shorten the Array 前言 由于最近在半期考试&#xff0c;更新稍微晚了一点&#xff0c;还望大家见谅 &#…

    QT聊天项目DAY06

    1.从git上同步项目 编译测试&#xff0c;编译通过 Post请求测试 测试成功 2. email is 打印有问题&#xff0c;检查 解析结果是存储在jsonResult中的&#xff0c;修改 3. 客户端实现Post验证码请求 3.1 同步Qt客户端项目 检查QT版本&#xff0c;由于我在公司用的还是QT5.12.9…

    GNU,GDB,GCC,G++是什么?与其他编译器又有什么关系?

    文章目录 前言1. GNU和他的工具1.1 gcc与g1.2 gdb 2.Windows的Mingw/MSVC3.LLVM的clang/clang4.Make/CMake 前言 在开始之前我们先放一段Hello World&#xff1a;hello.c #include <stdio.h>int main() {printf("Hello World");return 0; }然后就是一段老生常…

    笔记整理五

    STP生成树 stp生成树是用于解决二层环路问题的协议。 二层环路为有以下三种&#xff1a; 1.广播风暴 2.MAC地址的偏移&#xff08;每一次循环&#xff0c;都会导致交换机来回刷新MAC地址表记录&#xff09; 3.多帧复制 stp生成树&#xff1a;需要将原本的环型拓扑结构转换…

    奥比中光tof相机开发学习笔记

    针对奥比中光 tof相机&#xff0c;官方提供的资料如下ProcessOn Mindmap|思维导图 Orbbec SDK Python Wrapper基于Orbbec SDK进行设计封装&#xff0c;主要实现数据流接收&#xff0c;设备指令控制。下面就其开发适配进行如下总结&#xff1a; &#xff08;1&#xff09;系统配…

    【面试向】点积与注意力机制,逐步编码理解自注意力机制

    点积&#xff08;dot product&#xff09;两个向量点积的数学公式点积&#xff08;dot product&#xff09;与 Attention 注意力机制&#xff08;Attention&#xff09;注意力机制的核心思想注意力机制中的缩放点积自注意力机制中&#xff0c;谁注意谁&#xff1f; 逐步编码理解…

    一个 CTO 的深度思考

    今天和一些同事聊了一会&#xff0c;以下是我的观点 我的观点&#xff0c;成年人只能筛选&#xff0c;不能培养在组织中&#xff0c;应该永远向有结果的人看齐。不能当他站出来讲话的时候&#xff0c;大家还要讨论讨论&#xff0c;他虽然拿到结果了&#xff0c;但是他就是有一…

    SQL通用语法和注释,SQL语句分类(DDL,DML,DQL,DCL)及案例

    目录 SQL通用语法和注释 SQL语句分类&#xff08;DDL&#xff0c;DML&#xff0c;DQL&#xff0c;DCL&#xff0c;TPL&#xff0c;CCL&#xff09; DDL&#xff08;数据定义语言&#xff09; 数据库操作 查询&#xff08;SHOW、SELECT&#xff09; 创建&#xff08;CREAT…

    AUTOSAR图解==>AUTOSAR_SWS_KeyManager

    AUTOSAR KeyManager详细分析 AUTOSAR 4.4.0 版本密钥与证书管理模块技术分析 目录 1. 概述2. KeyManager架构 2.1 KeyManager在AUTOSAR架构中的位置2.2 架构说明 3. KeyManager模块结构 3.1 模块组件详解3.2 配置项说明 4. KeyManager证书验证流程 4.1 证书验证流程分析 5. Ke…

    Jsp技术入门指南【七】JSP动作讲解

    Jsp技术入门指南【七】JSP动作讲解 前言一、什么是JSP动作&#xff1f;二、核心JSP动作详解1. jsp:include&#xff1a;动态包含其他页面与<% include %>的区别 2. jsp:forward&#xff1a;请求转发到另一个页面3. jsp:param&#xff1a;为动作传递参数4. jsp:useBean&am…

    10软件测试需求分析案例-查询学习信息

    用户登录系统后&#xff0c;进入查询学生信息界面&#xff0c;输入查询字段值&#xff0c;点击查询按钮后&#xff0c;展示查询到的学生信息&#xff0c;可以重新输入字段值进行查询。 查询学生信息属于学生信息管理的子菜单&#xff0c;可以根据学号、姓名、性别查询。老师登录…

    基于尚硅谷FreeRTOS视频笔记——6—滴答时钟—上下文切换

    FreeRTOS滴答 FreeRTOS需要有一个时钟参照&#xff0c;并且这个时钟不会被轻易打断&#xff0c;所以最好选择systick 为什么需要时间参照 就是在高优先级任务进入阻塞态后&#xff0c;也可以理解为进入delay&#xff08;&#xff09;函数后&#xff0c;需要有一个时间参照&…