Acwing - 算法基础课 - 笔记(十一)

·  阅读 137

数学知识(二)

这一小节主要讲解的内容是:欧拉函数,快速幂,扩展欧几里得算法,中国剩余定理。

这一节内容偏重于数学推导,做好心理准备。

欧拉函数

公式法

什么是欧拉函数呢?

欧拉函数用 ϕ(n)\phi(n) 来表示,它的含义是,11nn 中与 nn 互质的数的个数

比如,ϕ(6)=2\phi(6) = 2,解释:1到6当中,与6互质的数只有1,5,共两个数。

两个数 aa , bb 互质的含义是 gcd(a,b)=1gcd(a,b) = 1

欧拉函数如何求解呢?

对于一个数 NN ,将其写为分解质因数的形式 N=P1k1×P2k2×...×PnknN = P_1^{k_1} \times P_2^{k_2} \times ... \times P_n^{k_n}

ϕ(N)=N×(11P1)×(11P2)×...×(11Pn)\phi(N) = N \times (1-\frac{1}{P_1}) \times (1-\frac{1}{P_2}) \times ... \times (1-\frac{1}{P_n}),这就是欧拉函数的求解公式

比如 N=6N = 6, 6有2个质因子2和3,则

ϕ(6)=6×(112)×(113)=2\phi(6) = 6 \times (1-\frac{1}{2}) \times (1-\frac{1}{3}) = 2

欧拉函数的公式如何证明呢?

欧拉函数的证明需要用到容斥原理,容斥原理的具体内容将在下一节进行讲解,本节直接借用容斥原理的结论。

注:下面的 NP\frac{N}{P} 实际指的都是 NP\lfloor\frac{N}{P}\rfloor,比如 157\frac{15}{7} 指的是 157=2\lfloor\frac{15}{7}\rfloor = 2,由于加上取整符号会导致编写latext语法变得非常繁琐,特此申明。

我们求 11nn 中,与 nn 互质的数的个数,仍然采用删数的思想,只要把那些和 nn 有公约数的数删掉就可以了。那么还是需要先对 nn 做分解质因数,因为 nn 的质因数是 nn 的约数的最小组成部分。

  1. 11NN 中,去掉其全部质因子 P1P2,...,PnP_1,P_2,...,P_n 的所有倍数。这些数都和 NN 不互质,因为都有公因子 PiP_i

    那么剩余的数的个数就是NNP1NP2...NPnN - \frac{N}{P_1} - \frac{N}{P_2} - ... - \frac{N}{P_n}

  2. 加上所有 Pi×PjP_i \times P_j 的倍数的个数,其中 iijj 是从 11nn 之中任选两个数

    假设第一步计算得到的结果是 S1S_1,那么第二步要做的就是 S1+NP1×P2+NP1×P3+...NP1×Pn+NP2×P3+...+NP2×Pn+...+NPn1×PnS_1 + \frac{N}{P_1 \times P_2} + \frac{N}{P_1 \times P_3} + ...\frac{N}{P_1 \times P_n} + \frac{N}{P_2 \times P_3} + ... + \frac{N}{P_2 \times P_n} + ... + \frac{N}{P_{n-1} \times P_n}

    因为在第一步时,会多删掉一些数,比如某个数既是 P1P_1 的倍数,也是 P2P_2 的倍数(即这个数是 P1×P2P_1 \times P_2 的倍数),那么这个数会被 P1P_1 减一次,被 P2P_2 减一次,一共减了两次 ,多减了一次,那么我们需要加一次加回来。

  3. 加上所有 Pi×Pj×PkP_i \times P_j \times P_k 的倍数的个数

    若有某个数,同时是3个质因子的倍数,假设一个数同时是 P1P_1P2P_2P3P_3 的倍数,那么这个数第一步会被减3次,会在第二步被加3次 ,实际是没减的。所以还要对这种数减去一次

    假设第二步计算得到的结果是 S2S_2,那么第三步要做的就是 S2NP1×P2×P3NP1×P2×P4...NPn2×Pn1×PnS_2 - \frac{N}{P_1 \times P_2 \times P_3} - \frac{N}{P_1 \times P_2 \times P_4} - ... - \frac{N}{P_{n-2} \times P_{n-1} \times P_n}

    即,对 i,j,k[1,n]i,j,k \in [1,n] 的全部组合,进行减

  4. 后续的步骤同理

    若若某个数,同时是4个质因子的倍数,这种数在第一步被减了4次,第二步被加了C42=4×32×1=6C_4^2 = \frac{4 \times 3}{2 \times 1} = 6 次,第三步被减了 C43=4C_4^3 = 4 次,总共是减了2次,则需要加回一次

    所以要做的就是 S3+NP1×P2×P3×P4+....+NPn3×Pn2×Pn1×PnS_3 + \frac{N}{P_1 \times P_2 \times P_3 \times P_4} + .... + \frac{N}{P_{n-3} \times P_{n-2} \times P_{n-1} \times P_n}

  5. 对于那些是5个质因子的倍数的数,第一次被减了 C51=5C_5^1 = 5 次,第二步加了 C52=10C_5^2 = 10 次,第三步被减了 C53=10C_5^3 = 10 次,第四步被加了 C54=5C_5^4 = 5 次,总共是0次操作,所以需要减一次

  6. .....

是不是感觉像是无限套娃?!hhhhh

这个过程会在第 nn 次后停止,而 nnNN 的质因数个数

所以,11NN 中所有和 NN 互质的数的个数,就等于

NNP1NP2...NPnN - \frac{N}{P_1} - \frac{N}{P_2} - ... - \frac{N}{P_n}

+NP1×P2+NP1×P3+...NP1×Pn+NP2×P3+...+NP2×Pn+...+NPn1×Pn+ \frac{N}{P_1 \times P_2} + \frac{N}{P_1 \times P_3} + ...\frac{N}{P_1 \times P_n} + \frac{N}{P_2 \times P_3} + ... + \frac{N}{P_2 \times P_n} + ... + \frac{N}{P_{n-1} \times P_n}

NP1×P2×P3NP1×P2×P4...NPn2×Pn1×Pn- \frac{N}{P_1 \times P_2 \times P_3} - \frac{N}{P_1 \times P_2 \times P_4} - ... - \frac{N}{P_{n-2} \times P_{n-1} \times P_n}

+NP1×P2×P3×P4+....+NPn3×Pn2×Pn1×Pn+ \frac{N}{P_1 \times P_2 \times P_3 \times P_4} + .... + \frac{N}{P_{n-3} \times P_{n-2} \times P_{n-1} \times P_n}

........

±NP1×P2×...×Pn\pm \frac{N}{P_1 \times P_2 \times ... \times P_n}

而这一大坨式子,化简后得到的就是

N×(11P1)×(11P2)×...×(11Pn)N \times (1-\frac{1}{P_1}) \times (1-\frac{1}{P_2}) \times ... \times (1-\frac{1}{P_n})

这也就是本节开头提到的欧拉函数的公式

练习题:acwing - 873: 欧拉函数

自己的解法(先分解质因数,再套用欧拉公式)

#include<iostream>
using namespace std;

typedef long long LL;

const int N = 2e8;

int primes[N];

int euler(int n) {
	int ctn = 0, t = n;
	for(int i = 2; i <= n / i; i++) {
		if(n % i == 0) {
			primes[ctn++] = i;
			while(n % i == 0) n /= i;
		}
	}
	if(n > 1) primes[ctn++] = n;

	int up = t, down = 1;
	for(int i = 0; i < ctn; i++) {
		up *= (primes[i] - 1);
		down *= primes[i];
		if(up % down == 0) {
			up /= down;
			down = 1;
		}
	}
	return up;
}

int main() {
	int m;
	scanf("%d", &m);
	while(m--) {
		int n;
		scanf("%d", &n);
		printf("%d\n", euler(n));
	}
	return 0;
}
复制代码

yxc的解法(边分解质因数边套用欧拉公式)

#include<iostream>
using namespace std;

int euler(int n) {
	int res = n;
	for(int i = 2; i <= n / i; i++) {
		if(n % i == 0) {
			res = res / i * (i  - 1);
			while(n % i == 0) n /= i;
		}
	}
	if(n > 1) res = res / n * (n - 1);
	return res;
}

int main() {
	int m;
	scanf("%d", &m);
	while(m--) {
		int n;
		scanf("%d", &n);
		printf("%d\n", euler(n));
	}
	return 0;
}
复制代码

注意,欧拉公式的计算过程中,每一项计算完毕后都应该是一个整数,不应该出现小数。

故 对于 N×(11P1)N \times (1-\frac{1}{P_1}) 这样的式子,计算时我们采用 NP1×(P11)\frac{N}{P_1} \times (P_1-1),这样能保证得到的结果是整数,而不出现小数。

由于 P1P_1NN 的质因子,故 NP1\frac{N}{P_1} 一定是整数,所以 NP1×(P11)\frac{N}{P_1} \times (P_1-1) 就一定是整数,令 S=NP1×(P11)S = \frac{N}{P_1} \times (P_1-1),那么在第二轮迭代中,我们需要计算 S×(11P2)S \times (1-\frac{1}{P_2}),同样的,我们计算 SP2×(P21)\frac{S}{P_2} \times (P_2-1),那么 SP2\frac{S}{P_2} 一定是整数吗?答案是肯定的。

因为 N=P1k1×P2k2×...×PnknN = P_1^{k_1} \times P_2^{k_2} \times ... \times P_n^{k_n},容易得到,第一步计算的结果 S=P1k11×P2k2×...×Pnkn×(P11)S=P_1^{k_1-1} \times P_2^{k_2} \times ... \times P_n^{k_n} \times (P_1-1),所以 SS 是一定能被 P2P_2 整除的,同理,在第三轮迭代时,第二轮的计算结果仍然能被 P3P_3 整除,所以我们就可以在每轮迭代时,计算 SiPi×(Pi1)\frac{S_i}{P_i} \times (P_i-1),这样能保证整个计算的过程中,不出现小数,即可保证最终结果的正确性。

筛法

上面的公式法,适用于求解某一个数的欧拉函数,就类似于用试除法判断某一个数是否是质数。

然而,有的时候,我们需要求解某一个范围内全部数的欧拉函数(比如求解 11NN 之间所有数的欧拉函数),此时若对每个数依次套用欧拉公式,则整体的时间复杂度为 O(N×N)O(N \times \sqrt{N}) ,因为欧拉函数的计算依赖于分解质因数,而分解质因数的时间复杂度是 O(N)O(\sqrt{N}) 。这个时间复杂度是不被接受的,太慢了,所以我们需要变换思路。

联想到在求解 11NN 之间全部质数的时候,我们采用的是筛法,而不是对每个数依次判断是否是质数。类似的,求解 11NN 之间全部数的欧拉函数,我们也可以用类似的思想。

借鉴前面筛选质数时所采用的线性筛法,能够在 O(N)O(N) 的时间复杂度内求解出 11NN 每个数的欧拉函数。在本章(数论)后面的学习中,会发现线性筛法在执行过程中不仅仅能求出欧拉函数,还能求出很多其他的内容。

我们先把线性筛法的代码写一遍

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 1e6 + 10;

bool st[N];

int primes[N], ctn;

void get_primes_linear(int n) {
	for(int i = 2; i <= n; i++) {
		if(!st[i]) primes[ctn++] = i;
		for(int j = 0; primes[j] <= n / i; j++) {
			st[primes[j] * i] = true;
			if(i % primes[j] == 0) break;
		}
	}
}
复制代码

当某一个数 ii 是质数时,根据欧拉函数的公式,我们容易得出,这个质数的欧拉函数 ϕ(i)=i1\phi(i)=i-1;而当我们在通过 pj×ip_j \times i 来筛数时。分两种情况

  • imodpj=0i \mod p_j =0

    容易推出 ϕ(pj×i)=pj×ϕ(i)\phi(p_j \times i) = p_j \times \phi(i)

    因为一个数 NN 的欧拉函数 ϕ(N)=N×(11p1)×...×(11pn)\phi(N) = N \times (1-\frac{1}{p_1}) \times ... \times (1-\frac{1}{p_n})

    假设 ii 一共有 kk 个质因子 p1p_1p2p_2,....,pkp_k

    ϕ(i)=i×(11p1)×...×(11pk)\phi(i) = i \times (1-\frac{1}{p_1}) \times ... \times (1-\frac{1}{p_k})

    imodpj=0i \mod p_j =0 ,说明 pjp_jii 的一个质因子。则 pj×ip_j \times i 这个数的质因子也是 p1p_1p2p_2,...,pkp_k

    ϕ(pj×i)=pj×i×(11p1)×...×(11pk)=pj×ϕ(i)\phi(p_j \times i) = p_j \times i \times (1-\frac{1}{p_1}) \times ... \times (1-\frac{1}{p_k}) = p_j \times \phi(i)

  • imodpj0i \mod p_j \ne 0

    根据类似上面的过程,容易推出

    ϕ(pj×i)=pj×i×(11p1)×...×(11pj)×...×(11pk)=(pj1)×ϕ(i)\phi(p_j \times i) = p_j \times i \times (1-\frac{1}{p_1}) \times ... \times (1-\frac{1}{p_j}) \times ... \times (1-\frac{1}{p_k}) = (p_j-1) \times \phi(i)

    pj×ip_j \times i 这个数的质因子,比 ii 这个数的质因子,多一个 pjp_j

由于在线性筛法的执行过程中,对于质数会保留,对于合数会用其最小质因子筛掉。所以线性筛法是会访问到所有数的。而根据上面的推导,在遇到每种情况时,我们都能求出欧拉函数

  • 当这个数是质数:ϕ(i)=i1\phi(i)=i-1

  • 当这个数是合数

    其一定会被某个 pj×ip_j \times i 筛掉

    • imodpj=0i \mod p_j = 0,则 ϕ(pj×i)=pj×ϕ(i)\phi(p_j \times i) = p_j \times \phi(i)
    • imodpj0i \mod p_j \ne 0,则 ϕ(pj×i)=(pj1)×ϕ(i)\phi(p_j \times i) = (p_j-1) \times \phi(i)
  • 特殊的,ϕ(1)=1\phi(1)=111 既不是质数也不是合数)

于是,我们便可以通过线性筛法来求解出 11NN 的所有数的欧拉函数,时间复杂度 O(N)O(N)

练习题2:acwing - 874: 筛法求欧拉函数

#include<iostream>
using namespace std;

typedef long long LL;

const int N = 1e6 + 10;

int primes[N], ctn;

bool st[N];

LL phi[N];

void get_eulers(int n) {
	phi[1] = 1;
	for(int i = 2; i <= n; i++) {
		if(!st[i]) {
			primes[ctn++] = i;
			phi[i] = i - 1;
		}
		for(int j = 0; primes[j] <= n / i; j++) {
			st[primes[j] * i] = true;
			if(i % primes[j] == 0) {
				phi[primes[j] * i] = primes[j] * phi[i];
				break;
			}
			phi[primes[j] * i] = (primes[j] - 1) * phi[i];
		}
	}
}

int main() {
	int n;
	scanf("%d", &n);
	get_eulers(n);
	LL sum = 0;
	for(int i = 1; i <= n; i++) sum += phi[i];
	printf("%lld", sum);
	return 0;
}
复制代码

欧拉定理

说了这么多,那么欧拉函数有什么实际的作用或者应用场景呢?

有一个定理叫做欧拉定理,它描述这样一个规律:

aann 互质,则 aϕ(n)modn=1a^{\phi(n)} \mod n = 1

比如 a=5a = 5n=6n = 6 ,由于 ϕ(6)=6×(112)×(113)=2\phi(6)=6 \times (1-\frac{1}{2}) \times (1-\frac{1}{3})=2

则有 aϕ(n)modn=52mod6=1a^{\phi(n)} \mod n = 5^2 \mod 6 = 1

那么欧拉定理如何证明呢?

yxc是这样讲的:

首先,11nn 以内与 nn 互质的数,有 ϕ(n)\phi(n) 个,将其编号如下

a1a_1a2a_2,...,aϕ(n)a_{\phi(n)}

aann 互质,则可以得知,在 modn\mod n 的语义下。

a×a1a \times a_1a×a2a \times a_2,...,a×aϕ(n)a \times a_{\phi(n)}

这个集合,与上面的与 nn 互质的数是同一个集合

进而有,两个集合中全部数的乘积,在 modn\mod n 的语义下是相等的

a1×a2×...×aϕ(n)aϕ(n)×a1×a2×...×aϕ(n)a_1 \times a_2 \times ... \times a_{\phi(n)} \equiv a^{\phi(n)} \times a_1 \times a_2 \times ... \times a_{\phi(n)}

将两边的 a1×a2×...×aϕ(n)a_1 \times a_2 \times ... \times a_{\phi(n)} 消掉,则有

aϕ(n)1a^{\phi(n)} \equiv 1,也即 aϕ(n)modn=1a^{\phi(n)} \mod n = 1

(其实我没有完全把推导的原理搞懂,这个有待后续补充,TODO)

特殊的,当 nn 是一个质数时(此时我们将 nn 替换成 pp 来表示),则有

aϕ(p)modp=1a^{\phi(p)} \mod p = 1

ap1modp=1a^{p-1} \mod p = 1

这个欧拉定理的特例,被称为费马小定理。

快速幂

快速幂,是用来快速求解出 akmodpa^k \mod p 的结果,时间复杂度为 O(logk)O(\log k),其中 aakkpp 都可以在 10910^9 内,如果是按照朴素做法的话,则需要 O(k)O(k) 的时间复杂度,如果 k=109k = 10^9 ,则这个复杂度就比较高了,而如果是 O(log2k)O(\log_2k),即便 k=109k=10^9,也大概只需要 3030 次计算即可,非常的快。

那么,快速幂是怎么做到的呢?

快速幂的核心思路是:反复平方法(思想上有点类似逆向二分。二分是每次在当前基础上减一半,快速幂是每次在当前基础上扩大一倍)。

原理说明如下

我们先预处理出来如下的值:a20modpa^{2^0} \mod pa21modpa^{2^1} \mod pa22modpa^{2^2} \mod p ,...,a2log2kmodpa^{2^{\log_2k}} \mod p

一共 log2k\log_2k 个数。随后,我们通过这些数,组合出 aka^k

即,将 aka^k 拆成 ak=a2i×a2j×...=a2i+2j+...a^k=a^{2^i} \times a^{2^j} \times ...=a^{2^i+2^j+...} ,即 k=2i+2j+...k = 2^i+2^j+...

即,将 kk 拆成 202^0212^1222^2,...,2log2k2^{\log_2k} 的某些数的和

其实,就只需要把 kk 转化为二进制即可。

比如 k=(54)10=(110110)2=21+22+24+25k=(54)_{10}=(110110)_2=2^1+2^2+2^4+2^5

由于一个十进制的数 kk ,一定可以转化为二进制,即,kk 一定能拆成若干个 2i2^i 的加和,所以 aka^k 一定能按照上面的式子拆开,所以快速幂算法能够生效。

预处理一共计算出 log2k\log_2k 个数,需要计算 log2klog_2k 次,将 kk 拆成二进制表示,并计算结果,需要 log2klog_2k 次,所以总共的时间复杂度就是 O(log2k)O(\log_2k)。其实编写代码时,可以将上面两步合在一起,实际只需要 log2k\log_2k 次运算

练习题:acwing - 875: 快速幂

#include<iostream>
using namespace std;

typedef long long LL;

// 快速幂求解 a^k mod p
int qmi(int a, int k, int p) {
	int res = 1;
	// 求 k 的二进制表示
	while(k > 0) {
		if(k & 1 == 1) res = (LL) res * a % p;
		k = k >> 1;
		a = (LL)a * a % p;
	}
	return res;
}

int main() {
	int n;
	scanf("%d", &n);
	while(n--) {
		int a, k, p;
		scanf("%d%d%d", &a, &k, &p);
		printf("%d\n", qmi(a, k, p));
	}
	return 0;
}
复制代码

练习题:acwing - 876: 快速幂求逆元

乘法逆元的定义:若两个数 bbmm 互质,则对任意整数 aa,如果 bb 能整除 aa,则存在一个整数 xx ,使得 aba×x\frac{a}{b} \equiv a \times xmodm\mod m

则称 xxbb 在模 mm 下的乘法逆元,记作 b1modmb^{-1} \mod m

其实,通俗的说,就是,对于两个互质的数 bbmm,一定存在一个数 xx,使得所有能够被 bb 整除的数 aa,都有 aba×x\frac{a}{b} \equiv a \times x ,则称 xxbb 在模 mm 下的乘法逆元,记作 b1modmb^{-1} \mod m

容易得到:b×b11b \times b^{-1} \equiv 1,即 b×b1modm=1b \times b^{-1} \mod m = 1

求一个数 bb 在模 mm 下的逆元,即求一个数 xx,满足 b×xmodm=1b \times x \mod m = 1 即可

mm 是一个质数,将其记为 pp,则根据上面的费马小定理,有 bp1modp=1b^{p-1} \mod p= 1,则 bb 的逆元 b1modpb^{-1} \mod p就等于 bp2modpb^{p-2} \mod p

mm 不是一个质数(但注意 bbmm 是互质的),则根据欧拉定理,有 bϕ(m)modm=1b^{\phi(m)} \mod m = 1,则 bb 的逆元 b1modpb^{-1} \mod p 就等于 bϕ(m)1modpb^{\phi(m)-1} \mod p

这道题考察的就是快速幂+费马小定理

#include<iostream>
using namespace std;

typedef long long LL;

int qmi(int a, int k, int p) {
	int res = 1;
	while(k > 0) {
		if(k & 1 == 1) res = (LL) res * a % p;
		k = k >> 1;
		a = (LL) a * a % p;
	}
	return res;
}

int main() {
	int n;
	scanf("%d", &n);
	while(n--) {
		int a, p;
		scanf("%d%d", &a, &p);
		if(a % p == 0) printf("impossible\n"); // a 和 p 不互质
		else printf("%d\n", qmi(a, p - 2, p));
	}
	return 0;
}
复制代码

扩展欧几里得算法

裴蜀定理:对任意的一对正整数 aabb,一定存在一对非零整数 xxyy,使得 ax+by=gcd(a,b)ax + by = gcd(a,b)

ax+by=dax + by = d,则 dd 一定是 gcd(a,b)gcd(a,b) 的倍数。这个是显而易见的,令 aabb 的最大公约数是 cc,即令 gcd(a,b)=cgcd(a,b)=c,则 aa 一定是 cc 的倍数,bb 也一定是 cc 的倍数,故 ax+byax+by 也一定是 cc 的倍数。则最小可以凑出的倍数就是 11。所以裴蜀定理是成立的。

那么给定一对正整数 aabb,如何求解出一对 xxyy,使得 ax+by=gcd(a,b)ax+by=gcd(a,b) 成立呢?

求解 xxyy 的过程,就可以采用扩展欧几里得算法。

扩展欧几里得算法,是在欧几里得算法上的扩展。而欧几里得算法,就是前面求解最大公约数时,用到的辗转相除法。

练习题:acwing - 877: 扩展欧几里得算法

#include<iostream>
using namespace std;

int gcd(int a, int b, int &x, int &y) {
	if(b == 0) {
		x = 1;
		y = 0;
		return a;
	} else {
		 int d = gcd(b, a % b, y, x); // 注意这里要交换 x 和 y 的位置
		 y -= a / b * x;
		 return d;
	}
}

int main() {
	int n;
	scanf("%d", &n);
	while(n--) {
		int a, b, x, y;
		scanf("%d%d", &a, &b);
		gcd(a, b, x ,y);
		printf("%d %d\n", x, y);
	}
	return 0;
}
复制代码

对代码的解释:

递归到当 b=0b=0 时,找到 gcd(a,b)=agcd(a,b)=a,所以 ax+by=aax+by=a,则此时更新 x=1y=0x=1,y=0

b0b \ne 0 时,先计算 d=gcd(b,amodb)d = gcd(b, a \mod b),并且传入 xxyy ,注意 xxyy 的位置需要交换一下,然后此时有 by+(amodb)x=dby + (a \mod b)x = d,展开得 by+(aabb)x=dby + (a-\lfloor\frac{a}{b}\rfloor b)x = d,变换一下得 ax+b(yabx)ax+b(y-\lfloor\frac{a}{b}\rfloor x)

aa 的系数 xx 不更新, bb 的系数 yy 需要更新为 yabxy-\lfloor\frac{a}{b}\rfloor x

由于我们在递归时会交换 xxyy 的位置,则实际两个数都会被交替更新。即我们在执行 gcd(b, a % b, y, x)这一句之后, xxyy 都已经满足 by+(amodb)x=dby + (a \mod b)x = d,我们再通过 bbamodba \mod b 的系数,来更新 aabb 的系数。

注意,xxyy 不是唯一的。由于我们的等式是 ax+by=gcd(a,b)ax+by=gcd(a,b),其中 aabbgcd(a,b)gcd(a,b) 都是固定的常数,所以这个式子其实可以转化为我们中学时学过的一次线性函数 y=ax+by=ax+b 的形式,这个函数的图像是一根直线,线上有多个点 (x,y)(x,y) 都满足这个等式,所以 (x,y)(x,y) 是有多组的(准确的说,有无穷多组)

我们只要求解出一组 (x0,y0)(x_0,y_0) 满足上面的等式。就可以求出所有的 (xy)(x,y)

ax+by=dax+by=d 可以写成 a(xbd)+b(y+ad)=da(x-\frac{b}{d}) + b(y+\frac{a}{d})=d

通解如下:x=x0bd×kx = x_0-\frac{b}{d} \times ky=y0ad×ky=y_0-\frac{a}{d} \times k,其中 kk 是任意整数

扩展欧几里得算法的应用 —— 求解线性同余方程

练习题:acwing - 878: 线性同余方程

即,给定 aabbmm,求解一个 xx,使得满足 axbax \equiv b (modm)(\mod m)

因为上面等式的含义是: axax 除以 mm,余数是 bb ,所以 axax 一定是 mm 的某个倍数,加上 bb,即 ax=my+bax = my+b

再变形一下,得 axmy=bax-my=b,令 y=yy^{'} = -y,则有 ax+my=bax+my^{'}=b

这样就转变成了上面使用扩展欧几里得算法能够求解问题,只需要保证 bbaamm 的最大公约数的倍数即可,否则无解。

为什么要保证 bbaamm 的最大公约数的倍数呢?解释如下

假设 aamm 的最大公约数是 kk,那么根据上面的裴属定理,一定有一组 (x,y)(x,y) ,使得 ax+my=kax+my=k 成立,那么对于 kk 的倍数,比如 tt 倍,就是 tktk,则一定有 atx+mty=tkatx+mty = tk。所以,只要保证 bbaamm 的最大公约数的某个倍数,这个线性同余方程就有解,否则无解。

#include<iostream>
using namespace std;

typedef long long LL;

int gcd(int a, int b, int &x, int &y) {
	if(b == 0) {
		x = 1;
		y = 0;
		return a;
	} else {
		 int d = gcd(b, a % b, y, x); // 注意这里要交换 x 和 y 的位置
		 y -= a / b * x;
		 return d;
	}
}

int main() {
	int n;
	scanf("%d", &n);
	while(n--) {
		int a, b, m, x, y;
		scanf("%d%d%d", &a, &b, &m);
		int d = gcd(a, m, x ,y);
		if(b % d != 0) printf("impossible\n");
        else printf("%d\n", (LL)x * b / d % m);
	}
	return 0;
}
复制代码

中国剩余定理

TODO

练习题:acwing - 2024: 表达整数的奇怪方式

//TODO
复制代码
分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改