基础数论学习笔记(施工中)

基础数论学习笔记

再一次学习数论!

约定:如果没有特别说明,文中字母均为整数,且大多数情况下我只关心正整数。

O. 基础知识

同余式的运算法则

考虑模 \(p\) 意义下的运算时,同余的两个数是完全等价的,也就是说我们不区分同一剩余类中的数。掌握了这一点后我们可以立即导出模意义下加法和乘法运算法则:

  1. (加法)如果 \(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} \]

  2. (乘法)如果 \(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\)):

\[\gcd(a, b) = \begin{cases} a \quad & \text{if} \ b = 0 \\ \gcd(b, a \bmod b) \quad & \text{Otherwise} \end{cases} \]

代码如下:

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\),每次迭代时有两种情况:

  1. \(a < b\),此时有 \(\gcd(a, b) = \gcd(b, a)\)
  2. \(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)用于求形如

\[ax + by = c \]

二元一次不定方程的所有整数解。

(为了方便,我们认为 \(a, b, c \ge 0\)。对于负数的情况可能需要多一点讨论。)

有解性判定——裴蜀定理

裴蜀定理(Bézout's lemma)给出了二元一次不定方程有整数解的充要条件:方程 \(ax + by = c\) 有解当且仅当

\[\gcd(a, b) \mid 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\),那么

\[\begin{aligned} r &= p - qd_0 \\ &= (x_1a + y_1b) - q(x_0a - y_0b) \\ &= (x_1 - qx_0)a + (y_1 + qy_0)b \end{aligned} \]

因此 \(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\),则方程的解集为

\[A = \left\{ \left( x_0 + kb', y_0 - ka' \right) | k \in \mathbb{Z} \right \} \]

换句话说,把 \(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(x' - x_0) + b(y' - y_0) = 0 \]

由于 \(a = a'g\)\(b = b'g\),所以

\[a'(x' - x_0) = -b'(y' - y_0) \]

左边是 \(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')\),因此

\[ax_0 + by_0 = bx_1 + (a \bmod b) y_1 \]

\(a \bmod b\) 改写为 \(a - b \cdot \left\lfloor \dfrac{a}{b} \right\rfloor\),那么

\[ax_0 + by_0 = bx_1 + \left( a - b \cdot \left\lfloor \dfrac{a}{b} \right\rfloor \right)y_1 \]

整理等式右边,得到

\[ax_0 + by_0 = ay_1 + b \left(x_1 - \left\lfloor \dfrac{a}{b} \right\rfloor y_1 \right) \]

对比两边得到

\[\begin{cases} x_0 = y_1 \\ y_0 = \left(x_1 - \left\lfloor \dfrac{a}{b} \right\rfloor y_1 \right) \end{cases} \]

这就得到了两个方程解的关系。

此算法时间复杂度的分析和欧几里得算法完全相同,二者的时间复杂度都是 \(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)

这道题不仅要求出解,还要回答有关解的一些其它问题。我们逐个来研究。

  1. 是否有解?

    我们用裴蜀定理判断是否有解。现在假设有解,并且已经用扩欧求出了一组解 \((x_0, y_0)\)

  2. 是否有正整数解?

    我们让 \(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\) 的最小正整数值。

  3. 如果有正整数解,有多少个?

    只需求出 \(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. 中国剩余定理

形如

\[\begin{cases} x \equiv a_1 \pmod{m_1} \\ x \equiv a_2 \pmod{m_2} \\ \cdots \\ x \equiv a_k \pmod{m_k} \end{cases} \]

的方程组称为线性同余方程组。当 \(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 \equiv S \pmod{M} \]

容易看出这些 \(x\) 都是方程的解。另一方面,设 \(x_1\)\(x_2\) 是方程组的两个解,则

\[x_1 \equiv x_2 \pmod{m_i}, i = 1, 2, \cdots, k \]

由于 \(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 的核心思想是:对于两个线性同余方程

\[\begin{cases} x \equiv a_{1} \pmod{m_1} \\ x \equiv a_{2} \pmod{m_2} \end{cases} \]

可以把它们合并成一个完全等价的线性同余方程

\[x \equiv x_{0} \pmod{\operatorname{lcm}(m_1, m_2)} \]

其中 \(x_0\) 是原方程组的任意一个解。”完全等价“的含义是:转化前后方程的解集相同。

通过这个结论,我们可以不断合并两个同余方程,最终只剩一个方程时我们就得到了解。

下面具体研究两个线性同余方程是怎么合并的。我们引入未知数 \(y_1\)\(y_2\),把同余方程改写成二元一次不定方程的形式:

\[\begin{cases} x = a_1 + m_1y_1 \\ x = a_2 + m_2y_2 \end{cases} \]

联立得

\[a_1 - a_2 = -m_1y_1 + m_2y_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 中使用的最重要的结论是

\[\begin{cases} x \equiv a_{1} \pmod{m_1} \\ x \equiv a_{2} \pmod{m_2} \end{cases} \]

的解集和

\[x \equiv x_{0} \pmod{\operatorname{lcm}(m_1, m_2)} \]

的解集完全相同,其中 \(x_0\) 是原方程组的任意一个解。下面来证明这一点。(暂略)

例题

1. P4774 [NOI2018] 屠龙勇士

首先发现用哪把剑攻击每一条龙是唯一确定的,可以用 multiset 模拟预处理。记攻击第 \(i\) 条龙的剑的攻击力为 \(b_i\)

设攻击次数为 \(x\),攻击第 \(i\) 条龙以后的回复次数为 \(y_i\),则我们要求的是以下方程组的非负整数

\[b_ix - p_iy_i = a_i, i = 1, 2, \cdots, n \]

其中 \(x\)\(y_i\) 是未知数。

先不考虑非负的限制。单个方程是二元一次不定方程的形式,但把多个二元一次不定方程联立在一起,并且要求所有解的 \(x\) 相同,这是不好处理的。套路性地把二元一次不定方程转化成线性同余方程(在用 exgcd 求逆元的过程中我们也使用了这种转化,但是转化的方向相反):

\[b_ix \equiv a_i \pmod{p_i}, i = 1, 2, \cdots, n \]

这个形式和 exCRT 的形式非常像,但是 \(x\) 前面多了系数,我们需要把它去掉。一般地,对于同余方程

\[ax \equiv b \pmod{m} \]

\(\gcd(a, m) = g\),如果 \(\gcd(a, m) \nmid b\) 则无解。否则设 \(a = a'g, b = b'g, m = m'g\),则

\[a'x \equiv b' \pmod{m'} \]

此时 \(\gcd(a', m') = 1\),两边同乘 \(a'\) 的逆元

\[x \equiv b'(a')^{-1} \pmod{m'} \]

就转化成了标准的线性同余方程。这样就可以用 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\) 的限制后就得到了满足所有限制的最小解。

代码

参考资料

posted @ 2025-05-27 20:59  DengStar  阅读(38)  评论(0)    收藏  举报