《夜深人静写算法》数论篇 - (20) 中国剩余定理

本文介绍了中国剩余定理,源于《孙子算经》的数学问题,探讨了同余方程组的解的情况,包括不存在解和存在无穷多解的情况。通过朴素算法解释了模数互素和不互素时的解法,并详细阐述了算法实现过程,包括如何通过扩展欧几里得定理求解同余方程组。

前言

    当初学这个算法,纯粹是因为她叫 —— 中国剩余定理它的另一个名字 —— 孙子定理。有兴趣的朋友可以自行百度它原本的名字。

一、 物不知数

1、引例

【例题1】有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二。问物几何?

    这个问题是中国南北朝时期的数学著作《孙子算经》卷下第二十六题,叫做 “物不知数” 问题,这个问题翻译成现代文的意思是:一个整数除以三余二,除以五余三,除以七余二,求这个整数。表示如下:
( S ) : { x ≡ 2 ( m o d   3 ) x ≡ 3 ( m o d   5 ) x ≡ 2 ( m o d   7 ) (S): \begin{cases}x \equiv 2(mod \ 3)\\ x \equiv 3(mod \ 5)\\ x \equiv 2(mod \ 7) \end{cases} (S):x2(mod 3)x3(mod 5)x2(mod 7)

    由于每一行都是一个同余方程,其中 x x x 是公共的未知数。
    用这样的形式表示的我们称它为同余方程组, S S S 就是这个同余方程组。

二、同余方程组

    我们将同余方程表示成更加一般的形式,如下:
( S ) : { x ≡ a 1 ( m o d   m 1 ) x ≡ a 2 ( m o d   m 2 ) ⋮ x ≡ a n ( m o d   m n ) (S): \begin{cases}x &\equiv a_1(mod \ m_1)\\ x &\equiv a_2(mod \ m_2)\\ &\vdots \\ x &\equiv a_n(mod \ m_n) \end{cases} (S):xxxa1(mod m1)a2(mod m2)an(mod mn)

    我们知道同余方程是可以表示成等式的形式的,那么同余方程组自然也可以,我们可以这么来表示:
( S ) : { x = k 1 m 1 + a 1 x = k 2 m 2 + a 2 ⋮ x = k n m n + a n ( k i ∈ Z ) (S): \begin{cases}x &= k_1m_1 + a_1\\ x &= k_2m_2 + a_2\\ &\vdots \\ x &= k_nm_n + a_n\end{cases} \\ \\ (k_i \in Z) (S):xxx=k1m1+a1=k2m2+a2=knmn+an(kiZ)

     n n n 个方程, n + 1 n+1 n+1 个未知数的情况下,解的情况可能存在,也可能不存在,存在的情只可能是无穷多解。

1、不存在解

    这种情况比较好理解,当存在 i i i j j j,同时满足 m i = m j m_i=m_j mi=mj a i ≠ a j a_i \neq a_j ai=aj 时,解必然是不存在的。
    因为任何一个数模另外一个数,得到的结果一定是确定的,不可能有两种结果。

2、存在无穷多解

    一旦存在解,那么对于任意两个 i i i j j j,只要 m i = m j m_i=m_j mi=mj ,必然满足 a i = a j a_i = a_j ai=aj,出现这种情况时,变量数和方程数同时减少。
    换言之,未知数永远比方程数多 1,这样就一定有无穷多解了。

三、朴素算法

1、模数互素

    朴素算法就是穷举,以 “物不知数” 问题为例,我们可以从零开始枚举 x x x 的值。然后不断试除,看什么时候能够满足三个同余方程同时成立。
( S ) : { x ≡ 2 ( m o d   3 ) x ≡ 3 ( m o d   5 ) x ≡ 2 ( m o d   7 ) (S): \begin{cases}x \equiv 2(mod \ 3)\\ x \equiv 3(mod \ 5)\\ x \equiv 2(mod \ 7) \end{cases} (S):x2(mod 3)x3(mod 5)x2(mod 7)

    代码实现如下:

#include <iostream>
using namespace std;

const int MAXN = 3;
int m[MAXN] = {3, 5, 7};
int a[MAXN] = {2, 3, 2};

int main() {
    for(int x = 0; x < 400; ++x) {      // (1)
        bool bfind = true;
        for(int i = 0; i < MAXN; ++i) { 
            if( x % m[i] != a[i] ) {    // (2)
                bfind = false;
                break;
            }
        }
        if(bfind) {
            printf("%d\n", x);          // (3)
        }
    }	
    return 0;
} 
  • ( 1 ) (1) (1) 由于解有无数个,所以为了输出的时候能够观测到一些规律,我们只输出小于 400 的解。
  • ( 2 ) (2) (2) 对于每个 ( m i , a i ) (m_i, a_i) (mi,ai) 的整数对,用枚举得到的 x x x 进行试除,看能否整除,一旦不满足就跳出循环;
  • ( 3 ) (3) (3) 输出满足所有等式情况的解。


    输出结果为:

23
128
233
338

    这时我们发现,它其实是一个等差数列,并且公差是105,正好是3 * 5 * 7,也就是所有模数的乘积。那么,如果令最小的非负整数解为 x ′ x' x,我们猜测这个同余方程组的通解为:
x = x ′ + k ∏ i = 1 n m i ( k = 0 , 1 , 2 , . . . ) x = x' + k \prod_{i=1}^n m_i \\ (k = 0, 1, 2, ...) x=x+ki=1nmi(k=0,1,2,...)

2、模数不互素

    事实上,上面这个公式只适合 模数互素 的情况,当模数不互素时,我们来看一个例子,如下:
( S ) : { x ≡ 2 ( m o d   4 ) x ≡ 4 ( m o d   6 ) x ≡ 7 ( m o d   15 ) (S): \begin{cases}x \equiv 2(mod \ 4)\\ x \equiv 4(mod \ 6)\\ x \equiv 7(mod \ 15) \end{cases} (S):x2(mod 4)x4(mod 6)x7(mod 15)

    只需要将上面的代码稍微改一下,数据部分改成:

int m[MAXN] = {4, 6, 15};
int a[MAXN] = {2, 4, 7};
  • 运行后得到输出结果如下:
22
82
142
202
262
322
382


    我们通过观察发现,它仍然是一个等差数列,并且公差是60,正好是 l c m ( 4 , 6 , 15 ) lcm(4,6,15) lcm(4,6,15),也就是所有模数的最小公倍数。我们再联想刚才所有数互素的情况,它正好是所有数的乘积。
    那么,如果令最小的非负整数解为 x ′ x' x,我们猜测这个同余方程组的通解为:
x = x ′ + k × l c m ( m 1 , m 2 , . . . , m n ) ( k = 0 , 1 , 2 , . . . ) x = x' + k \times lcm(m_1,m_2,...,m_n) \\ (k = 0, 1, 2, ...) x=x+k×lcm(m1,m2,...,mn)(k=0,1,2,...)

四、中国剩余定理

1、算法详解

    根据上述的猜测,我们能够发现 模数不互素 的情况,是完全包含 模数互素 的情况的。
    所以这里我直接介绍 模数不互素 的情况下,同余方程组通解的求解方式,网上有人叫它扩展中国剩余定理,我觉得没有 中国剩余定理 霸气,但是这不重要,知道算法原理就行啦!
    我们给同余方程组进行编号,如下:
( S ) : { x = k 1 m 1 + a 1 ( 1 ) x = k 2 m 2 + a 2 ( 2 ) ⋮ x = k n m n + a n ( n ) (S): \begin{cases}x &= k_1m_1 + a_1 & (1)\\ x &= k_2m_2 + a_2 & (2)\\ &\vdots \\ x &= k_nm_n + a_n & (n) \end{cases} \\ (S):xxx=k1m1+a1=k2m2+a2=knmn+an(1)(2)(n)

    然后尝试把第 ( 1 ) (1) (1) 个方程和第 ( 2 ) (2) (2) 个方程进行合并,得到:
k 1 m 1 + a 1 = k 2 m 2 + a 2 k_1m_1 + a_1 = k_2m_2 + a_2 k1m1+a1=k2m2+a2

    移项后得到:
k 1 m 1 − k 2 m 2 = a 2 − a 1 k_1m_1 - k_2m_2 = a_2 - a_1 k1m1k2m2=a2a1

    这是一个类似 A x + B y = C Ax + By = C Ax+By=C 的同余方程。
    其中 A A A B B B C C C 都是常数, X X X Y Y Y 为未知数,所以我们可以得到如下的变量转换关系:
{ A = m 1 B = − m 2 C = a 2 − a 1 X = k 1 Y = k 2 \begin{cases} A = m_1 \\ B = -m_2 \\ C = a_2 - a_1 \\ X = k_1 \\ Y = k_2\end{cases} A=m1B=m2C=a2a1X=k1Y=k2

    如果 C   m o d   g c d ( A , B ) ≠ 0 C \ mod \ gcd(A,B) \neq 0 C mod gcd(A,B)=0 ,则方程无解,即整个方程组无解。
    否则,令 A ′ = A g c d ( A , B ) A' = \frac A {gcd(A, B)} A=gcd(A,B)A B ′ = B g c d ( A , B ) B' = \frac B {gcd(A, B)} B=gcd(A,B)B C ′ = C g c d ( A , B ) C' = \frac C {gcd(A, B)} C=gcd(A,B)C,得到新的等式:
A ′ x + B ′ y = C ′ A'x + B'y = C' Ax+By=C

    通过扩展欧几里德定理,必然能够求出一个最小的正整数解 Y 0 Y_0 Y0,于是有 Y Y Y 也就是 k 2 k_2 k2 的通解如下:
Y = Y 0 + A ′ t Y = Y_0 + A't Y=Y0+At

    将 Y Y Y 代入等式 ( 2 ) (2) (2),得到:
x = k 2 m 2 + a 2 = Y m 2 + a 2 = ( Y 0 + A ′ t ) m 2 + a 2 = Y 0 m 2 + A ′ m 2 t + a 2 = A ′ m 2 t + ( Y 0 m 2 + a 2 ) = m 1 m 2 g c d ( m 1 , m 2 ) t + ( Y 0 m 2 + a 2 ) = l c m ( m 1 , m 2 ) t + ( Y 0 m 2 + a 2 ) \begin{aligned} x &= k_2m_2 + a_2 \\ &= Ym_2 + a_2 \\ &= (Y_0 + A't)m_2 + a_2 \\ &= Y_0m_2 + A'm_2t + a_2 \\ &= A'm_2t + (Y_0m_2 + a_2) \\ &= \frac {m_1m_2} {gcd(m_1,m_2)}t + (Y_0m_2 + a_2) \\ &= lcm(m_1,m_2)t + (Y_0m_2 + a_2)\end{aligned} x=k2m2+a2=Ym2+a2=(Y0+At)m2+a2=Y0m2+Am2t+a2=Am2t+(Y0m2+a2)=gcd(m1,m2)m1m2t+(Y0m2+a2)=lcm(m1,m2)t+(Y0m2+a2)

    其中 l c m ( m 1 m 2 ) lcm(m_1m_2) lcm(m1m2) ( Y 0 m 2 + a 2 ) (Y_0m_2 + a_2) (Y0m2+a2) 均为常数, t t t 为未知数,于是我们得到了一个新的关于 x x x 的方程,总的方程组数量减少了 1,用同样的方法,经过 n − 1 n-1 n1 次迭代,必然可以将 n n n 个方程转化成一个方程, 如下:
x = x ′ + k × l c m ( m 1 , m 2 , . . . , m n ) x = x' + k\times lcm(m_1,m_2,...,m_n) x=x+k×lcm(m1,m2,...,mn)

    而这个方程,正式我们上一章节猜测的那个方程,当 k = 0 k=0 k=0 时, x ′ x' x 就是最小非负整数解了。

2、算法综述

算法流程如下:
    1)将所有方程组标准化,即模数和余数都变成正数;
    2)利用【1、算法详解】中的方法合并两个方程,使得方程数减一;合并过程中可能发生两种情况:
    2.a)无解的情况,直接返回;
    2.b)存在解,进入 3);
    3)利用扩展欧几里得定理求出两个方程的通解,从而变成一个新的同余方程,如果只剩下最后一个同余方程了,那么它的余数就是同余方程组的一个最小非负整数解,否则回到 2);


    通解如下:
x = x m i n + k × l c m ( m 1 , m 2 , . . . , m n ) x = x_{min} + k\times lcm(m_1,m_2,...,m_n) x=xmin+k×lcm(m1,m2,...,mn)

     x m i n x_{min} xmin 即同余方程组的最小非负整数解, k = 0 , 1 , 2 , 3 , . . . k = 0,1,2,3,... k=0,1,2,3,...

3、算法实现

1)类设计

    首先,设计一个类,这个类只有两个成员m_(代表模数)和a_(代表余数),用来代表上文提到的 方程: x = k m + a x=km + a x=km+a,然后就可以用数组来代表方程组了。

#define ll long long

class ModPair {
private:
    ll m_;   // modulus
    ll a_;   // remainder
public:
    ModPair() {}
    ModPair(ll m, ll a) : m_(m), a_(a) {}
    ll getModulus() const { return m_;}
    void setModulus(ll m) {m_ = m;}
    ll getRemainder() const { return a_;}
    void setRemainder(ll a) {a_ = a;} 
    
    void standardization();               // (1)
    ll lcm (ll om) const;                 // (2)
};


    除了一些基本的setget接口,额外提供两个接口:

  • ( 1 ) (1) (1) standardization()用来做标准化,即把m_a_都标准化为正数,如下:
void ModPair::standardization() {
    if(m_ < 0) {
        m_ = -m_;
    }
    a_ = (a_ % m_ + m_) % m_;
}


     ( 2 ) (2) (2) lcm (ll om)返回模数和传参的最小公倍数,实现如下:

ll ModPair::lcm (ll om) const {
    ll g = GCD(om, m_);
    return om / g * m_;
}
2)求解过程

    实际求解过程,先给出代码,通过需要来逐一解释:

ModPair chineseRemain(int n, ModPair* mp) {
    /********************(1)********************/
    for(int i = 0; i < n; ++i) {
        mp[i].standardization();
    } 
    ModPair ans = mp[0];
    ll A, B, C, X, Y;
    for(int i = 1; i < n; i++) {
        /********************(2)********************/
        A = ans.getModulus();         
        B = -mp[i].getModulus();
        C = mp[i].getRemainder() - ans.getRemainder();
        ll g = GCD(A, B);
        if( C % g ) {     
            ans.setRemainder(-1);             
            return ans;
        }
        /********************(3)********************/
        A /= g, B /= g, C /= g;
        /********************(4)********************/
        if(A < 0) {
            A = -A, B = -B, C = -C;
            B = ((B % A) + A) % A;
        }
        /********************(5)********************/
        ExpGcd(A, B, X, Y);
        Y = Product_Mod(Y, C, A);
        /********************(6)********************/
        ll tmpm = A * mp[i].getModulus();
        ll tmpr = (mp[i].getRemainder() + Product_Mod(mp[i].getModulus(), Y, tmpm)) % tmpm;
        
        ans.setModulus(tmpm);
        ans.setRemainder(tmpr);
    }
    return ans;
}
  • ( 1 ) (1) (1) 可以这么认为,任何一个方程 x = k m + a x = km + a x=km+a k k k 可以是任意整数,所以 m m m 为正为负是无关紧要的,为了标准,将 m m m 转成整数;同样, a = x   m o d   m a = x \ mod \ m a=x mod m,自然取值要落在 [ 0 , m ) [0, m) [0,m) 才合理。于是,这一步就是对所有的方程都做了一次标准化,方便后续计算。
  • 并且将第一个方程的参数的两个参数赋值给 ans
  • ( 2 ) (2) (2) 这一步做的事情,就是抽象出一些变量 A A A B B B C C C,方便计算。并且判断 g c d ( A , B ) gcd(A, B) gcd(A,B) 是否能整除 C C C,如果不能够整除,则方程组无解,直接将 ans的模数参数定为-1返回,调用方根据这个标记为去判断接下来要做的事情;
  • ( 3 ) (3) (3) 让等式两边同时除以 g c d ( A , B ) gcd(A, B) gcd(A,B),等式是不变的,即 A ′ = A g c d ( A , B ) A' = \frac A {gcd(A, B)} A=gcd(A,B)A B ′ = B g c d ( A , B ) B' = \frac B {gcd(A, B)} B=gcd(A,B)B C ′ = C g c d ( A , B ) C' = \frac C {gcd(A, B)} C=gcd(A,B)C
  • ( 4 ) (4) (4) A A A B B B 进行标准化,转化成正数;
  • ( 5 ) (5) (5) 利用扩展欧几里得,求出方程的最小正整数解,这里的Product_Mod是一个乘法求幂的接口,能够确保 64位 整数相乘但不溢出。
  • ( 6 ) (6) (6) 最后一步,就是得到的通解回代回ans的过程。
  • 通过这样的方法,最后得到的ans的两个参数分别为所有模数的最小公倍数,以及方程组的最小非负整数解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

英雄哪里出来

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值