# 快速冪與倍增法 Binary Exponentiation and Lifting
本篇會細說快速冪與倍增法的原理,以圖示的方式探討兩者的關係
備註 : 倍增法的部分建議先讀過[樹的基本性質](https://hackmd.io/@ShanC/Trees-Basic-Properties)再回來讀
## 快速冪簡介
快速冪是一個處理整數冪次的演算法,可以在 $O(log ~n)$ 的複雜度完成任務
一般來說我們處理冪次問題 $a^{~n}$ 會使用以下方法:
```cpp
int ans = 1;
for(int i = 0; i < n; i++)
ans *= a;
cout << ans;
```
這種方法最直觀,但是卻需要 $O(n)$ 的時間才能完成。對於數字過大的數字實在太耗時,因此我們會需要用到快速冪來處理這個問題
## 快速冪原理說明
要解釋快速冪的原理,需要用到一個性質
### 二進制
每個十進制非負整數 $(n)_{10}$ 都可以被拆解成二進制
$(n)_{10}=a_0\times2^0+a_1\times2^1+...+a_k\times2^k$,其中 $a_i=0 ~or ~1, \forall ~i\in N$
則可寫成二進制: $(a_ka_{k-1}...a_1a_0)_2$
舉例來說: $(61)_{10}=1\times2^0+0\times2^1+1\times2^2+1\times2^3+1\times2^4+1\times2^5=(111101)_2$
換種角度來看,也可以將一個數字看作有一數列 $<2^{~n}>$
二進位就是看這數列中要取哪幾項相加才會是我要的數字
$1$ 就是取該項,$0$ 就是不取該項
### 快速冪原理
#### 原理說明
設今天有一個數字 $b^{~n}$,可以先把 $n$ 變成二進制的寫法
得到 $b^{~n}=b^{~(a_0\times2^0+a_1\times2^1+...+a_k\times2^k)}=b^{~a_0\times2^0}\times b^{~a_1\times2^1}\times...\times ~b^{~a_k\times2^k}$。其中,$a_i\in \{0, 1\}~(0\leq i\leq k)$
#### 列舉序列
要窮舉序列 $<b^{2^0}, b^{2^1}, b^{2^2},..., ~b^{2^k}>$ 是很簡單的,只需要先計算 $k$ 值,再使用 $for$ 迴圈就可以實現
```cpp
#define ll long long
void enumerate_sequence(ll b, ll n){
ll k = log2(n);
for(int i = 0; i <= k; i++){
b *= b;
}
}
```
#### 取 or 不取
再來就是要來判斷 $a_i$ 是 $0$ 還是 $1$。若 $1$ 則將答案乘上去, $0$ 就不理他
```cpp
#define ll long long
ll pick_sequence(ll b, ll n){
ll k = log2(n), ret = 1;
for(int i = 0; i <= k; i++){
if(n & (1 << i)) // 是 1 就乘上去
ret *= b;
b *= b;
}
return ret; // 回傳答案
}
```
### 舉例
$7^{97}=7^{~(1100001)_2}=7^{2^0}\times7^{2^6}\times7^{2^7}=7^{1}\times7^{32}\times7^{128}$
## 快速冪實作
可以用 $while$ 迴圈來實現,這樣就不需要 $k$ 值了
- 每次迴圈都重複 $n$ >>= $1$,這就相當於從左往右看 $n$ 的位元
- 判斷 $n$ 的每給位元是否為 $1$,是的話就乘起來
- 每次迴圈將 $b$ *= $b$,這就相當於在算第 $i$ 項時的 $b^{2^i}$
```cpp
#define ll long long
ll fpow(ll b, ll n){
ll ret = 1;
while(n){
if(n & 1)
ret *= b;
b *= b;
n >>= 1;
}
return ret;
}
```
## 圖解快速冪
快速冪,簡單來說就是用二進位本身的機制,去判斷每次到底要「跳」幾次冪。以下例子中,假設我們想要求 $7^7$,則我們知道 $7^7=7^{2^0+2^1+2^2}$
<center>
<img src="https://hackmd.io/_uploads/rkKDBqhvle.png" style="margin: 0 auto; display: block; width: 300px">
</center>
我們可以驚奇地發現,快速冪整個「跳」的過程可以畫成一條路徑 (path)。原本我們習慣的跳法是一步一步地跳;快速冪的跳法是 $2$ 的冪次步跳
## 倍增法簡介
不難發現,其實類似快速冪的方法也可以運用在連通圖的走訪,尤其適合在「兩點路徑唯一的圖」中進行此操作。在有向圖,[functional graph](https://hackmd.io/@ShanC/basic-graph-theory-2#Functional-Graph-Successor-Graph) 有時具有此特性;在無向圖,樹本身的定義 ([這篇的定義 D](https://hackmd.io/@ShanC/Trees-Basic-Properties)) 就滿足了這個性質。倍增法這個技巧,以樹狀資料結構的詢問最常見
<center>
<img src="https://hackmd.io/_uploads/ryLLHN48R.png" style="margin: 0 auto; display: block; width: 400px">
functional graph (擷取自海大競程講義)
</center>
## 樹狀結構的倍增法與實作
在一棵樹中,假設已給定根節點 (有根節點才能討論祖先是誰)。我們||~~的題目~~||會好奇任一個節點 $x$ 的上一代、上兩代、...、上 $m$ 代祖先是誰。根據樹的定義,任兩點的路徑都是唯一的,這意味著我們可以構造一個函數 $g(x, m)$ 來找 $x$ 的第 $m$ 代祖先。在數學上,這樣寫其實很夠了,但是在程式中,我們必須每個節點的前幾代祖先都先找到並定義清楚。
### 資料結構定義
在資料結構上,我們只需要定義一個二維陣列當作是類似上面的函數 $f(x, n)$ 就好了,不會很複雜,只是 $n$ 在這裡代表的是第 $2^n$ 代祖先
#### 輸入
一般來說,題目都會給每個節點的父節點,所以會有以下輸入 :
```cpp
int f[MXN][logN]; // MXN: 最大節點數、logN: 最大祖先數
cin >> n; // 輸入節點數量
for (int i = 2; i <= n; i++){
cin >> father >> x; // 輸入 x 的父節點、x
f[x][0] = father; // 父節點就是 2^0 倍祖先
}
```
其中,父節點就是 $2^0$ 倍祖先,所以可以直接存在 $f[x][0]$ 當中
#### 初始化
因為 $x$ 的第 $2^i$ 代祖先就會是 $x$ 的第 $2^{i-1}$ 代祖先的第 $2^{i-1}$ 代祖先
<center>
<img src="https://hackmd.io/_uploads/BkxwLs2vel.png" style="margin: 0 auto; display: block; width: 400px">
</center>
因此考慮遞迴式 $f(x, i)=\begin{cases}
\text{the father of x , if }i=0\\
f(f(x, i-1), i-1)\text{ , otherwise} \\
\end{cases}$
這邊我們使用[動態規劃法](https://hackmd.io/@ShanC/Intro-to-DP)當中的 bottom-up 手法來初始化整張圖
```cpp
// logN: 最大的祖先數
for (int i = 1; i < logN; i++) { // binary lifting
for (int x = 1; x <= n; x++) {
f[x][i] = f[f[x][i - 1]][i - 1];
}
}
```
要注意的是,有些節點是沒有第 $i$ 代祖先的,但是我們沒必要想那麼多。由於 $f$ 在宣告時就設為 $0$ 的緣故,因此對 $1$-based 圖的編號沒有影響
### 資料結構大小
我們都知道在競程中,圖論題目的節點數 `MXN` 最大差不多就是 $2\times 10^6$ 這個量級。而 $f(x, i)$ 中最大的 $i$ 當然就是取 $2$ 為底的對數,也就是 $\approx 20.9$,因此在上面的程式碼當中,`logN` 只要取大約 $21$ 就夠了
## 倍增法例題說明: [Company Queries I](https://cses.fi/problemset/task/1687/)
### 題目
給你一棵 $n$ 節點的樹,已知節點 $1$ 是根節點。共訪問 $q$ 次,求 $x$ 的第 $k$ 代祖先
### 限制
- $1 \le n,q \le 2 \cdot 10^5$
- $1 \le x \le n$
- $1 \le k \le n$
### 題解
把輸入輸出看清楚,之後再去看 $k$ 的二進位去判斷要「跳」到幾代祖先,用類似快速冪的方法來判斷就好了
### 程式實作
```cpp
/* 略 */
while (q--) {
cin >> x >> k;
for (int i = 0; i < logN; i++) {
if (k & (1 << i))
x = f[x][i];
}
cout << (x ? x : -1) << '\n';
}
/* 略 */
```
## 備註
- 二維陣列 `f` 通常會定義在全域,如此一來初始化就會是 $0$
- 矩陣乘法也可以快速冪,之後的章節會探討矩陣乘法的意義
- 競程題目數字最大 $10^{18}$,若要求 $a^{10^{18}}$,使用快速冪可以把迴圈數壓在 $60$ 以內
~~(幾乎就是常數了)~~
- 通常倍增法都會跟 LCA 一起出現,真的只有倍增法的裸題真的不多
- 本篇把快速冪與倍增法寫在一起是因為他們共用同一個觀念
- 最低共同祖先會在之後的章節提及
## 題目練習
[Zerojudge d636. 大爆炸bomb](https://zerojudge.tw/ShowProblem?problemid=d636)
[Zerojudge d219. 00374 - Big Mod](https://zerojudge.tw/ShowProblem?problemid=d219)
[Zerojudge k137. 次方練習](https://zerojudge.tw/ShowProblem?problemid=k137)
[CSES Exponentiation](https://cses.fi/problemset/task/1095)
[CSES Exponentiation II](https://cses.fi/problemset/task/1712/)
[CSES Company Queries I](https://cses.fi/problemset/task/1687/)
----
## 參考資料
[海大競程 - Special Graph & 0-1 BFS & 差分約束](https://hackmd.io/@LeeShoWhaodian/summer-advanced-graph#/1)
[海大競程 - Tree Algorithm 1](https://hackmd.io/@LeeShoWhaodian/2025Tree1#/)
----
> [ShanC 程式競賽筆記](https://hackmd.io/@ShanC/B1ouGxqcC)
> 作者: ShanC
> 更新: 2024/9/13