把递推写成矩阵 M=[[1,1],[1,0]],f_n 是 M^(n-1) 的左上角;对指数做二进制拆分,用矩阵快速幂把 O(n) 降到 O(log n)。
OJ: roj
题目 ID: 1642
难度:普及+/提高-
标签:数学递推矩阵快速幂python
创建: 2026-09-30 23:43
更新: 2026-10-07 13:50
形式化题目
Fibonacci 数列由
定义。给定整数
规模瓶颈只有一个:
样例
| 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|
| 1 | 1 | 2 | 3 | 5 | |
| 1 | 1 | 2 | 3 | 5 |
表中最后一列是答案,因此样例输出 5。这张表同时说明了「每一步都可以先取模」:
第三行每一格都是第二行对 1000 取模的结果,继续用它往下推得到的仍然是正确值。
正解
思路
朴素做法及其瓶颈。 最直接的想法是从 fibo10.in 的
所以关键问题是:能不能找到一种操作,一次就完成「连续推
第一步:把「推进一步」写成一个固定的线性变换。 观察递推只用到
这一步是可行的,因为递推系数固定:每推进一步都作用同一个矩阵
第二步:把「推
矩阵乘法满足结合律,这正是「可以合并」的来源:先推
具体到答案在哪一格,可以更强地断言:
下表用样例
| 0 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| 1 | 1 | 2 | 3 | 5 | |
| 等于 |
只关心左上角这一格是本题实现上的关键简化:不需要真的存下整个矩阵用来回溯,
也不需要
第三步:用倍增快速求幂。 把指数做二进制拆分:若
base=, 是当前扫描到的二进制位,每轮平方一次; result= 已经累乘进来的那些的乘积,初始为单位矩阵( )。
扫描 base 乘进 result,然后 base 平方、e 右移一位。
整个过程的正确性由不变量 base」加 base 平方」把权重翻倍,正好对应
指数 power() 的逐轮快照:
| 轮次 | 当前位 | 动作 | base |
result |
|---|---|---|---|---|
| 初始 | — | — | [1 1; 1 0] |
[1 0; 0 1] |
| 1 | bit0 = 1 | 乘入 | [1 1; 1 0] |
[1 1; 1 0] |
| 2 | bit1 = 0 | 跳过 | [2 1; 1 1] |
[1 1; 1 0] |
| 3 | bit2 = 0 | 跳过 | [5 3; 3 2] |
[1 1; 1 0] |
| 4 | bit3 = 1 | 乘入 | [34 21; 21 13] |
[55 34; 34 21] |
读法:base 每轮平方,result 只在遇到 1 时更新一次;最后一轮结束后 result 的左上角是 55,
正是
取模可以随时进行:加法和乘法在
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 23:43
# update_at: 2026-09-30 23:43
import sys
BASE = ((1, 1), (1, 0)) # 递推矩阵:F(n+1) = F(n) + F(n-1)
def mat_mul(a: tuple, b: tuple, mod: int) -> tuple:
"""2x2 矩阵乘法后取模:结果的第 i 行是 a 的第 i 行分别点乘 b 的两列。"""
(a00, a01), (a10, a11) = a
(b00, b01), (b10, b11) = b
row0 = (a00 * b00 + a01 * b10) % mod, (a00 * b01 + a01 * b11) % mod
row1 = (a10 * b00 + a11 * b10) % mod, (a10 * b01 + a11 * b11) % mod
return row0, row1
def power(base: tuple, exp: int, mod: int) -> tuple:
"""矩阵快速幂:base 的 exp 次方,每步取模 m。"""
result = ((1, 0), (0, 1)) # 单位矩阵,相当于乘法的 1
while exp:
if exp & 1:
result = mat_mul(result, base, mod)
base = mat_mul(base, base, mod)
exp >>= 1
return result
def solve() -> None:
n, m = map(int, sys.stdin.buffer.read().split())
# 由 [F(n+1), F(n); F(n), F(n-1)] = BASE^n 得 F(n) 是 BASE^(n-1) 的左上角;
# n = 1 时幂为 0,单位矩阵的左上角正是 F(1) = 1。
print(power(BASE, n - 1, m)[0][0] % m)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 00:12
* update_at: 2026-10-06 00:12
*/
// main.cpp:Fibonacci 第 n 项 —— 2x2 矩阵快速幂。
// 递推矩阵 M = [[1,1],[1,0]],f_n 就是 M^(n-1) 的左上角;
// n 可到 2e9,必须用快速幂把 O(n) 递推降到 O(log n)。
#include <cstdio>
typedef long long ll;
ll m; // 模数
// 2x2 矩阵,固定大小直接用全局函数处理
struct Mat {
ll a[2][2];
};
// 矩阵乘法,结果每个元素都取模
Mat mul(const Mat &x, const Mat &y) {
Mat z;
for (int i = 0; i < 2; ++i)
for (int j = 0; j < 2; ++j) {
ll sum = 0;
for (int k = 0; k < 2; ++k)
sum += x.a[i][k] * y.a[k][j]; // 每项 < m^2 < 2e18,不溢出
z.a[i][j] = sum % m;
}
return z;
}
// 计算 M^e 的左上角:指数二进制拆分,base 每轮平方,该位为 1 时先乘入再平方
ll fib_pow(ll e) {
Mat base; // base 表示 M^(2^b),b 是当前扫到的二进制位
base.a[0][0] = 1; base.a[0][1] = 1;
base.a[1][0] = 1; base.a[1][1] = 0;
Mat result; // result 累乘已经扫过的 M^(2^b_i),初始为单位矩阵 M^0
result.a[0][0] = 1; result.a[0][1] = 0;
result.a[1][0] = 0; result.a[1][1] = 1;
while (e > 0) {
if (e & 1) result = mul(result, base);
base = mul(base, base);
e >>= 1;
}
return result.a[0][0]; // M^e 的左上角就是 f_(e+1)
}
int main() {
ll n; // 要求的项数
scanf("%lld %lld", &n, &m);
// 指数为 0(n=1)时 result 是单位矩阵,左上角 1 = f_1,无需特判
printf("%lld\n", fib_pow(n - 1));
return 0;
}复杂度
- 时间复杂度:
。指数 有 位, 每位至多一次乘法加一次平方,单次 2×2 矩阵乘法是常数次运算。 - 空间复杂度:
,只保存常数个 2×2 矩阵。
总结
Fibonacci 的递推是常系数线性递推,因此总可以写成固定矩阵的幂; 有了幂,就能用结合律对指数做二进制拆分,把线性的迭代次数压到对数级。 本题的实现要点只有三个:
- 状态取相邻两项,
; - 答案是
的左上角,不需要额外的向量乘法,也不需要特判 (指数 0 时单位矩阵给出 1); - 快速幂中「判断是否乘入」必须发生在平方之前。
同一套推导适用于任何常系数线性递推(如 Pell 数列
图示解析
下面这张图只保留从输入到答案的主线,说明每个环节解决了什么问题:
输入 n, m(n 可到 2e9)
│
▼
瓶颈:逐项递推要 n-1 次,次数与 n 同阶
│
▼
第一步 状态取相邻两项 → 每步作用同一矩阵 M = [[1,1],[1,0]]
│ v_k = M · v_(k-1)
▼
第二步 合并 n-1 步 → 只需算 M^(n-1)
│ 结合律 M^a · M^b = M^(a+b)
▼
第三步 指数二进制拆分 n-1 = Σ 2^b
│ base 每轮平方;result 遇 1 就乘入
▼
答案 = (M^(n-1))[0][0] mod m 时间 O(log n),空间 O(1)图中前两层是「为什么慢」,后两层是「怎么变快」:矩阵把递推变成幂,倍增把幂变成