本文涉及的域论主要是域的扩张理论。域是对加、减、乘、除都封闭的代数结构,算法竞赛中经常需要对质数 p 取模,就相当于在有限域 Fp 上进行运算。和实数域 R 的情形类似,有些问题的求解在更大的域(即复数域 C)上进行计算更为方便,常见的例子比如利用 快速傅里叶变换 加速实系数多项式的乘法。对于有限域也可以做类似的操作。多数读者对于有限域的扩张相对陌生,因而,了解一般的域上的扩张理论是有益的。文末给出了一些需要对有限域进行扩张的算法应用,同时也简单地讨论了部分应用中可能需要的整数环上的扩张。
证明的第一部分从 F 出发,构造了扩张 K1/F 使得所有 F 上的多项式在 K1 中都有至少一个根。对于域 F,考察多元多项式环 4R=F[⋯,xf,⋯],其中的不定元 xf 的下标取遍所有 F 上的首一多项式。此时,由所有 f(xf) 生成的理想记作 I。首先,I=R,故而极大理想 M⊇I 存在。否则,如果 1∈I,必然存在有限多个域 F 上的首一多项式 fi 和相应的环 R 中的元素 gi 使得 g1f1(xf1)+⋯+gkfk(xfk)=1 成立。设 F(α1,⋯,αk) 为向 F 中添加 fi(x) 的根 αi 后得到的代数扩张,则在 F(α1,⋯,αk) 中令上面得到的恒等式中 xfi 都代入 αi,而在各个 gi 中出现的其它不定元 xf 都带入 0,则得到 F(α1,⋯,αk) 上的等式 0=1,这矛盾。故而,I=R,极大理想 M 的构造是合法的。此时商环 R/M 是域,记作 K1,且任何 F 上的首一多项式 f(x) 都在 K1 中有根 xf。
证明的第二部分则归纳地得到了包含 F 的一个代数闭域 K(定义见下文)。重复上述构造,基于域 Ki 可以构造出域 Ki+1,使得 Ki 的多项式在 Ki+1 中都至少有一个根。而且,Ki 自然地嵌入到 Ki+1 中,所以可以定义它们的并集 K=⋃i=1∞Ki。容易验证,这也是域,而且 K 上的任何多项式的系数必然全部包含在某个 Ki 中,故而它的一个根必然出现 Ki+1⊆K 中。这说明 K 上的所有多项式都在 K 上至少有一个根,所以,K 是代数闭域。
最后,令 K 中所有 F 上的代数元组成的集合记作 F。它显然是域;因为对于任何 α,β∈F,都有 α±β,αβ,α/β∈F(α,β)⊆F。它也是 F 的代数扩张,因为它的元素都是 F 上的代数元。对于 F 上的多项式 f(x),它的所有根都是 F 上的代数元,故而也在 F 中,故而必然可以分裂为一次因子的乘积。这就说明 F 是 F 上的代数闭包。
例子
实数域 R 的代数闭包是复数域 C。
有理数域 Q 的代数闭包是全体代数数(即域扩张 C/Q 中的代数元)的集合,记作 Q。
所有的代数闭包的代数扩张都是平凡的。这样的域称为代数闭域。
代数闭域
如果域 F 上任意非常数多项式 f(x) 都至少有一个根 α∈F,那么就称域 F 为一个 代数闭域(algebraically closed field)。
事实上,它有如下等价定义:
定理
对于域 F,以下性质都是等价的:
域 F 是代数闭域;
域 F 上的所有多项式 f(x) 都分裂;
域 F 上的不可约多项式只有一次多项式;
域 F 没有非平凡的代数扩张;
域 F 没有非平凡的有限扩张;
域 F 是某个域的代数闭包。
证明
前五条性质的等价性显然,考察定义即可。对于第六条,代数闭域显然是自身的代数闭包,因为它没有非平凡的代数扩张;反过来,要证明域 F 的代数闭包必然是代数闭域。设 F 是域 F 的代数闭包,且 f(x) 是 F 上的多项式。设 α 是 f(x) 的分裂域中 f(x) 的一个根,且 f(x) 的非零系数的集合是 S⊆F。那么,因为 F(S)(α)=F(S∪{α}) 是有限扩张,α 必然也是 F 上的代数元,故而根据代数闭包的定义,α 在 F 上的极小多项式在域 F 中分裂,故而 α∈F。这说明,F 上任意多项式都有至少一个根。
最后,代数基本定理5 说明,C 是代数闭域。实数域 R 上的不可约多项式至多是二次的,或者等价地,它上面的代数扩张至多是二次扩张,就是因为通过代数扩张能够得到的最大的域就是 C。
复数域 C 中,多项式 xn=1 的根称为 n 次单位根(n-th root of unity)。记 ζn=e2πi/n。那么,全体 n 次单位根就是集合 Cn={ζnk:k∈Z}。在乘法运算下,Cn 构成 n 次循环群,可以记作 ⟨ζn⟩,称为 n 次单位根群。群 Cn 的生成元,也就是那些阶恰好为 n 的元素,称为 n 次本原单位根(primitive n-th root of unity)。n 次本原单位根的集合 Pn={ζnk:k∈Z,k⊥n},恰有 φ(n) 个元素;其中,φ(n) 是 欧拉函数。将单位根群 Cn 的元素按照它的阶分类,就得到如下分解:
Cn=d∣n⋃Pd.
对两边元素计数,就得到恒等式 n=∑d∣nφ(d)。
分圆域
分圆域是将单位根添加到有理数域中得到的扩域。
分圆域
将 n 次复单位根 ζn=e2πi/n 添加到有理数域 Q 中得到的扩域 Q(ζn) 称为 n 次分圆域(n-th cyclotomic field)。
因为全体 n 次单位根在乘法运算下构成循环群 ⟨ζn⟩,分圆域 Q(ζn) 也包括所有这些 n 次单位根。其实,Q(ζn) 正是域 Q 上多项式 xn−1 的分裂域。
设 G 为域 F 的乘法群的子群且 ∣G∣=n。因而,G 是有限 Abel 群。根据有限 Abel 群基本定理,群 G 有不变因子分解 Cn1×⋯×Cns 且 n1∣⋯∣ns。所以,对于 G 中的所有元素 x,都有 xns=1。也就是说,群 G 中的元素都是域 F 上多项式 xns−1 的根。但是,多项式 xns−1 至多有 ns 个相异的根,即 n≤ns。但是,ns≤n,所以其实有 ns=n。这说明 G≅Cns,即群 G 是循环群。
个。这恰为不计旋转意义下,q 个颜色的珠子能够串成的长度为 n 的项链的种类个数(证明),所以又称为项链多项式(necklace polynomial)。
定理
有限域 Fq 上存在任意次数的不可约多项式。
因为有限域上的不可约多项式有着简单的结构,这使得有限域上的多项式的因式分解十分容易。比如,要确定给定多项式的全体 n 次不可约因子,只要求解给定多项式与 xqn−x 的最大公因子即可 6。类似地,只要 n 次多项式对所有的 k<n 都与多项式 xqk−1 互素,就可以断定该 n 次多项式在 Fq 上不可约。
前文已经指出,有限域上的不可约多项式的根未必是相应扩域作为有限域的本原元。有限域 Fq 的本原元在它的素子域 Fp 上的极小多项式也称为域 Fp 上的 本原多项式7(primitive polynomial)。用这样的多项式实现扩域,就可以保证 x 必然是扩域中的本原元。域 Fp 上的 n 次本原多项式可以通过在 Fp 上对分圆多项式 Φn(x) 进行因式分解得到。
定理
设 p 为素数,n 为正整数,且 p⊥n。又设 d 是乘法群 (Z/nZ)× 中元素 p 的阶。那么,分圆多项式 Φn(x) 在域 Fp 可以分解为 dφ(n) 个 Fp 上的 d 次本原多项式的乘积。特别地,分圆多项式 Φn(x) 在域 Fp 上不可约,当且仅当 p 是模 n 的原根。
证明
如果注意到,n 次分圆多项式的根是所有 Fp 的 n 次本原单位根,而 n 次本原单位根的极小多项式的次数 d 就是它的共轭(包括自身)的数量,也就等于它在自同构群 ⟨σp⟩ 下的轨道长度,那么就可以知道 d 是 {ζi:i⊥n} 中映射 ζi↦ζip 的循环子群的轨道长度,亦即乘法群 (Z/nZ)× 中元素 p 的阶。如果不想依赖于 Galois 理论,也可以通过说明 d 是最小的正整数使得 (xn−1)∣(xpd−1−1) 成立来证明此事。其余结论显然。
虽然不可约多项式对于有限域的实现很重要,但是要找到有限域 Fq 上的一个 n 次不可约多项式却并没有较好的确定性的方法。在一般的情况下,可以使用随机方法生成这样的不可约多项式。因为所有 n 次首一多项式中,不可约多项式占的比例是 Θ(1/n) 的,所以可以先随机生成一个 n 次首一多项式再判断它是否可约。这样做可以在生成期望 Θ(n) 个首一多项式后找到一个不可约多项式。当然,这样生成的不可约多项式未必是本原多项式,系数也未必是简单的。在实际操作中,如果有限域的大小提前给定,往往可以通过查表 8 的方式找到系数简单的本原多项式,方便后续的计算。
参考实现
本节提供一个朴素的有限域的实现,仅供参考。代码中实现了随机生成不可约多项式的方法。
参考实现
#include <iostream>#include <random>#include <vector>class FiniteField { int p, k; std::vector<int> mod; // Monic. // Remove leadings zeros of a polynomial. static void trim(std::vector<int>& poly) { int m = poly.size(); for (; m && !poly[m - 1]; --m); poly.resize(m); } // Binary exponentiation mod p. int pow(int a, int b) const { int res = 1, po = a; while (b) { if (b & 1) res = (long long)res * po % p; po = (long long)po * po % p; b >>= 1; } return res; } // Multiplicative inverse mod p. int inv(int a) const { return pow(a, p - 2); } // Polynomial GCD. Inputs are supposed to have no leading zeros. std::vector<int> poly_gcd(const std::vector<int>& lhs, const std::vector<int>& rhs) const { if (lhs.size() < rhs.size()) { return poly_gcd(rhs, lhs); } else if (rhs.size()) { auto rem = lhs; // remainder. long long v = inv(rhs.back()); for (int i = rem.size() - rhs.size(); i >= 0; --i) { auto d = v * (p - rem[i + rhs.size() - 1]) % p; for (int j = 0; j < rhs.size(); ++j) { rem[i + j] = (rem[i + j] + d * rhs[j]) % p; } } trim(rem); // Remove leading zeros. return poly_gcd(rhs, rem); } else { return lhs; } } // Polynomials Ex-GCD. Inputs are supposed to have no leading zeros. void poly_ex_gcd(const std::vector<int>& lhs, const std::vector<int>& rhs, std::vector<int>& x, std::vector<int>& y) const { if (lhs.size() < rhs.size()) { poly_ex_gcd(rhs, lhs, y, x); } else if (rhs.size()) { std::vector<int> quo(lhs.size() - rhs.size() + 1); // quotient. auto rem = lhs; // remainder. long long v = inv(rhs.back()); for (int i = rem.size() - rhs.size(); i >= 0; --i) { quo[i] = v * rem[i + rhs.size() - 1] % p; long long d = p - quo[i]; // d = -quo[i]. for (int j = 0; j < rhs.size(); ++j) { rem[i + j] = (rem[i + j] + d * rhs[j]) % p; } } trim(rem); // Remove leading zeros. // Recursively ex_gcd. poly_ex_gcd(rhs, rem, y, x); // y -= a/b*x. if (y.size() < quo.size() + x.size() - 1) { y.resize(quo.size() + x.size() - 1, 0); } for (int i = 0; i < quo.size(); ++i) { for (int j = 0; j < x.size(); ++j) { y[i + j] = (y[i + j] - (long long)quo[i] * x[j]) % p; if (y[i + j] < 0) y[i + j] += p; } } trim(y); // Remove leading zeros. } else { // x = 1, y = 0. x.assign(1, inv(lhs.back())); y.clear(); } } public: // Class for Finite Field Elements. struct Element { const FiniteField* gf; std::vector<int> a; // Element initialization as zero. Element(const FiniteField* gf) : gf(gf), a(gf->k) {} // Element initialization from the numeric representation. Element(const FiniteField* gf, int id) : gf(gf), a(gf->k) { for (int i = 0; i < gf->k; ++i) { a[i] = id % gf->p; id /= gf->p; } } // Generate the numeric representation from an element. int idx() const { int id = 0; for (int i = gf->k - 1; i >= 0; --i) { id = id * gf->p + a[i]; } return id; } // Access the i-th coefficient. int& operator[](int i) { return a[i]; } // Addition. Element& operator+=(const Element& rhs) { for (int i = 0; i < gf->k; ++i) { a[i] += rhs.a[i]; if (a[i] >= gf->p) a[i] -= gf->p; } return *this; } // Addition. Element operator+(const Element& rhs) const { Element res(*this); res += rhs; return res; } // Subtraction. Element& operator-=(const Element& rhs) { for (int i = 0; i < gf->k; ++i) { a[i] -= rhs.a[i]; if (a[i] < 0) a[i] += gf->p; } return *this; } // Subtraction. Element operator-(const Element& rhs) const { Element res(*this); res -= rhs; return res; } // Multiplication by a scalar. Element& operator*=(int x) { for (int i = 0; i < gf->k; ++i) { a[i] = (long long)a[i] * x % gf->p; } return *this; } // Multiplication by a scalar. Element operator*(int x) const { Element res(*this); res *= x; return res; } // Multiplication by x. Element& shift() { long long d = gf->p - a.back(); // d = -a[k-1]. for (int i = gf->k - 1; i >= 0; --i) { a[i] = ((i ? a[i - 1] : 0) + d * gf->mod[i]) % gf->p; } return *this; } // Multiplication. Element& operator*=(const Element& rhs) { Element prod(*this); *this *= rhs.a[0]; for (int j = 1; j < gf->k; ++j) { prod.shift(); *this += prod * rhs.a[j]; } return *this; } // Multiplication. Element operator*(const Element& rhs) const { Element res(*this); res *= rhs; return res; } // Binary exponentiation. Element pow(int b) const { Element res(gf, 1); Element po(*this); while (b) { if (b & 1) res *= po; po = po * po; b >>= 1; } return res; } // Multiplicative inverse. Element inv() const { Element res(gf); auto& x = res.a; std::vector<int> y; auto lhs = a; trim(lhs); auto rhs = gf->mod; gf->poly_ex_gcd(lhs, rhs, x, y); x.resize(gf->k); return res; } // Division. Element& operator/=(const Element& rhs) { *this *= rhs.inv(); return *this; } // Division. Element operator/(const Element& rhs) const { Element res(*this); res /= rhs; return res; } }; private: // Check whether the current MOD is irreducible. bool checkIrreducible() const { Element f(this, p); for (int j = 1; j < k; ++j) { // F = X^{p^j} mod MOD. f = f.pow(p); // G = X^{p^j}-X mod MOD. auto g = f.a; g[1] = g[1] ? g[1] - 1 : p - 1; trim(g); // H = MOD. auto h = mod; // Reducible if deg(GCD(G,H))>0. if (poly_gcd(h, g).size() > 1) return false; } return true; } public: FiniteField(int p, int k) : p(p), k(k), mod(k + 1, 1) { do { // Randomly generate a polynomial. for (int i = 0; i < k; ++i) { mod[i] = rand() % p; } } while (!checkIrreducible()); }};int main() { int p = 13331, k = 50; FiniteField gf(p, k); FiniteField::Element e1(&gf, rand() + 1); FiniteField::Element e2(&gf, rand() + 1); FiniteField::Element e3(&gf, rand() + 1); // Test Frobenius endomorphism. std::cout << ((e1 * e2 + e3).pow(p) - e1.pow(p) * e2.pow(p) - e3.pow(p)).idx(); // Test inverse. std::cout << ((e1 * e2).inv() - e1.inv() / e2).idx(); return 0;}
正如上一节所展示的那样,域的扩张有着各种各样的限制。对于斐波那契数列的计算,只用扩域的方法只能解决模数 p 是素数且 5 不是 p 的二次剩余的情形。但是应当注意,在 代数扩张 一节中的论述表明,如果不要求在扩张后的结构中做除法运算,那么可以对环进行扩张 10。本节以任意模数 n 下斐波那契数列的计算为例,简要讨论这种方法。其他的不涉及过多除法运算的常见情景,包括行列式的计算、快速傅里叶变换等,有必要的时候都可以尝试应用这种方法。
设 m 为任意正整数,f(n) 为斐波那契数列的第 n 项。问题是要计算 f(n)modm 的值。原则上,需要在 Z/mZ 上进行计算。但是,正如上节所表明的,在不同模数下,多项式 x2−x−1 的可约性和有无重根的情形不一致,所以斐波那契数列的通项可能相差甚远。而且,如果本身 Z/mZ 并不是域,扩张后的元素也往往没有合法的逆(比如模 5 的时候,分母 5 直接就是零)。虽然有着诸多问题,但是其实在系数对 m 取模的条件下,计算余数