位運算就是基於整數的二進位制表示進行的運算。由於計算機內部就是以二進位制來儲存資料,位運算是相當快的。
之前有總結過位運算的技巧,但稍微對以前寫的文章不太滿意,所以重新總結一下
常用的運算子共 6 種,分別為與( &
)、或( |
)、異或( ^
)、取反( ~
)、左移( <<
)和右移( >>
)。
與、或、異或
與( &
)或( |
)和異或( ^
)這三者都是兩數間的運算,因此在這裡一起講解。
它們都是將兩個整數作為二進位制數,對二進位制表示中的每一位逐一運算。
運算子 | 解釋 |
---|---|
& |
只有兩個對應位都為 1 時才為 1 |
| |
只要兩個對應位中有一個 1 時就為 1 |
^ |
只有兩個對應位不同時才為 1 |
異或運算的逆運算是它本身,也就是說兩次異或同一個數最後結果不變,即 \(a \text{^} b \text{^} b = a\) 。
舉例:
取反
取反是對一個數 \(num\) 進行的計算,即單目運算。
~
把 \(num\) 的補碼中的 0 和 1 全部取反(0 變為 1,1 變為 0)。有符號整數的符號位在 ~
運算中同樣會取反。
補碼:在二進位制表示下,正數和 0 的補碼為其本身,負數的補碼是將其對應正數按位取反後加一。
舉例(有符號整數):
左移和右移
num << i
表示將 \(num\) 的二進位制表示向左移動 \(i\) 位所得的值。
num >> i
表示將 \(num\) 的二進位制表示向右移動 \(i\) 位所得的值。
舉例:
在 C++ 中,右移操作中右側多餘的位將會被捨棄,而左側較為複雜:對於無符號數,會在左側補 0;而對於有符號數,則會用最高位的數(其實就是符號位,非負數為 0,負數為 1)補齊。左移操作總是在右側補 0。
複合賦值位運算子
和 +=
, -=
等運算子類似,位運算也有複合賦值運算子: &=
, |=
, ^=
, <<=
, >>=
。(取反是單目運算,所以沒有。)
關於優先順序
位運算的優先順序低於算術運算子(除了取反),而按位與、按位或及異或低於比較運算子,所以使用時需多加註意,在必要時新增括號。
位運算的應用
位運算一般有三種作用:
-
高效地進行某些運算,代替其它低效的方式。
-
表示集合。(常用於 狀壓 DP 。)
-
題目本來就要求進行位運算。
需要注意的是,用位運算代替其它運算方式(即第一種應用)在很多時候並不能帶來太大的優化,反而會使程式碼變得複雜,使用時需要斟酌。(但像“乘 2 的非負整數次冪”和“除以 2 的非負整數次冪”就最好使用位運算,因為此時使用位運算可以優化複雜度。)
乘 2 的非負整數次冪
int mulPowerOfTwo(int n, int m) { // 計算 n*(2^m)
return n << m;
}
除以 2 的非負整數次冪
int divPowerOfTwo(int n, int m) { // 計算 n/(2^m)
return n >> m;
}
!!! warning
我們平常寫的除法是向 0 取整,而這裡的右移是向下取整(注意這裡的區別),即當數大於等於 0 時兩種方法等價,當數小於 0 時會有區別,如: -1 / 2
的值為 \(0\) ,而 -1 >> 1
的值為 \(-1\) 。
判斷一個數是不是 2 的正整數次冪
bool isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; }
對 2 的非負整數次冪取模
int modPowerOfTwo(int x, int mod) { return x & (mod - 1); }
取絕對值
在某些機器上,效率比 n > 0 ? n : -n
高。
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
高。
// 如果 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); }
判斷符號是否相同
bool isSameSign(int x, int y) { // 有 0 的情況例外
return (x ^ y) >= 0;
}
交換兩個數
void swap(int &a, int &b) { a ^= b ^= a ^= b; }
獲取一個數二進位制的某一位
// 獲取 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\) 實現
遍歷某個集合的子集
// 遍歷 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 中還有一些用於位運算的內建函式:詳細文章介紹
-
int __builtin_ffs(int x)
:返回 \(x\) 的二進位制末尾最後一個 \(1\) 的位置,位置的編號從 \(1\) 開始(最低位編號為 \(1\) )。當 \(x\) 為 \(0\) 時返回 \(0\) 。 -
int __builtin_clz(unsigned int x)
:返回 \(x\) 的二進位制的前導 \(0\) 的個數。當 \(x\) 為 \(0\) 時,結果未定義。 -
int __builtin_ctz(unsigned int x)
:返回 \(x\) 的二進位制末尾連續 \(0\) 的個數。當 \(x\) 為 \(0\) 時,結果未定義。 -
int __builtin_clrsb(int x)
:當 \(x\) 的符號位為 \(0\) 時返回 \(x\) 的二進位制的前導 \(0\) 的個數減一,否則返回 \(x\) 的二進位制的前導 \(1\) 的個數減一。 -
int __builtin_popcount(unsigned int x)
:返回 \(x\) 的二進位制中 \(1\) 的個數。 -
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容器
。
題目推薦
參考
位運算技巧: https://graphics.stanford.edu/~seander/bithacks.html
Other Builtins of GCC: https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html