# 快速冪與倍增法 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