这篇文章上次修改于 316 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

ACM程序课算法笔记18——中国剩余定理

问题引入

《孙子算经》中有问「物不知数」:有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?

即希望x同时满足多个模线性方程,然后求解。比如:

x≡2(mod 3)

x≡3(mod 5)

x≡2(mod 7)

同时明朝数学家程大位在《算法统宗》中给出:

三人同行七十希,五树梅花廿一支,七子团圆正半月,除百零五便得知。

2 x 70 + 3 x 21 + 2 x 15 = 233 ;233 % 105 = 23

中国剩余定理

中国剩余定理可求解如下形式的一元线性同余方程组

x≡a1(mod m1)

x≡a2(mod m2)

x≡an(mod mn)

前提

若m,m2,m3,是两两互质的正整数

结论

  1. 先得到N = ∏m = m1m2m3…mn
  2. 得到Mi = N / mi
  3. xi 就是线性同余方程 Mixi ≡ 1 (mod mn)的一个解,也就是Mi的逆元
  4. x = a1M1x1+a2M2x2+…+anMnxn

分析

限制条件的分析,为什么一定要互质?

在求xi的时候,我们得到的同余方程是Mixi ≡ 1 (mod mn),要使它有解,也就是mn和Mi需要互质,它俩的gcd = 1

答案的正确性分析

对于Mi是所有的m的乘积,除了mi,所以在计算除了第i条同余方程的时候,Mi都会被其他的m直接除掉,因为是它们的倍数。所以在考虑第i条式子的时候,实际上就是 ai Mi xi ≡ ai (mod mi)也就是

Mi xi ≡ 1(mod mi),又因为xi和Mi是逆元,所以成立,答案合理。

using i64 = long long;
i64 extendGcd(i64 a, i64 b, i64 &x, i64 &y){
    if (b == 0){
        x = 1;
        y = 0;
        return a;
    }
    i64 d = extendGcd(b, a % b, y, x);
    y -= a / b * x;
    return d;
}

long long CRT(int k, const vector<int>& arrA, const vector<int>& arrMod) {
  long long N = 1, ans = 0;
  for (int i = 1; i <= k; i++) N *= arrMod[i];
  for (int i = 1; i <= k; i++) {
    long long M = N / arrMod[i], x, y;
    extendGcd(M , arrMod[i], x, y);  // xi * M ≡ 1 (mod mi)
    ans = (ans + arrA[i] * M * x % N) % N;
  }
  return (ans % N + N) % N;
}

局限性

也就是算法成立的前提,所有m之间要互质。

另一个角度看算法

这是个人想法,算法本质上就是不断地让x适配某个同余方程,对x缝缝补补,比如我先让x适配第一个方程,再让x适配第二个方程,重点在于,适配方程的同时,我不能去导致其他同余方程的值变化。

同余方程的特点是,只要我在x上加了m的倍数,改成立条件就不变。所以我每次为了适配方程i成立,我不能去修改其他同余方程的余数。有点像拼魔方,我一点点往后拼,都不破坏之前已经拼好的面。

在适配的时候,我只要每次适配增加的值都是其他m的倍数,那么其他条件式的余数就都不会变了。然后根据这个增加值,直到这个式子成立。

如果能理解这个原理,那么求一般情况也会更加简单

中国剩余定理一般情况

一般情况就指的是模数可能不互质。

但是有我们的另一个角度去思考算法,实际上问题也可以从这个角度出发。

对于n条同余式子,如果不互质,就有可能出现无解的情况。

同样使用适配的思路,先满足第一个式子,此时再去满足第二个式子的时候,只要让答案加上第一个式子模数的倍数即可。

那么第三个式子也就是增加前面两个的模数的倍数。

以此类推每次的添加,实际上就是前面的模数的lcm。

result + k lcm ≡ ai (mod mi)

k lcm ≡ (ai - result) % mi (mod mi)

求得k后

result += k lcm

即可。