模运算
模运算是大数运算中的常用操作。如果一个数太大,无法直接输出,或者不需要直接输出,可以把它取模后缩小数值再输出。定义取模运算为a除以m的余数,记为:
a m o d m = a % m a\ \ mod\ \ m\ =\ a\ \%\ m a mod m = a % m
取模的结果满足 0 ≤ a % m ≤ m − 1 0≤a\%m≤m-1 0≤a%m≤m−1,题目用给定的 m m m限制计算结果的范围。例如 m = 10 m=10 m=10,就是取计算结果的个位数。取模操作满足以下性质:
加: ( a + b ) % m = ( ( a % m ) + ( b % m ) ) % m 加:(a+b)\%\ m=((a\%\ m)+(b\%\ m))\%\ m 加:(a+b)% m=((a% m)+(b% m))% m
减: ( a − b ) % m = ( ( a % m ) − ( b % m ) ) % m 减:(a-b)\%\ m=((a\%\ m)-(b\%\ m))\%\ m 减:(a−b)% m=((a% m)−(b% m))% m
乘: ( a × b ) % m = ( ( a % m ) × ( b % m ) ) % m 乘:(a\ ×\ b)\%\ m=((a\%\ m)\ ×\ (b\%\ m))\%\ m 乘:(a × b)% m=((a% m) × (b% m))% m
然而,对除法取模进行下面的类似操作是错误的:
( a / b ) % m = ( ( a % m ) / ( b % m ) ) % m (a/b)\%\ m=((a\%\ m)/(b\%\ m))\%m (a/b)% m=((a% m)/(b% m))%m
如: ( 100 / 50 ) % 20 = 2 , ( 100 % 20 ) / ( 50 % 20 ) % 20 = 0 (100/50)\%20=2,(100\%20)/(50\%20)\%20=0 (100/50)%20=2,(100%20)/(50%20)%20=0,两者不相等。除法的取模需要用到逆元。
快速幂
快速幂以及扩展的矩阵快速幂,由于应用场景比较常见,也是竞赛中常见的题型。幂运算 a n a^n an即 n n n个 a a a相乘。快速幂就是高效地算出 a n a^n an。当 n n n很大时,例如: n = 1 0 9 n=10^9 n=109,计算 a n a^n an这样大的数,一是数字太大,二是计算时间很长。
下面先考虑如何缩短计算时间,如果用暴力的方法直接算 a n a^n an,即逐个做乘法,复杂度是 O ( n ) O(n) O(n),即使能算出来,也会超时。
读者很容易想到快速幂的办法:先算 a 2 a² a2,然后继续算平方 ( a 2 ) 2 (a²)² (a2)2,一直算到 n n n次幂。这
是分治法的思想,复杂度为 O ( l o g 2 n ) O(log₂n) O(log2n)。下面是代码,请读者自己理解:
cpp
int fastPow(int a, int n) {
if (n == 1) return a;
int temp = fastPow(a, n / 2); //分治
if (n % 2 == 1) { //奇数个a,此处也可以写为if(n &1)
return temp * temp * a;
}
else { //偶数个a
return temp * temp;
}
}
程序中的递归,层数只有 l o g 2 n log_2n log2n,不用担心溢出的问题。
矩阵快速幂
给定一个 m × m m×m m×m的矩阵 A A A,求它的 n n n次幂 A n A^n An,这也是常见的计算。同样有矩阵快速幂的算法,原理是把矩阵当作变量来操作,程序和上面的很相似。
首先需要定义矩阵的结构体,并且定义矩阵相乘的操作。注意矩阵相乘也需要取模:
cpp
const int MAXN = 2; //根据题目要求定义矩阵的阶,本例中是2
const int MOD = 1e4; //根据题目要求定义模
struct Matrix { //定义矩阵
int m[MAXN][MAXN];
Matrix() { //初始化矩阵
memset(m, 0, sizeof(m));
}
};
Matrix Multi(Matrix a, Matrix b) { //矩阵的乘法
Matrix res;
for (int i = 0; i < MAXN; i++)
for (int j = 0; j < MAXN; j++)
for (int k = 0; k < MAXN; k++)
res.m[i][j] = (res.m[i][j] + a.m[i][k] * b.m[k][j]) % MOD;
return res;
}
下面是矩阵快速幂的程序代码,和前面单变量的快速幂的代码非常相似:
cpp
Matrix fastm(Matrix a, int n) {
Matrix res;
for (int i = 0; i < MAXN; i++) res.m[i][i] = 1;
//初始化为单位矩阵,相当于前面程序中的"int res =1;"
while (n) {
if (n & 1)res = Multi(res, a);
a = Multi(a, a);
n /= 2;
}
return res;
}
最大公约数,最小公倍数
最大公约数GCD和最小公倍数LCM是竞赛中常见的知识点,虽然这两个知识点很容易理解,但往往会与其他知识点结合起来出综合题。
cpp
int GCD(int a, int b){
if(a % b == 0)return b;
return GCD(b, a % b);
}
int LCM(int a, int b){
return a * b / GCD(a, b);
}
扩展欧几里得算法与二元一次方程的整数解
给出整数 a 、 b 、 n a、b、n a、b、n,问方程 a x + b y = n ax+by=n ax+by=n什么时候有整数解?如何求所有的整数解?
有解的充分必要条件是: g c d ( a , b ) gcd(a,b) gcd(a,b)可以整除 n n n。简单证明如下:
令:a = gcd(a, b)a'、b = gcd(a, b)b';
有:ax + by = gcd(a, b) (a'x + b'y) = n;
如果x、y、a'、b'都是整数,
那么:n必须是gcd(a,b)的倍数才有整数解。
例如 4 x + 6 y = 8 4x+6y=8 4x+6y=8、 2 x + 3 y = 4 2x+3y=4 2x+3y=4有整数解, 4 x + 6 y = 7 4x+6y=7 4x+6y=7则没有整数解。如果确定有解,一种解题方法是先找到一个解 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),那么通解公式如下:
{ x = x 0 + b t y = y 0 − a t t 为任意整数 \begin{cases} x = x_0 + bt\\ y = y_0 -at\ \ \ t为任意整数 \end{cases} {x=x0+bty=y0−at t为任意整数
所以,问题转化为如何求 ( x 0 , y 0 ) (x_0,y_0) (x0,y0)。利用扩展欧几里得算法可以求出这个特解。
扩展欧几里得算法
当方程符合 a x + b y = g c d ( a , b ) ax+by=gcd(a,b) ax+by=gcd(a,b)时,可以用扩展欧几里得算法求 ( x 0 , y 0 ) (x_0,y_0) (x0,y0)。程序如下:
cpp
void ex_gcd(int a, int b, int &x, int &y) { //x, y变化后形成特解(x0, y0)
if (b == 0) {
x = 1, y = 0;
return;
}
ex_gcd(b, a % b, x, y);
int tmp = x;
x = y;
y = tmp - (a / b) * y;
}
有时候为了简化描述,在 a x + b y = g c d ( a , b ) ax+by=gcd(a,b) ax+by=gcd(a,b)两边除以 g c d ( a , b ) gcd(a,b) gcd(a,b),得到 c x + d y = 1 cx+dy=1 cx+dy=1。
其中: c = a / g c d ( c , b ) , d = b / g c d ( a , b ) c=a/gcd(c,b),d=b/gcd(a,b) c=a/gcd(c,b),d=b/gcd(a,b)。
很明显, c 、 d c、d c、d是互质的。 c x + d y = 1 cx+dy=1 cx+dy=1的通解如下:
{ x = x 0 + d t y = y 0 − c t t 为任意整数 \begin{cases} x = x_0 + dt\\ y = y_0 -ct\ \ \ t为任意整数 \end{cases} {x=x0+dty=y0−ct t为任意整数
求任意方程ax+by=n的一个整数解
用扩展欧几里得算法求解 a x + b y = g c d ( a , b ) ax+by=gcd(a,b) ax+by=gcd(a,b)后,利用它可以进一步解任意方程 a x + b y = n ax+by=n ax+by=n,得到一个整数解。其步骤如下:
- 判断方程 a x + b y = n ax+by=n ax+by=n是否有整数解,有解的条件是 g c d ( a , b ) gcd(a,b) gcd(a,b)可以整除 n n n
- 用扩展欧几里得算法求 a x + b y = g c d ( a , b ) ax+by=gcd(a,b) ax+by=gcd(a,b)的一个解 ( x 0 , y 0 ) (x_0,y_0) (x0,y0)
- 在 a x 0 + b y 0 = g c d ( a , b ) ax_0+by_0=gcd(a,b) ax0+by0=gcd(a,b)两边同时乘以 n / g c d ( a , b ) n/gcd(a,b) n/gcd(a,b)得: a x 0 n / g c d ( a , b ) + b y 0 n / g c d ( a , b ) = n ax_0n/gcd(a,b)+by_0n/gcd(a,b)=n ax0n/gcd(a,b)+by0n/gcd(a,b)=n
- 对照 a x + b y = n ax+by=n ax+by=n,得到它的一个解 ( x ' , y ' ) (x`,y`) (x',y')是:
{ x ' = x 0 ∗ n / g c d ( a , b ) y ' = y 0 ∗ n / g c d ( a , b ) \begin{cases} x` = x_0*n/gcd(a,b)\\ y` = y_0*n/gcd(a,b) \end{cases} {x'=x0∗n/gcd(a,b)y'=y0∗n/gcd(a,b)
应用场合
扩展欧几里得算法是一个很有用的工具,在竞赛题目中常用于以下场合:
- 求解不定方程
- 求解模的逆元
- 求解同余方程
虽然用扩展欧几里得算法可以算 a x + b y = g c d ( a , b ) ax+by=gcd(a,b) ax+by=gcd(a,b)的通解,不过一般没有这个需求,而是用于求某些特殊的解,例如求解逆元,逆元是除法取模操作常用的工具。
同余与逆元
同余在数论中非常有用,它用类似处理等式的方式来处理整除关系,非常简便。
同余
两个整数 a 、 b a、b a、b和一个正整数 m m m,如果 a a a除以 m m m所得的余数和 b b b除以 m m m所得的余数相等。
即: a % m = b % m a\%m=b\%m a%m=b%m,称 a a a和 b b b对于 m m m同余, m m m称为同余的模。
同余的概念也可以这样理解: m ∣ ( a − b ) m|(a-b) m∣(a−b)。
即: a − b a-b a−b是 m m m的整数倍。例如 6 ∣ ( 23 − 5 ) 6|(23-5) 6∣(23−5), 23 23 23和 5 5 5对模 6 6 6同余。
同余的符号记为 a ≡ b ( m o d m ) a\equiv b(mod\ m) a≡b(mod m)
一元线性同余方程
a x ≡ b ( m o d m ) ax\equiv b(mod\ m) ax≡b(mod m),即 a x ax ax除以 m , b m,b m,b除以 m m m的余数相同,这里 a 、 b 、 m a、b、m a、b、m都是整数,求解 x x x的值。
方程也可以这样理解: a x − b ax-b ax−b是 m m m的整数倍。设 y y y是倍数,那么 a x − b = m y ax-b=my ax−b=my。
移项得到: a x − m y = b ax-my=b ax−my=b。因为 y y y可以是负数,改写为: a x + m y = b ax+my=b ax+my=b。
这就是在扩展欧几里得算法中的二元一次不定方程 。
当且仅当 g c d ( a , m ) gcd(a,m) gcd(a,m)能整除 b b b时有整数解。例如 15 x + 6 y = 9 15x+6y=9 15x+6y=9,有整数解 x = 1 , y = − 1 x=1,y=-1 x=1,y=−1。当 g c d ( a , m ) = b gcd(a,m)=b gcd(a,m)=b时,可以直接用扩展欧几里得算法求解 a x + m y = b ax+my=b ax+my=b。
如果不满足 g c d ( a , m ) = b gcd(a,m)=b gcd(a,m)=b,还能用扩展欧几里得算法求解 a x + m y = b ax+my=b ax+my=b吗?答案是肯定的,但是需要结合逆元。
逆元
给出 a a a和 m m m,求解方程 a x ≡ 1 ( m o d m ) ax\equiv 1(mod\ m) ax≡1(mod m),即 a x ax ax除以 m m m余数是 1 1 1。
根据前面的讨论,有解的条件是 g c d ( a , m ) = 1 gcd(a,m)=1 gcd(a,m)=1,即 a 、 m a、m a、m互质。该问题等价于求解$ax+my=1,可以用上一节的扩展欧几里得算法求解。例如 8 x ≡ 1 ( m o d 31 ) 8x\equiv 1(mod\ 31) 8x≡1(mod 31),等价于求解 8 x + 31 y = 1 8x+31y=1 8x+31y=1,用扩展欧几里得算法求得一个特解是 x = 4 , y = − 1 x=4,y=-1 x=4,y=−1。
方程 a x ≡ 1 ( m o d m ) ax\equiv 1(mod\ m) ax≡1(mod m)的一个解 x x x,称 x x x为 a a a模 m m m的逆 。注意,这样的 x x x有很多,把它们都称为逆 。
求逆元的代码如下:
cpp
int inverse_mod(int a, int m){
int x, y;
ex_gcd(a, m, x, y);
return(m + x % m ) % m;
}
试除法判断质数
问题:输入一个很大的数 n n n,判断它是不是素数。
素数定义:一个数 n n n,如果不能被 [ 2 , n − 1 ] [2,n-1] [2,n−1]内的所有数整除, n n n就是素数。当然,并不需
要把 [ 2 , n − 1 ] [2,n-1] [2,n−1]内的数都试一遍,这个范围可以缩小到 [ 2 , n ] [2,\sqrt n] [2,n ]。
给定 n n n,如果它不能整除 [ 2 , n ] [2,\sqrt n] [2,n ]内的所有数,它就是素数。证明如下:
设 n = a × b n=a×b n=a×b,有 m i n ( a , b ) ≤ n min(a,b)≤\sqrt n min(a,b)≤n ,令 a ≤ b a≤b a≤b。只要检查 [ 2 , n ] [2,\sqrt n] [2,n ]内的数,如果 n n n不是素数,就能找到一个 a a a。如果不存在这个 a a a,那么 [ n , n − 1 ] [\sqrt n,n-1] [n ,n−1]内也不存在 b b b。
以上判断的范围可以再缩小一点: [ 2 , n ] [2,\sqrt n] [2,n ]内所有的素数。其原理很简单,读者在学过下文提到的埃式筛法之后更容易理解。
用试除法判断素数,复杂度是 O ( n ) O(\sqrt n) O(n ),对于 n ≤ 1 0 12 n≤10^{12} n≤1012的数是没有问题的。下面是试除法判断素数的代码:
cpp
bool is_pri(long long n){
if(n <= 1)return false;
for(int i = 2; i * i <= n; i++){
if(n % i == 0)return false;
}
return ture;
}