中国剩余定理,求的是模线性方程组的通解,如图1所示,其中(mi, ai)都是已知量,x是未知量。需要求的就是x,让它满足以下n个等式:
举个最简单的例子,如图2所示,x满足三个同余方程,我们可以将三个方程进行一个转化。
很明显,图2和图3的方程组等价,图3表示的是4个未知数,3个方程。由于未知数个数大于方程数,所以这个方程组要么无解,要么有无穷多解。
朴素的做法就是枚举x,然后对每个方程进行试除,令x=(0,1,2,3,...),那么我们发现当x=26时可以满足所有三个方程。然而事情并没有那么简单,除数的不同,或者方程数的增多,都可能导致x的值变得很大。
如图4所示的这种情况,仅仅三个方程,x就已经是10^8的数量级了,所以枚举这种做法显然是行不通的。
现在介绍一种O(N)的算法,其中N为方程组的个数。首先,我们需要定义一些向量,向量a[]代表除数集合,向量m[]代表余数集合,向量x[]则表示未知数集合。如图5所示,其中K为需要求的数,我们将n个方程从0开始按顺序编号。
然后,我们合并(0)和(1)两个等式,得到:
代换后A,B,C均为常数,X,Y为未知数,然后求A和B的最大公约数G,如果G不能整除C,则方程组无解;否则方程两边A,B,C同时除以G,等式不变。接着,就可以通过扩展欧几里得算法求解Y了,需要注意的是,扩展欧几里得求解时需要保证A和B都大于等于零,所以我们可以做如下处理:
if(A < 0) {
// 将A转化成正数,但还需保证等式性质,所以A,B,C同时变号
A = -A, B = -B, C = -C;
// 将B转化成正数,C++的取模%可能出现负数,所以需要模完后加上模数再取模
B = (B % A + A) % A;
}
扩展欧几里得算法求解二元一次方程的解法可以参考这篇文章:初等数论基础算法
Y的通解表示如下(其中Y0已经求出为常数):
Y就是(1)方程中的x[1],我们将Y代入方程(1),得到等式如下:
这时候,我们发现两个等式合并以后的等式,仍然可以表示成同余方程的形式。这说明可以将这个等式和(2)继续合并,合并完的等式,继续和(3)合并,直到最后只剩一个等式,令最后这个等式为:K=ax+b,由于x可以取任意整数,所以我们可以保证a和b都大于等于0,并且a >= b,那么这时候K=b就是方程组的最小非负整数解,K=ax+b就是方程组的通解。
这就是中国剩余定理的O(N)解法。
最后来看实际算法流程:
0)枚举 i = 1 to n-1
1) 合并等式(0)和(i), a[0]*x[0] + m[0]= a[i]*x[i] + m[i];
a[0]*x[0] - a[i]*x[i] = m[i] - m[0];
A * X + B * Y = C;
2) 根据扩展欧几里得,求得x[i] = Y = Y0 + A*t; (t为任意整数)
3) 将x[i]代入等式(i),得到: K = a[0]*a[i]*t + (m[i]+a[i]*Y0);
4) 更新等式(0), a[0] = a[0]*a[i]
m[0] = (m[i]+a[i]*Y0) % a[0];
5) n-1次迭代完毕,m[0]就是最小非负整数解。
C++代码实现地址:中国剩余定理O(N)算法
1)Strange Way to Express Integers
题意:给定N组数(ai, ri)。求一个最小的非负整数K,满足所有的K mod ai = ri。
题解:中国剩余定理的模板题。
2)Biorhythms
题意:慢慢读题吧,读到最后就是N=3的中国剩余定理。
题解:直接上模板。
3)X问题
题意:给定N和n组数(ai, ri),求满足所有的X mod ai = ri,且X<=N的正整数X的个数。
题解:首先套模板求出最小解x,并且求LCM = ai的最小公倍数;然后分情况:
1) x>N 或 无解,输出0;(我的模板无解返回的是-1,结果因为这个WA了很多次,无语)
2)x=0,输出N/LCM
3)x>0, 输出(N-x)/LCM+1
4)Circle
题意:给定长度为n(n <= 20)的约瑟夫环,按顺序报数,每次报到k的那个人出列;然后继续从下一个人报数,每次报到k的那个人出列。现在给定出队序列,求最小的满足条件的k。
题解:hash+中国剩余定理。出队序列已经给出,那么可以按顺序模拟,每次往环上走一步,如果遇到本次出队的人,将出队的人hash掉,记录上一次到本次的步数。以剩余的人个数为模数,步数为余数建立一个方程。N次建立N个方程然后求一次中国剩余即可。
5)Chinese Remainder Theorem Again
题意:给定数组Mi和数字a,求一个最小的数K,满足K mod Mi = (Mi - a)。
题解:看起来赤裸裸的中国剩余定理,如果直接套模板会WA,因为数据范围的问题,可能会超int64,所以由于这题的特殊性,有更简单的解法。还是按照中国剩余定理的思路,合并等式发现,最后的解其实是所有Mi的最小公倍数再减去a。
6)AndNow,a Remainder from Our Sponsor
题意:按照同余来加解密的题,有点意思。
题解:模拟 + 中国剩余定理。
7)HelloKiki
题意:给定N组数(ai, ri)。求一个最小的正整数K,满足所有的K mod ai = ri。
题解:中国剩余定理的模板题。注意最小正整数,如果结果为0,需要输出ai的最小公倍数。
8)A Poor Officer
题意:N(N <= 10000)个伞兵编号递增排列(即12345...),给定一种重排规则A[N],A[i]表示将第A[i]个伞兵放到第i个位置(1<=i<=N)。然后给定一个目标状态,问从起始状态到达目标状态,最少需要经过多少步。
题解:置换群 + 中国剩余定理。首先,对于任意的重排序列,经过有限次重排后必然回到初始状态,如图10所示,重排数组A=[2,1,4,5,7,6,3];目标数组T=[2,1,7,3,4,6,5]。
然后对起始状态和目标状态分别进行一次重排,就能算出各自的置换群了,如果两者的置换群不一致,显然状态不可达,直接输出-1即可;如图11所示,两者的置换群是一致的,因为(3457)可以通过循环左移经过(4573)、(5734)变成(7345),同理,(12)可以经过一步变成(21),所以两者置换群一致。
然后,计算起始状态和目标状态的置换群中每个循环可达需要的最少步数,例如(12)到(21)需要1步,(3457)到(7345)需要3步,(6)到(6)需要0步;然后就可以列出三个方程,即总步数K满足:
成功转换成中国剩余定理求解。
9)Shuffling
解法同A Poor Officer 。
10)Mysterious For
题意:给定n(n <= 10^6),表示循环最多执行n次,再给定两种类型的循环,定义如下:
现在总共有m(m <= 10^5)个嵌套,【类型1】的循环不大于16个。问所有嵌套的循环一共执行多少次。由于次数很大, 需要将答案模上364875103。
题解:Lucas定理 + 中国剩余定理。
首先,以【类型1】为断点,如果a[i]是【类型1】的循环,那么它和a[i-1]必然没关系。图中XX()的执行次数代表一个【类型1】循环嵌套了两个【类型2】的循环。
我们用S(n,t)表示1个【类型1】循环和t个【类型2】循环的总执行次数。那么S(n,0)=n, S(n,1)=1+2+...+n。利用数学归纳法,我们总结出S(n,t)的递推公式:S(n,t)=S(n-1,t)+S(n,t-1)。
然后将n和t记录到表格中,每个格子的值 = 它上面格子的值 + 它左边格子的值,如图所示:
观察可得,它是一个斜着的杨辉三角,并且可以转化成组合数求解:S(n,t)=C(n+t,t+1)。于是转变成了求组合数C(a,b) mod 364875103。
C(a,b) mod 364875103的求法采用Lucas定理 + 中国剩余定理。由于364875103 = 97 * 3761599,为两个素数的乘积,我们可以先求出x = C(a,b) mod 97,和y = C(a, b) mod 3761599,那么令K = C(a,b) mod 364875103,K必然满足:
如果x和y都是已知的,那么就可以通过中国剩余定理来求K了。
组合数C(a,b)%p(其中p为素数)可以采用Lucas定理求解。Lucas定理的递归形式如下:
Lucas(n, m, p) {
if(m == 0) return 1;
return Lucas(n/p, m/p, p) * Comb(n%p, m%p, p) % p;
}
其中Comb(n,m,p)表示求C(n,m)%p (n
那么现在就是要求n! mod p的值,其中n的范围<=1100000,直接预处理存到数组中,O(1)获取即可。n!^(p-2)mod p可以采用二分快速幂取模。
11)The Lastest Math Theory Problem
最后推荐一道我出的题^_^
题意:给定n(n <= 6)和数组a[n](a[i] < 10000),要求求一个最小的N,满足n个等式(其中x[i]为未知量):
由于N可能很大,所以只需要输出N mod 19880502即可;如果不存在这样的N,则输出-1。
题解:中国剩余定理 + 二分快速幂。任何数都可以表示成素数的乘积,又由于N满足以上六个等式,所以N的素因子一定是a[i]的素因子,假设N的表示如下:
任何一个p[i]必定小于10000,那么我们只需要枚举所有小于10000的素因子,然后想办法求出指数e[i]即可。对于每个小于10000的素数p[i],枚举所有a[j], 检查a[j]这个数中有多少p的素因子,记为cnt[j],则e[i] = k*a[j] + cnt[j](k>=0);那么对于每个e[i]利用中国剩余定理求n个同余方程,得到的e[i]利用二分快速幂求p[i]^e[i] mod 19880502即可。最后所有素数分量连乘再取模就是最后的答案了。
这题需要注意一点,求中国剩余定理的时候,由于值的范围在int64,所以乘法可能溢出。所以如果两个数相乘需要采用二进制法。这里提供一种非递归版本的a*b mod c,全程只有加法和取模。
LL Product_Mod(LL a, LL b, LL c) {
LL sum = 0;
while(b) {
if(b & 1) sum = (sum + a) % c;
a = (a + a) % c;
b >>= 1;
}
return (sum + c) % c;
}