<style> .reveal .slides { text-align: left; font-size:30px; } </style> # 位元運算 & 快速冪 #### Bitwise Operation & Binary Exponentiation 10/13 preTrain --- ## 位元運算 ### Bitwise Operation ---- 電腦儲存數字都是用一堆 $0$ 和 $1$ (位元) 因此也理所當然可以對「位元」做運算 ---- 複習離散數學中「邏輯」那章學過的邏輯運算真值表 |$P$|$Q$|$P\lor Q$|$P\land Q$|$\neg P$|| |----|----|----|----|----|----| |0|0|0|0|1| |0|1|1|0|1| |1|0|1|0|0| |1|1|1|1|0| ---- 「位元運算」也繼承了這些運算,只是多了**XOR運算** $\oplus$ ```cpp AND運算: OR運算: 0 & 0 => 0 0 | 0 => 0 0 & 1 => 0 0 | 1 => 1 1 & 0 => 0 1 | 0 => 1 1 & 1 => 1 1 | 1 => 1 XOR運算(⊕): NOT運算: 0 ^ 0 => 0 !0 => 1 0 ^ 1 => 1 1 ^ 0 => 1 !1 => 0 1 ^ 1 => 0 ``` 但是跟「邏輯運算」不同的是,位元運算是一整個位元陣列的運算, 而非單一元素的運算 ---- ## Ex: - $1001\text{ & }0111=0001$ - $1001\text{ | }0111=1111$ - $1001\text{ ^ }0111=1110$ ---- ### XOR 運算 因為 XOR 太多常用性質,所以特別拿出來講 先來幾個基本原則(bitwise xor 的符號一樣用 $\oplus$): 1. $A \oplus B = B \oplus A$、$(A \oplus B) \oplus C = A \oplus (B \oplus C)$, 也就是有交換律和結合律 2. $A \oplus A = 0$ 3. 若 $A \oplus B = C$,則 $A \oplus C = B$ 4. $A \oplus 0 = A$ 奇妙的[延伸閱讀](https://sam571128.codes/2022/03/29/XOR-Basis/)(建議讀完線性代數再來看) <!-- .element: class="fragment" data-fragment-index="1" --> ---- ### 左移/右移運算 C/C++ 程式裡使用 "<<" 與 ">>" 來表示,表示為位元左移(右移)一格 左移時最左邊會補上 0 而右移時最右邊位元會被移除 ```= 9=1001, 9<<1 = 10010 = 18 (乘 2) 15=1111, 15>>1 = 111 = 7 (除 2 取 floor) 常見用法 (x>>1) 快速除 2, 以及 (x<<1) 和 (x<<1)|1 左子節點和右子節點 ``` ---- ### 補數運算 C/C++ 程式裡使用 "~" 來表示,補數運算為逐位元取反 顛倒一個變數的每個位元的 0 和 1 ```= ~0 = -1, ~1 = -2 常見用法(位元優化 dp 總有一天會遇到) 15 & ~(1 << 1) = 13 // 將第 2 個 bit 設為 0 8 | (1 << 2) = 12 // 將第 3 個 bit 設為 1 ``` ---- ### 位元性質 `if (status)` `while (status)` `for ( ; status ; )` 以上的 `status` 都是非 0 就是 true,也就是裡面都是布林值 所以可以常常看到這種寫法 ---- ```cpp= if(array[x]){ // 若 array[x] 的值非零即會進入 "True" cout << "True"; } else{ // 只有再 array[x] == 0 的時候才會進入這邊 cout << "False"; } while(x){ // 當 x 不是 0 的時候就不會跳出迴圈 x--; cout << x << endl; } ``` ---- 規律性質 例題: [[CPE例題](https://cpe.cse.nsysu.edu.tw/cpe/file/attendance/problemPdf/12208.pdf)] 詢問 $x$~$y$ 中所有的數字轉換成二進制後有幾個 $1$ ? 當你把位元攤開來之後 0 : 0 0 0 0 1 : 0 0 0 1 2 : 0 0 1 0 3 : 0 0 1 1 4 : 0 1 0 0 5 : 0 1 0 1 6 : 0 1 1 0 7 : 0 1 1 1 8 : 1 0 0 0 ||每個bit拆開來即可發現規律|| ---- ## 應用 : lowbit(x) 最低的 '1' 位元 ```cpp= x & -x ``` - 根據二的補數,-x 為 ~x + 1 - x 中最低位的位元 1 經過 not 運算後,將在 ~x 中變為最低位的 '0' - 因此對 ~x 加上 1 後,最低位的位元 0 重新變為位元 1 (進位) 且其後的位元都將被設定為 0 - 在 x & -x 後,相同的位元僅剩下最低位的 '1' ---- ## 應用 : 檢查冪次 是否為二的冪次 ```cpp= (x & -x) == x ``` 由於若你為二的冪次,則該數字只會有一個位元(也是最高位元) ---- ## 實作例題講解 ### 練習 1 --- XOR 練習 ```cpp= x = 1919, y = 810 x ^= y ^= x ^= y ``` 請問 x, y 各為多少? ---- 拆開來看即為 ```cpp= x = x ^ y; y = y ^ x; x = x ^ y; ``` 答案為 x 為 $810$, y 為 $1919$ (相等於 swap ( x , y )) but why? <!-- .element: class="fragment" data-fragment-index="1" --> ---- 回想一下 XOR 真值表 ``` XOR運算(⊕): 0 ^ 0 => 0 0 ^ 1 => 1 1 ^ 0 => 1 1 ^ 1 => 0 ``` 會發現 : 兩個位元不同,就會回傳 $1$ 所以若令 $z=x\oplus y$,則 $z$ 儲存的就會是「$x$ 與 $y$ 不同的位元」 相同的位存 $0$,不同則存 $1$ 之後我們再計算 $x \oplus z, y \oplus z$,分別會變成 $y, x$ ---- ### 練習 2 --- 優先級的練習 會輸出多少? ```cpp= cout << (15 >> 1 + 2); ``` ---- ```cpp= cout<< ( 15 >> 1 + 2 ); ``` ans : $1$ 大多數的位元運算子的運算優先級都比較低, 因此此題的計算過程為 $1 + 2 = 3$, $15=(1111)_{2}$ >> $3 = 1$. 所以不確定優先級的時候最好都要加括號 ---- ### 練習 3 --- 常用的技巧 以下這些能幹嘛? ```cpp= if(x & 1) return 1; else return 0; ``` or ```cpp= return (x & 1); ``` ---- ```cpp= if(x & 1) return 1; // (奇數) else return 0; // (偶數) ``` 判斷 $x$ 的奇偶性 也是常見用法 (比較快) 原因: 由於一個數字的位元組成都是 $2$ 的冪次方 因此 $2^n$ 只有在 $n==0$ 的時候會影響到數字的奇偶性 故只需要判斷最後一個位元 ---- ## bitset - 之前在 STL 中講過,但嚴格上來說他不算 STL - 由於這容器支援位元運算,所以我們還是會用到它 - bitset 算是一種優化過的位元運算容器 - 當我們需要開超過 64 位元時,可以考慮 bitset ---- ### 使用範例 ```cpp= #include<bitset> // bitset #include<iostream> bitset<10> a = 7; bitset<10> b(10); bitset<10> c("10010"); int main(){ b = 5; // 101 b >>= 1; // 10 cout << b << endl; cout << (a | c) << " " << (a & c) << " " << (a ^ c) << endl; } ``` --- ## 快速冪 ### Binary Exponentiation ---- ## 目標 快速計算 $x^y$,其中 $y$ 為非負整數 ---- 原本我們在計算 $x^y$ 的時候呢,有一個最直觀且簡短的寫法,就是跑一個時間複雜度為 $O(y)$ 的迴圈 ```cpp= int ans = 1; for(int i = 1; i <= y; i++){ ans * = x; } cout << ans << "\n"; ``` 但是如果今天 $y$ 太大的時候,就會需要優化這個程式,需要讓他更快速的解決這個問題,於是就衍生出了快速冪的這個算法。 ---- 先把 y 換成二進位 以 $2^{13}$ 為例 $$ \begin{split} 13&= (1101)_2\\ &= 1\cdot 2^3 + 1\cdot 2^2 + 0\cdot 2^1 + 1\cdot 2^0\\ &= 8 + 4 + 1\\ \end{split} $$ $$ \Rightarrow 2^{13} = 2^{8+4+1} = 2^8 \cdot 2^4 \cdot 2^1 $$ ---- 我們只要求出 $2^8, 2^4, 2^1$ 就好 也就是 $x^y$ 只需要求出每個目標的 $2^i$ 再做相加 時間是 $\log_2 y$ ,也就是 $y$ 的二進位編碼長度 ---- ## 圖解快速冪 快速冪簡單來說就是用二進位本身的機制,判斷每次到底要「跳」幾次 ---- ## 圖解快速冪 以下例子中,假設我們想要求 $7^7$,則我們知道 $7^7=7^{2^0+2^1+2^2}$ 透過二進位編碼,我們可以選擇每個 $2^i$ 要「跳」/「不跳」($0\text{ or }1$) ![](https://hackmd.io/_uploads/rkKDBqhvle.png) 記住這個觀念,以後 **倍增法/LCA** 會再出現 <!-- .element: class="fragment" data-fragment-index="1" --> ---- ## 實作想法 考慮窮舉所有的 $2^i$ ($0\leq i \leq \log_2 y$), 若 $y$ 相對應的 bit 是 $1$ 就將答案乘上 $2^i$ 否則答案不變 ---- ## 實作細節 - 可以用 $y$ & $1$ 來取最右那一位的值 - 每次配合 $y$ >>= $1$ 來跑完整個 $y$ 每一位 - 用 $\text{ans}$ 維護答案,初始化為 $1$ - 開 long long 處理 ---- ## 程式碼 ```cpp= typedef long long ll; // 方便簡寫 ll mypow(ll x, ll y) { ll ans = 1; // 記得這裡一定要設為 1 才有辦法乘 while (y) { // 當 y 的所有 bit 都判斷完就離開迴圈 if (y & 1) ans = ans * x; // 判斷每次要跳 / 不跳 x = x * x; // 每次把自己平方 y >>= 1; // 每次右移一格 } return ans; } ``` ---- ## 過程 以 $2^{13}$ 為例 : | 原本 $\text{ans}$ | $x$ | $y$| $y$ 的最右 | 計算後 $\text{ans}'$ | | -------- | -------- | -------- | -------- | -------- | | $1$ | $2^1$ | $1101$ | $1$ | $2^1=1\cdot2=2$ | | $2$ | $2^2$ | $110$ | $0$ | 不變 | | $2$ | $2^4$ | $11$ | $1$ | $2^4=2\cdot 2^4=32$ | | $32$ | $2^8$ | $1$ | $1$ | $2^8=32\cdot 2^4=8192$ | 算出 $2^{13}$ 的答案為 $8192$ ---- ## 溢位處理 如果數字太大的話,很有可能會超過 long long 的範圍 因此許多題目會要我們把結果 mod 一個數字 ---- ## 加入 mod 後的程式碼 ```cpp= const ll MOD = 1e9 + 7; // 1000000007 是個質數 ll mypow(ll x, ll y){ ll ans = 1; while(y){ if(y & 1) ans = (ans * x) % MOD; // 這裡 % MOD x = (x * x) % MOD; // 這裡 % MOD y >>= 1; } return ans; } ``` ---- ## mod與乘法的交換律 為什麼我們在實作的過程中可以邊 mod 邊乘,答案還會對? 根據餘式定理 : $$ a = (c_1 \times m) + r_1, \quad b = (c_2 \times m) + r_2 $$ 將 $a$ 與 $b$ 帶入 $(a\times b) \% m$,並根據 mod 運算的分配律: $$ \begin{split} (a \times b) \% m &= ((c_1 \times m) \times (c_2 \times m)) \% m\\ &+((c_1 \times m) \times r_2)\%m +((c_2 \times m) \times r_1)\% m\\ &+ (r_1 \times r_2) \% m \end{split} $$ ---- 根據定義,由於 $$ r_1=a\%m, r_2=b\%m $$ 又我們發現上一頁有些項可以被 $m$ 整除,因此我們得到以下結論 $$ \begin{split} (a \times b) \% m &= \cancel{((c_1 \times m) \times (c_2 \times m)) \% m}\\ &+\cancel{((c_1 \times m) \times r_2)\% m} +\cancel{((c_2 \times m) \times r_1)\% m}\\ &+ (r_1 \times r_2) \% m=((a \% m) \times (b \% m)) \% m \end{split} $$ ---- 簡單來說就是: $$ \text{先乘後 %}m=\text{先 %}m\text{ 後乘} $$ 所以我們的程式碼可以這樣弄 ---- 換成 $2$ 進位,只需要計算 $\log_2 y$ 次 當要計算 $x^y$ ,$y$ 到 $10^{18}$ 的時候 $\log_2 10^{18} \approx 60$ 只需做 $60$ 次就可以算出 $x^{10^{18}}$ --- ## 作業 & 練習時間 https://vjudge.net/contest/756605 ---- Any questions?
{"title":"位元運算 & Binary Exponentiation","description":"快速計算 x^y","slideOptions":"{\"type\":\"slide\",\"slideOptions\":{\"transition\":\"fade;\"}}","contributors":"[{\"id\":\"4f67a8cd-06ae-45dc-a8e3-62c6a41e5a37\",\"add\":8311,\"del\":976,\"latestUpdatedAt\":1760622278899}]","image":"https://hackmd.io/_uploads/HyFgxX96gx.png"}
    157 views
   Owned this note