LeetCode算法日记 - Day 69: 第 N 个泰波那契数、三步问题
目录
1. 第 N 个泰波那契数
1.1 题目解析
1.2 解法
1.3 代码实现
2. 三步问题
2.1 题目解析
2.2 解法
2.3 代码实现
1. 第 N 个泰波那契数
https://leetcode.cn/problems/n-th-tribonacci-number/description/
泰波那契序列 Tn 定义如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
给你整数 n
,请返回第 n 个泰波那契数 Tn 的值。
示例 1:
输入:n = 4 输出:4 解释: T_3 = 0 + 1 + 1 = 2 T_4 = 1 + 1 + 2 = 4
示例 2:
输入:n = 25 输出:1389537
提示:
0 <= n <= 37
- 答案保证是一个 32 位整数,即
answer <= 2^31 - 1
。
1.1 题目解析
题目本质
这是一个序列递推问题,每个数由前三个数的和决定。本质上是"根据递推关系求第n项"。
常规解法
最直观的想法是用递归——直接按照定义 Tn+3 = Tn + Tn+1 + Tn+2 来实现,遇到 tribonacci(n) 就递归调用 tribonacci(n-1) + tribonacci(n-2) + tribonacci(n-3)。
问题分析
纯递归会产生大量重复计算。比如计算 T5 时要算 T4、T3、T2,而计算 T4 时又要重复计算 T3、T2、T1。时间复杂度是指数级 O(3^n),当 n=25 时几乎无法接受。
思路转折
要想高效 → 必须避免重复计算 → 动态规划(记忆化):用数组存储已计算的值,每个位置只算一次,从小到大递推,时间复杂度降到 O(n)
1.2 解法
算法思想
动态规划。定义 dp[i] 表示第 i 个泰波那契数,递推公式为:
dp[i] = dp[i-1] + dp[i-2] + dp[i-3] (i >= 3)
初始状态:dp[0] = 0, dp[1] = 1, dp[2] = 1
i)处理边界情况:n=0 返回 0,n=1 或 n=2 返回 1
ii)创建 dp 数组,长度为 n+1
iii)初始化前三个值:dp[0]=0, dp[1]=1, dp[2]=1
iv)从 i=3 开始循环到 n,每次用前三项的和更新当前项
v)返回 dp[n]
易错点
- 数组越界:创建数组时要用 n+1 而不是 n,因为要访问 dp[n]
- 边界判断顺序:先判断 n=0,再判断 n=1 或 n=2,避免 n 较小时创建不必要的数组
- 循环起点:必须从 i=3 开始,因为前三个值已经初始化
1.3 代码实现
class Solution {public int tribonacci(int n) {// 边界情况处理if (n == 0) return 0;if (n == 1 || n == 2) return 1;// 创建dp数组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),需要循环 n-2 次,每次做常数次加法运算
-
空间复杂度:O(n),需要长度为 n+1 的数组存储所有泰波那契数
2. 三步问题
https://leetcode.cn/problems/three-steps-problem-lcci/description/
三步问题。有个小孩正在上楼梯,楼梯有 n 阶台阶,小孩一次可以上 1 阶、2 阶或 3 阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模 1000000007。
示例 1:
输入:n = 3 输出:4 说明:有四种走法
示例 2:
输入:n = 5 输出:13
提示:
- n 范围在[1, 1000000]之间
2.1 题目解析
题目本质
这道题的核心是三阶线性递推关系。由于每个状态依赖前三个状态,我们需要三个独立的初始值作为"动态规划基"(dp[0]=1, dp[1]=1, dp[2]=2),就像向量空间需要基向量一样,后续所有 dp[i](i≥3)都可以从这组基通过递推公式生成。这也解释了为什么空间优化时只需保留三个变量——恰好对应递推的阶数。
常规解法
最直观的想法是递归回溯——站在第n阶时,可能是从第n-1阶、第n-2阶或第n-3阶走过来的,所以 ways(n) = ways(n-1) + ways(n-2) + ways(n-3),递归计算即可。
问题分析
纯递归存在大量重复计算。比如计算 ways(5) 时要算 ways(4)、ways(3)、ways(2),而计算 ways(4) 时又要重复计算 ways(3)、ways(2)。每个节点会分裂成3个子问题,时间复杂度是 O(3^n)。当 n=1000000 时,完全不可接受。
思路转折
要想高效 → 必须消除重复计算 → 动态规划记忆化。关键观察:
-
每个状态只需计算一次,用数组保存结果
-
从小到大递推,先算 dp[1]、dp[2]...再算 dp[n],时间复杂度降到 O(n)
-
大数取模,由于结果可能很大,需要边算边取模避免溢出
-
空间优化,实际只需保存最近3个值,可以用滚动数组将空间从 O(n) 优化到 O(1)
2.2 解法
算法思想
动态规划。定义 dp[i] 表示到达第 i 阶的方法总数,递推公式为:
dp[i] = (dp[i-1] + dp[i-2] + dp[i-3]) % MOD
初始状态:dp[0] = 1(起点),dp[1] = 1(只能走1步),dp[2] = 2(1+1 或 2两种)
i)处理边界情况:n=1 返回 1,n=2 返回 2(也可以统一到dp数组处理)
ii)创建 dp 数组,长度为 n+1
iii)初始化:dp[0]=1, dp[1]=1, dp[2]=2
iv)从 i=3 循环到 n,每次用前三项之和更新当前项,并立即取模
v)返回 dp[n](已取模,直接转int)
易错点
-
取模时机:必须在累加时就取模 (dp[i-1] + dp[i-2] + dp[i-3]) % MOD,而不是最后才取模,否则中间结果可能超出 long 范围
-
边界初始化:dp[0]=1 不是"走0步",而是"已在起点是一种状态",这样才能让 dp[3] 正确包含"直接走3步"的情况
-
数组越界:数组长度必须是 n+1,因为要访问 dp[n]
2.3 代码实现
class Solution {public int waysToStep(int n) {// 边界处理if (n == 1) return 1;if (n == 2) return 2;long[] dp = new long[n + 1];int MOD = 1000000007;// 初始化dp[0] = 1;dp[1] = 1;dp[2] = 2;// 递推计算,边算边取模for (int i = 3; i <= n; i++) {dp[i] = (dp[i - 1] + dp[i - 2] + dp[i - 3]) % MOD;}return (int)dp[n];}
}
复杂度分析
- 时间复杂度:O(n),需要循环计算 n-2 次,每次做3次加法和1次取模
- 空间复杂度:O(n),需要长度为 n+1 的数组存储所有状态