文章目录
-
- 算法起手式:斐波那契数列模型
- [一、 前言](#一、 前言)
-
- [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. 算法流程分析
这道题完全就是"照着答案抄",题目把公式都给我们了,我们只需要把它翻译成代码。
动态规划五步法:
- 状态表示 :
dp[i]表示:第i个泰波那契数的值。 - 状态转移方程 :
题目直接给出了公式:
dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3] - 初始化 :
为了能算出dp[3],我们需要前三个数。
dp[0] = 0,dp[1] = 1,dp[2] = 1。 - 填表顺序 :
从左往右(从小到大)。 - 返回值 :
返回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阶,小孩只能从哪里跳过来?- 从
i-1阶跳 1 步上来。 - 从
i-2阶跳 2 步上来。 - 从
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层,有两种可能:- 从
i-1层迈一步上来:那你得先支付cost[i-1](那是i-1层的过路费)。总花费 =dp[i-1] + cost[i-1]。 - 从
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]拼起来,在10到26之间(比如 "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-1、i-2 等最近的几个状态,就能把问题解决。
下一篇,我们将离开一维数组,挑战路径问题(二维网格里的动态规划)。准备好迎接二维数组的挑战了吗?
👍 求三连:如果你觉得这种**"带题目 + 详细解析"**的模式对你有帮助,请务必点个赞!我们下期见!