CSES Problem Set — Exponentiation 題解
===
題目
---
你的任務是有效率地算出 $a^b$ 模 $10^9+7$ 的值。
注意到在這題我們令 $0^0 = 1$。
### 輸入
- 第一行輸入一個整數 $n$,代表計算的個數。($1 \le n \le 2 \cdot 10^5$)
- 接著有 $n$ 行,每一行有兩個整數 $a$、$b$。($0 \le a,b \le 10^9$)
### 輸出
輸出 $a^b$ 模 $10^9+7$ 的值。
範例測資
---
```
Input :
3
3 4
2 8
123 123
Output :
81
256
921450052
```
想法:快速冪
---
雖然數學告訴我們 $a^b = \exp(b \cdot \log a)$,但機器實現的浮點數運算總有誤差,並不能完美符合數學公式,所以這題我們必須使用整數運算老老實實地乘出結果。
另一方面,這題的 $a$ 與 $b$ 最大都可能到 $10^9$ 這麼多,表示 $a^b$ 的實際值非常有可能超出程式容許的整數上限,先算出 $a^b$ 後再取模也是不實際的。所以我們必須利用以下的數論事實:\\[
(x \times y) \bmod m = ((x \bmod m) \times (y \bmod m)) \bmod m.
\\] 也就是說我們可以在每次乘法之前就先對要運算的數值取模,這樣可以避免運算過程中數值超出限制。
雖然說我們要老老實實地乘出結果,但連續乘 $a$ 乘 $b$ 次(並且取模)並不是最有效率的方式。為了提升運算的效率,我們可以用俗稱「快速冪」的演算法,它只有 $O(\log b)$ 的時間複雜度,比起 $O(b)$ 的直接連乘快上許多。
快速冪只適用於滿足結合律的「乘法」運算 $x \otimes y$。這題的「乘法」可以定義成我們需要的 $(x \times y) \bmod m$,我們可以驗證說這個「乘法」的確滿足結合律。
因為快速冪有不少種寫法,下面的範例程式碼先省略不定義 `fastpow`,後面再把幾種寫法列出來。
### 範例程式碼
```cpp=
#include <iostream>
using namespace std;
int MOD = 1000000007;
int fastpow(int x, int n, int mod);
int main() {
int n, a, b;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> a >> b;
cout << fastpow(a, b, MOD);
}
}
```
### 快速冪核心概念
考慮一個滿足結合律的「乘法」運算 $\otimes$,並且把 $n$ 個 $x$「乘」在一起用 $x^n$ 表示。滿足結合律的意思是連續運算時括號順序並不影響結果:\\[
(x \otimes y) \otimes z = x \otimes (y \otimes z).
\\] 而因為「乘法」滿足結合律,所以「次方」也會滿足指數律:\\[
x^{m+n} = x^m \otimes x^n,\quad
x^{mn} = (x^m)^n.
\\] 快速冪的核心概念就是用指數律來降低「乘法」的次數。我們通常會使用 $2$ 來除,比如說把 $x^{14}$ 變成 $(x^7)^2$ 或 $(x^2)^7$。像這樣一直遞迴下去,我們就能用 $O(\log n)$ 次的「乘法」算出 $x^n$。
下面根據拆法的不同分為兩種。
### 快速冪 1:$x^{2k} = (x^k)^2$
例如 $x^{14} = ((x^2 \otimes x)^2 \otimes x)^2$。
這裡使用以下的規則:\\[
x^n = \begin{cases}
1, & n = 0;\\
x^k \otimes x^k, & n = 2k;\\
x^k \otimes x^k \otimes x, & n = 2k+1.
\end{cases}
\\] 並使用遞迴來實現。
> 如果「乘法」沒有單位元素 $x^0$,要把初始條件改成 $n = 1$ 時回傳 $x$。
```cpp=13
int fastpow(int x, int n, int mod) {
if (n == 0)
return 1;
int r = fastpow(x, n/2);
r = (r * r) % mod;
if (n % 2)
r = (r * x) % mod;
return r;
}
```
### 快速冪 2:$x^{2k} = (x^2)^k$
例如 $x^{14} = x^8 \otimes x^4 \otimes x^2$,其中 $x^{2n}$ 都是從已經算過的 $x^n$「平方」之後算出來的。
這裡使用以下的規則: \\[
x^n = \begin{cases}
1, & n = 0;\\
(x \otimes x)^k, & n = 2k;\\
(x \otimes x)^k \otimes x, & n = 2k+1.
\end{cases}
\\] 並使用遞迴來實現。
> 如果「乘法」沒有單位元素 $x^0$,要把初始條件改成 $n = 1$ 時回傳 $x$。
```cpp=13
int fastpow(int x, int n, int mod) {
if (n == 0)
return 1;
int r = fastpow((x * x) % mod, n/2);
if (n % 2)
r = (r * x) % mod;
return u;
}
```
我們也可以寫成迴圈。這個順序恰好跟遞迴版本顛倒,以 $x^7$ 為例,上面的遞迴版會算 $x^4 \otimes x^2 \otimes x$,而下面的迴圈版是算 $x \otimes x^2 \otimes x^4$。
```cpp=13
int fastpow(int x, int n, int mod) {
int r = 1;
while (n) {
if (n % 2)
r = (r * x) % mod;
x = (x * x) % mod;
n /= 2;
}
return r;
}
```