斐波那契:矩阵快速幂、通项与扩域

#技术

平凡的,我们有斐波那契数列:

F0=0,F1=1,Fn+1=Fn+Fn1 (n1)F_0 = 0,\quad F_1 = 1,\quad F_{n+1} = F_n + F_{n-1}\ (n \ge 1)

递推的矩阵形式

(Fn+1Fn)=(1110)(FnFn1)\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}

A=(1110)A = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}

(Fn+1Fn)=An(F1F0)=An(10)\begin{pmatrix} F_{n+1} \\ F_n \end{pmatrix} = A^n \begin{pmatrix} F_1 \\ F_0 \end{pmatrix} = A^n \begin{pmatrix} 1 \\ 0 \end{pmatrix}

因此算 FnF_n 等价于算 AnA^n

复杂度模型

我们约定:

  • M(t)M(t) 为两个 tt 位整数相乘的代价;MM 单调非降。
  • Fnλ1n/5F_n \sim \lambda_1^n / \sqrt{5}λ1\lambda_1 是其中一个特征值,证明过程见后文),故 FnF_n 的二进制位数
N:=log2Fn+1=nlog2λ1+O(1)=Θ(n).N := \bigl\lfloor\log_2 F_n\bigr\rfloor + 1 = n\log_2\lambda_1 + O(1) = \Theta(n).
  • 二进制快速幂算 gng^n(按最优左→右计数):恰做 log2n\lfloor\log_2 n\rfloor 次平方,以及 popcount(n)1\mathrm{popcount}(n)-1 次一般乘,环乘法次数为
μ(n):=log2n+popcount(n)1.\mu(n) := \lfloor\log_2 n\rfloor + \mathrm{popcount}(n) - 1.

于是

log2nμ(n)2log2n,\lfloor\log_2 n\rfloor \le \mu(n) \le 2\lfloor\log_2 n\rfloor,

μ(n)=Θ(logn)\mu(n) = \Theta(\log n)

  • 矩阵快速幂时,中间矩阵的各元素的位数都不超过 Fn+1F_{n+1},与 FnF_n 同阶位数,故其中的大整数乘按 M(N)M(N) 计代价。
  • 扩域法的中间量是 Yn=Fn2n1Y_n = F_n\cdot 2^{n-1},位数
N:=N+(n1)=n(1+log2λ1)+O(1)=Θ(n),N' := N + (n-1) = n(1+\log_2\lambda_1) + O(1) = \Theta(n),

大整数乘按 M(N)M(N') 计——与 M(N)M(N) 同阶,但常数更大,位数比 N/N2.44N'/N\to 2.44(详见 §位数比的解析效应)。

1. 矩阵快速幂

AA 做二进制快速幂得 AnA^n,再右乘 (1,0)(1,0)^\top,读出第二分量即 FnF_n

2×22\times 2 矩阵朴素乘法恰含 88 元素乘法(44 个点积,各 22 次乘)。故整数乘次数为 8μ(n)8\,\mu(n),总代价

Θ(8M(N)μ(n))=Θ(M(N)logn).\Theta\bigl(8\,M(N)\,\mu(n)\bigr) = \Theta\bigl(M(N)\log n\bigr).

这是最直接的精确 O(logn)O(\log n) 次环运算算法。

2. 特征值与对角化

目标是把 AA 对角化成 A=PDP1A = PDP^{-1},得到

An=(PDP1)n=PDnP1.A^n = (PDP^{-1})^n = PD^n P^{-1}.

因为对角阵的幂次有一条极优雅的性质:D=diag(a,b)D = \mathrm{diag}(a,b)Dn=diag(an,bn)D^n = \mathrm{diag}(a^n, b^n)。 这可以极大地减轻运算负担。

特征方程

Ax=λx(AλI)x=0det(AλI)=0A\vec{x} = \lambda\vec{x} \quad\Rightarrow\quad (A - \lambda I)\vec{x} = 0 \quad\Rightarrow\quad \det(A - \lambda I) = 0 1λ11λ=(1λ)(λ)1=0\begin{vmatrix} 1-\lambda & 1 \\ 1 & -\lambda \end{vmatrix} = (1-\lambda)(-\lambda) - 1 = 0

λ2λ1=0.\lambda^2 - \lambda - 1 = 0.

解得两个特征值

λ1=1+52,λ2=152.\lambda_1 = \frac{1+\sqrt{5}}{2},\qquad \lambda_2 = \frac{1-\sqrt{5}}{2}.

易见 λ1>1\lambda_1 > 1λ2<1|\lambda_2| < 1,且 λ1+λ2=1\lambda_1 + \lambda_2 = 1λ1λ2=1\lambda_1\lambda_2 = -1λ1λ2=5\lambda_1 - \lambda_2 = \sqrt{5}

特征向量

(AλI)x=0(A - \lambda I)\vec{x} = 0,第二行给出 x1=λx2x_1 = \lambda x_2,取特解

x1=(λ11),x2=(λ21).\vec{x}_1 = \begin{pmatrix} \lambda_1 \\ 1 \end{pmatrix},\quad \vec{x}_2 = \begin{pmatrix} \lambda_2 \\ 1 \end{pmatrix}.

PP 的列为这两个特征向量:

P=(λ1λ211),D=(λ100λ2).P = \begin{pmatrix} \lambda_1 & \lambda_2 \\ 1 & 1 \end{pmatrix},\quad D = \begin{pmatrix} \lambda_1 & 0 \\ 0 & \lambda_2 \end{pmatrix}.

AP=PDAP = PD,即 A=PDP1A = PDP^{-1}

求逆并推出通项

det(P)=λ1λ2=5,\det(P) = \lambda_1 - \lambda_2 = \sqrt{5}, P1=15(1λ21λ1).P^{-1} = \frac{1}{\sqrt{5}} \begin{pmatrix} 1 & -\lambda_2 \\ -1 & \lambda_1 \end{pmatrix}.

于是

(Fn+1Fn)=PDnP1(10).\begin{pmatrix} F_{n+1} \\ F_n \end{pmatrix} = P D^n P^{-1} \begin{pmatrix} 1 \\ 0 \end{pmatrix}.

先算

P1(10)=15(11),DnP1(10)=15(λ1nλ2n),P^{-1} \begin{pmatrix} 1 \\ 0 \end{pmatrix} = \frac{1}{\sqrt{5}} \begin{pmatrix} 1 \\ -1 \end{pmatrix},\quad D^n P^{-1} \begin{pmatrix} 1 \\ 0 \end{pmatrix} = \frac{1}{\sqrt{5}} \begin{pmatrix} \lambda_1^n \\ -\lambda_2^n \end{pmatrix},

再左乘 PP

(Fn+1Fn)=15(λ1λ211)(λ1nλ2n)=15(λ1n+1λ2n+1λ1nλ2n).\begin{pmatrix} F_{n+1} \\ F_n \end{pmatrix} = \frac{1}{\sqrt{5}} \begin{pmatrix} \lambda_1 & \lambda_2 \\ 1 & 1 \end{pmatrix} \begin{pmatrix} \lambda_1^n \\ -\lambda_2^n \end{pmatrix} = \frac{1}{\sqrt{5}} \begin{pmatrix} \lambda_1^{n+1} - \lambda_2^{n+1} \\ \lambda_1^n - \lambda_2^n \end{pmatrix}.

得到 Binet 公式(对一切整数 n0n\ge 0 精确成立):

Fn=λ1nλ2n5=15((1+52)n(152)n).F_n = \frac{\lambda_1^n - \lambda_2^n}{\sqrt{5}} = \frac{1}{\sqrt{5}} \left( \left(\frac{1+\sqrt{5}}{2}\right)^n - \left(\frac{1-\sqrt{5}}{2}\right)^n \right).

最近整数形式

把它改写成

λ1n5Fn=λ2n5,\frac{\lambda_1^n}{\sqrt{5}} - F_n = \frac{\lambda_2^n}{\sqrt{5}},

λ1n5Fn=λ2n5<12.\left| \frac{\lambda_1^n}{\sqrt{5}} - F_n \right| = \left| \frac{\lambda_2^n}{\sqrt{5}} \right|<{\frac{1}{2}}.

FnF_n 就是距 λ1n/5\lambda_1^n/\sqrt{5}唯一最近整数。

浮点快速幂

在浮点语义下,对 λ1n\lambda_1^n 做快速幂再除以 5\sqrt{5}、四舍五入,环运算次数仍是 μ(n)=Θ(logn)\mu(n)=\Theta(\log n),每次是机器浮点乘——这是有明确适用范围的精确算法:当浮点算出的 x~\widetilde{x} 仍落在 (Fn1/2, Fn+1/2)(F_n-1/2,\ F_n+1/2) 内时,四舍五入就得到 FnF_n

能力边界由精度决定。IEEE-754 binary64 有效位数约 5353 bit;λ1n/5\lambda_1^n / \sqrt{5} 的量级约为 FnF_n,相对误差一旦大到使 x~\widetilde{x} 跨过半整数边界,四舍五入就会错。后文实测:对本机 f64 快速幂实现,与精确值一致到 n75n \le 75,从 n=76n=76 起失准。

因此:小 nn 用浮点 Binet 最省事;要任意大的精确 FnF_n,必须离开浮点,改做整数算术。下一节正是通项在整数环上的落地。

3. 扩域

已知 n, FnZ\forall n,\ F_n \in \mathbb{Z},Binet 里的 5\sqrt{5} 最终消掉。把计算放进环 Z[5]\mathbb{Z}[\sqrt{5}],全程整数,就不受浮点精度限制。

d1=1+5,d2=15,d_1 = 1 + \sqrt{5},\quad d_2 = 1 - \sqrt{5},

λ1=d1/2\lambda_1 = d_1/2λ2=d2/2\lambda_2 = d_2/2,且

Fn=15((d12)n(d22)n).F_n = \frac{1}{\sqrt{5}} \left( \left(\frac{d_1}{2}\right)^n - \left(\frac{d_2}{2}\right)^n \right).

运算法则类似复数:把 5\sqrt{5} 当作「虚部」单位(52=5\sqrt{5}^2 = 5)。

d1d_1d2d_2 共轭,故类似地,存在 Xn,YnZX_n, Y_n \in \mathbb{Z} 使

d1n=Xn+Yn5,d2n=XnYn5.d_1^n = X_n + Y_n\sqrt{5},\qquad d_2^n = X_n - Y_n\sqrt{5}.

于是

λ1nλ2n=d1nd2n2n=2Yn52n=Yn52n1,\lambda_1^n - \lambda_2^n = \frac{d_1^n - d_2^n}{2^n} = \frac{2 Y_n \sqrt{5}}{2^n} = \frac{Y_n \sqrt{5}}{2^{n-1}}, Fn=λ1nλ2n5=Yn2n1.F_n = \frac{\lambda_1^n - \lambda_2^n}{\sqrt{5}} = \frac{Y_n}{2^{n-1}}.

算法:快速幂算 d1n=Xn+Yn5d_1^n = X_n + Y_n\sqrt{5},取出系数 YnY_n,再除以 2n12^{n-1}。因 FnZF_n \in \mathbb{Z}YnY_n 必被 2n12^{n-1} 整除;实现时用整数除法。

拓展:环乘法的整数乘次数

α=a+b5\alpha = a + b\sqrt{5}β=c+d5\beta = c + d\sqrt{5}。朴素乘法

αβ=(ac+5bd)+(ad+bc)5\alpha\beta = (ac + 5bd) + (ad + bc)\sqrt{5}

恰需 44 次整数乘。但是,我们发现用一次分配律可以利用上之前计算的 acacbdbd,这样可以减少一次乘法,代价是两次加法:

ad+bc=(a+b)(c+d)acbd,ad + bc = (a+b)(c+d) - ac - bd,

得到

αβ=(ac+5bd)+((a+b)(c+d)acbd)5,\alpha\beta = \bigl(ac + 5bd\bigr) + \bigl((a+b)(c+d) - ac - bd\bigr)\sqrt{5},

恰需 33 次大整数乘(acacbdbd(a+b)(c+d)(a+b)(c+d));其中 ×5\times 5 用移位加法 5v=(v2)+v5v=(v\ll 2)+v,不计入大整数乘。

平方更简单:

(a+b5)2=(a2+5b2)+2ab5,(a + b\sqrt{5})^2 = (a^2 + 5b^2) + 2ab\sqrt{5},

也是 33 次(a2a^2b2b^2abab)。

中间量位数是 N=log2(Yn)=log2(Fn2n1)=N+n1N'=\log_2(Y_n)=\log_2(F_n\cdot 2^{n-1})=N+n-1,故总代价

Θ(3M(N)μ(n))=Θ(M(N)logn).\Theta\bigl(3\,M(N')\,\mu(n)\bigr) = \Theta\bigl(M(N')\log n\bigr).

与矩阵的 Θ(8M(N)μ(n))\Theta\bigl(8\,M(N)\,\mu(n)\bigr) 同属 Θ(M(n)logn)\Theta(M(n)\log n)。单看乘法次数时 3<83<8,但 N>NN'>N乘次数优势会被更大的中间位数吃掉——这跟乘法的效率息息相关,具体分析见下。

位数比的解析效应

两种算法的总代价分别是 8μ(n)M(N)8\,\mu(n)\,M(N)3μ(n)M(N)3\,\mu(n)\,M(N')。环乘次数 μ(n)\mu(n) 在比值中消去,关键是 M(N)/M(N)M(N')/M(N)假设乘法代价是 α\alpha-次多项式增长

M(t)=Θ(tα),α1.M(t) = \Theta(t^\alpha),\qquad \alpha \ge 1.

则位数比的效应可以解析地写出来:

M(N)M(N)=(NN)α,TextTmatrix=38(NN)α.\frac{M(N')}{M(N)} = \left(\frac{N'}{N}\right)^\alpha,\qquad \frac{T_{\mathrm{ext}}}{T_{\mathrm{matrix}}} = \frac{3}{8}\left(\frac{N'}{N}\right)^\alpha.

nn\to\infty 时求位数比的极限(N=log2(Fn)+O(1)=log2(λ1n)+O(1)=nlog2λ1+O(1)N=\log_2(F_n)+O(1)=\log_2(\lambda_1^n)+O(1)=n\log_2\lambda_1+O(1)N=n(1+log2λ1)+O(1)N'=n(1+\log_2\lambda_1)+O(1)):

NN=n(1+log2λ1)+O(1)nlog2λ1+O(1)  n  1+log2λ1log2λ1=1+1log2λ12.44,\frac{N'}{N} = \frac{n(1+\log_2\lambda_1)+O(1)}{n\log_2\lambda_1+O(1)} \;\xrightarrow{n\to\infty}\; \frac{1+\log_2\lambda_1}{\log_2\lambda_1} = 1 + \frac{1}{\log_2\lambda_1}\approx 2.44,

故大 nn 极限为

  TextTmatrix  n  382.44α.  \boxed{\;\frac{T_{\mathrm{ext}}}{T_{\mathrm{matrix}}} \;\xrightarrow{n\to\infty}\; \frac{3}{8}\cdot 2.44^{\,\alpha}.\;}

α\alpha 就是「乘法效率」参数:α\alpha 越接近 11,位数膨胀 N/NN'/N 的惩罚越轻,扩域的次数优势 3<83<8 才显出来。

α\alpha乘法模型382.44α\frac{3}{8}\cdot 2.44^{\,\alpha}谁快
11近线性(NTT/FFT)0.920.92扩域略快
log231.585\log_2 3 \approx 1.585Karatsuba1.54\approx 1.54矩阵快
22朴素 O(n2)O(n^2)2.232.23矩阵快

转折点382.44α=1\frac{3}{8}\cdot 2.44^{\,\alpha} = 1 解出:

α=log(8/3)log2.441.10.\alpha^\star = \frac{\log(8/3)}{\log 2.44} \approx 1.10.

α<1.10\alpha < 1.10 \rightarrow 扩域快;α>1.10\alpha > 1.10 \rightarrow 矩阵快。当然,实际的乘法模型也并非如此简单,α\alpha 的值也并非一直不变,这不仅是因为这个简化模型本身的局限,也是因为大整数乘法算法本身又有与 NN 的数量级相关的常数。

方法对比

方法精确范围运算模型总代价
朴素递推任意 nnnn 次大整数加法,第 kk 次位数 Θ(k)\Theta(k)Θ(n2)\Theta(n^2) 位运算
矩阵快速幂任意 nn8μ(n)8\,\mu(n)NN 位乘Θ(8M(N)μ(n))\Theta\bigl(8\,M(N)\,\mu(n)\bigr)
Binet + 浮点n75n\le 75(本机 f64 实测)μ(n)\mu(n) 次机器浮点乘Θ(logn)\Theta(\log n) 浮点运算
扩域快速幂任意 nn3μ(n)3\,\mu(n)NN' 位乘Θ(3M(N)μ(n))\Theta\bigl(3\,M(N')\,\mu(n)\bigr)

其中 μ(n)=log2n+popcount(n)1=Θ(logn)\mu(n)=\lfloor\log_2 n\rfloor+\mathrm{popcount}(n)-1=\Theta(\log n)N=bitlen(Fn)N=\mathrm{bitlen}(F_n)N=N+n1N'=N+n-1

浮点 Binet 在精度半径内最快;半径外用整数算法。矩阵与扩域同阶,实际快慢 M(N)M(N)M(N)M(N') 的差距也影响很大。

本质上:矩阵法算 AnA^n,扩域法算 d1nd_1^n;都是「线性递推 \leftrightarrow 代数结构上的幂」。

具体实现

下面用 std.math.big.int.Managed 做精确整数,f64 做浮点 Binet。编译:

Terminal window
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);
}

扩域 Z[5]\mathbb{Z}[\sqrt{5}]

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-OReleaseFastx86_64,CPU 为 13th Gen Intel Core i7-13700HX。计时为 wall time 平均(含调用内部分配)。

正确性:

  • fibFloat 与线性递推对照:精确到 n75n\le 75n=76n=76 起失准;本机约 35ns3\text{–}5\,\mathrm{ns}n=1075n=10\to75)。
  • fibMatrixfibExtn{0,1,2,5,10,20,50,75,100,1000,10000}n\in\{0,1,2,5,10,20,50,75,100,1000,10000\} 上结果一致。
  • 样例:F10=55F_{10}=55F75=2111485077978050F_{75}=2111485077978050F100=354224848179261915075F_{100}=354224848179261915075

两种精确算法同为 Θ(M(n)logn)\Theta\bigl(M(n)\log n\bigr),区别只在常数(矩阵 88、扩域 33)与位数(NNN2.44NN'\approx 2.44\,N)。wall-clock 几乎整段泡在大整数乘法里——两者的相对快慢由 MM 的增长指数 α\alpha 严格决定(推导见前文 §位数比的解析效应):默认 std.math.big 走 Karatsuba(α1.58\alpha\approx 1.58)时矩阵更快;要让「少做几次乘」变成胜势,得把 MM 拉近线性——下面用手写 NTT。

手写 NTT 大整数乘法

Zig 标准库没有 FFT/NTT 路径。这里用三模 NTT + CRT做卷积乘法:

  • 素数(原根均为 33):998244353998244353469762049469762049167772161167772161
  • 数字基 2322^{32}(每个 u64 limb 拆成两个 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 还原

x=i=02riMmi(Mmi)1modmi(modM)x=\sum_{i=0}^{2} r_i\cdot\frac{M}{m_i}\cdot\Bigl(\frac{M}{m_i}\Bigr)^{-1}\bmod m_i \pmod M
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-2322^{32} 进位 → 写回 Managed。矩阵/扩域里把 t.mul(a,b) 换成 mulNtt(t,a,b) 即可。

浮点 vs 矩阵 vs 扩域

矩阵 / 扩域底层乘法均为 NTT(mulNtt)。浮点只画精确范围 n75n\le 75

浮点 vs 矩阵 vs 扩域

nn浮点矩阵扩域扩域 / 矩阵
10103.0ns3.0\,\mathrm{ns}
20203.4ns3.4\,\mathrm{ns}
50503.9ns3.9\,\mathrm{ns}
75754.9ns4.9\,\mathrm{ns}1.61ms1.61\,\mathrm{ms}0.63ms0.63\,\mathrm{ms}0.39×0.39\times
10310^{3}2.18ms2.18\,\mathrm{ms}0.88ms0.88\,\mathrm{ms}0.40×0.40\times
10410^{4}5.13ms5.13\,\mathrm{ms}3.48ms3.48\,\mathrm{ms}0.68×0.68\times
10510^{5}39.2ms39.2\,\mathrm{ms}28.5ms28.5\,\mathrm{ms}0.73×0.73\times
10610^{6}443ms443\,\mathrm{ms}339ms339\,\mathrm{ms}0.76×0.76\times

浮点是 Θ(logn)\Theta(\log n) 次机器浮点乘(纳秒级);矩阵 / 扩域是 Θ(logn)\Theta(\log n) 次大整数乘。NTT 下扩域始终更快,但表里 扩域 / 矩阵0.4\sim 0.4 抬到 0.76\sim 0.76——相对优势在缩小。

测试代码: