模算術(modular arithmetic)又可稱為同餘算術,是一種特別的整數算術系統。
這個系統要先指定一個整數
日常生活中,表示時刻周而復始的時候就很常應用模算術。
比如說 22 點過了
一天
如同上面一天
換句話說,我們只在乎一個數值除以模數之後的餘數。
所以在談論模算術之前,我們可以先認識同餘關係(congruence),顧名思義就是「餘數相同的關係」。
對於整數
上面的定義並沒有直接提及「餘數」,但意思其實是相同的。
根據除法原理,任何整數都可以寫成
如果兩個整數
按照同餘符號的定義,之前的例子就會表達成「
這裡表示同餘的符號「
不過,互相同餘的整數也是有無限多個的,比如在模
我們往往會需要一個代表元素來概括這些等價的分身,就像是我們講 6 點而不是 30 點一樣。
在模數
我們討論有理數的時候並不在乎分子分母的數值組合,而只在乎它們的比值,所以說
要說兩組數字
雖然平常我們都是以最簡分數當代表來去思考這些比值,但實際要寫數學定義或定理的時候,依賴最簡分數反而不太好用。
比如要說
由
並且由
因為
而「同餘關係」與「餘數」的關聯就類似於「比值相等」與「最簡分數」的關聯。
如果我們討論模算術的時候都依賴餘數,就必須先用除法原理講清楚何謂餘數才能進一步討論,並且最後也必須回到餘數才能做結論。
相比之下,雖然將同餘定義為「兩數的差是模數的倍數」可能不太直覺,但論述起來其實相對容易許多。
另一方面,同餘關係的觀點更注重在「視為相同」這件事上。
或許,刻意避免直觀卻過於細節的操作,能讓我們更容易理解它之後衍伸出的各種性質與定理。
程式語言通常也有提供取餘數的運算子或函式,相當於前面提到的二元運算
我們可以透過比較兩數的餘數是否相等來判斷兩數是否同餘,也能透過判斷餘數是否為
30 % 24 // 6
54 % 24 == 26670 % 24 // true
bool isEven(int n) {
return n % 2 == 0;
}
30 % 24 # 6
54 % 24 == 26670 % 24 # True
def is_even(n):
return n % 2 == 0
30 `mod` 24 -- 6
54 `mod` 24 == 26670 `mod` 24 -- True
isEven :: Int -> Bool
isEven n = n `mod` 2 == 0
模算術系統不單單只是「拿餘數表示數值」的系統,既然被稱作算術系統,就代表它在運算方面也是有規則的。
比如 6 點經過
又比如相對於 9 點,在
這告訴我們,當我們只在乎運算結果的餘數時,我們不必等到所有加法或減法運算結束之後才取餘數,而是能直接取那些加數或減數的餘數來做運算。
在實際應用時,雖然「先取餘數做運算,再取結果的餘數」可能比起「全部算完再取餘數」多了好幾次取餘數的步驟,但卻會因為減少運算中途的大數字而省下了整體運算所花費的時間與空間。
這樣的好事不僅僅發生在加減法上,乘法也能直接拿餘數來做運算。
而事實上這些性質並不侷限在餘數,只要是互相同餘的數都可以自由替換,這也進一步體現了「視為相同」的觀點。
於是我們有下面的性質,並且在之後附上乘法性質的證明。
如果
如果
並且因為
而兩個
我們也不難驗證說普通整數運算的某些常見性質在模算術系統中也是存在的,比如加法與乘法的交換律與結合律,或乘法對加減法的分配律。
普通整數運算時,我們會說
在模算術系統中也有一樣的規則,比如說 9 點同時是 17 點之前
雖然模算術系統所謂的相反數不像是普通整數加法那樣,互相位在數線上的正負兩邊,但它們的性質是一樣的:兩者相加之後會互相消滅而變成加法單位元素。
這裡的「加法單位元素」就是指跟任何其他數相加都不會讓那個其他數有所增減的數,在普通整數加法裡面是
如果
不同於加、減、乘,我們會發現在模算術系統裡面,除法並不能跟普通運算直接做對應。
比如說我們知道
另外,就算是能夠直接整除的情況,似乎也不能像其他運算那樣自由替換。
比如我們知道
會發生這些問題都是因為我們直接拿普通運算的方式來做除法,但像這樣直接對應是錯誤的。
為了要弄清楚在模算術的世界裡該怎麼做除法,我們應該從另一個角度看待除法:「除以某數就是乘以某數的倒數」。
普通有理數運算裡面,非零整數
這裡的「乘法單位元素」就是指跟任何其他數相乘都不會讓那個其他數有所改變的數,也就是
上面這段的關於倒數的敘述跟前一節談論相反數的敘述是差不多的,它們都是指運算上相對應的反元素(inverse),互為反元素的數在運算後能夠互相抵消變成不做事的單位元素(identity)。
所以,減法與除法並不是獨立的存在,它們分別是代表能夠抵消加法跟乘法的逆運算:
既然如此,要在模算術系統裡面做除法,我們也要用相同的運算性質去得到它。
也就是說,當我們要在模算術系統中除以某數,就是要乘以它所謂的倒數。
不難檢查,模算術世界裡的乘法單位元素也是
對於整數
我們習慣用
回到這節一開始
我們從
但我們會發現除以
在模
例如
而
但要注意,這並不代表在模
那麼,倒數存在的條件又是什麼呢?
從上面的例子看到,我們從
對於整數
假設
因為
那麼就有
但因為
所以說一開始的假設「
在數學上,如果兩個不同餘
而除以零因子與除以
上面的性質告訴我們如果一個數是零因子的倍數,那麼它就沒有倒數。
那麼反過來,如果一個數不是零因子的倍數,或換句話說它與模數互質,它就會有倒數嗎?
答案是肯定的,下面提供一個用到 Bézout’s 引理的證明,但我們不會去證明那個引理。
對於整數
對於整數
我們已經在前面的性質推導過,
現在假設
根據 Bézout’s 引理,我們知道二元一次方程式
那麼就有
也就是說,
前面我們知道了做除法的前提是除數必須要與模數互質。
但如果不互質又會怎麼樣?
我們回去看在模
之前說過透過乘法我們能知道
如果我們考慮
顯然,如果想要從
這個情況可以非常糟糕,比如考慮
代表如果想算
我們可以稍微深入研究一下。
假設兩個整數
接著就會發現,對於任意的整數
在模
所以說,如果除數不與模數互質,那麼除法就一定不會有唯一解。
除非,我們把這些
如果我們放寬條件,不再固守模數
下面的命題是我們勉強能做到的事情。
對於整數
前一節我們提到只要除數與模數互質我們就有倒數能做除法,但我們還沒提到如何找出對應的倒數。
我們當然可以窮舉並測試算術系統裡所有的數,但這種暴力做法在模數大的時候顯然會曠日廢時。
那我們有沒有更有效率的方法呢?
下面兩個小節我們分別提供兩種不同的算法,並且都附上對應的程式語言實作範例。
Python 3.8 之後可以直接使用
pow(a, -1, m)
算出。
擴充歐幾里得算法(extended Euclidean algorithm)是歐幾里得算法的擴充。
歐幾里得算法就是俗稱的輾轉相除法,可能可以稱為西方數學最早的演算法,它被使用來求得給定兩個整數
而當我們把演算法步驟倒推回來時,我們能進一步得到最大公因數的整係數組合
擴充算法就是建立在這件事上,在算出
有了擴充歐幾里得算法,那就如同之前的證明一樣,如果要求
下面提供程式語言的實作範例,其中
std::tuple<int, int, int> extEuclid(int a, int b) {
if (b == 0)
return {a, 1, 0};
auto [d, m, n] = extEuclid(b, a % b);
return {d, n, m - (a / b) * n};
}
int reciprocal(int a, int m) {
return std::get<1>(extEuclid(a, m));
}
def ext_euclid(a, b):
if b == 0:
return (a, 1, 0)
q, r = divmod(a, b)
d, m, n = ext_euclid(b, r)
return (d, n, m - q * n)
def reciprocal(a, m):
return ext_euclid(a, m)[1]
extEuclid :: Int -> Int -> (Int, Int, Int)
extEuclid a 0 = (a, 1, 0)
extEuclid a b = (d, n, m - n * q)
where (q, r) = a `divMod` b
(d, m, n) = extEuclid b r
reciprocal :: Int -> Int -> Int
reciprocal a m = r
where (_, r, _) = extEuclid a m
數論裡面還有費馬–歐拉定理,它告訴我們任意整數
而費馬小定理是當
下面的
比如小於
我們也不難看出對於任何質數
對於整數
對於整數
根據定理,我們能看到
實務上通常會利用像是反覆平方法(repeated squaring,或叫做快速冪)的求次方法,並配合前面提過的「先取餘數做運算,再取結果的餘數」來提高計算次方的效率。
雖然
例如
不過,若因此要去分析各個數值分別要取幾次方就不切實際了,所以實務上的算法都還是直接取
這也讓這種算法的運算次數能不依賴於
下面提供程式語言的實作範例,這裡我們只提供模數是質數的狀況,也就是利用到費馬小定理求倒數。
int modpow(int b, int e, int p) {
b %= p;
int result = 1;
while (e) {
if (e & 1)
result = result * b % p;
b = b * b % p;
e >>= 1;
}
return result;
}
int reciprocal(int a, int p) {
return modpow(a, p - 2, p);
}
def reciprocal(a, p):
return pow(a, p - 2, p)
modpow :: Int -> Int -> Int -> Int
modpow _ 0 _ = 1
modpow b e p | odd e = b * r `mod` p
| otherwise = r
where r = modpow (b * b `mod` p) (e `div` 2) p
reciprocal :: Int -> Int -> Int
reciprocal a p = modpow a (p - 2) p