基础数论学习笔记(施工中)
基础数论学习笔记
再一次学习数论!
约定:如果没有特别说明,文中字母均为整数,且大多数情况下我只关心正整数。
O. 基础知识
同余式的运算法则
考虑模 \(p\) 意义下的运算时,同余的两个数是完全等价的,也就是说我们不区分同一剩余类中的数。掌握了这一点后我们可以立即导出模意义下加法和乘法运算法则:
-
(加法)如果 \(a_1 \equiv b_1 \pmod{p}\) 且 \(a_2 \equiv b_2 \pmod{p}\),则
\[a_1 + a_2 \equiv b_1 + b_2 \pmod{p} \] -
(乘法)如果 \(a_1 \equiv b_1 \pmod{p}\) 且 \(a_2 \equiv b_2 \pmod{p}\),则
\[a_1 a_2 \equiv b_1 b_2 \pmod{p} \]
那么对于减法和除法呢?减法就是加法的逆运算,减去一个数 \(x\) 等价于加上 \(x\) 的加法逆元。在实数加法中中,加法逆元就是相反数。在模意义下的加法中,加法逆元也是一定存在的:显然一定可以找到一个数 \(x^{-1}\) 使得 \(x + x^{-1} \equiv 0 \pmod{p}\)——取 \(x^{-1} \equiv p - x \pmod{p}\) 即可。但除法比较特殊。除法是乘法的逆运算,除以一个数等价于乘上它的乘法逆元。但在模意义下,除法逆元不一定存在。
I. 欧几里得算法
过程
欧几里得算法用于求出两个数的最大公约数(\(\gcd\)),它的核心是以下引理:
引理. \(\gcd(a, b) = \gcd(b, a \bmod b)\)。
证明 实际上我们有更强的结论:\(a\),\(b\) 的公因子集合与 \(b\),\(a \bmod b\) 的公因子集合完全相同。
设 \(a\) 与 \(b\) 带余除法的结果为 \(a = qb + r\),则 \(a \bmod b = r\)。设 \(g\) 是 \(a, b\) 的任一公因数,\(a = a'g\),\(b = b'g\),那么 \(a'g = qb'g + r\),因此 \(r = (a' - qb')g\) 是 \(g\) 的倍数。直白地说,\(a\) 与 \(b\) 都是 \(g\) 的倍数,而 \(a \bmod b\) 可以看作 \(a\) 减去若干个 \(b\),自然仍是 \(g\) 的倍数。这就证明了 \(a\),\(b\) 的公因数都是 \(b\),\(a \bmod b\) 的公因数。
另一方面,也可以类似地证明 \(a\),\(b\) 的公因数都是 \(b\),\(a \bmod b\) 的公因数,这就证明了二者公因数都是相同的。既然如此,二者的最大公约数也会相同。\(\Box\)
根据这个引理,结合一个边界条件,我们就得到了得到 \(\gcd\) 的递归式(假设 \(a > b\)):
代码如下:
int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
时间复杂度
我们来具体分析这个算法的时间复杂度,同时证明它确实会正确地终止。
引理 如果 \(a > b\),则 \(a \bmod b \le \lfloor a / 2 \rfloor\)。
证明 暂略。
设 \(a\),\(b\) 的值域为 \(w\),每次迭代时有两种情况:
- \(a < b\),此时有 \(\gcd(a, b) = \gcd(b, a)\);
- \(a \ge b\),比较迭代前后 \(\gcd\) 的两个参数,有 \(b < a\) 且 \(a \bmod b < b\),二者都在减小,因此 \(b\) 会在有限次操作后变成 \(0\),这说明了该算法一定会终止。又根据引理,\(a \bmod b\) 不超过 \(a\) 的一半,所以这一过程最多发生 \(O(\log w)\) 次。
由于情况 1 发生后会转化为情况 2,所以迭代的总次数仍为 \(O(\log w)\)。
如果用欧几里得算法求斐波那契数列相邻两项的 \(\gcd\),则复杂度来到最坏情况。证明似乎比较复杂,暂略。
II. 扩展欧几里得算法
扩展欧几里得算法(Extended Euclidean algorithm, EXGCD)用于求形如
的二元一次不定方程的所有整数解。
(为了方便,我们认为 \(a, b, c \ge 0\)。对于负数的情况可能需要多一点讨论。)
有解性判定——裴蜀定理
裴蜀定理(Bézout's lemma)给出了二元一次不定方程有整数解的充要条件:方程 \(ax + by = c\) 有解当且仅当
证明
设 \(\gcd(a, b) = g\)。
必要性 由于等式左边一定是 \(g\) 的倍数,因此等式右边也必须是 \(g\) 的倍数。
充分性 为了方便,不妨设 \(c = g\)。如果在这种情况下存在某组解,那么当 \(c\) 是 \(g\) 的倍数时,把这组解扩大若干整数倍,就可以得到原方程的解。所以研究有解性时只需考虑这种情况。
设 \(S = \{xa + yb | x, y \in \mathbb{Z}\}\)。下面证明 \(S\) 中的最小正元素为 \(g\)。
首先,\(S\) 中至少有一个正元素(\(a\) 和 \(b\),说“一个”是因为二者可能相同),设最小的正元素为 \(d_0 = x_0a + y_0b\)。另取 \(S\) 中任一正元素 \(p = x_1a + y_1b\),考虑它对 \(d\) 的带余除法 \(p = qd_0 + r\),那么
因此 \(r \in S\)。由于 \(r\) 是 \(p\) 除以 \(d_0\) 的余数,所以 \(0 \le r < d_0\)。如果 \(r\) 为正数,就与 \(d_0\) 的最小性矛盾。因此 \(r = 0\)。那么可以推出:\(S\) 中任意正元素都是 \(d_0\) 的倍数。特别地,有 \(d_0 \mid a\) 和 \(d_0 \mid b\),因此 \(d_0\) 是 \(a\) 和 \(b\) 的公约数。
另一方面,由于 \(a\) 和 \(b\) 都是 \(g\) 的倍数,而 \(d_0\) 是 \(a\) 和 \(b\) 的线性组合,因此 \(d_0\) 也是 \(g\) 的倍数(在 I 章中我们已经用过了这个结论)。结合两点可知 \(d_0 = g\)。至此,我们就证明了当 \(c = g\) 时 \(ax + by = c\) 一定有解。对于更一般的情况,如果 \(c = c'g\),把 \(ax + by = g\) 的每个解乘 \(c'\) 倍就得到了 \(ax + by = c\) 的解,所以两个方程的有解性相同。\(\Box\)
解的形式及个数
我们可以进一步说明,只要有解,就有无数组解。设 \((x_0, y_0)\) 是任意一组解,\(a = a'g\),\(b = b'g\),则方程的解集为
换句话说,把 \(x\) 加上若干个 \(b'\),同时把 \(y\) 减去若干个 \(a'\) 不改变 \(ax + by\) 的值,因为 \(a \cdot \dfrac{kb}{g} = b \cdot \dfrac{ka}{g}\)。另外,这两者显然都是整数,所以变换后得到的还是整数解。
另一方面,对于任意的解 \((x', y')\) 满足 \(ax' + by' = c\),把此等式与 \(ax_0 + by_0 = c\) 相减,有
由于 \(a = a'g\),\(b = b'g\),所以
左边是 \(a'\) 的倍数,因此右边也是 \(a'\) 的倍数。又因为 \(a'\) 与 \(b'\) 互质,所以 \(y' - y_0\) 是 \(a'\) 的倍数。设 \(y' - y_0 = a'k\),则 \(y' = y_0 + a'k\),代入得 \(x' = x_0 - b'k\),这就把 \((x', y')\) 转化成了 \(A\) 中的形式。所以,\(A\) 确实包含了方程的所有整数解。
过程
现在我们要求出 \(ax + by = c\) 的任意一组解。先根据裴蜀定理判断是否有解,如果有解,为了方便仍然假设 \(c = g\)。求出一组解以后我们可以根据通解的公式求出其它的解。
我们使用和欧几里得算法类似的迭代方法:每次迭代都会使 \(a\) 和 \(b\) 减小,最终 \(b\) 减小到 \(0\) 时就可以得到显然的解,然后反推原方程的解。
具体地,如果 \(b = 0\),那么 \(\gcd(a, b) = a\),方程可以写为 \(ax + 0y = a\)。此时显然 \(x = 1, y = 0\) 是一组解。当然,\(y\) 可以取任何整数,但取 \(0\) 比较方便,并且这保证了最终得到的原方程的解不会太大,
否则,我们把原方程迭代为一个新的方程:\(a'x + b'y = \gcd(a', b')\),其中 \(a' = b\),\(b' = a \bmod b\)。这样做的意义和欧几里得算法是相同的:迭代后方程的系数会减小,最终到达边界情况时就能得到解。那么,假设我们求出了这个方程的一组解 \((x_1, y_1)\),要如何反推回原方程的一组解 \((x_0, y_0)\) 呢?
由于 \(\gcd(a, b) = \gcd(b, a \bmod b)\),所以 \(\gcd(a, b) = \gcd(a', b')\),因此
把 \(a \bmod b\) 改写为 \(a - b \cdot \left\lfloor \dfrac{a}{b} \right\rfloor\),那么
整理等式右边,得到
对比两边得到
这就得到了两个方程解的关系。
此算法时间复杂度的分析和欧几里得算法完全相同,二者的时间复杂度都是 \(O(\log w)\),其中 \(w\) 是 \(a\) 和 \(b\) 的值域,这里不再赘述。
以下是一份 exgcd 的示例代码,它返回一个二元组(pair
),表示方程的解 \(x, y\):
auto exgcd(int a, int b) {
if(b == 0) {
return make_pair(1, 0);
}
auto [x, y] = exgcd(b, a % b);
return make_pair(y, x - (a / b) * y);
}
这种写法比较美观,因为函数的返回值恰好就是方程的解。但我不知道使用 pair
会不会导致常数偏大。大多数人使用的是一种传引用的写法:
void exgcd(int a, int b, int &x, int &y) {
if(b == 0) {
x = 1, y = 0;
return;
}
int x0, y0;
exgcd(b, a % b, x0, y0);
x = y0, y = x0 - (a / b) * y0;
}
值域分析
\(ax + by = g\) 的解有无穷个,显然我们希望扩欧求出的是比较小的解,否则无法存下。幸运的是,如果在迭代的最后一步选取的解是 \(x = 1, y = 0\),那么最终求出的解满足 \(|x| \le b\),\(|y| \le a\)。实际上,这种情况下 \(|x|\) 和 \(|y|\) 同时取到最小值。证明暂略,可以参考 OI wiki。
需要注意的是,我们只是说明了 \(ax + by = g\) 的解的值域范围,但对于更一般的 \(ax + by = c\) 的情况,把 \(x\) 和 \(y\) 同乘上 \(c / g\) 以后,值域是没有限制的。
例题
0. UVA10104 Euclid Problem
模板题。需要注意的是 UVA 只支持 C++11,而 C++14 开始 auto
才能作为函数返回值,所以需要把上文代码中函数返回值的 auto
改成 tuple<int, int, int>
。
tuple<int, int, int> exgcd(int a, int b) {
if(b == 0) {
return make_tuple(1, 0, a);
}
auto [x, y, g] = exgcd(b, a % b);
return make_tuple(y, x - (a / b) * y, g);
}
1. P5656 【模板】二元一次不定方程 (exgcd)
这道题不仅要求出解,还要回答有关解的一些其它问题。我们逐个来研究。
-
是否有解?
我们用裴蜀定理判断是否有解。现在假设有解,并且已经用扩欧求出了一组解 \((x_0, y_0)\)。
-
是否有正整数解?
我们让 \(x_0\) 对 \(b'\) 取模,就得到了 \(x\) 为正整数的解中最小的 \(x\)。(为了保证 \(x\) 为正,当 \(x_0 \bmod b' = 0\) 时应取 \(x = b'\) 而不是 \(0\)。)求出对应的 \(y\),由于 \(x\) 和 \(y\) 是此消彼长的关系,现在已经尽可能让 \(x\) 在为正的情况下尽可能小了,如果存在正整数解,那么 \(y\) 此时一定为正,据此可以判断是否有正整数解。
如果有正整数解,那么显然现在求出的 \(x\) 和 \(y\) 分别是正整数解中最小的 \(x\) 和最大的 \(y\),记为 \(x_{\min}\) 和 \(y_{\max}\)。同理还可以求出正整数解中最大的 \(x\) 和最小的 \(y\),记为 \(x_{\max}\) 和 \(y_{\min}\)。而如果没有正整数解,我们实际上也已经求出了 \(x\) 和 \(y\) 的最小正整数值。
-
如果有正整数解,有多少个?
只需求出 \(x_{\max}\) 和 \(x_{\min}\) 之间有多少个可以作为解的 \(x\) 即可,也就是
\[\dfrac{x_{\max} - x_{\min}}{b'} + 1 \]
2. CF1728E Red-Black Pepper
对于一组询问 \((c, d)\)(这里的 \(c, d\) 分别代表题面中的 \(x_i, y_i\)),先用 exgcd 判断 \(cx + dy = n\) 有没有非负整数解。然后考虑最大化收益。
先想想如果固定了红辣椒的数量 \(i\),怎么求出最大收益。记 \(f(i)\) 表示使用了 \(i\) 份红辣椒的收益,则 \(f(n)\) 就是强制所有菜品都放红辣椒的收益,可以直接求出。然后把所有菜品按 \(b_i - a_i\) 排序,倒序枚举 \(i\),每次把总收益加上 \(b_i - a_i\),相当于把一份红辣椒换成蓝辣椒。那么 \(f\) 数组的差分就是 \(a_i - b_i\),由于这个值已经排序了,所以差分单调减,因此 \(f(i)\) 是上凸函数。
那么我们可以预处理 \(f\) 的极值点 \(k\),求出所有非负整数解中,\(c \cdot x_l\) 不超过 \(k\) 的最大 \(x_l\),和 \(c \cdot x_r\)超过 \(k\) 的最小 \(x_r\),则答案为 \(\max(f(c \cdot x_l), f(c \cdot x_r))\)。
III. 中国剩余定理
形如
的方程组称为线性同余方程组。当 \(m_i\) 两两互质时,可以使用中国剩余定理(Chinese Remainder Theorem,CRT)来求解。
过程
CRT 的核心思想是求出若干个 \(x_i\)(\(1 \le i \le k\)),分别满足 \(x_i \equiv a_i \pmod{m_i}\) 且 \(x_i \equiv 0 \pmod{m_j}\)(\(i \neq j\)),则所有 \(x_i\) 的和就是方程组的解。(这个过程和拉格朗日插值很类似。)
下面只需考虑如何求出每个 \(x_i\)。记 \(M = m_1m_2 \cdots m_k\),\(M_i = M / m_i\)。由于 \(x_i \equiv 0 \pmod{m_j}\)(\(i \neq j\)),所以 \(x_i\) 同时是所有 \(x_j\)(\(i \neq j\))的倍数,因此 \(x_i\) 是 \(M_i\) 的倍数。设 \(x_i = c \cdot M_i\)。
另一方面,\(x_i \equiv a_i \pmod{m_i}\),即 \(c \cdot M_i \equiv a_i \pmod{m_i}\)。由于 \(M_i\) 和 \(m_i\) 互质,所以一定存在满足条件的 \(c\),容易看出取 \(c = a_i \cdot (M_i)^{-1}\) 即可,其中 \((M_i)^{-1}\) 是 \(M_i\) 在模 \(m_i\) 意义下的逆元。注意 \(m_i\) 不一定是质数,所以只能用 exgcd 求逆元。
那么 \(S = \sum_{i = 1}^{k} x_i\) 是方程组的一个解。实际上方程的解集为
容易看出这些 \(x\) 都是方程的解。另一方面,设 \(x_1\),\(x_2\) 是方程组的两个解,则
由于 \(m_i \perp m_j\)(\(i \neq j\)),所以 \(x_1 \equiv x_2 \pmod{M}\),因此上式确实给出了方程组的所有解。
易知 CRT 的时间复杂度为 \(O(k \log w)\),其中 \(w\) 是 \(m_i\) 的值域。
参考代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long i64;
auto exgcd(i64 a, i64 b) {
if(b == 0) {
return make_pair(1LL, 0LL);
}
auto [x, y] = exgcd(b, a % b);
return make_pair(y, x - (a / b) * y);
}
i64 inv(i64 a, i64 m) {
auto [x, y] = exgcd(a, m);
return (x % m + m) % m;
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n;
cin >> n;
i64 M = 1;
vector<int> a(n + 1), b(n + 1);
for(int i = 1; i <= n; i++) {
cin >> a[i] >> b[i];
M *= a[i];
}
i64 ans = 0;
for(int i = 1; i <= n; i++) {
i64 m = M / a[i];
i64 add = (__int128)m * b[i] % M * inv(m, a[i]) % M;
ans = (ans + add) % M;
}
cout << ans << '\n';
return 0;
}
IV. 扩展中国剩余定理
过程
当线性同余方程组的模数不两两互质时,不保证 \(M_i\) 在模 \(m_i\) 意义下存在逆元,所以 CRT 失效了,这时候我们需要扩展中国剩余定理(exCRT)。实际上,除了求解的问题相似,它和 CRT 的过程没有太大联系。
exCRT 的核心思想是:对于两个线性同余方程
可以把它们合并成一个完全等价的线性同余方程
其中 \(x_0\) 是原方程组的任意一个解。”完全等价“的含义是:转化前后方程的解集相同。
通过这个结论,我们可以不断合并两个同余方程,最终只剩一个方程时我们就得到了解。
下面具体研究两个线性同余方程是怎么合并的。我们引入未知数 \(y_1\),\(y_2\),把同余方程改写成二元一次不定方程的形式:
联立得
其中 \(a_1\),\(a_2\),\(m_1\),\(m_2\) 都是已知量,所以这是一个二元一次不定方程。根据裴蜀定理,其有解的充要条件是 \(\gcd(y_1, y_2) \mid (a_1 - a_2)\)。如果无解,说明整个同余方程组都无解。否则可以用 exgcd 求出任意一组解,接着代回得到原先两个同余方程的一个特解 \(x_0\)。这样就把两个方程合并为了一个,重复这个过程知道全部方程都合并为一个即可。易知 exCRT 的时间复杂度和 CRT 相同,都为 \(O(k \log w)\)。
参考代码(模板 P4777):
#include<bits/stdc++.h>
using namespace std;
typedef long long i64;
typedef __int128 i128;
auto exgcd(i64 a, i64 b) {
if(b == 0) {
return make_tuple(1LL, 0LL, a);
}
auto [x, y, g] = exgcd(b, a % b);
return make_tuple(y, x - (a / b) * y, g);
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
// 代码中变量名称和题目不同
int n;
i64 a, b;
cin >> n >> b >> a;
for(int i = 2; i <= n; i++) {
i64 c, d;
cin >> d >> c;
// 把 x equiv a (mod b) 和 x equiv c (mod d) 两个方程合并
auto [y1, y2, g] = exgcd(-b, d);
i128 y = (i128)y1 * (a - c) / g;
i64 lcm = abs(d / g * b);
i128 x0 = ((a + y * b) % lcm + lcm) % lcm;
a = (i64)x0, b = lcm;
}
cout << a << '\n';
return 0;
}
正确性证明
exCRT 中使用的最重要的结论是
的解集和
的解集完全相同,其中 \(x_0\) 是原方程组的任意一个解。下面来证明这一点。(暂略)
例题
1. P4774 [NOI2018] 屠龙勇士
首先发现用哪把剑攻击每一条龙是唯一确定的,可以用 multiset
模拟预处理。记攻击第 \(i\) 条龙的剑的攻击力为 \(b_i\)。
设攻击次数为 \(x\),攻击第 \(i\) 条龙以后的回复次数为 \(y_i\),则我们要求的是以下方程组的非负整数解
其中 \(x\) 和 \(y_i\) 是未知数。
先不考虑非负的限制。单个方程是二元一次不定方程的形式,但把多个二元一次不定方程联立在一起,并且要求所有解的 \(x\) 相同,这是不好处理的。套路性地把二元一次不定方程转化成线性同余方程(在用 exgcd 求逆元的过程中我们也使用了这种转化,但是转化的方向相反):
这个形式和 exCRT 的形式非常像,但是 \(x\) 前面多了系数,我们需要把它去掉。一般地,对于同余方程
设 \(\gcd(a, m) = g\),如果 \(\gcd(a, m) \nmid b\) 则无解。否则设 \(a = a'g, b = b'g, m = m'g\),则
此时 \(\gcd(a', m') = 1\),两边同乘 \(a'\) 的逆元
就转化成了标准的线性同余方程。这样就可以用 exCRT 求出某个特解 \(x_0\)。
接下来要考虑 \(x, y_{i}\) 非负的限制。我们知道 exCRT 的解系是 \(x \equiv x_{0} \pmod{M}\),其中 \(M\) 是所有 \(m_{i}\) 的 \(\operatorname{lcm}\)。所以先把 \(x_{0}\) 对 \(M\) 取模使其非负。然后分别考虑每个 \(y_{i}\) 非负的限制,这等价于 \(b_{i}x \ge a_{i}\)。我们令 \(x_0\) 不断加上 \(M\) 直到满足条件,可以用除法优化。考虑完所有 \(y_i\) 的限制后就得到了满足所有限制的最小解。
参考资料
- 同余代数 - qAlex_Weiq - 博客园
- (扩展)中国剩余定理 - 知乎
- 《初等数论(第三版)》闵嗣鹤,严士健