平凡的,我们有斐波那契数列:
递推的矩阵形式
记
则
因此算 等价于算 。
复杂度模型
我们约定:
- 设 为两个 位整数相乘的代价; 单调非降。
- ( 是其中一个特征值,证明过程见后文),故 的二进制位数
- 二进制快速幂算 (按最优左→右计数):恰做 次平方,以及 次一般乘,环乘法次数为
于是
即 。
- 矩阵快速幂时,中间矩阵的各元素的位数都不超过 ,与 同阶位数,故其中的大整数乘按 计代价。
- 扩域法的中间量是 ,位数
大整数乘按 计——与 同阶,但常数更大,位数比 (详见 §位数比的解析效应)。
1. 矩阵快速幂
对 做二进制快速幂得 ,再右乘 ,读出第二分量即 。
矩阵朴素乘法恰含 次 元素乘法( 个点积,各 次乘)。故整数乘次数为 ,总代价
这是最直接的精确 次环运算算法。
2. 特征值与对角化
目标是把 对角化成 ,得到
因为对角阵的幂次有一条极优雅的性质: 时 。 这可以极大地减轻运算负担。
特征方程
即
解得两个特征值
易见 ,,且 ,,。
特征向量
由 ,第二行给出 ,取特解
令 的列为这两个特征向量:
则 ,即 。
求逆并推出通项
于是
先算
再左乘 :
得到 Binet 公式(对一切整数 精确成立):
最近整数形式
把它改写成
即
即 就是距 的唯一最近整数。
浮点快速幂
在浮点语义下,对 做快速幂再除以 、四舍五入,环运算次数仍是 ,每次是机器浮点乘——这是有明确适用范围的精确算法:当浮点算出的 仍落在 内时,四舍五入就得到 。
能力边界由精度决定。IEEE-754 binary64 有效位数约 bit; 的量级约为 ,相对误差一旦大到使 跨过半整数边界,四舍五入就会错。后文实测:对本机 f64 快速幂实现,与精确值一致到 ,从 起失准。
因此:小 用浮点 Binet 最省事;要任意大的精确 ,必须离开浮点,改做整数算术。下一节正是通项在整数环上的落地。
3. 扩域
已知 ,Binet 里的 最终消掉。把计算放进环 ,全程整数,就不受浮点精度限制。
设
则 ,,且
运算法则类似复数:把 当作「虚部」单位()。
、 共轭,故类似地,存在 使
于是
算法:快速幂算 ,取出系数 ,再除以 。因 , 必被 整除;实现时用整数除法。
拓展:环乘法的整数乘次数
设 ,。朴素乘法
恰需 次整数乘。但是,我们发现用一次分配律可以利用上之前计算的 和 ,这样可以减少一次乘法,代价是两次加法:
得到
恰需 次大整数乘(、、);其中 用移位加法 ,不计入大整数乘。
平方更简单:
也是 次(、、)。
中间量位数是 ,故总代价
与矩阵的 同属 。单看乘法次数时 ,但 ,乘次数优势会被更大的中间位数吃掉——这跟乘法的效率息息相关,具体分析见下。
位数比的解析效应
两种算法的总代价分别是 与 。环乘次数 在比值中消去,关键是 。假设乘法代价是 -次多项式增长:
则位数比的效应可以解析地写出来:
时求位数比的极限(, ):
故大 极限为
就是「乘法效率」参数: 越接近 ,位数膨胀 的惩罚越轻,扩域的次数优势 才显出来。
| 乘法模型 | 谁快 | ||
|---|---|---|---|
| 近线性(NTT/FFT) | 扩域略快 | ||
| Karatsuba | 矩阵快 | ||
| 朴素 | 矩阵快 |
转折点由 解出:
扩域快; 矩阵快。当然,实际的乘法模型也并非如此简单, 的值也并非一直不变,这不仅是因为这个简化模型本身的局限,也是因为大整数乘法算法本身又有与 的数量级相关的常数。
方法对比
| 方法 | 精确范围 | 运算模型 | 总代价 |
|---|---|---|---|
| 朴素递推 | 任意 | 次大整数加法,第 次位数 | 位运算 |
| 矩阵快速幂 | 任意 | 次 位乘 | |
| Binet + 浮点 | (本机 f64 实测) | 次机器浮点乘 | 浮点运算 |
| 扩域快速幂 | 任意 | 次 位乘 |
其中 ,,。
浮点 Binet 在精度半径内最快;半径外用整数算法。矩阵与扩域同阶,实际快慢 与 的差距也影响很大。
本质上:矩阵法算 ,扩域法算 ;都是「线性递推 代数结构上的幂」。
具体实现
下面用 std.math.big.int.Managed 做精确整数,f64 做浮点 Binet。编译:
zig build-exe fib.zig -OReleaseFast矩阵快速幂
const std = @import("std");const Big = std.math.big.int.Managed;const Allocator = std.mem.Allocator;
const Mat2 = struct { a: Big, b: Big, c: Big, d: Big,
fn deinit(self: *Mat2) void { self.a.deinit(); self.b.deinit(); self.c.deinit(); self.d.deinit(); }
fn initSet(allocator: Allocator, av: anytype, bv: anytype, cv: anytype, dv: anytype) !Mat2 { return .{ .a = try Big.initSet(allocator, av), .b = try Big.initSet(allocator, bv), .c = try Big.initSet(allocator, cv), .d = try Big.initSet(allocator, dv), }; }
fn identity(allocator: Allocator) !Mat2 { return initSet(allocator, 1, 0, 0, 1); }
fn baseA(allocator: Allocator) !Mat2 { return initSet(allocator, 1, 1, 1, 0); }
fn copyFrom(self: *Mat2, other: *const Mat2) !void { try self.a.copy(other.a.toConst()); try self.b.copy(other.b.toConst()); try self.c.copy(other.c.toConst()); try self.d.copy(other.d.toConst()); }
/// self = x * y,朴素 8 次整数乘 fn mulInto(self: *Mat2, x: *const Mat2, y: *const Mat2, t1: *Big, t2: *Big) !void { try t1.mul(&x.a, &y.a); try t2.mul(&x.b, &y.c); try self.a.add(t1, t2);
try t1.mul(&x.a, &y.b); try t2.mul(&x.b, &y.d); try self.b.add(t1, t2);
try t1.mul(&x.c, &y.a); try t2.mul(&x.d, &y.c); try self.c.add(t1, t2);
try t1.mul(&x.c, &y.b); try t2.mul(&x.d, &y.d); try self.d.add(t1, t2); }};
/// F_n:A^n * (1,0)^T 的第二分量fn fibMatrix(allocator: Allocator, n: u64) !Big { if (n == 0) return try Big.initSet(allocator, 0); if (n == 1) return try Big.initSet(allocator, 1);
var result = try Mat2.identity(allocator); defer result.deinit(); var base = try Mat2.baseA(allocator); defer base.deinit(); var tmp = try Mat2.identity(allocator); defer tmp.deinit(); var t1 = try Big.init(allocator); defer t1.deinit(); var t2 = try Big.init(allocator); defer t2.deinit();
var e = n; while (e > 0) : (e >>= 1) { if (e & 1 != 0) { try tmp.mulInto(&result, &base, &t1, &t2); try result.copyFrom(&tmp); } try tmp.mulInto(&base, &base, &t1, &t2); try base.copyFrom(&tmp); } var out = try Big.init(allocator); try out.copy(result.c.toConst()); return out;}浮点 Binet
/// 成功时返回 F_n;超出 f64 可靠范围时返回 null(本机实测可靠到 n<=75)fn fibFloat(n: u64) ?u64 { if (n == 0) return 0; if (n > 93) return null; // F_94 起 u64 装不下 const lam1: f64 = (1.0 + @sqrt(5.0)) / 2.0; var base = lam1; var res: f64 = 1.0; var e = n; while (e > 0) : (e >>= 1) { if (e & 1 != 0) res *= base; base *= base; } const approx = res / @sqrt(5.0); if (!std.math.isFinite(approx)) return null; const rounded = @round(approx); if (rounded < 0.0 or rounded > @as(f64, @floatFromInt(std.math.maxInt(u64)))) return null; return @intFromFloat(rounded);}扩域
const Ext = struct { x: Big, y: Big, // 表示 x + y√5
fn deinit(self: *Ext) void { self.x.deinit(); self.y.deinit(); }
fn initSet(allocator: Allocator, xv: anytype, yv: anytype) !Ext { return .{ .x = try Big.initSet(allocator, xv), .y = try Big.initSet(allocator, yv), }; }
fn copyFrom(self: *Ext, other: *const Ext) !void { try self.x.copy(other.x.toConst()); try self.y.copy(other.y.toConst()); }
/// 5v = (v<<2) + v fn mul5(out: *Big, v: *const Big) !void { try out.shiftLeft(v, 2); try out.add(out, v); }
/// self = p * q,恰 3 次大整数乘 fn mulInto(self: *Ext, p: *const Ext, q: *const Ext, t1: *Big, t2: *Big, t3: *Big, t4: *Big) !void { try t1.mul(&p.x, &q.x); // ac try t2.mul(&p.y, &q.y); // bd try mul5(t4, t2); // 5 bd try self.x.add(t1, t4);
try t3.add(&p.x, &p.y); try t4.add(&q.x, &q.y); try self.y.mul(t3, t4); // (a+b)(c+d) try t3.sub(&self.y, t1); try self.y.sub(t3, t2); // ad+bc }
/// self = p²,恰 3 次大整数乘 fn sqrInto(self: *Ext, p: *const Ext, t1: *Big, t2: *Big, t3: *Big) !void { try t1.mul(&p.x, &p.x); try t2.mul(&p.y, &p.y); try mul5(t3, t2); try self.x.add(t1, t3); try t1.mul(&p.x, &p.y); try self.y.add(t1, t1); // 2ab }};
/// F_n = Y_n / 2^{n-1}fn fibExt(allocator: Allocator, n: u64) !Big { if (n == 0) return try Big.initSet(allocator, 0); if (n == 1) return try Big.initSet(allocator, 1);
var result = try Ext.initSet(allocator, 1, 0); // 1 defer result.deinit(); var base = try Ext.initSet(allocator, 1, 1); // 1+√5 defer base.deinit(); var tmp = try Ext.initSet(allocator, 0, 0); defer tmp.deinit(); var t1 = try Big.init(allocator); defer t1.deinit(); var t2 = try Big.init(allocator); defer t2.deinit(); var t3 = try Big.init(allocator); defer t3.deinit(); var t4 = try Big.init(allocator); defer t4.deinit();
var e = n; while (e > 0) : (e >>= 1) { if (e & 1 != 0) { try tmp.mulInto(&result, &base, &t1, &t2, &t3, &t4); try result.copyFrom(&tmp); } try tmp.sqrInto(&base, &t1, &t2, &t3); try base.copyFrom(&tmp); }
var out = try Big.init(allocator); try out.shiftRight(&result.y, @intCast(n - 1)); return out;}实测
环境:Zig 0.15.2,-OReleaseFast,x86_64,CPU 为 13th Gen Intel Core i7-13700HX。计时为 wall time 平均(含调用内部分配)。
正确性:
fibFloat与线性递推对照:精确到 , 起失准;本机约 ()。fibMatrix与fibExt在 上结果一致。- 样例:,,。
两种精确算法同为 ,区别只在常数(矩阵 、扩域 )与位数( 对 )。wall-clock 几乎整段泡在大整数乘法里——两者的相对快慢由 的增长指数 严格决定(推导见前文 §位数比的解析效应):默认 std.math.big 走 Karatsuba()时矩阵更快;要让「少做几次乘」变成胜势,得把 拉近线性——下面用手写 NTT。
手写 NTT 大整数乘法
Zig 标准库没有 FFT/NTT 路径。这里用三模 NTT + CRT做卷积乘法:
- 素数(原根均为 ):,,
- 数字基 (每个
u64limb 拆成两个u32)
核心:蝶形与点积
/// 原地 Cooley–Tukey NTT。twiddles[s] 为第 s 层(len=2^{s+1})的步长根。fn nttWithTwiddles(a: []u32, mod: u32, twiddles: []const u32) void { const n = a.len; const log_n: u6 = @intCast(std.math.log2(n)); bitReversePermute(a, log_n);
var len: usize = 2; var stage: usize = 0; while (len <= n) : ({ len <<= 1; stage += 1; }) { const wlen = twiddles[stage]; var i: usize = 0; while (i < n) : (i += len) { var w: u32 = 1; var j: usize = 0; while (j < len / 2) : (j += 1) { const u = a[i + j]; const v = modMul(a[i + j + len / 2], w, mod); a[i + j] = modAdd(u, v, mod); a[i + j + len / 2] = modSub(u, v, mod); w = modMul(w, wlen, mod); } } }}
fn nttMulPoly(fa: []u32, fb: []u32, mod: u32, tw_f: []const u32, tw_i: []const u32, n_inv: u32) void { nttWithTwiddles(fa, mod, tw_f); nttWithTwiddles(fb, mod, tw_f); for (fa, fb) |*x, y| x.* = modMul(x.*, y, mod); nttWithTwiddles(fa, mod, tw_i); // 逆变换 for (fa) |*x| x.* = modMul(x.*, n_inv, mod);}CRT 还原
fn crt3(r0: u32, r1: u32, r2: u32) u128 { var x: u128 = 0; const rs = [_]u32{ r0, r1, r2 }; inline for (0..3) |i| { x += Mods.Mi[i] * @as(u128, rs[i]) * @as(u128, Mods.yi[i]); } return x % Mods.M;}流程:toDigits32 → 三模 nttMulPoly → 逐系数 crt3 + base- 进位 → 写回 Managed。矩阵/扩域里把 t.mul(a,b) 换成 mulNtt(t,a,b) 即可。
浮点 vs 矩阵 vs 扩域
矩阵 / 扩域底层乘法均为 NTT(mulNtt)。浮点只画精确范围 :

| 浮点 | 矩阵 | 扩域 | 扩域 / 矩阵 | |
|---|---|---|---|---|
| — | ||||
| — | ||||
| — | ||||
| — |
浮点是 次机器浮点乘(纳秒级);矩阵 / 扩域是 次大整数乘。NTT 下扩域始终更快,但表里 扩域 / 矩阵 从 抬到 ——相对优势在缩小。
测试代码: