【动态规划篇】专题(一):斐波那契模型——从数学递推到算法思维

文章目录

    • 算法起手式:斐波那契数列模型
    • [一、 前言](#一、 前言)
      • [1.1 为什么从这里开始?](#1.1 为什么从这里开始?)
    • [二、 题目一:第 N 个泰波那契数](#二、 题目一:第 N 个泰波那契数)
      • [2.1 题目描述](#2.1 题目描述)
      • [2.2 解法(动态规划)](#2.2 解法(动态规划))
        • [1. 算法流程分析](#1. 算法流程分析)
        • [2.3 代码实现](#2.3 代码实现)
    • [三、 题目二:三步问题](#三、 题目二:三步问题)
      • [3.1 题目描述](#3.1 题目描述)
      • [3.2 解法(动态规划)](#3.2 解法(动态规划))
        • [1. 状态分析](#1. 状态分析)
        • [2. 这里的坑:数据溢出](#2. 这里的坑:数据溢出)
        • [3. 代码实现](#3. 代码实现)
    • [四、 题目三:使用最小花费爬楼梯](#四、 题目三:使用最小花费爬楼梯)
      • [4.1 题目描述](#4.1 题目描述)
      • [4.2 解法(动态规划)](#4.2 解法(动态规划))
        • [1. 状态分析](#1. 状态分析)
        • [2. 初始化与返回值](#2. 初始化与返回值)
        • [3. 代码实现](#3. 代码实现)
    • [五、 题目四:解码方法](#五、 题目四:解码方法)
      • [5.1 题目描述](#5.1 题目描述)
      • [5.2 解法(动态规划)](#5.2 解法(动态规划))
        • [1. 状态分析](#1. 状态分析)
        • [2. 技巧:辅助节点初始化](#2. 技巧:辅助节点初始化)
        • [3. 代码实现](#3. 代码实现)
    • [六、 总结](#六、 总结)

算法起手式:斐波那契数列模型

一、 前言

1.1 为什么从这里开始?

💬 开篇 :欢迎来到动态规划(Dynamic Programming)的世界!很多同学听到 DP 就头大,觉得它是玄学。其实,DP 就像是填表格

🚀 循序渐进 :我们将用约 60 道题目,把动态规划拆解成一个个具体的模型。今天的第一篇,我们从最基础、最亲切的"斐波那契数列模型"讲起。这个模型虽然简单,但它包含了 DP 的核心思想:利用之前的计算结果,推导当前的结果

👍 点赞、收藏与分享:如果这篇万字长文对你有帮助,请不要吝啬你的点赞和收藏,这是我持续更新的动力!


二、 题目一:第 N 个泰波那契数

2.1 题目描述

题目链接1137. 第 N 个泰波那契数

描述

泰波那契序列 T n T_n Tn 定义如下:
T 0 = 0 , T 1 = 1 , T 2 = 1 T_0 = 0, T_1 = 1, T_2 = 1 T0=0,T1=1,T2=1,且在 n > = 0 n >= 0 n>=0 的条件下 T n + 3 = T n + T n + 1 + T n + 2 T_{n+3} = T_n + T_{n+1} + T_{n+2} Tn+3=Tn+Tn+1+Tn+2。

给你整数 n n n,请返回第 n n n 个泰波那契数 T n T_n Tn 的值。

示例 1

输入:n = 4

输出:4

解释:

T_3 = 0 + 1 + 1 = 2

T_4 = 1 + 1 + 2 = 4

示例 2

输入:n = 25

输出:1389537

2.2 解法(动态规划)

1. 算法流程分析

这道题完全就是"照着答案抄",题目把公式都给我们了,我们只需要把它翻译成代码。

动态规划五步法:

  1. 状态表示
    dp[i] 表示:第 i 个泰波那契数的值。
  2. 状态转移方程
    题目直接给出了公式:
    dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3]
  3. 初始化
    为了能算出 dp[3],我们需要前三个数。
    dp[0] = 0, dp[1] = 1, dp[2] = 1
  4. 填表顺序
    从左往右(从小到大)。
  5. 返回值
    返回 dp[n]
2.3 代码实现

这里我们展示空间优化后的版本(滚动数组),把空间复杂度从 O(N) 降到 O(1)。

cpp 复制代码
class Solution {
public:
    int tribonacci(int n) {
        // 1. 处理边界情况
        if(n == 0) return 0;
        if(n == 1 || n == 2) return 1;

        // 2. 初始化滚动变量
        int a = 0, b = 1, c = 1, d = 0;

        // 3. 从第3个开始填表
        for(int i = 3; i <= n; i++)
        {
            d = a + b + c; // 算出当前值
            // 滚动更新:像排队一样往后挪一位
            a = b; 
            b = c; 
            c = d;
        }

        // 4. 返回结果
        return d;
    }
};

三、 题目二:三步问题

3.1 题目描述

题目链接面试题 08.01. 三步问题

描述

三步问题。有个小孩正在上楼梯,楼梯有 n 阶台阶,小孩一次可以上 1 阶、2 阶或 3 阶。实现一种方法,计算小孩有多少种上楼梯的方式。
注意 :结果可能很大,你需要对结果模 1000000007

示例 1 :

输入:n = 3

输出:4

说明: 有四种走法

示例 2 :

输入:n = 5

输出:13

提示 :

n 范围在 [1, 1000000] 之间

3.2 解法(动态规划)

1. 状态分析

这道题本质上就是泰波那契数列的实际应用版

  • 状态表示dp[i] 表示到达第 i 阶楼梯的方法总数。

  • 状态转移

    想跳到第 i 阶,小孩只能从哪里跳过来?

    1. i-1 阶跳 1 步上来。
    2. i-2 阶跳 2 步上来。
    3. i-3 阶跳 3 步上来。
      所以:dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
2. 这里的坑:数据溢出

警告 :这道题最大的陷阱在于取模

C++ 中的 int 范围有限。如果我们写 dp[i] = (dp[i-1] + dp[i-2] + dp[i-3]) % MOD,可能会出错。因为 dp[i-1] + dp[i-2] 这两个数相加可能就已经爆 int 了(变成负数),再加第三个数还是错的。

正确做法:每加一次,就取一次模。

3. 代码实现
cpp 复制代码
class Solution {
public:
    const int MOD = 1e9 + 7;

    int waysToStep(int n) {
        // 1. 边界处理:直接返回简单的层数
        if(n == 1) return 1;
        if(n == 2) return 2;
        if(n == 3) return 4; // 1+1+1, 1+2, 2+1, 3 (共4种)

        // 2. 创建 dp 表
        vector<int> dp(n + 1);

        // 3. 初始化
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 4;

        // 4. 填表
        for(int i = 4; i <= n; i++) {
            // 防溢出的写法:先加前两个取模,再加第三个取模
            dp[i] = ((dp[i - 1] + dp[i - 2]) % MOD + dp[i - 3]) % MOD;
        }

        // 5. 返回结果
        return dp[n];
    }
};

四、 题目三:使用最小花费爬楼梯

4.1 题目描述

题目链接746. 使用最小花费爬楼梯

描述

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1

输入:cost = [10,15,20]

输出:15

解释:你将从下标为 1 的台阶开始。支付 15 ,向上爬两个台阶,到达楼梯顶部。总花费为 15 。

注意

在这道题中,数组内的每一个下标 [0, n - 1] 表示的都是楼层,而顶楼 的位置其实是在 n 的位置!!!

4.2 解法(动态规划)

1. 状态分析

这道题引入了权值 (Cost)和最值(Min)。

  • 状态表示
    dp[i] 表示:到达第 i 个位置(楼层)时的最小花费

  • 状态转移

    想站稳在第 i 层,有两种可能:

    1. i-1 层迈一步上来:那你得先支付 cost[i-1](那是 i-1 层的过路费)。总花费 = dp[i-1] + cost[i-1]
    2. i-2 层迈两步上来:那你得先支付 cost[i-2]。总花费 = dp[i-2] + cost[i-2]
      我们要省钱,所以取两者的最小值
      dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
2. 初始化与返回值
  • 初始化 :题目说可以从 0 或 1 开始。意味着站到 0 层和 1 层不需要花钱(还没开始爬呢)。
    dp[0] = 0, dp[1] = 0
  • 返回值 :题目要求到达顶部 ,顶部是 cost 数组长度 n 的位置。
    返回 dp[n]
3. 代码实现
cpp 复制代码
class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n = cost.size();
        // dp[i] 表示到达 i 位置的最小花费
        // 注意大小是 n+1,因为楼顶在 n
        vector<int> dp(n + 1); 

        // 初始化:可以直接站在 0 或 1 层,花费为 0
        dp[0] = 0;
        dp[1] = 0;

        // 填表
        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];
    }
};

五、 题目四:解码方法

5.1 题目描述

题目链接91. 解码方法

描述

一条包含字母 A-Z 的消息通过以下映射进行了 编码

'A' -> "1", 'B' -> "2", ... 'Z' -> "26"。

解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母。

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数

示例 1

输入:s = "12"

输出:2

解释:它可以解码为 "AB"(1 2)或者 "L"(12)。

示例 2

输入:s = "226"

输出:3

解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。

注意:"06" 不能映射为 "F",因为 "6" 和 "06" 不等价。

5.2 解法(动态规划)

1. 状态分析

这道题是斐波那契模型的终极变种 。它不再是无脑加法,而是加入了条件判断

  • 状态表示dp[i] 表示字符串前 i 个字符(即区间 [0, i-1])一共有多少种解码方法。

  • 状态转移

    对于当前的第 i 个字符(对应字符串下标 s[i-1]),我们有两种"翻译"方式:

    情况一:单独翻译

    如果当前数字 s[i-1]'1''9',那它可以单独变成一个字母。

    这时候,方案数继承自去掉这个字符后的方案数,即 dp[i-1]
    if (s[i-1] != '0') dp[i] += dp[i-1];

    情况二:和前一个数字组合翻译

    如果当前数字 s[i-1] 和前一个数字 s[i-2] 拼起来,在 1026 之间(比如 "12"、"26"),那它可以组合成一个字母。

    这时候,方案数继承自去掉这两个字符后的方案数,即 dp[i-2]
    if (组合数 >= 10 && 组合数 <= 26) dp[i] += dp[i-2];

2. 技巧:辅助节点初始化

为了避免处理 i-2 越界的问题,我们通常多开一个格子的空间。

dp[0] = 1。这只是一个辅助值,为了保证当前两个字符能组合成功时,dp[2] += dp[0] 能加到一个 1(代表一种方案)。

3. 代码实现
cpp 复制代码
class Solution {
public:
    int numDecodings(string s) {
        int n = s.size();
        // dp[i] 表示 s 中前 i 个字符的解码方法数
        // 为了方便处理边界,多开一个空间
        vector<int> dp(n + 1);

        // 初始化
        dp[0] = 1; // 辅助位,保证后续填表正确
        
        // 注意:dp[i] 对应 s[i-1]
        // 先处理第一个字符 s[0] 对应的 dp[1]
        if(s[0] != '0') dp[1] = 1; 

        // 填表,从第二个字符开始(也就是 dp[2])
        for (int i = 2; i <= n; i++) {
            // 1. 尝试单独解码 s[i-1]
            char current = s[i - 1]; // 当前字符
            if (current != '0') {
                dp[i] += dp[i - 1];
            }

            // 2. 尝试和前一个字符组合 s[i-2]s[i-1]
            char prev = s[i - 2]; // 前一个字符
            int num = (prev - '0') * 10 + (current - '0'); // 拼成数字
            
            // 必须是 10-26 之间才是有效字母
            // 比如 01, 09 不行,30 也不行
            if (num >= 10 && num <= 26) {
                dp[i] += dp[i - 2];
            }
        }

        return dp[n];
    }
};

六、 总结

💬 复盘:恭喜你!一口气拿下了四道动态规划题目。

我们来回顾一下这四道题的演变过程:

题目 核心模型 变体点 备注
泰波那契数 dp[i] = dp[i-1] + ... 纯公式递推 DP 的 Hello World
三步问题 dp[i] = dp[i-1] + ... + 取模操作 实际场景应用,注意溢出
最小花费爬楼梯 dp[i] = min(...) + 权值选择 到了某一步,还要看怎么走最划算
解码方法 dp[i] = 条件 ? dp[i-1] : 0 + 条件判断 斐波那契的逻辑升级版

🧠 核心心法

不管题目怎么变,斐波那契模型 的本质都是"以 i 位置为结尾 "。当我们站在 i 位置时,只需要回过头看 i-1i-2 等最近的几个状态,就能把问题解决。

下一篇,我们将离开一维数组,挑战路径问题(二维网格里的动态规划)。准备好迎接二维数组的挑战了吗?

👍 求三连:如果你觉得这种**"带题目 + 详细解析"**的模式对你有帮助,请务必点个赞!我们下期见!

相关推荐
一碗姜汤1 小时前
【计算机图形学】Bresenham直线绘制算法
人工智能·算法
郝学胜-神的一滴2 小时前
FastAPI:Python 高性能 Web 框架的优雅之选
开发语言·前端·数据结构·python·算法·fastapi
样例过了就是过了2 小时前
LeetCode热题100 回文链表
数据结构·算法·leetcode·链表
汉克老师2 小时前
GESP2023年12月认证C++二级( 第二部分判断题(1-10))
c++·循环结构·分支结构·gesp二级·gesp2级
地平线开发者2 小时前
【地平线 征程 6 工具链进阶教程】算子优化方案集锦
算法·自动驾驶
callJJ2 小时前
深入浅出 MVCC —— 从零理解 MySQL 并发控制
数据库·mysql·面试·并发·mvcc
多恩Stone2 小时前
【3D-AICG 系列-14】Trellis 2 的 Texturing Pipeline 保留单层薄壳,而 Textured GLB 会变成双层
人工智能·python·算法·3d·aigc
Solitary-walk2 小时前
前缀和思想
数据结构·c++·算法
智驱力人工智能2 小时前
机场鸟类活动智能监测 守护航空安全的精准工程实践 飞鸟检测 机场鸟击预防AI预警系统方案 机场停机坪鸟类干扰实时监测机场航站楼鸟击预警
人工智能·opencv·算法·安全·yolo·目标检测·边缘计算