- bitworld 的博客
【数论】斐波那契数列
- 2025-4-25 20:53:37 @
斐波那契数列
1. 定义与性质
我们先从最基础的定义开始。斐波那契数列 通常定义如下:
$$F_n = F_{n-1} + F_{n-2} \quad (\text{对于 } n \ge 2) $$数列的前几项是:0, 1, 1, 2, 3, 5, 8, 13, 21, 34,
这个定义简洁明了,描述了一个非常自然的增长过程。除了这个核心递推关系,斐波那契数列还有许多有趣的性质,比如:
-
黄金分割比: 相邻两项的比值 随着 的增大趋近于黄金分割比
-
通项公式 (Binet 公式):
其中 (黄金分割比) 和 是特征方程 的两个根。这个公式在理论分析中有用,但在竞赛中直接计算涉及浮点数和精度问题,且效率不高,通常不用于求解具体数值,除非题目有特殊要求或需要推导性质。
-
组合意义: 可以表示用 1x1 和 1x2 的骨牌铺满 1x(n-1) 的地板的方案数,或者一个人爬 n-1 级楼梯,每次可以爬 1 级或 2 级的方案数(若定义 则对应爬 级楼梯)。这种组合意义是识别某些问题本质为斐波那契数列的关键。
-
其他恒等式: 比如 Cassini 恒等式:
以及 和恒等式:
这些恒等式有时能在特定问题中提供解题思路或简化计算。
我们今天的重点是计算 ,特别是当 很大或者需要模运算时。
2. 朴素递归
最直观的实现方式就是直接根据定义写递归函数:
#include <bits/stdc++.h>
using namespace std;
// 朴素递归计算斐波那契数
long long fib_rec(int n) {
// Base cases
if (n == 0) return 0;
if (n == 1) return 1;
// Recursive step based on definition
// F_n = F_{n-1} + F_{n-2}
return fib_rec(n - 1) + fib_rec(n - 2);
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n;
cin >> n;
cout << fib_rec(n) << endl;
return 0;
}
- 代码解释: 这个
fib_rec
函数完全是定义 的直接翻译,基础情况是 和 。 - 复杂度分析:
- 时间复杂度: 呈指数级增长,大致为 或 。为什么?计算
fib_rec(n)
需要计算fib_rec(n-1)
和fib_rec(n-2)
,而这两者又会进一步递归。画出递归树你会发现,很多子问题(如fib_rec(n-k)
)被重复计算了无数次。对于 左右,计算量就非常庞大了,在竞赛中几乎一定会超时(TLE)。 - 空间复杂度: 递归深度为 ,所以栈空间复杂度是 。
- 时间复杂度: 呈指数级增长,大致为 或 。为什么?计算
这个实现虽然简单,但效率极低,仅适用于 非常小的情况,或者作为理解后续优化的起点。
动态规划优化:记忆化与迭代
递归方法的主要问题在于重复计算。解决重复计算的经典方法就是动态规划 (Dynamic Programming, DP)。对于斐波那契数列,DP 有两种主要形式:记忆化搜索(自顶向下)和迭代(自底向上)。
3. 记忆化搜索 (Memoization)
记忆化搜索本质上还是递归,但增加了一个“备忘录”(通常是数组或哈希表)来存储已经计算过的子问题的结果。每次计算 fib(k)
时,先检查备忘录里是否已有结果,如果有,直接返回;如果没有,才进行计算,并将结果存入备忘录后再返回。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005; // 根据题目 n 的范围调整
long long memo[MAXN]; // 备忘录数组,存储已计算的 F(k)
bool vis[MAXN]; // 标记数组,记录 memo[k] 是否已计算
// 记忆化搜索计算斐波那契数
long long fib_mem(int n) {
// Base cases
if (n == 0) return 0;
if (n == 1) return 1;
// 检查备忘录
if (vis[n]) {
return memo[n];
}
// 计算并存入备忘录
// F_n = F_{n-1} + F_{n-2}
memo[n] = fib_mem(n - 1) + fib_mem(n - 2);
vis[n] = true; // 标记已计算
// 注意:如果需要模运算,加法后要取模
// memo[n] = (fib_mem(n - 1) + fib_mem(n - 2)) % MOD;
return memo[n];
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
// 初始化备忘录 (可以只初始化 vis,或者用特殊值如 -1 标记 memo)
// memset(vis, false, sizeof(vis));
// 或者全局变量默认初始化为 0,可以利用 vis 区分
int n;
cin >> n;
// 如果 n 很大,需要注意 long long 也会溢出,可能需要模运算
// 如果需要模运算,需要在 fib_mem 函数内部进行
cout << fib_mem(n) << endl;
return 0;
}
- 代码解释: 使用
memo
数组存储 的结果,vis
数组标记memo[k]
是否有效。计算前先查vis[n]
,如果为true
则直接返回memo[n]
。否则,递归计算fib_mem(n-1)
和fib_mem(n-2)
,将结果相加(如果需要取模则在此处取模),存入memo[n]
,标记vis[n] = true
,然后返回结果。 - 复杂度分析:
- 时间复杂度: 每个状态
fib(k)
(从 0 到 n) 只会被实际计算一次,因为计算后结果就被存储了。每次计算涉及常数次操作(加法、赋值、查表)。所以总时间复杂度是 。 - 空间复杂度: 需要 的额外空间存储备忘录
memo
和标记vis
,另外递归栈深度仍然是 。
- 时间复杂度: 每个状态
记忆化搜索保留了递归的直观性,同时通过空间换时间,将复杂度降至线性,对于 在 范围且不需要模运算(或模数不大,long long
能存下中间结果)的情况是可行的。
4. 迭代实现
迭代方法是自底向上计算斐波那契数。我们知道 和 ,然后可以计算 ,接着计算 ,依此类推,直到计算出 。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005; // 同样根据 n 的范围调整
long long dp[MAXN]; // DP 数组,dp[i] 存储 F_i
// 迭代计算斐波那契数
long long fib_iter(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; ++i) {
// dp[i] = dp[i-1] + dp[i-2]
dp[i] = dp[i - 1] + dp[i - 2];
// 如果需要模运算
// dp[i] = (dp[i - 1] + dp[i - 2]) % MOD;
}
return dp[n];
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n;
cin >> n;
// 同样,考虑 long long 溢出和模运算
cout << fib_iter(n) << endl;
return 0;
}
- 代码解释: 初始化
dp[0]
和dp[1]
。然后通过一个循环,从 到 ,利用dp[i-1]
和dp[i-2]
计算dp[i]
。最后返回dp[n]
。 - 复杂度分析:
- 时间复杂度: 一个简单的循环,从 2 到 n,每次循环内部是常数时间操作。总时间复杂度 。
- 空间复杂度: 需要一个大小为 的数组
dp
来存储 到 。
迭代方法通常比记忆化搜索有更小的常数因子(没有函数调用开销),且空间复杂度分析更直接。
5. 空间优化:滚动数组
观察迭代方法,你会发现计算 其实只需要 和 的值。我们并不需要存储从 到 的所有历史值。因此,可以用滚动数组的思想来优化空间。我们只需要维护最近的两个斐波那契数即可。
#include <bits/stdc++.h>
using namespace std;
// 迭代计算斐波那契数,空间优化 O(1)
long long fib_opt(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
long long a = 0; // 代表 F_{i-2}
long long b = 1; // 代表 F_{i-1}
long long cur; // 代表 F_i
for (int i = 2; i <= n; ++i) {
// cur = F_{i-1} + F_{i-2}
cur = a + b;
// 如果需要模运算
// cur = (a + b) % MOD;
// 更新 a 和 b,为下一轮计算做准备
a = b; // a 更新为 F_{i-1}
b = cur; // b 更新为 F_i
}
// 循环结束后, b 存储的是 F_n
return b;
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int n;
cin >> n;
cout << fib_opt(n) << endl;
return 0;
}
- 代码解释: 使用三个变量
a
,b
,cur
。a
存储 ,b
存储 。在循环中计算cur = a + b
(即 ),然后更新a
为原来的b
(即 ),b
更新为cur
(即 ),这样就滚动到了下一个状态。 - 复杂度分析:
- 时间复杂度: 仍然是 ,因为循环次数不变。
- 空间复杂度: 只使用了常数个变量 (
a
,b
,cur
,i
),所以空间复杂度是 。
这种 时间、 空间的迭代方法是计算单个斐波那契数(当 不太大时)的标准且最高效的线性方法。对于 在 级别,这个方法是首选。
矩阵快速幂:处理巨大 n 的利器
当 非常大时,比如 达到 甚至 , 的时间复杂度就无法接受了。这时,我们需要一个更快的算法。斐波那契数列的递推关系是线性的,这提示我们可以使用矩阵快速幂 (Matrix Exponentiation) 将复杂度降到 。
6. 斐波那契数列与矩阵
我们的目标是找到一个 2x2 矩阵 ,使得它能将状态向量 转移到下一个状态向量 。即:
$$\begin{pmatrix} F_{n+1} \\ F_n \end{pmatrix} = \begin{pmatrix} ? & ? \\ ? & ? \end{pmatrix} \begin{pmatrix} F_n \\ F_{n-1} \end{pmatrix} $$根据斐波那契定义:
将这个关系写成矩阵形式:
$$\begin{pmatrix} F_{n+1} \\ F_n \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix} \begin{pmatrix} F_n \\ F_{n-1} \end{pmatrix} $$我们令这个转移矩阵
那么,我们可以进行连续的转移:
$$\begin{pmatrix} F_n \\ F_{n-1} \end{pmatrix} = M \begin{pmatrix} F_{n-1} \\ F_{n-2} \end{pmatrix} = M^2 \begin{pmatrix} F_{n-2} \\ F_{n-3} \end{pmatrix} = \dots = M^{n-1} \begin{pmatrix} F_1 \\ F_0 \end{pmatrix} $$注意这里的指数是 ,因为我们是从 开始转移 次得到 。 由于 ,我们有:
$$\begin{pmatrix} F_n \\ F_{n-1} \end{pmatrix} = M^{n-1} \begin{pmatrix} 1 \\ 0 \end{pmatrix} $$如果我们计算出矩阵 $M^{n-1} = \begin{pmatrix} a & b \\ c & d \end{pmatrix}$,那么:
$$\begin{pmatrix} F_n \\ F_{n-1} \end{pmatrix} = \begin{pmatrix} a & b \\ c & d \end{pmatrix} \begin{pmatrix} 1 \\ 0 \end{pmatrix} = \begin{pmatrix} a \cdot 1 + b \cdot 0 \\ c \cdot 1 + d \cdot 0 \end{pmatrix} = \begin{pmatrix} a \\ c \end{pmatrix} $$所以, 就是 这个矩阵左上角的元素 !(这里假设 。如果 , 可以直接处理)。
-
一个稍微不同的、更常用的形式: 使用状态向量 和初始向量 。
$$\begin{pmatrix} F_2 \\ F_1 \end{pmatrix} = M^1 \begin{pmatrix} F_1 \\ F_0 \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix} \begin{pmatrix} 1 \\ 0 \end{pmatrix} = \begin{pmatrix} 1 \\ 1 \end{pmatrix} $$推广可得:
$$\begin{pmatrix} F_{n+1} \\ F_n \end{pmatrix} = M^n \begin{pmatrix} F_1 \\ F_0 \end{pmatrix} = M^n \begin{pmatrix} 1 \\ 0 \end{pmatrix} $$在这种情况下,计算 ,则:
$$\begin{pmatrix} F_{n+1} \\ F_n \end{pmatrix} = \begin{pmatrix} a & b \\ c & d \end{pmatrix} \begin{pmatrix} 1 \\ 0 \end{pmatrix} = \begin{pmatrix} a \\ c \end{pmatrix} $$所以, 是 矩阵的左下角元素 。 我们也可以用初始向量 $\begin{pmatrix} F_2 \\ F_1 \end{pmatrix} = \begin{pmatrix} 1 \\ 1 \end{pmatrix}$,然后计算 :
$$\begin{pmatrix} F_{n+1} \\ F_n \end{pmatrix} = M^{n-1} \begin{pmatrix} F_2 \\ F_1 \end{pmatrix} = M^{n-1} \begin{pmatrix} 1 \\ 1 \end{pmatrix} $$如果 $M^{n-1} = \begin{pmatrix} a' & b' \\ c' & d' \end{pmatrix}$,则 。这个形式稍微复杂一点。
最常用的结论:计算 $M^n = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}^n$,则 就是结果矩阵的 元素(0-based index,即左下角 )。 这个形式很方便,因为指数是 。
现在的问题转化为:如何快速计算矩阵 的 次幂?
7. 矩阵快速幂算法
计算一个数 的 次幂可以用快速幂 (Binary Exponentiation / Exponentiation by Squaring),时间复杂度 。其原理是利用 的二进制表示:
$$x^n = x^{\sum_{i=0}^k b_i 2^i} = \prod_{i=0}^k (x^{2^i})^{b_i} $$其中 是 的二进制表示的第 位。 我们可以通过反复平方 来得到 ,然后根据 的二进制位 决定是否将对应的 乘入最终结果。
这个思想完全适用于矩阵。我们需要定义矩阵乘法,并注意矩阵乘法不满足交换律 A B B A。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll; // 使用 long long 避免溢出
// 定义矩阵结构体
struct Mat {
ll mat[2][2];
// 构造函数
Mat() { memset(mat, 0, sizeof(mat)); }
};
ll MOD = 1e9 + 7; // 假设模数为 10^9 + 7, 根据题目要求修改
// 矩阵乘法 (计算 C = A * B)
Mat mul(Mat a, Mat b) {
Mat res; // 结果矩阵 C
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
for (int k = 0; k < 2; ++k) {
// C[i][j] = sum(A[i][k] * B[k][j])
// 注意取模!
res.mat[i][j] = (res.mat[i][j] + a.mat[i][k] * b.mat[k][j]) % MOD;
}
}
}
return res;
}
// 矩阵快速幂 (计算 base^p mod MOD)
Mat mat_pow(Mat base, ll p) {
Mat res;
// 初始化 res 为单位矩阵 I
// I = [[1, 0], [0, 1]]
res.mat[0][0] = res.mat[1][1] = 1;
res.mat[0][1] = res.mat[1][0] = 0;
while (p > 0) {
// 如果 p 的当前最低位是 1 (p % 2 == 1)
if (p & 1) {
res = mul(res, base); // res = res * base^(2^k)
}
// base 自乘,准备下一位
base = mul(base, base); // base = base^(2^k) -> base^(2^(k+1))
// p 右移一位 (p = p / 2)
p >>= 1;
}
return res;
}
// 使用矩阵快速幂计算 F(n) mod MOD
ll fib_mat(ll n) {
if (n == 0) return 0;
if (n == 1) return 1 % MOD; // 注意 n=1 时也要对 MOD 取模
Mat base;
// base = [[1, 1], [1, 0]]
base.mat[0][0] = 1; base.mat[0][1] = 1;
base.mat[1][0] = 1; base.mat[1][1] = 0;
// 我们需要计算 base^n (对应 F_n = (M^n)[1][0] 的形式)
Mat res_mat = mat_pow(base, n); // 计算 M^n mod MOD
// F(n) 是结果矩阵的 [1][0] (左下角) 元素
return res_mat.mat[1][0];
// 如果用 F(n) = (M^(n-1))[0][0] 的形式,则调用 mat_pow(base, n-1) 并返回 res_mat.mat[0][0]
}
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
ll n;
cin >> n;
// 设置模数 (如果需要从输入读取)
// cin >> MOD;
cout << fib_mat(n) << endl;
return 0;
}
- 代码解释:
- 定义
Mat
结构体存储 2x2 矩阵。 mul
函数实现两个 2x2 矩阵的乘法 。注意每次加法和乘法后都要取模,即(res.mat[i][j] + a.mat[i][k] * b.mat[k][j]) % MOD
,防止中间结果溢出long long
以及保证最终结果在模MOD
下正确。mat_pow
函数实现矩阵的快速幂 。res
初始化为单位矩阵(矩阵乘法的幺元)。循环体内,如果p
的当前二进制位为 1,则将当前的base
(代表 ) 乘入res
;然后base
自乘变为 ;p
右移一位。fib_mat
函数处理 的边界情况,然后构造基础转移矩阵base
,调用mat_pow
计算 ,最后根据我们选择的递推形式返回结果矩阵的[1][0]
元素作为 。
- 定义
- 复杂度分析:
- 时间复杂度: 矩阵乘法
mul
的复杂度是 ,其中 是矩阵的维度。这里 ,所以是 ,即 常数时间。矩阵快速幂mat_pow
需要进行 次矩阵乘法( 是指数,这里是 )。因此,总时间复杂度是 。 - 空间复杂度: ,只使用了常数个矩阵变量和辅助变量。
- 时间复杂度: 矩阵乘法
矩阵快速幂是解决形如 这类常系数线性齐次递推关系在 很大时的标准武器。
模运算下的斐波那契数列
在算法竞赛中,斐波那契数列常常需要对一个模数 取模,即计算 。前面矩阵快速幂的代码已经考虑了模运算。但模运算下的斐波那契数列还有一些独特的性质。
8. 斐波那契数列模 M 的周期性 (Pisano Period)
将斐波那契数列的每一项都对 取模,得到一个新的序列 。这个序列是周期性的。这个周期被称为皮萨诺周期 (Pisano Period),记作 。
例如,模 3:
: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34,
: 0, 1, 1, 2, 0, 2, 2, 1, 0, 1,
序列是 0, 1, 1, 2, 0, 2, 2, 1
, (0, 1, ...)
重复了。周期是从 这个状态对开始,到下一次出现 前结束。这里的周期长度是 8。即 。
关键性质: 对于 ,有
这意味着,如果我们需要计算 且 非常巨大,我们可以先计算出皮萨诺周期 ,然后计算 ,最后只需求 即可。由于 ,计算 可以用 的迭代法或者 的矩阵快速幂。
如何求 ? 没有已知的简单公式可以直接计算 。但我们可以通过模拟斐波那契数列模 的生成过程来找到周期: 从 开始,不断计算下一项 ,直到再次遇到 这个状态对。由于模 下的状态对 只有 种可能(),根据鸽巢原理,这个序列必然会出现重复的状态对,并且一旦 重复,整个序列就开始循环。周期长度 满足 。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 计算斐波那契数列模 m 的皮萨诺周期 pi(m)
ll get_pisano_period(ll m) {
if (m <= 1) return 1; // 特殊情况处理:pi(1)=1
ll a = 0, b = 1;
ll c; // F_{i+1} mod m
// 周期从 (F_0 mod m, F_1 mod m) = (0, 1) 开始
// 我们寻找下一个 (0, 1) 出现的位置
for (ll i = 0; i < m * m; ++i) { // 周期长度理论上界是 m*m
c = (a + b) % m;
a = b;
b = c;
// 找到周期模式 (0, 1)
// 注意 i=0 时 (a,b)=(1,1), i=1 时 (a,b)=(1,2) or (1,0) ...
// 当 (a, b) 重新变为 (0, 1) 时,表示一个周期结束
// 此时已经产生了 i+1 对 (从 (F_0,F_1) 到 (F_i, F_{i+1}))
// 周期长度是 i+1
if (a == 0 && b == 1) {
return i + 1;
}
}
// 理论上对于 m > 1,总能在 m*m 步内找到周期
return m * m; // 实际上不会执行到这里,可以视为错误或未找到
}
// 迭代计算 F(n) mod m, 空间 O(1)
ll fib_mod(ll n, ll m) {
if (m <= 1) return 0; // F(n) mod 1 = 0
if (n <= 1) return n % m;
ll a = 0, b = 1;
ll cur;
for (ll i = 2; i <= n; ++i) {
cur = (a + b) % m;
a = b;
b = cur;
}
return b;
}
// (需要包含上面定义的 Mat 结构体, mul, mat_pow 函数)
// ll fib_mat(ll n, ll mod) { ... } // 使用矩阵快速幂计算 F(n) mod mod
int main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
ll n; // 可能非常大
ll m; // 模数
cin >> n >> m;
if (m <= 1) {
cout << 0 << endl;
return 0;
}
ll period = get_pisano_period(m);
ll n_reduced = n % period; // 使用周期性缩减 n
// 计算 F(n_reduced) mod m
// 方法1:使用 O(n_reduced) = O(period) 的迭代法
cout << fib_mod(n_reduced, m) << endl;
// 方法2:如果 period 仍然很大 (e.g., m 很大导致 m^2 很大),
// 可以用 O(log n_reduced) = O(log period) 的矩阵快速幂
// 注意:需要确保 MOD 在 fib_mat 中设置为当前的 m
// MOD = m; // 临时设置全局 MOD 或将 m 作为参数传入 fib_mat
// cout << fib_mat(n_reduced, m) << endl; // 假设 fib_mat 接受模数参数
return 0;
}
- 代码解释:
get_pisano_period(m)
函数通过迭代模拟 的过程,寻找 的再次出现,返回周期长度。循环上界设为 保证能找到。fib_mod(n, m)
是前面 空间迭代计算 的函数。main
函数先读入 和 ,处理 的边界。然后调用get_pisano_period
得到周期period
。计算 。最后调用fib_mod
(或者fib_mat
如果 仍然很大) 计算 。
- 复杂度分析:
get_pisano_period(m)
的时间复杂度是 ,最坏情况 。- 计算 :
- 如果用迭代法
fib_mod
,时间复杂度 ,最坏 。 - 如果用矩阵快速幂
fib_mat
,时间复杂度 ,最坏 。
- 如果用迭代法
- 总复杂度主要取决于求周期和最后计算这两部分中较大的一个。通常是 或 。
- 适用场景: 当 极其巨大 (例如 ,甚至更大,以字符串形式给出),而模数 相对较小 (例如 ) 时,皮萨诺周期是唯一可行的方法。你需要先对大数 取模 (这需要大数模运算,或者如果 是指数形式 ,可以用费马小定理/欧拉定理结合指数循环节来求 )。
关于皮萨诺周期的一些补充:
- 存在且 。
- 如果 ( 是素数),有 的关系(有一些例外,如 for )。
- 如果 是 的标准素数分解,则 $\pi(M) = \text{lcm}(\pi(p_1^{a_1}), \pi(p_2^{a_2}), \dots, \pi(p_r^{a_r}))$。结合中国剩余定理,可以分解问题,但这通常比直接模拟找周期更复杂,竞赛中较少这样考。
- 的值有规律,可以预计算或查找 ($\pi(10)=60, \pi(100)=300, \pi(1000)=1500, \pi(10^k) = 15 \cdot 10^{k-1}$ for )。
斐波那契数列的推广与应用
斐波那契数列不仅仅是它本身,它的思想和解决方法(尤其是矩阵快速幂)可以推广到更广泛的线性递推问题。
9. 广义斐波那契数列与线性递推
形如
$$F_n = c_1 F_{n-1} + c_2 F_{n-2} + \dots + c_k F_{n-k} $$的 阶常系数线性齐次递推关系,都可以用矩阵快速幂在 的时间内求解 (通常是模 意义下)。
构造 转移矩阵: 状态向量通常选为 $\mathbf{v}_n = \begin{pmatrix} F_n \\ F_{n-1} \\ \vdots \\ F_{n-k+1} \end{pmatrix}$。 下一个状态是 $\mathbf{v}_{n+1} = \begin{pmatrix} F_{n+1} \\ F_n \\ \vdots \\ F_{n-k+2} \end{pmatrix}$。 我们需要找到矩阵 使得 。
根据递推关系 $F_{n+1} = c_1 F_n + c_2 F_{n-1} + \dots + c_k F_{n-k+1}$,以及 , , , ,可以构造出转移矩阵 :
$$M = \begin{pmatrix} c_1 & c_2 & \dots & c_{k-1} & c_k \\ 1 & 0 & \dots & 0 & 0 \\ 0 & 1 & \dots & 0 & 0 \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & \dots & 1 & 0 \end{pmatrix} $$这是一个 的矩阵。第一行是递推系数 到 ,下面是一个 的单位矩阵,嵌入在左上角,并向右移动一列,右侧和底行补 0。
然后,计算 ( 通常是 或 等,取决于初始状态向量的选择和 的定义),再乘以初始状态向量 $\mathbf{v}_{\text{init}} = \begin{pmatrix} F_{k-1} \\ F_{k-2} \\ \vdots \\ F_0 \end{pmatrix}$(或其他合适的初始向量),就能得到目标状态向量 ,从而求出 。
带常数项的线性递推: 可以通过增广矩阵解决。构造 的矩阵。状态向量增加一个常数项 1: $\mathbf{v}'_n = \begin{pmatrix} F_n \\ F_{n-1} \\ \vdots \\ F_{n-k+1} \\ 1 \end{pmatrix}$。 转移矩阵 类似:
$$M' = \begin{pmatrix} c_1 & c_2 & \dots & c_k & C \\ 1 & 0 & \dots & 0 & 0 \\ 0 & 1 & \dots & 0 & 0 \\ \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & \dots & 1 & 0 \\ 0 & 0 & \dots & 0 & 1 \end{pmatrix} $$然后计算 乘以初始状态向量 $\mathbf{v}'_{\text{init}} = \begin{pmatrix} F_{k-1} \\ \vdots \\ F_0 \\ 1 \end{pmatrix}$。
例题场景: 很多 DP 问题最后归结为一个线性递推关系,但 的范围很大(如 ),就需要用矩阵快速幂加速。例如,某些限制步数的路径计数、特定模式串的构造计数等。
10. 组合计数中的斐波那契
前面提到, 可以解释为用 1x1 和 1x2 骨牌铺满 1x(n-1) 地板的方案数(假设 对应 1x0 地板, 对应 1x1 地板)。
- 铺 1x(n-1) 地板:
- 最后一块是 1x1,则前面需要铺满 1x(n-2),方案数 (对应 项)
- 最后一块是 1x2,则前面需要铺满 1x(n-3),方案数 (对应 项)
- 总方案数 = 。若定义 , 则 是铺 1x(n-1) 的方案数。
- 若定义 (铺 1x1), (铺 1x2),则 是铺 1x n 地板的方案数。这 (如果 )。要看清题目定义和边界。
类似地,爬楼梯问题:每次爬 1 或 2 级,爬 级楼梯的方案数。 令 为爬 级楼梯的方案数。
- 最后一步是爬 1 级,之前在 级,方案数
- 最后一步是爬 2 级,之前在 级,方案数
- 总方案数 。
- 初始条件: (只能爬1级), (可以 1+1 或 2)。
- 这个序列是 1, 2, 3, 5, 8, ... 即 (基于 )。
识别技巧: 当你发现一个问题的状态转移可以分解为来自前一个状态或前两个(或前 k 个)状态,且转移方式固定(系数是常数)时,就要警惕是不是斐波那契或类似的线性递推。
赛场实战策略与避坑指南
理解了各种方法后,如何在比赛中正确、快速地使用?
11. 如何选择合适的算法?
关键在于分析数据范围 和模数 (如果有)。
- 很小 (e.g., 使得 不超过
long long
):- 迭代 + 空间优化 (
fib_opt
) 是最佳选择。速度快,代码简单,不易出错。 - 如果需要模运算,且 在 内,这个方法也足够快。
- 迭代 + 空间优化 (
- 较大 (e.g., ),通常需要模运算:
- 迭代 + 空间 (
fib_opt
带模运算) 仍然是首选。 - DP 数组 (
fib_iter
带模运算) 也可以,但空间占用稍大。 - 记忆化搜索 (
fib_mem
带模运算) 也行,但常数可能稍大,且有递归深度限制风险(虽然 C++ 默认栈深通常够用)。
- 迭代 + 空间 (
- 非常大 (e.g., , ): 必须使用 算法。
- 矩阵快速幂 (
fib_mat
) 是标准解法。 需要熟练掌握矩阵乘法和快速幂模板,并正确处理模运算。复杂度 ,对于斐波那契是 。 - 如果题目只要求 且 相对较小 (e.g., ),可以考虑皮萨诺周期。先 或 求周期 ,然后计算 。计算 时,因为指数 变小了,可以用 迭代或 矩阵快速幂。当 极其巨大 (远超
long long
范围,如以字符串给出) 时,这是唯一方法。
- 矩阵快速幂 (
- 问题本质是 阶线性递推, 很大:
- 构造 转移矩阵,使用 的矩阵快速幂。关键是正确构造转移矩阵。
12. 常见陷阱与注意事项 (Pitfalls)
-
整数溢出 (Integer Overflow):
- 斐波那契数列增长很快 ()。即使 只有 93 左右, 也会超过 64 位整数 (
unsigned long long
的最大值约为 )。 - 解决方案:
- 如果题目不需要模运算且 较大,可能需要使用高精度计算 (模拟大数加法)。
- 如果题目要求模 ,必须在每次加法和乘法后都取模,尤其是在矩阵乘法中。
res.mat[i][j] = (res.mat[i][j] + a.mat[i][k] * b.mat[k][j]) % MOD;
这一步至关重要,不能只在最后结果取模,否则中间计算会溢出。 - 确保使用
long long
存储中间结果和最终结果,即使模数 只是int
范围,因为两个long long
相乘可能超过long long
,但在取模前如果结果保存在long long
中是允许的(只要 不超long long
,或者乘法时小心处理)。更安全的做法是确保 不会溢出,例如使用(a % MOD * b % MOD) % MOD
。
- 斐波那契数列增长很快 ()。即使 只有 93 左右, 也会超过 64 位整数 (
-
模运算错误 (Modulo Arithmetic Errors):
- 负数取模: 在 C++ 中,
(-a) % M
的结果可能是负数 (例如-5 % 3 == -2
)。如果需要保证结果在 范围内,标准做法是(x % M + M) % M
。虽然斐波那契数列非负,但在推广的线性递推或中间计算(如系数为负)中可能遇到负数,需要注意。对于斐波那契本身,只要 是正数,(a % M + b % M) % M
不会产生负数。 - 忘记取模: 在迭代、矩阵乘法等步骤中漏掉
% MOD
。
- 负数取模: 在 C++ 中,
-
边界条件错误 (Off-by-One Errors / Base Cases):
- 的定义要明确(题目可能会有不同的起始定义 )。
- 矩阵快速幂的指数是 还是 ?取决于你的状态转移矩阵和初始向量的选择。务必推导清楚或用小数据验证。例如,用 ,若求 ,通常计算 作用于 或 作用于 。
- 循环的起始和结束条件是否正确。比如迭代是从 开始。
-
复杂度选择错误 (Complexity Misjudgment):
- 看到 仍然使用 算法导致 TLE。
- 不必要地使用矩阵快速幂:如果 只有 , 迭代比 矩阵快速幂更快(因为矩阵乘法常数 较大)且更容易写对。
-
矩阵构造错误:
- 对于广义线性递推,转移矩阵的系数 ()、1 的位置、0 的位置要仔细核对。特别是对于非齐次项 的增广矩阵。
-
皮萨诺周期使用不当:
- 求周期
get_pisano_period
算法写错,或者死循环 (忘记检查a == 0 && b == 1
)。 - 计算 时,如果 是超大数 (字符串),需要用大数模运算。如果 形式,可能需要指数循环节和欧拉定理/费马小定理。
- 求周期
总结
斐波那契数列,这个古老而优美的数学序列,在现代算法竞赛中依然扮演着重要角色。它不仅是一个常见的考点,更是学习和掌握以下核心算法思想的绝佳载体:
- 递归与分治: 理解递归思想和子问题重复计算的弊端。
- 动态规划: 学习记忆化搜索和迭代两种 DP 实现方式,以及状态压缩(滚动数组)优化空间。
- 矩阵快速幂: 掌握处理线性递推在指数级 下的高效计算方法 (),这是 ICPC 竞赛中的必备技能。
- 模运算与数论: 理解模运算规则和周期性(皮萨诺周期 )在处理大数问题中的应用。
- 组合意义: 培养从组合问题中识别斐波那契(或类似递推)模式的能力。
经典例题
P4994 终于结束的起点
题目描述
广为人知的斐波拉契数列 是这么计算的
$$\mathrm{fib}(n)=\begin{cases} 0,& n=0 \\ 1,& n=1 \\ \mathrm{fib}(n-1) + \mathrm{fib}(n-2),& n>1 \end{cases} $$也就是 ,每一项都是前两项之和。
小 F 发现,如果把斐波拉契数列的每一项对任意大于 的正整数 取模的时候,数列都会产生循环。
当然,小 F 很快就明白了,因为 () 和 ( 最多只有 种取值,所以在 次计算后一定出现过循环。
甚至更一般地,我们可以证明,无论取什么模数 ,最终模 下的斐波拉契数列都会是 。
现在,给你一个模数 ,请你求出最小的 ,使得 $\mathrm{fib}(n) \bmod M = 0, \mathrm{fib}(n + 1) \bmod M = 1$。
输入格式
输入一行一个正整数 。
输出格式
输出一行一个正整数 。
输入输出样例 #1
输入 #1
2
输出 #1
3
输入输出样例 #2
输入 #2
6
输出 #2
24
说明/提示
样例 1 解释
斐波拉契数列为 ,在对 取模后结果为 。
我们可以发现,当 时,,也就是我们要求的 的最小值。
数据范围
对于 的数据,;
对于 的数据,;
对于 的数据,。
提示
如果你还不知道什么是取模 ,那我也很乐意告诉你,模运算是求整数除法得到的余数,也就是竖式除法最终「除不尽」的部分,也即
$$a \bmod M =k \iff a = bM + k\ (M > 0, 0 \leq k < M) $$其中 都是非负整数。
如果你使用 C
/ C++
,你可以使用 %
来进行模运算。
如果你使用 Pascal
,你可以使用 mod
来进行模运算。
参考代码
#include<bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int M;
cin >> M;
// 斐波那契数列初始化
int a = 0, b = 1;
int n = 0;
// 从第2项开始计算斐波那契数列
while (true) {
n++;
int c = (a + b) % M; // 计算下一项
a = b; // 更新a
b = c; // 更新b
// 检查是否满足条件:fib(n) % M == 0 和 fib(n+1) % M == 1
if (a == 0 && b == 1) {
cout << n << endl;
break;
}
}
return 0;
}
P4000 斐波那契数列
题目描述
大家都知道,斐波那契数列是满足如下性质的一个数列:
•
•
• ( 且 为整数)
请你求出 的值。
输入格式
-
第 1 行:一个整数 。
-
第 2 行:一个整数 。
输出格式
- 第 1 行: 的值。
输入输出样例 #1
输入 #1
5
1000000007
输出 #1
5
输入输出样例 #2
输入 #2
10
1000000007
输出 #2
55
说明/提示
对于 的数据,
参考代码
#include <bits/stdc++.h>
using namespace std;
// 类型别名,方便书写
#define ll long long // 使用 ll 作为 long long 整数的简写
#define ull unsigned long long // 使用 ull 作为 unsigned long long 整数的简写
// 全局变量和结构体
unordered_map < ull , ll > circ; // 哈希表,用于检测斐波那契数列模 MOD 的循环节(皮萨诺周期)
// 使用类似 Pollard's Rho 的方法进行循环查找
// 键 (Key): 状态表示,将 (F_k, F_{k+1}) 两个模 MOD 的值合并成一个 ull
// 值 (Value): 首次遇到这个状态时的步数 'k'
ll len; // 存储检测到的循环节长度(皮萨诺周期)
int MOD; // 输入的模数 p
int MX = 1 << 18; // 预计算块的大小 (2^18 = 262144)。用于类似 "Baby-step Giant-step" 的分块优化
mt19937_64 rnd(time(0)); // 64 位梅森旋转伪随机数生成器,使用当前时间作为种子
// 定义 2x2 矩阵结构体,用于斐波那契数列计算
struct matrix{
ll arr[2][2]; // 存储矩阵元素的 2x2 数组
// 默认构造函数:将矩阵初始化为零矩阵
matrix(){memset(arr , 0 , sizeof(arr));}
// 重载 [] 运算符,方便访问矩阵元素 (例如 matrix[row][col])
ll* operator [](int x){return arr[x];}
// 重载 * 运算符,实现矩阵乘法 (结果对 MOD 取模)
friend matrix operator *(matrix p , matrix q){
matrix x; // 结果矩阵,已由构造函数初始化为零
// 标准矩阵乘法循环
for(int i = 0 ; i < 2 ; ++i)
for(int j = 0 ; j < 2 ; ++j)
for(int k = 0 ; k < 2 ; ++k)
// 累加乘积项
// 注意:这里没有在循环内部取模,依赖于最后的结果取模
// 这假设 MOD * MOD * 2 不会溢出 long long,对于 MOD < 2^31 是安全的
x[i][k] += p[i][j] * q[j][k];
// 将结果矩阵的每个元素对 MOD 取模
for(int i = 0 ; i < 2 ; ++i)
for(int j = 0 ; j < 2 ; ++j) x[i][j] %= MOD;
return x; // 返回结果矩阵
}
}G , T[2][1 << 18 | 1]; // G: 斐波那契转移矩阵 [[1, 1], [1, 0]] (虽然代码中没显式定义 G,但 T[0][1] 就是它)
// T: 预计算矩阵幂的数组
// T[0][i] = M^i mod MOD (M 是斐波那契转移矩阵)
// T[1][i] = (M^MX)^i mod MOD (用于大步计算)
signed main(){
// 使用静态数组读取超长数字 n,避免动态分配的开销
// 数组大小 300000003 足够容纳 10^30000000
static char str[300000003];
// 使用 scanf 读取,可能比 cin 更快
scanf("%s %d" , str + 1 , &MOD); // 读取 n (存入 str+1 开始) 和模数 MOD
// 初始化预计算矩阵数组 T
// T[0][0] 和 T[1][0] 设置为单位矩阵 I = [[1, 0], [0, 1]]
T[0][0][0][0] = T[0][0][1][1] = T[1][0][0][0] = T[1][0][1][1] = 1;
// 初始化斐波那契转移矩阵 M = [[1, 1], [1, 0]],并存储在 T[0][1] 中
// T[0][1] 对应 M^1
T[0][1][0][1] = T[0][1][1][0] = T[0][1][1][1] = 1;
// 预计算 M^i mod MOD (小步)
// T[0][i] = T[0][i-1] * T[0][1] = M^(i-1) * M = M^i
for(int i = 2 ; i <= MX ; ++i) T[0][i] = T[0][i - 1] * T[0][1];
// 预计算 (M^MX)^i mod MOD (大步)
// T[1][1] 存储 M^MX
T[1][1] = T[0][MX];
// T[1][i] = T[1][i-1] * T[1][1] = (M^MX)^(i-1) * (M^MX) = (M^MX)^i
for(int i = 2 ; i <= MX ; ++i) T[1][i] = T[1][i - 1] * T[1][1];
// 使用类似 Pollard's Rho 的随机化方法查找循环节长度 (皮萨诺周期)
while(1){
// 生成一个随机步数 x (只取低 46 位,因为 18+18+10 < 64,但这里用了奇怪的位移 >> 28)
// x 的范围理论上很大,但实际只需要在循环节内即可
// (rnd() << 28 >> 28) 是一种有点奇怪的取低位随机数的方式,可能只是为了获得一个不太大的随机数
ll x = (rnd() << 28 >> 28); // 取一个随机步数 k
// 计算 M^x mod MOD
// 通过预计算数组快速计算 M^x = M^(x % MX) * (M^MX)^(x / MX)
// x & (MX - 1) 等价于 x % MX
// x >> 18 等价于 x / MX (因为 MX = 2^18)
matrix C = T[0][x & (MX - 1)] * T[1][x >> 18];
// 将矩阵 C 的第一行 (F_{x+1}, F_x) 编码成一个 ull 作为状态标识符
// val = (F_{x+1} << 32) | F_x
ull val = ((1ull * C[0][0]) << 32) | C[0][1]; // C[0][0]=F_{x+1}, C[0][1]=F_x
// 检查哈希表中是否已存在该状态
if(circ.find(val) != circ.end()){
// 如果找到,说明遇到了循环
// 循环节长度 len = 当前步数 x - 第一次遇到该状态的步数 circ[val]
len = abs(circ[val] - x); // 取绝对值,防止 x 恰好等于之前的 circ[val] (虽然概率极低)
break; // 找到循环节,退出循环
}
// 如果没找到,将当前状态和步数存入哈希表
circ[val] = x;
} // 循环直到找到循环节
// 计算 n mod len (n 是输入的大数,len 是循环节长度)
ll sum = 0; // 用于存储 n mod len 的结果
// 逐位读取大数 n 的字符串表示,计算模 len 的值
for(int i = 1 ; str[i] ; ++i) sum = (sum * 10 + str[i] - '0') % len;
// 计算最终结果 F_n mod MOD = F_{n mod len} mod MOD
// 使用预计算的 T 数组快速计算 M^sum mod MOD
// M^sum = M^(sum % MX) * (M^MX)^(sum / MX)
// (T[0][sum & (MX - 1)] * T[1][sum >> 18]) 计算出 M^sum
// 结果矩阵的第一行是 (F_{sum+1}, F_{sum})
// 我们需要 F_sum,它在结果矩阵的 [0][1] 位置 (或 [1][0] 位置)
cout << (T[0][sum & (MX - 1)] * T[1][sum >> 18])[0][1]; // 输出 F_sum mod MOD
return 0; // 程序正常结束
}
P1962 斐波那契数列
题目描述
大家都知道,斐波那契数列是满足如下性质的一个数列:
$$F_n = \left\{\begin{aligned} 1 \space (n \le 2) \\ F_{n-1}+F_{n-2} \space (n\ge 3) \end{aligned}\right. $$请你求出 的值。
输入格式
一行一个正整数
输出格式
输出一行一个整数表示答案。
输入输出样例 #1
输入 #1
5
输出 #1
5
输入输出样例 #2
输入 #2
10
输出 #2
55
说明/提示
【数据范围】
对于 的数据,;
对于 的数据,。
参考代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 定义模数常量,用于结果取模
const ll MOD = 1e9 + 7; // 即 1,000,000,007
// 定义 2x2 矩阵结构体
struct Matrix {
ll m[2][2]; // 存储矩阵元素的二维数组
};
// 矩阵乘法函数,计算 a * b 并返回结果矩阵 res
// 参数 a 和 b 是常量引用,避免不必要的拷贝,提高效率
Matrix multiply(const Matrix& a, const Matrix& b) {
Matrix res; // 用于存储结果的矩阵
// 遍历结果矩阵的行
for(int i = 0; i < 2; ++i) {
// 遍历结果矩阵的列
for(int j = 0; j < 2; ++j) {
// 初始化结果矩阵当前元素为 0
res.m[i][j] = 0;
// 根据矩阵乘法定义计算 res.m[i][j] = sum(a.m[i][k] * b.m[k][j])
for(int k = 0; k < 2; ++k) {
// 累加乘积,注意每次乘法后取模,加法后也要取模,防止溢出
res.m[i][j] = (res.m[i][j] + a.m[i][k] * b.m[k][j] % MOD) % MOD;
}
}
}
// 返回计算结果矩阵
return res;
}
// 矩阵快速幂函数,计算 base 矩阵的 n 次幂 (base^n)
// 参数 base 是待求幂的矩阵,n 是指数
Matrix fast_pow(Matrix base, ll n) {
Matrix res; // 用于存储最终结果的矩阵
// 初始化 res 为单位矩阵 I = [[1, 0], [0, 1]]
// 单位矩阵是矩阵乘法的幺元,相当于整数乘法中的 1
res.m[0][0] = res.m[1][1] = 1;
res.m[0][1] = res.m[1][0] = 0;
// 使用二进制快速幂算法
while(n > 0) { // 当指数 n 大于 0 时循环
// 检查 n 的二进制表示的最低位是否为 1
if(n & 1) {
// 如果最低位为 1,说明当前幂次的 base 需要乘入结果 res
res = multiply(res, base); // res = res * base^(2^k)
}
// base 自乘,为计算下一位做准备
base = multiply(base, base); // base = base^(2^k) -> base^(2^(k+1))
// 将 n 右移一位(相当于 n = n / 2),处理下一位二进制位
n >>= 1;
}
// 返回最终结果矩阵 res = base^n
return res;
}
// 计算斐波那契数列第 n 项的函数 (模 MOD)
// 注意:此实现基于 F_0=0, F_1=1, F_2=1, F_3=2...
ll Fibonacci(ll n) {
// 定义斐波那契递推关系的转移矩阵
// M = [[1, 1],
// [1, 0]]
// 使得 [[F_{k+1}], [F_k]] = M * [[F_k], [F_{k-1}]]
Matrix base;
base.m[0][0] = 1; // M[0][0]
base.m[0][1] = 1; // M[0][1]
base.m[1][0] = 1; // M[1][0]
base.m[1][1] = 0; // M[1][1]
// 处理特殊/边界情况
if (n == 0) return 0; // F_0 = 0
if (n == 1) return 1; // F_1 = 1
// n=2 的情况可以不特殊处理,由下面的公式计算
// 根据矩阵乘法性质:
// [[F_n], [F_{n-1}]] = M^(n-1) * [[F_1], [F_0]]
// [[F_n], [F_{n-1}]] = M^(n-1) * [[1], [0]]
// 设 M^(n-1) = [[a, b], [c, d]]
// 则 F_n = a * 1 + b * 0 = a
// 所以 F_n 是矩阵 M^(n-1) 的左上角元素 (m[0][0])
// 注意:n >= 1
return fast_pow(base, n - 1).m[0][0]; // 返回 M^(n-1) 的 [0][0] 元素
}
// 主函数
int main() {
ll n; // 定义长整型变量 n 用于存储输入的斐波那契项数
cin >> n; // 从标准输入读取 n
// 调用 Fibonacci 函数计算第 n 项斐波那契数 (模 MOD) 并输出
cout << Fibonacci(n) << endl;
return 0; // 程序正常结束
}
P1939 矩阵加速(数列)
题目描述
已知一个数列 ,它满足:
$$a_x= \begin{cases} 1 & x \in\{1,2,3\}\\ a_{x-1}+a_{x-3} & x \geq 4 \end{cases} $$求 数列的第 项对 取余的值。
输入格式
第一行一个整数 ,表示询问个数。
以下 行,每行一个正整数 。
输出格式
每行输出一个非负整数表示答案。
输入输出样例 #1
输入 #1
3
6
8
10
输出 #1
4
9
19
说明/提示
- 对于 的数据 ;
- 对于 的数据 ;
- 对于 的数据 ,。
参考代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 定义模数常量
const ll MOD = 1e9 + 7;
// 定义矩阵维度
const int DIM = 3;
// 定义 DIM x DIM 矩阵结构体
struct Matrix {
ll m[DIM][DIM];
// 构造函数,默认初始化为零矩阵
Matrix() {
for (int i = 0; i < DIM; ++i) {
for (int j = 0; j < DIM; ++j) {
m[i][j] = 0;
}
}
}
};
// 矩阵乘法函数 (计算 C = A * B), 结果模 MOD
Matrix multiply(const Matrix& a, const Matrix& b) {
Matrix res; // 结果矩阵 C
for (int i = 0; i < DIM; ++i) {
for (int j = 0; j < DIM; ++j) {
// 计算 C[i][j] = sum(A[i][k] * B[k][j]) for k from 0 to DIM-1
for (int k = 0; k < DIM; ++k) {
// 累加乘积,注意使用 long long 避免中间溢出,并在每次加法后取模
res.m[i][j] = (res.m[i][j] + (a.m[i][k] * b.m[k][j]) % MOD) % MOD;
}
}
}
return res; // 返回结果矩阵
}
// 矩阵快速幂函数 (计算 base^p mod MOD)
Matrix fast_pow(Matrix base, ll p) {
Matrix res;
// 初始化 res 为单位矩阵
for (int i = 0; i < DIM; ++i) {
res.m[i][i] = 1;
}
// 二进制快速幂
while (p > 0) {
// 如果 p 的当前最低位是 1
if (p & 1) {
res = multiply(res, base); // res = res * base^(2^k)
}
// base 自乘,准备下一位
base = multiply(base, base); // base = base^(2^k) -> base^(2^(k+1))
// p 右移一位
p >>= 1;
}
return res; // 返回 base^p
}
int main() {
// 输入输出优化 (可选, 在数据量大或 T 大时有用)
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int T; // 测试用例数量
cin >> T;
while (T--) {
ll n; // 数列项数
cin >> n;
// 处理 n <= 3 的基本情况
if (n <= 3) {
cout << 1 << "\n"; // a_1, a_2, a_3 都是 1
continue; // 处理下一个测试用例
}
// 定义转移矩阵 M
Matrix M;
M.m[0][0] = 1; M.m[0][1] = 0; M.m[0][2] = 1;
M.m[1][0] = 1; M.m[1][1] = 0; M.m[1][2] = 0;
M.m[2][0] = 0; M.m[2][1] = 1; M.m[2][2] = 0;
// 计算 M^(n-3)
// 指数是 n-3,因为我们从 v_3 状态转移 n-3 次得到 v_n 状态
Matrix Mn_3 = fast_pow(M, n - 3);
// 计算最终结果 a_n
// a_n = M^(n-3)[0][0]*a_3 + M^(n-3)[0][1]*a_2 + M^(n-3)[0][2]*a_1
// 因为 a_1 = a_2 = a_3 = 1,所以 a_n = M^(n-3)[0][0] + M^(n-3)[0][1] + M^(n-3)[0][2]
ll ans = (Mn_3.m[0][0] + Mn_3.m[0][1] + Mn_3.m[0][2]) % MOD;
// 确保结果非负 (虽然在此特定问题中系数和初始值非负,不太可能出现负数,但好习惯)
ans = (ans % MOD + MOD) % MOD;
cout << ans << "\n"; // 输出结果
}
return 0;
}