1. 程式人生 > >位運算的奇技淫巧(二)

位運算的奇技淫巧(二)

位運算就是基於整數的二進位制表示進行的運算。由於計算機內部就是以二進位制來儲存資料,位運算是相當快的。 > 之前有總結過位運算的技巧,但稍微對[以前寫的文章](https://www.cnblogs.com/RioTian/p/12371164.html)不太滿意,所以重新總結一下 常用的運算子共 6 種,分別為與( `&` )、或( `|` )、異或( `^` )、取反( `~` )、左移( `<<` )和右移( `>>` )。 ## 與、或、異或 與( `&` )或( `|` )和異或( `^` )這三者都是兩數間的運算,因此在這裡一起講解。 它們都是將兩個整數作為二進位制數,對二進位制表示中的每一位逐一運算。 | 運算子 | 解釋 | | :----: | :-------------------------------: | | `&` | 只有兩個對應位都為 1 時才為 1 | | `|` | 只要兩個對應位中有一個 1 時就為 1 | | `^` | 只有兩個對應位不同時才為 1 | 異或運算的逆運算是它本身,也就是說兩次異或同一個數最後結果不變,即 $a \text{^} b \text{^} b = a$ 。 舉例: $$ \begin{aligned} 5 &=(101)_2\\ 6 &=(110)_2\\ 5\&6 &=(100)_2 =\ 4\\ 5|6 &=(111)_2 =\ 7\\ 5\text{^}6 &=(011)_2 =\ 3\\ \end{aligned} $$ ## 取反 取反是對一個數 $num$ 進行的計算,即單目運算。 `~` 把 $num$ 的補碼中的 0 和 1 全部取反(0 變為 1,1 變為 0)。有符號整數的符號位在 `~` 運算中同樣會取反。 補碼:在二進位制表示下,正數和 0 的補碼為其本身,負數的補碼是將其對應正數按位取反後加一。 舉例(有符號整數): $$ \begin{aligned} 5&=(00000101)_2\\ \text{~}5&=(11111010)_2=-6\\ -5\text{ 的補碼}&=(11111011)_2\\ \text{~}(-5)&=(00000100)_2=4 \end{aligned} $$ ## 左移和右移 `num << i` 表示將 $num$ 的二進位制表示向左移動 $i$ 位所得的值。 `num >> i` 表示將 $num$ 的二進位制表示向右移動 $i$ 位所得的值。 舉例: $$ \begin{aligned} 11&=(00001011)_2\\ 11<<3&=(01011000)_2=88\\ 11>>2&=(00000010)_2=2 \end{aligned} $$ 在 C++ 中,右移操作中右側多餘的位將會被捨棄,而左側較為複雜:對於無符號數,會在左側補 0;而對於有符號數,則會用最高位的數(其實就是符號位,非負數為 0,負數為 1)補齊。左移操作總是在右側補 0。 ## 複合賦值位運算子 和 `+=` , `-=` 等運算子類似,位運算也有複合賦值運算子: `&=` , `|=` , `^=` , `<<=` , `>>=` 。(取反是單目運算,所以沒有。) ## 關於優先順序 位運算的優先順序低於算術運算子(除了取反),而按位與、按位或及異或低於比較運算子,所以使用時需多加註意,在必要時新增括號。 ## 位運算的應用 位運算一般有三種作用: 1. 高效地進行某些運算,代替其它低效的方式。 2. 表示集合。(常用於 [狀壓 DP]() 。) 3. 題目本來就要求進行位運算。 需要注意的是,用位運算代替其它運算方式(即第一種應用)在很多時候並不能帶來太大的優化,反而會使程式碼變得複雜,使用時需要斟酌。(但像“乘 2 的非負整數次冪”和“除以 2 的非負整數次冪”就最好使用位運算,因為此時使用位運算可以優化複雜度。) ### 乘 2 的非負整數次冪 ```cpp int mulPowerOfTwo(int n, int m) { // 計算 n*(2^m) return n << m; } ``` ### 除以 2 的非負整數次冪 ```cpp int divPowerOfTwo(int n, int m) { // 計算 n/(2^m) return n >
> m; } ``` !!! warning 我們平常寫的除法是向 0 取整,而這裡的右移是向下取整(注意這裡的區別),即當數大於等於 0 時兩種方法等價,當數小於 0 時會有區別,如: `-1 / 2` 的值為 $0$ ,而 `-1 >> 1` 的值為 $-1$ 。 ### 判斷一個數是不是 2 的正整數次冪 ```cpp bool isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; } ``` ### 對 2 的非負整數次冪取模 ```cpp int modPowerOfTwo(int x, int mod) { return x & (mod - 1); } ``` ### 取絕對值 在某些機器上,效率比 `n >
0 ? n : -n` 高。 ```cpp int Abs(int n) { return (n ^ (n >> 31)) - (n >> 31); /* n>>31 取得 n 的符號,若 n 為正數,n>>31 等於 0,若 n 為負數,n>>31 等於 -1 若 n 為正數 n^0=n, 數不變,若 n 為負數有 n^(-1) 需要計算 n 和 -1 的補碼,然後進行異或運算, 結果 n 變號並且為 n 的絕對值減 1,再減去 -1 就是絕對值 */ } ``` ### 取兩個數的最大/最小值 在某些機器上,效率比 `a > b ? a : b` 高。 ```cpp // 如果 a>
=b,(a-b)>>31 為 0,否則為 -1 int max(int a, int b) { return b & ((a - b) >> 31) | a & (~(a - b) >> 31); } int min(int a, int b) { return a & ((a - b) >> 31) | b & (~(a - b) >> 31); } ``` ### 判斷符號是否相同 ```cpp bool isSameSign(int x, int y) { // 有 0 的情況例外 return (x ^ y) >= 0; } ``` ### 交換兩個數 ```cpp void swap(int &a, int &b) { a ^= b ^= a ^= b; } ``` ### 獲取一個數二進位制的某一位 ```cpp // 獲取 a 的第 b 位,最低位編號為 0 int getBit(int a, int b) { return (a >> b) & 1; } ``` ### 表示集合 一個數的二進位制表示可以看作是一個集合(0 表示不在集合中,1 表示在集合中)。比如集合 `{1, 3, 4, 8}` ,可以表示成 $(100011010)_2$ 。而對應的位運算也就可以看作是對集合進行的操作。 | 操作 | 集合表示 | 位運算語句 | | ------ | :-------------: | :-------------------------: | | 交集 | $a \cap b$ | `a & b` | | 並集 | $a \cup b$ | `a|b` | | 補集 | $\bar{a}$ | `~a` (全集為二進位制都是 1) | | 差集 | $a \setminus b$ | `a & (~b)` | | 對稱差 | $a\triangle b$ | `a ^ b` | ### 二進位制的狀態壓縮 二進位制狀態壓縮,是指將一個長度為 $m$ 的 $bool$ 陣列用一個 $m$ 位的二進位制整數表示並存儲的方法。利用下列位運算操作可以實現原 $bool$ 陣列中對應下標元素的存取。(xor 等價於 ^) | 操作 | 運算 | | :-------------------------------------------------: | :----------------: | | 取出整數 n 在二進位制表示下的第 k 位 | (n >> k) & 1 | | 取出整數n 在二進位制表示下的第 0 ~ k - 1 位 (後 k 位) | n & ((1 << k) - 1) | | 對整數 n 在二進位制表示下的第 k 位取反 | n xor (1 << k) | | 對整數 n 在二進位制表示下的第 k 位賦值 1 | n \| (1 << k) | | 對整數 n 在二進位制表示下的第 k 位賦值 0 | n & (~(1 << k)) | 這種方法運算簡便,並且節省了程式執行的時間和空間。當m不太大時,可以直接使用一個整數型別儲存。當m較大時,可以使用若干個整數型別(int陣列),也可以直接利用 $C++STL$ 為我們提供的 $bitset$ 實現 ### 遍歷某個集合的子集 ```cpp // 遍歷 u 的非空子集 for (int s = u; s; s = (s - 1) & u) { // s 是 u 的一個非空子集 } ``` 用這種方法可以在 $O(2^{popcount(u)})$ ( $popcount(u)$ 表示 $u$ 二進位制中 1 的個數)的時間複雜度內遍歷 $u$ 的子集,進而可以在 $O(3^n)$ 的時間複雜度內遍歷大小為 $n$ 的集合的每個子集的子集。(複雜度為 $O(3^n)$ 是因為每個元素都有 不在大子集中/只在大子集中/同時在大小子集中 三種狀態。) ## 內建函式 GCC 中還有一些用於位運算的內建函式:[詳細文章介紹](https://www.cnblogs.com/RioTian/p/13527410.html) 1. `int __builtin_ffs(int x)` :返回 $x$ 的二進位制末尾最後一個 $1$ 的位置,位置的編號從 $1$ 開始(最低位編號為 $1$ )。當 $x$ 為 $0$ 時返回 $0$ 。 2. `int __builtin_clz(unsigned int x)` :返回 $x$ 的二進位制的前導 $0$ 的個數。當 $x$ 為 $0$ 時,結果未定義。 3. `int __builtin_ctz(unsigned int x)` :返回 $x$ 的二進位制末尾連續 $0$ 的個數。當 $x$ 為 $0$ 時,結果未定義。 4. `int __builtin_clrsb(int x)` :當 $x$ 的符號位為 $0$ 時返回 $x$ 的二進位制的前導 $0$ 的個數減一,否則返回 $x$ 的二進位制的前導 $1$ 的個數減一。 5. `int __builtin_popcount(unsigned int x)` :返回 $x$ 的二進位制中 $1$ 的個數。 6. `int __builtin_parity(unsigned int x)` :判斷 $x$ 的二進位制中 $1$ 的個數的奇偶性。 這些函式都可以在函式名末尾新增 `l` 或 `ll` (如 `__builtin_popcountll` )來使引數型別變為 ( `unsigned` ) `long` 或 ( `unsigned` ) `long long` (返回值仍然是 `int` 型別)。 例如,我們有時候希望求出一個數以二為底的對數,如果不考慮 `0` 的特殊情況,就相當於這個數二進位制的位數 `-1` ,而一個數 `n` 的二進位制表示的位數可以使用 `32-__builtin_clz(n)` 表示,因此 `31-__builtin_clz(n)` 就可以求出 `n` 以二為底的對數。 由於這些函式是內建函式,經過了編譯器的高度優化,執行速度十分快(有些甚至只需要一條指令)。 ## 更多位數 如果需要操作的集合非常大,可以使用 `bitset容器` 。 ## 題目推薦 [Luogu P1225 黑白棋遊戲](https://www.luogu.com.cn/problem/P1225) ## 參考 位運算技巧: Other Builtins of GCC: 英文文件參考: