中国剩余定理(CRT && exCRT) 数论学习笔记

前言

很久就仰慕数论中的中国剩余定理很久了 , 但由于之前被 \(exgcd\) 拷打的缘故 , 迟迟不敢学习 , 今天总算迈开第一步了

学习 \(CRT\) 的动机很简单 , 就只是看到了一道题而已 , 见洛谷 \(P1495\)

例题

题目意思大概就是给你 \(2 \times n\) 个数 , 分别是 \(a_{1},a_{2},a_{3} ... a_{n}\)\(b_{1},b_{2},b_{3} ... b_{n}\)
保证 \(b_1 , b_2 ... b_n\) 互质 , 组成 \(n\) 个方程组:

\[\begin{cases} x \equiv a_1 \pmod {b_1}\\ x \equiv a_2 \pmod {b_2}\\ ...\\ x \equiv a_n \pmod {b_n}\\ \end{cases} \]

其中 \(x\) 就是这题要求的答案

引入

看到这里 , 首先要知道这些东西是怎么个回事

警告 , 以下是针对从来没有学习过数论的补课内容 , 若已经学过了 , 则可以跳过这一部分

本文采用自顶向下的渗透学习方式 , 因此后文会出现比较多的概念

同余

我们从小学起就开始学习除法 , 而对于整数除法 , 我们有除不禁的情况 , 此时我们就引入了一个神奇的东西 —— 余数

举个及其简单的栗子 , \(7 / 2 = 3 ...... 1\) , 这是我们小学时候列的式子 , 此时我们稍微的变一下形 , 转化为这个样子
\(7 = 2 \times 3 + 1\) , 我们使用高级的字母来替换这个式子 , 就变成了 \(a = bk + r\) , 有时候 , 我们对于除法的不怎么感兴趣 , 我们只要研究
一个式子的余数的时候 模(mod) 诞生了 , 此时我们省略结果就变成了这样子 \(a\mod b = r\) , \(mod\) 也就是我们熟知的 %

对于 \(5\)\(7\) 来说 , 它们在除 \(2\) 时的余数是一样的 , 那么它们就是同余 , 顾名思义同一个余数,我们把\(5,7\)的关系这么表达:

\(5 \equiv 7 \pmod 2\) , 这就是同余了

同余的部分性质

这一块我学习的也不深,就把所知道的讲一下

显而易见的:

  1. \(a \equiv a \pmod n\)
  2. 如果 \(a \equiv b \pmod n\) ,那么 \(b \equiv a \pmod n\)
  3. 如果 \(a \equiv b \pmod n , b \equiv c \pmod n\) , 那么 \(a \equiv c \pmod n\)
  4. \(a \equiv a + kn \pmod n , k \in \mathbb{Z}\) (\(k\) 属于整数集合)

有了这四条十分基础的性质 , 我们就可以先解决一些问题了 , 比如 负数的取模 , 如果单纯的负数取模在 \(c++\) 中会是一个负数的,比如我让\(-10 \mod 3\)
结果是 \(-1\) , 有些时候我们是不想要负数的 , 我们就可以使用性质4先取模成 \(-1\) 再加上 \(3\) 然后取模 , 这就是一个十分简单的应用了

逆元

对于 \(x\) , 一般情况下有一个数 \(a\) , 使得 \(ax = 1\) , 会称 \(a\) 为什么 , 首先肯定不是倒数 , 在取模意义下我们把
\(ax \equiv 1 \pmod b\) , 中的 \(a\) 称为逆元

线性同余方程

线性同余方程的定义如下:

我们对于 \(x,a,b\) ,其中 \(x\) 为未知数,那么形如:

\(px \equiv a \pmod b\)

的方程就叫线性同余方程 , 对于本题 \(p\) 等于 \(1\)

我们要如何去解一个线性同余方程呢?

最简单的一个方法就是直接套,但是时间肯定无法接受 , 仔细观察 , 有没有发现逆元像是线性同余方程的一种特殊形势,

因此我们可以找到一个方法一次性把这两个玩意儿都找一个办法解开来 , 这时候 \(exgcd\) 就发力了 , 但我们稍微百度一下就发现
\(exgcd\) 好像是解不定方程的 , 难道又要学新东西了吗

这里我们之介绍二元一次不定方程:

对于如下式子 \(ax + ny = b\) , \(a , n\) 都是整数 , 我们要找他的全体整数解 , 这就是二元一次不定方程 , 我们要怎么解呢?

exgcd

裴(péi)蜀定理 (\(bezout\)定理)

裴蜀太难打了 , 我喜欢打成 \(bezout\)

这个定理是这样的

\(a,b\) 是不全为零的整数。那么,对于任意整数 \(x , y\) ,都有 \(gcd(a,b) | ax + by\) 成立; 而且,存在整数 \(x,y\) , 使得 \(ax + by = gcd(a,b)\)

我们来解析一下 , 首先我们整理一下这个定理的大意 , 前提条件是 \(a,b \in \mathbb{Z}\) , 然后就有两个结论:

  1. 对于所有的 \(x,y \in \mathbb{z}\) , \(ax + by \mod gcd(a,b) = 0\) 就是上面 \(gcd(a,b) | ax + by\) 的意思(整除)
  2. 存在 \(x , y\) , 使得 \(ax + by = gcd(a,b)\)

我试着给出证明

  • 步骤一

我们定义一个特殊的集合

\(d\) 是对于所有 \(ax + by\) 中的最小的一个正整数

例如 , \(a = 8 , b = 12\) , 则 \(ax + by\) \(x = -1 , y = 1\) 时 , 为最小的正整数 \(4\)

  • 步骤二

我们要证明 \(d | a 且 d | b\) , 即 \(d\) 能整除 \(a\)\(b\)

这一步采用反证法 , 假设 \(d | a\) 不成立 , 那么 \(a = qd + r\) , \(d\) 又等于 \(ax + by\) , 代入进去

\(a = q(ax + by) + r\) 整理一下得到

\(r = a(1-qx) + b(-qy)\) , 发现 \(r\) 也符合 \(ax+by\) 的格式 , 而 \(a = qd + r\) 中说明了 \(r < d\) , 但 \(d\) 又是在 \(ax + by\) 中最小的 , 两者矛盾

所以 \(d | a\) 成立 , 如法炮制 , \(d | b\) 也能成立

  • 步骤三

证明 \(d\)\(gcd(a,b)\)

首先因为 \(d|a\)\(d|b\) , \(d\) 肯定是 \(a,b\) 的公约数

又因为 \(d = ax_0 + by_0\) , 所以 \(a\)\(b\) 的任何一个公约数都能整除 \(d\) , 所以 \(d\) 只能为最大公约数

所以 \(d = gcd(a,b)\)

结论

存在 \(x,y \in \mathbb{Z}\) , \(ax+by = gcd(a,b)\)

对于第一条性质,我就使用比较感性的方法去理解它 , \(d\) 既然是 \(gcd(a,b)\) , 也一定是 \(x , y \in \mathbb{Z}, ax+by\) 的约数 , 所以无论
\(x,y\) 怎么取都是 \(d\) 的倍数,又根据前面同余的性质可知,它对于 \(gcd(a,b)\) 的模数是 \(0\)

--------分割线-------

证明完没什么必要证明的 \(bezout\) 定理后 , 我们终于可以开开心心的使用 \(exgcd\) 了 , \(exgcd\) 既然有 \(ex\) 的前缀,那么方法
也就是 \(gcd\) 扩展而来的

\(gcd\) 的步骤如下:

  1. \(b = 0\) 时最大公约数是 \(a\)
  2. 求出 \(b , a % b\) 的最大公约数 , 设为 \(gcd_0\)
  3. \(gcd = gcd_0\) , \(gcd\)就是 \(a,b\)的最大公约数

仿照\(gcd\) , \(exgcd\) 的步骤通常如下:(所有除法操作均为向下取整)

  1. \(b = 0\) \(ax + by = d\) 的一组解是 \(x = \frac{d}{a}\) , \(y\) 任意
  2. 求出 \(bx + (a \mod b)y = d\) 的一组解设为 \((x_0,y_0)\)
  3. \((x,y) = f((x_0,y_0))\) , \(f(x,y)\) 是一种变换 , 对于这个变换 , 我们可以记为
    \((x,y) = (y_0,x_0-\frac{a}{b} * y_0)\)

下面就是 \(exgcd\) 的代码了 , 讲了很多代码却很简单,是不是很意外

void exgcd(ll a, ll b, ll& x, ll& y) {
    if (b == 0) {
        x = 1, y = 0;
        return;
    }
    exgcd(b, a % b, y, x);
    y -= a / b * x;
}

CRT 求解

终于可以开始解不定方程组了 , 首先把上面的方程组贴下来

\[\begin{cases} x \equiv a_1 \pmod {b_1}\\ x \equiv a_2 \pmod {b_2}\\ ...\\ x \equiv a_n \pmod {b_n}\\ \end{cases} \]

对于 \(b_1 , b_2 ... b_n\) 它们是互质的 , 那么就有几个有优美的性质

\((a_1, a_2, \dots, a_n)\) 两两互质(即任意两个数的最大公约数为 \(1\)),则它们的乘积 \((N = a_1 \times a_2 \times \dots \times a_n)\) 的所有约数都可以唯一表示为:\((d = d_1 \times d_2 \times \dots \times d_n)\),其中 \((d_i)\) 是 \((a_i)\) 的约数

若质数整除乘积,则必整除其中一个数

虽然不能完全理解这些性质 , 但我们还是可以直接将这些数全部乘起来

\[N = \prod_{i=1}^n b_i \]

同时在对于每一个单独的式子 , 我们切走一块自己本身 , \(m_i = \frac{N}{b_i} , i = 1 \to n\)

然后我们取 \(m_i\) 的逆元写作 \(m_i^{-1}\) (在模 \(b_i\) 意义下)

计算 \(c_i = m_i \times m_i^{-1}\) , 此时不需要对 \(c_i\) 取模

那么方程组在模 \(N\) 意义下的唯一解就是 \(x = \sum_{i=1}^n a_i \times c_i \pmod N\) , 其余解不在模 \(N\) 意义下的,就可以从这个解推出来

我们先不管证明 , 先把实现代码贴出来

ll CRT(ll k , ll a[] , ll r[]){
	ll M = 1 ,ans = 0;
	for(int i = 1 ; i <= k ; i++){
		M *= r[i];
	}
	for(int i = 1 ; i <= k ; i++){
		ll m = M / r[i] , b , y;
		b = y = 0;
		exgcd(m , r[i] , b , y);
		ans = (ans + a[i] * m * b % M) % M;
	}
	return (ans % M + M) % M ;
}

数组 \(a\) 代表前面不定方程组的 \(a\) , 数组 \(r\) 代表前面的 \(b\)

CRT的证明

不知道讲没讲对 , 欢迎大佬指出错误

既然 \(x\) 是方程组的解 , 那么 \(x\) 一定可以使方程组成立 ,即 \(x \equiv a_i \pmod {n_i}\) , 我们需要证明这个式子成立

我们要将每一个数都证明成立 , 并且还要供给式子 \(x = \sum_{i=1} ^ n a_ic_i \pmod N\) 一个变量 , 因此我们提供两个变量 , \(i,j\) ,两者不相等 , \(m_j = \frac{N}{b_j}\) , 此时 \(m_j\) 已经在 \(N\) 这块蛋糕中把 \(b_j\) 分走了 , 所以除了 \(b_j\) 以外的所有的 \(b\) 都可以整除 \(m_j\) , 即 \(m_j \equiv 0 \pmod {b_i}\) , 而 \(c_j\) 是由 \(m_jm_j^{-1}\) 得来的 , 在不模 \(b_j\) 的情况下, \(c_j \equiv m_j \equiv 0 \pmod {b_i}\)

我们现在要由 \(x \equiv \sum_{j = 1}^n a_jc_j \pmod {b_i}\) 变成 \(x \equiv a_i \pmod {b_i}\)

有了前面的辅助,我们发现 \(c_j\) 在模 \(n_i\) 意义下等于 \(0\)

所以 , 对于所有的 \(j \neq i\) , 他们的贡献在模 \(b_i\) 下是没有的 , 因此 \(\sum_{j = 1}^n a_jc_j\) 只剩下 \(a_ic_i\)

又由于 \(c_i = m_im_i^{-1}\) , 在模 \(b_i\) 下等于 \(1\),所有只剩下 \(a_i\)

所以 \(x \equiv a_i \pmod {b_i}\) 成立

总结一下过程就是如下:

\[x \equiv \sum_{j = 1} ^ n a_jc_j \pmod {b_i}\\ x \equiv a_ic_i \pmod {b_i}\\ x \equiv a_i \times 1 \pmod {b_i}\\ x \equiv a_i \pmod {b_i} \]

看上去还是很舒服的

但问题就解决了吗 , 若 \(b_i\) 不两两互质呢

exCRT

例题:洛谷P4777

在一个方程组中 , 我对于每一个不互质的 \(b_i\) 肯定是合并掉最好 , 那么什么时候可以合并 , 合并成什么样子呢?

首先我们假设要合并 \(x \equiv a_1 \pmod {b_1}\)\(x \equiv a_2 \pmod {b_2}\) ,

先把两个式子转化为不定方程 \(x = pb_1 + a_1\)\(x = qb_2 + a_2\)
\(p,q\)为整数

把式子整理一下 \(pb_1 - qb_2 = a_2 - a_1\) , 由右边式子想到 \(bezout\) 定理 , 若 \(a_2 - a_1 \equiv 0 \pmod {gcd(b_1,b_2)}\) 时 , 即无法被整除时 , 无解 , 无法合并

其余情况 ,毕竟也是不定方程, 我们使用 \(exgcd\) 求出对应的 \(p,q\) , 对应合并的式子就长这样 \(x = pb_1 + a_1 \pmod {lcm(m_1,m_2)}\)

例题代码

P1495代码 \(CRT\)

#include <bits/stdc++.h>
using namespace std;

#define ll __int128

const ll N = 100007;
ll n;

ll a[N] , b[N];

void exgcd(ll a, ll b, ll& x, ll& y) {
	if (b == 0) {
		x = 1, y = 0;
		return;
	}
	exgcd(b, a % b, y, x);
	y -= a / b * x;
}

ll CRT(ll k , ll a[] , ll r[]){
	ll M = 1 ,ans = 0;
	for(int i = 1 ; i <= k ; i++){
		M *= r[i];
	}
	for(int i = 1 ; i <= k ; i++){
		ll m = M / r[i] , b , y;
		b = y = 0;
		exgcd(m , r[i] , b , y);
		ans = (ans + a[i] * m * b % M) % M;
	}
	return (ans % M + M) % M ;
}

int main(){
	scanf("%lld",&n);
	for(int i = 1 ; i <= n ; i++){
//		cin>>a[i]>>b[i]; 
		scanf("%lld%lld",&a[i],&b[i]);
	}
	
	long long ans = CRT(n , b , a);
	cout<<ans;
//	printf("%lld",ans);
	return 0;
}

P4777 \(exCRT\)

注意针对 \(int128\) 做特殊输入

#include <bits/stdc++.h>
using namespace std;

#define ll __int128
const int N = 1000007;

ll n;
ll a[N], b[N];
ll p[N], mode[N], rem[N], cnt;

void add(ll &x, ll y, ll i) {
    ll nmod = 1;
    while (x % p[i] == 0) {
        x /= p[i], nmod *= p[i];
    }
    if (nmod > mode[i]) {
        mode[i] = nmod, rem[i] = y % nmod;
    }
}

void exgcd(ll a, ll b, ll &x, ll &y) {
    if (!b) {
        x = 1, y = 0;
        return;
    }
    exgcd(b, a % b, y, x);
    y -= a / b * x;
}

ll CRT(ll k, ll a[], ll r[]) {
    ll ans = 0, n = 1;
    for (int i = 1; i <= k; i++) {
        n *= r[i];
    }
    for (int i = 1; i <= k; i++) {
        ll mi = n / r[i];
        ll b, y;
        exgcd(mi, r[i], b, y);
        ans = (__int128)(ans + (a[i] * mi) % n * b % n) % n;
    }
    return (ans % n + n) % n;
}

void input(ll &n) {
    long long t;
    cin >> t;
    n = t;
}

void output(ll n) {
    long long t = n;
    cout << t;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    input(n);
    for (int i = 1; i <= n; i++) {
        input(a[i]), input(b[i]);
        // cin >> a[i] >> b[i];
        for (int j = 1; j <= cnt; j++) {
            add(a[i], b[i], j);
        }
        if (a[i] > 1) {
            for (ll np = 2; np * np <= a[i]; np++) {
                if (a[i] % np == 0) {
                    p[++cnt] = np;
                    add(a[i], b[i], cnt);
                }
            }
            if (a[i] > 1) {
                p[++cnt] = a[i];
                add(a[i], b[i], cnt);
            }
        }
    }
    output(CRT(cnt, rem, mode));
    // cout << CRT(cnt, rem, mode);

    return 0;
}
posted @ 2025-08-22 21:20  reisa_awa  阅读(46)  评论(0)    收藏  举报