本系列文章将于2021年整理出版。前驱教材:《算法竞赛入门到进阶》 清华大学出版社
网购:京东 当当 作者签名书:点我
公众号同步:算法专辑
暑假福利:胡说三国
有建议请加QQ 群:567554289
文章目录
同余是很巧妙的工具,它使得人们能够用等式的形式来简洁地描述整除关系。
在阅读本节内容时,请对照上一节“线性丢番图方程”的内容,有很多类似的地方。
1. 同余概述
1.1. 同余定义
设m是正整数,若a和b是整数,且
,则称
和
模
同余。也就是说,
除以
得到的余数,和
除以
的余数相同;或者说,
除以
,余数是0。
把
和
模
同余记为
,
称为同余的模。例子:
(1)因为7|(18-4),所以18
4 (mod 7),18除以7余数是4,4除以7的余数也是4;
(2)3
6 (mod 9),3除以9余数是3,-6除以9的余数也是3;
(3)13和5模9不同余,因为13除以9余数是4,5除以9余数是5。
1.2. 一些定理和性质
(1)若
和
是整数,
为正整数,则
当且仅当
。
(2)把同余式转换为等式。若
和
是整数,则
当且仅当存在整数,使得
。例如:19-2 (mod 7),有19 = -2 + 3 ∙ 7。
(3)设
是正整数,模
的同余满足下面的性质:
1)自反性。若
是整数,则
2)对称性。若
和
是整数,且
,则
。
3)传递性。若
是整数,且
,则
。
2. 一元线性同余方程
一元线性同余方程:设
是未知数,给定
,求整数
,满足
。
研究线性同余方程有什么用处?
表示
是
的倍数,设为
倍,则有
,这就是二元线性丢番图方程。所以,求解一元线性同余方程等同于求解二元线性丢番图方程。
方程是否有解?如果有解,有多少解?如何求出解?与线性丢番图的定理一样,线性同余方程也有类似的定理。
定理:设
和
是整数,
,若
不能整除b,则
无解,若
能整除
,则
有
个模
不同余的解。
定理的前半部分可以概况为:
有解的充分必要条件是
能整除b。
定理的后半部分说明了解的情况。与线性丢番图方程类似,如果有一个特解是
,那么通解是
,当
时,有
个模
不同余的解。
推论:
和
互素时,因为
,所以线性同余方程
有唯一的模
不同余的解。这个推论在下一节的逆中有应用。
最后回到求解
的问题:首先求逆,然后利用逆求得
。
3. 逆
求解一般形式的同余方程 ,需要用到逆。
3.1.逆的概念
给定整数a,且满足
,称
的一个解为
模
的逆。记为
。
例如:
,有一个解是
= 4,4是8模31的逆。所有的解,例如35、66等,都是8模31的逆。
可以借助丢番图方程理解逆的概念,
即方程
,
= 4是8模31的逆,4×8-1能整除31。
3.2.求逆
有多种方法可以求逆。
(1)扩展欧几里得求单个逆
下面的例题是求逆,即求解同余方程
。
同余方程(求逆) 洛谷 P1082
题目描述:求关于x的同余方程
的最小正整数解。2≤a, m≤2,000,000,000。
题解: ,即丢番图方程 ,先用扩展欧几里得求出 的一个特解 ,通解是 。然后通过取模操作算最小整数解 ,因为 > 0,可以保证结果是正整数。
long long mod_inverse(long long a, long long m){ //求逆
long long x,y;
extend_gcd(a,m,x,y);
return (x%m + m) % m; //保证返回最小正整数
}
int main(){
long long a,m; cin >> a >>m;
cout << mod_inverse(a,m);
return 0;
}
(2)费马小定理求单个逆
费马小定理:设
是素数,
是正整数且与
互质,那么有
。
,那么
就是
模
的逆。计算需要用到快速幂取模fast_pow(),参考前面章节“大素数的判定”。快速幂取模的复杂度是
的。
long long mod_inverse(long long a,long long mod){
return fast_pow(a,mod - 2,mod);
}
(3)递推求多个逆
如果要求1 ~ n内所有的逆,可以用递推。复杂度是O(n)的。
乘法逆元 洛谷 P3811
题目描述:给定
,求
中所有整数在模
意义下的乘法逆元。
,
,
为质数。
输入:一行两个正整数
。
输出:输出
行,第
行表示
在模
下的乘法逆元。
首先,
时逆是1。下面求
时的逆,用递推法。
设
,余数是
,即
;
在两边乘
,得到
;
移项得
,即
,
。
long long inv[maxn];
void inverse(long long n, long long p){
inv[1]=1;
for(int i = 2;i<maxn;i++)
inv[i]= (p - p/i) * inv[p%i] % p;
}
下面给出一个求逆的例题。
A/B hdu 1576
题目描述:求(A/B)%9973,但由于A很大,我们只给出n (n = A%9973)(我们给定的A必能被B整除,且gcd(B, 9973) = 1)。
输入:第一行是T,表示有T组数据。每组数据有两个数n(0 <= n < 9973)和B(1 <= B <= 10^9)。
输出:对每组数据,输出(A/B)%9973。
题解:
设答案
。
做以下变换:
;
把
代入得
,即
;
两边除
得
,这是形如
的丢番图方程,即
,其中
。求解逆
,得到
,再乘以
,就是
。
3.3. 用逆求解同余方程
逆有什么用?如果有
模
的一个逆,可以用来解如
的任何同余方程。
记
是
的一个逆,有
。在
的两边同时乘以
,得到
,即
。
例如:为了求出
的解,可以两边乘以4,4是8模31的一个逆,得
,因此
。
定理:设
是素数,正整数
是其自身模
的逆,当且仅当
或
。
证明:若
或
,有
,所以
是其自身模
的逆。反过来也成立。
3.4. 逆与除法取模
逆的一个重要应用是求除法的模。例如在catalan数中,有这样一个需求:求
,即
除以
,然后对
取模。由于这里
和
都是很大的数,做除法后再取模,会损失精度。用逆可以避免除法计算,设b的逆元是
,有:
经过上述推导,除法的模运算转换成了乘法模运算:
。
下面是一个除法取模的例题。
Detachment http://acm.hdu.edu.cn/showproblem.php?pid=5976
题目描述:把一个整数X分成多个整数的和:x = a1 + a2 + …,且ai ≠ aj,使得s = a1 ∗a2 * …最大。
输入:第一行是T,表示测试用例数量,后面有T行,每一行一个整数表示X。1 ≤ T ≤ 10^6, 1 ≤ X ≤ 10^9。
输出:对每个用例,首先计算最大的s,然后输出它对mod = 10^9+7的取模。
题解:如何分解X,才能使积s最大?这是小学奥数题,读者可以自己举例子推理。
首先,分解的数越多,积s越大。比如X分解为2个数,对比不分解的1个数X:(X - k)(X + k) > X。
其次,分解的数越接近,积越大。例如分成2个数,对比X/2 - 1、X/2 +1和X/2 - k、X/2 +k,有:(X/2 - 1)(X/2 +1) > (X/2 - k)(X/2 + k)。
结论是:把X尽量分为更多连续数的和,得到的积s最大。为了分解更多,就从2开始分解:X = 2 + 3 + 4 + 5 + …。从1开始分解不好,因为1对乘积没有贡献。如果还有余数,就把余数拆开,加到其他数上:从后往前加,每个数加上1,这样可以保证每个数都不同。例如17 = 2 + 3 + 4 + 5 + 3,最后有个余数3,把它拆成3个1,加到后面3个数上,得:17 = 2 + 4 + 5 + 6。再例如13 = 2 + 3 + 4 + 4,后面的余数4,拆成4个1,但是前面只有3个数,不够用,多的1再加在最后,得:13 = 3 + 4 + 6。
分解有两种情况:
(1)
,中间少个
;
(2)
,前面少个2,后面多个
;
求s的时候,先计算连续的乘积
,然后除以
,或者除以2乘以k+2。例如第(1)种情况,
,输出的结果是
。除法取模需要用到逆。本题的模10^9 + 7正好是个素数,所以求逆用扩展欧几里得或费马小定理都行。
下面给出编码,细节有:
(1)先预计算出从2开始的前缀和和连续积,用于判断分解到哪个数为止。并用upper_bound()查找x的位置。
(2)把余数加到后面的数上去,并查找缺少的
。
(3)计算结果。用逆计算除法取模。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxnum = 1e5; //分解的数不会超过50000个,请自己分析
const int mod = 1e9 + 7;
ll sum[maxnum], mul[maxnum]; //前缀和、连续积
ll fast_pow(ll x,ll y,int m){ //快速幂取模:x^y mod m
ll res = 1;
while(y) {
if(y&1) res*=x, res%=m;
x = (x*x) % m;
y>>=1;
}
return res;
}
long long mod_inverse(long long a,long long mod){ //费马小定理求逆,或者用扩展欧几里得求逆
return fast_pow(a,mod - 2,mod);
}
void init(){ //预计算前缀和、连续积
sum[1] = 0; mul[1] = 1;
for(int i=2; i<=maxnum; i++){
sum[i] = sum[i-1] + i; //计算前缀和
mul[i] = (i*mul[i-1]) % mod;//计算连续积
}
}
int main(){
init();
int T; scanf("%d",&T);
while(T--){
int x; scanf("%d",&x);
if( x == 1) {puts("1"); continue;} //特殊情况
int k = upper_bound(sum+1,sum+1+maxnum,x)-sum-1; //分解成k个数
int m = x - sum[k]; //余数
ll ans;
if(k==m)
ans = mul[k] * mod_inverse(2,mod) %mod * (k+2) % mod; //第2种情况
else
ans = mul[k+1] * mod_inverse(k-m+1,mod) % mod % mod; //第1种情况
printf("%lld\n",ans);
}
return 0;
}
4. 同余方程组
根据上一节的讨论,同余方程
有解时,即
能整除
时,可以解得
,所以这也是同余方程的一般形式。本节讨论同余方程组的求解:
…
例:有一个数
,被3除余2,被5除余3,被7除余2,列成同余方程就是:
求解结果是:
,
,或者写为
,
的最小正整数解是23。
本节介绍中国剩余定理和迭代法,前者是用于
两两互素情况下的优秀解法,后者是更一般条件下的通用解法。适用中国剩余定理的方程组肯定有解,而迭代法处理的更一般情况,可能是无解的。
4.1. 中国剩余定理
中国剩余定理1:设
是两两互素的正整数,则同余方程组
…
有整数解,并且模
唯一,解为:
其中
,
为
模
的逆元。
读者可以尝试自己证明。
例题:解同余方程组
。
解题步骤:
(1)
。
(2)求逆:
。
(3)最后计算
。
4.2. 迭代法
中国剩余定理的编码很容易,但是它的限制条件是方程组的
两两互素。如果不互素,该如何解题呢?这就是迭代法。
迭代法的思路很简单,就是每次合并两个同余式,逐步合并,直到合并完所有等式,只剩下一个,就得到了答案。
合并的时候,把同余方程转化为等式更容易操作。这是根据同余的一个性质:若
和
是整数,则
当且仅当存在整数,使得
。
1、 示例
以方程组
为例,说明合并过程。下面的计算步骤,前3步合并了第1和第2个同余式,后面3步继续合并第3个同余式。
1)把第1个同余式
转换为
,代入第2个同余式得
。
2)求解
。
首先变为
,即
,因为
能整除1,所以有解。
然后求解
:先求3模5的逆,是2,所以解得
,转换为等式
。
3)第1个和第2个同余式合并的结果。把
代入
得
,即
。
4)把
代入第3个同余式得:
。
5)求解
。
首先变为
,
能整除
,有解。
然后求
:先求15模7的逆,是1,解得
,转换为
。
6)得到合并结果。把
代入
,得
,即
。结束。
2、 编码步骤
下面改用丢番图方程的形式,总结合并两个同余式的编码方法,并以合并上面的前2个等式为例。
步骤 | 例子 |
---|---|
合并两个等式: |
|
两个等式相等:
移项得: |
|
这是形如
的丢番图方程。 下面求解它。先用扩展欧几里得求 |
得: |
的通解是
最小值是 |
|
把
代入
求得原等式的一个特解 |
得 |
合并后的新
: |
合并后的新方程是 即 |
3、 例程
下面用一个模板题给出线性同余方程组的代码。
扩展中国剩余定理 洛谷P4777
题目描述:给定n组非负整数
,求解关于
的方程组的最小非负整数解。1≤n≤10^5,1 ≤ ai ≤ 10^12,
,保证所有
的最小公倍数不超过 10^18。
…
输入:第一行是整数n,后面n行,每行两个非负整数
。
输出:输出一行,为满足条件的非负整数x。
下面给出的代码2,和前面“编码步骤”基本一致。
注意代码中的细节,例如求
的代码是
,目的是避免越界。其他细节见注释。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 100010;
int n;
ll ai[maxn], mi[maxn];
ll mul(ll a,ll b,ll mod){ //乘法取模:a*b % mod
ll res=0;
while(b>0){
if(b&1) res=(res+a)%mod;
a=(a+a)%mod;
b>>=1;
}
return res;
}
ll extend_gcd(ll a,ll b,ll &x,ll &y){ //扩展欧几里得
if(b == 0){ x=1; y=0; return a;}
ll d = extend_gcd(b,a%b,y,x);
y -= a/b * x;
return d;
}
ll excrt(){ //求解同余方程组,返回最小正整数解
ll x,y;
ll m1 = mi[1], a1 = ai[1]; //第1个等式
ll ans = 0;
for(int i=2;i<=n;i++){ //合并每2个等式
ll a2 = ai[i], m2 = mi[i]; // 第2个等式
//合并为:aX + bY = c
ll a = m1, b = m2, c = (a2 - a1%m2 + m2) % m2;
//下面求解 aX + bY = c
ll d = extend_gcd(a,b,x,y); //用扩展欧几里得求x0
if(c%d != 0) return -1; //无解
x = mul(x,c/d,b/d); //aX + bY = c 的特解t,最小值
ans = a1 + x* m1; //代回原第1个等式,求得特解x'
m1 = m2/d*m1; //先除再乘,避免越界。合并后的新m1
ans = (ans%m1 + m1) % m1; //最小正整数解
a1 = ans; //合并后的新a1
}
return ans;
}
int main(){
scanf("%d", &n);
for(int i=1;i<=n;++i)
scanf("%lld%lld",&mi[i],&ai[i]);
printf("%lld",excrt());
return 0;
}
公元3世纪《孙子算经》中有一个问题:“今有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二,问物几何?答曰:二十三。”1247年,秦九韶在《数学九章》中给出了求解的一般方法“大衍求一术”,被称为“中国剩余定理(Chinese Remainder Theorem)”。秦九韶是全能型的天才,在多个领域有建树。 ↩︎