1. 程式人生 > 其它 >中國剩餘定理(CRT)

中國剩餘定理(CRT)

中國剩餘定理

在《孫子算經》中有這樣一個問題:“今有物不知其數,三三數之剩二(除以3餘2),五五數之剩三(除以5餘3),七七數之剩二(除以7餘2),問物幾何?”

這個問題稱為“孫子問題”,該問題的一般解法國際上稱為“中國剩餘定理”。具體解法分三步:

  1.找出三個數:從3和5的公倍數中找出被7除餘1的最小數15,從3和7的公倍數中找出被5除餘1 的最小數21,最後從5和7的公倍數中找出除3餘1的最小數70。

  2.用15乘以2(2為最終結果除以7的餘數),用21乘以3(3為最終結果除以5的餘數),同理,用70乘以2(2為最終結果除以3的餘數),然後把三個乘積相加152+213+702得到和233。

  3.用233除以3,5,7三個數的最小公倍數105,得到餘數23,即233%105=23。這個餘數23就是符合條件的最小數。

就這麼簡單。我們在感嘆神奇的同時不禁想知道古人是如何想到這個方法的,有什麼基本的數學依據嗎?

我們將“孫子問題”拆分成幾個簡單的小問題,從零開始,試圖揣測古人是如何推匯出這個解法的。

首先,我們假設n1是滿足除以3餘2的一個數,比如2,5,8等等,也就是滿足3k+2k>=0的一個任意數。同樣,我們假設n2是滿足除以5餘3的一個數,n3是滿足除以7餘2的一個數。

有了前面的假設,我們先從n1這個角度出發,已知n1滿足除以3餘2,能不能使得n1+n2的和仍然滿足除以3餘2?進而使得

n1+n2+n3的和仍然滿足除以3餘2?

這就牽涉到一個最基本數學定理,如果有a%b=c,則有(a+kb)%b=c(k),換句話說,如果一個除法運算的餘數為c,那麼被除數與k倍的除數相加(或相減)的和(差)再與除數相除,餘數不變。這個是很好證明的。

以此定理為依據,如果n2是3的倍數,n1+n2就依然滿足除以3餘2。同理,如果n3也是3的倍數,那麼n1+n2+n3的和就滿足除以3餘2。這是從n1的角度考慮的,再從n2,n3的角度出發,我們可推匯出以下三點:

  1.為使n1+n2+n3的和滿足除以3餘2,n2和n3必須是3的倍數。

  2.為使n1+n2+n3的和滿足除以5餘3,n1和

n3必須是5的倍數。

  3.為使n1+n2+n3的和滿足除以7餘2,n1和n2必須是7的倍數。

因此,為使n1+n2+n3的和作為“孫子問題”的一個最終解,需滿足:

  1.n1除以3餘2,且是5和7的公倍數。

  2.n2除以5餘3,且是3和7的公倍數。

  3.n3除以7餘2,且是3和5的公倍數。

所以,孫子問題解法的本質是從5和7的公倍數中找一個除以3餘2的數n1,從3和7的公倍數中找一個除以5餘3的數n2,從3和5的公倍數中找一個除以7餘2的數n3,再將三個數相加得到解。

在求n1,n2,n3時又用了一個小技巧,以n1為例,並非從5和7的公倍數中直接找一個除以3餘2的數,而是先找一個除以3餘1的數,再乘以2。

也就是先求出5和7的公倍數模3下的逆元,再用逆元去乘餘數

這裡又有一個數學公式,如果a%b=c,那麼(ak)%b = k*(a%b) = kc(k>0),也就是說,如果一個除法的餘數為c,那麼被除數的k倍與除數相除的餘數為k∗c。

最後,我們還要清楚一點,n1+n2+n3只是問題的一個解,並不是最小的解。如何得到最小解?我們只需要從中最大限度的減掉掉3,5,7的公倍數105即可。

道理就是前面講過的定理“如果a%b=c,則有(akb)%b=c”。所以n1+n2+n3%105就是最終的最小解。

這樣一來就得到了中國剩餘定理的公式:

設正整數兩兩互素,則同餘方程組

                             

有整數解。並且在模下的解是唯一的,解為

                               

其中,而為模的逆元。

演算法流程

  1.計算所有模數的乘積M;

  2.對於第i個方程:
    a. 計算Mi=M/mi
    b. 計算Mi在模mi意義下的逆元 Mi^(-1);
    c. 計算 ci=Mi×Mi^(1)(不要對mi取模)。

  3.方程組的唯一解為:x=∑(k,i=1) ai*ci mod mi

參考程式碼

ll exgcd(ll a,ll b,ll &x,ll &y){
    if(!b){
        x = 1;
        y = 0;
        return a;
    }
    ll res = exgcd(b,a%b,x,y);
    ll t = x;
    x = y;
    y = t - a/b*y;
    return res;
}
ll CRT(int n,ll a[],ll m[]){
    ll M = 1,ans = 0;
    for(int i = 1;i <= n;++i) M = M * m[i];
    for(int i = 1;i <= n;++i){
        ll Mi = M/m[i],x,y;
        exgcd(Mi,m[i],x,y);// x * Mi mod m[i] = 1 即 Mi在mod m[i]下的逆元為x
        ans = (ans + a[i] * Mi * x % M) % M;
    }
    return (ans % M + M) % M;
}