Chapter III. 漸入佳境
基礎數學
常用數學演算法
作者WiwiHo
先備知識學校教的數學

模運算


「取餘數」,也就是 C++ 中的 % 運算子,是一個在程式競賽中很常使用到的運算,除了餘數本身的性質有許多派上用場的時機之外,也有許多題目是因為答案非常大,而要求要先把答案取個餘數後再輸出。「取餘數」這個運算有個名字叫作模運算(modulo),也叫作模或模除,在寫數學式子的時候通常會寫成 $\bmod$,例如 $5 \bmod 2$ 就是取 $5$ 除以 $2$ 的餘數,也就是 $1$,讀作「五模除二」(更多時候會簡稱成「五模二」)或是「$5$ modulo $2$」。

C++ 中的除法與模運算

在這個小節中,寫成等寬字體的 a / b 是指 C++ 程式碼之中的整數除法,數學字體 $a / b$ 或 $\frac{a}{b}$ 是指數學上除法的結果,也就是沒有經過取整的有理數。

a % b 就是 a 除以 b 取餘數這件事情聽起來非常簡單,畢竟我們國小就學過怎麼取餘數了,不過這件事情其實沒有表面上這麼單純──如果哪天我們需要對負數進行模運算怎麼辦?$-5 \div 3$ 應該是 $-5 \div 3 = -1 \ldots -2$ 還是 $-5 \div 3 = -2 \ldots 1$?在 C++ 中,a % b 就等同於 a - a / b * b,而在 C++ 裡面的整數除法是「向零取整」,也就是除完的結果不是整數的時候,會取比較靠近 0 的那個整數,所以 -5 / 3 的結果會是 -1-5 % 3 的結果會是 -5 - (-5) / 3 * 3 == -25 / -3 的結果也是 -1,不過 5 % -3 的結果是 5 - 5 / (-3) * (-3) == 2

不過,在實際的使用上,大多時候我們的模數 $b$ 都是正整數,而且我們其實會比較希望 $a \bmod b$ 是一個非負整數,也就是 $a \bmod b$ 的定義是

Definition 1
模運算

$a \bmod b$ 是將 $a$ 表示成 $kb+r$、$k$ 是整數時,最小的可能的非負整數 $r$。在這個時候,$k$ 會是 $\lfloor \frac ab \rfloor$,而 $r$ 肯定是 $[0, \lvert b \rvert)$ 之間的整數。

如果在對一個負數取模的時候,希望獲得非負的餘數,也就是 $a - \lfloor \frac ab \rfloor$ 的話,可以寫成 (a % b + b) % b,這樣就一定會得到 $[0,b)$ 之內的結果了。這麼做的原因是因為 a % b 肯定是一個在 $(-b,b)$ 之內的整數,加上 $b$ 之後肯定會變成 $\geq 0$,最後需要再模 $b$ 一次是因為有可能本來 a % b 的結果就是 $\geq 0$ 了,這樣 $+b$ 之後會 $\geq b$,所以要再模一次。

注意 (a % b + b) % b 這個寫法需要 $b > 0$。

Tips 1
整數除法

剛剛提到 a / b 會「向零取整」,也就是在 $\frac ab$ 的結果是正時會向下取整($\lfloor \frac ab \rfloor$)、是負時會向上取整($\lceil \frac ab \rceil$),那麼要怎麼實作向下取整或向上取整的運算呢?不要使用 <cmath> 提供的 floorceil 函數,因為它們都是浮點數運算,會有浮點數誤差的問題,正確的作法是:

  • 如果 $a,b$ 同號,那 $\frac ab$ 本來就是正的,可以放心算 a / b
  • 如果 $a,b$ 異號,那 $\frac ab$ 就是負的,a / b 就是 $\lceil \frac ab \rceil$,如果 $\frac ab$ 不整除的話,$\lfloor \frac ab \rfloor=\lceil \frac ab \rceil - 1$,所以答案是 a / b - 1

同餘與模下的運算

剛剛我們其實做了一些不太容易理解的事情:把 $a$ 先模 $b$、再加上 $b$、再模 $b$,這樣答案真的跟 $a \bmod b$ 一樣嗎?要暸解為什麼可以這樣,我們先來暸解同餘的概念:

Definition 2
同餘

$a \equiv b \pmod m$ 的條件是 $m \mid (a - b)$,也就是 $\frac{a-b}{m}$ 整除。這個式子完整讀作「$a$ 跟 $b$ 在模 $m$ 下同餘」。

再寫一次模運算的定義:$k = \lfloor \frac ab \rfloor$、$r = a \bmod b$ 時,$a = kb+r$,移項一下就變成 $\frac{a - r}{b} = k$,而 $k$ 是一個整數,因此 $a \equiv r \pmod{b}$。很明顯地

\[ a \equiv b \pmod m \iff a \bmod m = b \bmod m \]

也就是模 $m$ 後一樣的數字就同餘。$\equiv$ 這符號跟 $=$ 長得很像,從某種角度來說,可以視為同餘就是「在模運算之下的等於」,它跟等於有多像呢?

Lemma 1
同餘的遞移律

對於任意整數 $a,b,c$、正整數 $m$,如果 $a \equiv b \equiv c \pmod m$,就代表 $a \equiv c$。

Lemma 2
同餘的運算

對於任意整數 $a,b,c$、正整數 $m$,如果 $a \equiv b \pmod m$,那麼:

  • $a + c \equiv b + c \pmod m$
  • $a - c \equiv b - c \pmod m$
  • $a \times c \equiv b \times c \pmod m$

這些的證明都很簡單,只要代入同餘的定義即可,讀者可以練習看看。

所以說,$\equiv$ 兩邊的兩個數字一起加減乘一個數字,都不會改變它們同餘的關係,而且還有遞移律,就跟我們平常認識的 $=$ 很像!不過也有一些不一樣的地方,注意到我們只提到「加減乘」,但四則運算中的除法並沒有跟它們一起出現,這是因為把兩邊除上一個數後,不見得會保持同餘關係,先不談不整除的時候不知該怎麼辦,光是整除的狀況就可能會出問題,像是 $6 \equiv 12 \pmod 6$,但是如果把兩邊各自除以 $3$,$2 \equiv 4 \pmod 6$ 顯然是錯的。

有一個真實存在的性質是當 $c$ 是 $a,b,m$ 的公因數時,$\frac{a}{c} \equiv \frac{b}{c} \pmod{\frac{m}{c}}$,但通常改變模數並不是我們想要的,之後的篇章會再處理除法的問題,這裡我們先不討論。

既然同餘類似於某種「等於的關係」,那其實可以更進一步地說:

Lemma 3
同餘的運算 2

對於任意整數 $a,b,c,d$、正整數 $m$,如果 $a \equiv b \pmod m$ 且 $c \equiv d \pmod m$,那麼:

  • $a + c \equiv b + d \pmod m$
  • $a - c \equiv b - d \pmod m$
  • $a \times c \equiv b \times d \pmod m$

回來看看我們剛剛寫過的式子:(a % b + b) % b,雖然 % b 出來的結果可能是負的,但它總是個取餘數的運算,a % b 的結果永遠都會跟 $a$ 在模 $b$ 下同餘。所以:

\[
\begin{align*}
& ((a\ \texttt{%}\ b) + b)\ \texttt{%}\ b \\
\equiv\ & (a\ \texttt{%}\ b) + b & (\texttt{%}\text{ 會保持同餘}) \\
\equiv\ & a\ \texttt{%}\ b & (\texttt{\(b\) 跟 \(0\) 同餘}) \\
\equiv\ & a & (\texttt{%}\text{ 會保持同餘}) \\
& (\text{mod}\ \ b)
\end{align*}
\]

得到結果真的會跟 $a$ 模 $b$ 下同餘,我們又知道運算結果是一個 $[0, b)$ 以內的數,所以這個答案就只能是 $a \bmod b$ 了。

運算過程取模

在那種「因為答案很大所以要取模再輸出」的題目中,很有可能光是在運算過程中,數字就會大到 long long int 也存不下,所以算完最後再取模肯定是不行的!舉例來說,我們想要算 $2^{100} \bmod 10^9+7$,雖然答案是個連 int 都存的下的數,但要是我們先算完 $2^{100}$ 再模,那一般的整數型態肯定存不下。

同餘的運算 2 其實也告訴了我們,在加減乘運算之前先把數字模一次,也不會改變運算結果,所以其實可以寫成

const long long MOD = 1'000'000'007;
long long ans = 1;
for (int i = 0; i < 100; i++) {
    ans = ans * 2 % MOD;
}

就可以得到正確的 $2^{100} \bmod 10^9 + 7$。在做這種題目的時候,可以每做一次運算就模一次,這樣就不會溢位了。

世人其實對於 $\bmod$ 和其他運算符號的優先順序沒有什麼共識,所以在有各種運算交錯出現時,最好還是加個括號才比較不容易搞混。一般來說,$a \bmod 10^9+7$ 就是 $a \bmod (10^9+7)$ 的意思,因為這個式子單獨出現時不太會搞混所以經常省略括號。

注意在 C++ 中 % 的優先度比 + 高、和 */ 同級,在寫程式的時候要特別注意加減乘模的優先順序,例如 a + b % MOD 的意思其實是 a + (b % MOD) 而非 (a + b) % MOD

除法很慢

/% 這兩個運算比 +-* 慢很多!在運算量很大(如超過 $10^8$)的時候,要是每次加減乘完都模一次,時限又設比較緊的話,就有可能會 TLE。有一些減少模運算次數又不會溢位的小技巧:

不過乘法完通常就非模不可了。

快速冪


剛才我們學會怎麼在不溢位的前提下算出 $2^{100} \bmod 10^9+7$,進階一點,如果我們要算的是 $a^b \bmod m$ 呢?

例題
Exponentiation
Source:CSES 1095

給你非負整數 $a,b$,求 $a^b \bmod 10^9+7$。特別地,定義 $0^0=1$。

條件限制
  • 有 $1 \leq n \leq 2 \times 10^5$ 個詢問
  • $0 \leq a,b \leq 10^9$

慢慢地把 $a$ 乘 $b$ 次是不行的,那得花上 $O(b)$ 的時間,所以我們得想辦法把指數運算算得更快。在國中的時候,我們有學過指數律:

\[ a^b \times a^c = a^{b+c} \]

前面我們計算 $a^b$ 的方法,就是直接算 $a^{b-1} \times a = a^b$,所以我們會依序算出 $a^1,a^2,\dots,a^b$,不過我們需要知道的只有最後那個 $a^n$ 而已,可不可以少知道一點?一種想法是我們乾脆把 $b$「分一半」,如果 $b$ 是個偶數 $b=2k$,那我們知道 $a^b=(a^k)^2$,只需要知道 $a^k=a^{b/2}$ 就能算出 $a^b$ 了。照著這個邏輯,要是 $b$ 是個二的冪次 $b=2^k$ 那就很快樂,因為我們只需要一直把 $a$ 平方,就會依序得出

\[ a=a^{2^0}, \quad (a^{2^0})^2=a^{2^1}, \quad (a^{2^1})^2 = a^{2^2}, \quad \dots, \quad a^{2^k}=a^b \]

只要花 $O(k)=O(\log b)$ 的時間就能得到 $a^b$ 了!

如果 $b$ 不是二的冪次,就代表我們一直除以 2 的話,總有一天會變成一個奇數,這時我們就不能直接分一半了。解決辦法很簡單,把它變成偶數就好了嘛,在 $b$ 是奇數 $b = 2k+1$ 的時候,就有 $a^b=(a^k)^2 \times a$,其實也只要算 $a^k=a^{(b-1)/2}$ 就好了。這樣一來,我們可以先得到一個遞迴的算法:

\[
a^b = \begin{cases}
(a^{b/2})^2, &\qquad \text{if \(b\) 是偶數} \\
(a^{(b-1)/2})^2 \times a, &\qquad \text{if \(b\) 是奇數}
\end{cases}
\]

每次遞迴時 $b$ 就會少一半,所以時間複雜度就是 $O(\log b)$。仔細想想,其實這個過程就等同於去看 $b$ 的二進位分解

\[ b = \underbrace{b_{\lfloor \log_2 b \rfloor} \dots b_1b_0}_{\text{寫成二進位}}
= b_{\lfloor \log_2 b \rfloor} \times 2^{\lfloor \log_2 b \rfloor} + \dots + b_1 \times 2^1 + b_0 \times 2^0 \]

舉例來說,

\[
182_{(10)} = 10110110_{(2)} = 2^7 + 2^5 + 2^4 + 2^2 + 2^1
\]

所以

\[
a^{182} = a^{2^7} \times a^{2^5} \times a^{2^4} \times a^{2^2} \times a^{2^1}
\]

右邊的東西就是一堆「$a$ 的二的冪次次方」相乘,剛剛我們就已經知道這要怎麼算了,所以可以輕鬆寫成迴圈版本:

using ll = long long;
const ll MOD = 1'000'000'007;
ll fastpow(ll a, ll b) {
    ll ans = 1;
    while (b > 0) {
        if (b & 1) ans = ans * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return ans;
}

在那個 while 迴圈第 $i$ 次執行(從 $0$ 開始數)時,當下的 a 會是 $a^{2^i}$,b 會是 $b$ 少掉最右邊的 $i$ 個 bit,所以 b & 1 是在檢查「$b$ 的第 $i$ 個 bit 是不是 1」,是的話就把答案乘上 $a^{2^i}$。用個簡單的小例子,$b=13=1101_{(2)}$,在每次迴圈開始的時候:

時間複雜度和遞迴版本同樣是 $O(\log b)$。

不要直接用 <cmath> 裡面的 pow()!那是一個浮點數函數。

輾轉相除法


國小的時候,老師教我們用短除法計算兩個數 $a,b$ 的最大公因數,作法是先神祕地通靈出一個它們的共同因數 $d$ 之後,把它們都除以 $d$,然後反覆這個動作,直到它們互質為止,過程中所有 $d$ 相乘就是最大公因數了。不過寫程式的時候,我們無法看著數字就猜一個公因數出來,要是像我們在基礎演算法 / 學校教的數學枚舉因數那樣做的話也得要花 $O(\max(\sqrt{a},\sqrt{b}))$ 的時間。

要更快算出最大公因數的話,就要用到輾轉相除法了,輾轉相除法也叫作歐幾里得演算法(Euclidean algorithm),它用到一個最大公因數的性質:

Lemma 4
輾轉相除法

$\gcd(a,b) = \gcd(a,b-a)$,其中 $\gcd(a,b)$ 是 $a,b$ 的最大公因數,$\gcd(a,0)=\gcd(0,a)=a$。

證明:

輾轉相除法的作法很簡單,就是不斷把 $a,b$ 之中大的減去小的,有一個變成 $0$ 之後就直接回傳另一個數字。不過一直慢慢減不見得很快,要是 $a$ 很大、$b=1$,那就要減到天荒地老了,比較聰明的作法是直接令 $a \gets a \bmod b$,畢竟 $a$ 一直扣掉 $b$ 直到變成比 $b$ 還小,那不就是 $a$ 模 $b$ 嗎?

這樣真的很快嗎?分析一下時間複雜度,假設 $a > b$,那事實上 $a \bmod b \leq a/2$,因為要是 $b \leq a/2$ 那自然是對的,如果 $b > a/2$ 的話,那麼 $a \bmod b = a - b < a/2$,所以每一次都有其中一個數會少一半,時間複雜度就是 $O(\log a + \log b)$。

其實在只是要用最大公因數的時候,不需要自己寫輾轉相除法,從 C++17 開始 <numeric> 提供 gcd() function,內部的實作就是用輾轉相除法,開心地用下去就好了。在更低的版本的話,在 <algorithm> 裡面有一個叫作 __gcd() 的 function,不過要注意這個 function 不在標準之中,能用 gcd 還是用 gcd 比較好。

那為什麼要學輾轉相除法呢?不久後就知道了。

質數篩法


檢查一個數 $n$ 是不是質數可以 $O(\sqrt{n})$ 時間內做到,但是如果我們想要找 $1$ 到 $n$ 內的所有質數,花上 $O(n\sqrt{n})$ 的時間一一檢查好像太慢了,這時就要用更聰明的辦法找質數了。

一個直覺的想法是,我們枚舉 $2$ 到 $n$ 的每一個數 $i$,但我們不是要檢查 $i$ 是不是質數,而是去把 $i$ 的兩倍以上倍數都標記成「不是質數」,而沒被標記的那些數就是質數了。

vector<bool> sieve1(int n) { // prime[i] = i 是不是質數
    vector<bool> prime(n + 1, true);
    prime[0] = prime[1] = false;
    for (int i = 2; i <= n; i++)
        for (int j = 2 * i; j <= n; j += i)
            prime[j] = false;
    return prime;
}

聽起來好暴力,但這其實很快,被枚舉到的倍數數量會是
\[ \frac{n}{1} + \frac{n}{2} + \dots + \frac{n}{n} = O(n \log n) \]
正整數的倒數和 $\frac{1}{1}+\frac{1}{2}+\dots+\frac{1}{n}$ 叫作調和級數,它的成長率就是 $O(\log n)$,調和級數很常見,在需要枚舉「$1$ 到 $n$ 所有數字在 $n$ 以內的倍數」或是「$1$ 到 $n$ 所有數字的因數」時,要記得總數就只有 $O(n \log n)$ 那麼多。

不過篩質數還可以更快!其實我們剛剛做了一些多餘的事情:要是我們已經知道 $i$ 不是質數了,那代表我們曾經用一個它的因數 $x$ 來把它標記成不是質數,而這個 $x$ 當然也已經去砍掉所有 $i$ 的倍數了,因此不是質數的 $i$ 我們就可以跳過:

vector<bool> sieve2(int n) {
    vector<bool> prime(n + 1, true);
    prime[0] = prime[1] = false;
    for (int i = 2; i <= n; i++)
        if (prime[i])
            for (int j = 2 * i; j <= n; j += i)
                prime[j] = false;
    return prime;
}

這有比較快嗎?有,質數的倒數和成長率是 $O(\log \log n)$,所以這麼做的時間複雜度只要 $O(n \log \log n)$,幾乎跟線性一樣快了,還可以順便知道每個數的質因數。

Tips 2
因數與質因數的數量

一個數 $x$ 會有幾個因數和幾個質因數呢?因為每個質因數都 $\geq 2$,所以 $x$ 的質因數數量是 $O(\log n)$。至於因數數量,如果先找好質因數分解

\[ x = p_1^{e_1} \times p_2^{e_2} \times \dots \times p_k^{e_k} \]

的話,就可以枚舉每個質因數要取幾個,用 $O(\text{因數數量})$ 的時間找到所有因數。在競賽中常見的 $x \leq 10^{9}$ 與 $x \leq 10^{18}$ 範圍中,因數數量最多分別是 $1344$ 與 $103680$,打競賽時通常會直接用 $O(x^{\frac{1}{3}})$ 去算。不過實際上,一個數的因數數量是 $o(x^\epsilon)\ \forall \epsilon > 0$,也就是說,在 $x$ 很大的時候其實是遠少於 $x^{\frac{1}{3}}$ 的。

習題


習題
最大公因數(GCD)

給兩個整數 $a,b$,求它們的最大公因數。

條件限制
  • $1 \leq a,b < 2^{31}$
習題
[Tutorial] 找質數
Source:NCOJ 100

給你一個 $\geq 2$ 的正整數 $N$,請輸出所有 $[1,N]$ 之間的質數。

條件限制
  • $2 \leq N \leq 10^ 6$

可以試試看用 $O(C \log C)$($C=$ $x$ 的值域大小)的時間做出這題:

習題
Counting Divisors
Source:CSES 1713

給 $n$ 個數 $x$,求每個 $x$ 的因數數量。

條件限制
  • $1 \leq n \leq 10^5$
  • $1 \leq x \leq 10^6$

請試試用類似於快速冪的作法做出這題,請不要寫大數運算,也不要用如 __int128 等範圍較大的整數型態:

習題
簡單乘法

給 $a,b,n$,求 $ab \bmod n$。

條件限制
  • 有多筆測資
  • $0 \leq a,b \leq 10^{18}$
  • $1 \leq n \leq 10^{18}$