位元運算是許多程式設計中常見的技巧,最主要的優點不外乎是效率與程式碼的簡潔。但其最大的缺點就是可讀性非常差,而且限制也較多

位元運算往往不直覺,較為艱深,對位元的變化要了若指掌才行。一般不建議在程式碼中使用這些技巧,不僅使他人理解困難外,就算是自己寫的程式碼,也不見得能在短時間內理解,甚至是自己都無法理解。

但若是在講求效率的程式競賽中,位元運算是個常常使用的技巧。不但執行效率高,而且程式碼量少又簡潔。當然還是有個致命的缺點是除錯困難。不過,在像 ACM-ICPC 的競賽中,有些題目就是要使用位元運算來解決。因此,對於競賽的選手來說,基本的位元運算還是非常重要的!

C/C++ 中的位元運算符號

由於我對於 C/C++ 比較熟悉,加上 C/C++ 也是 ACM-ICPC 可使用的語言之一,接下來的程式碼部分,將都使用 C/C++ 中的語法。以下為 C/C++ 中位元運算的運算元:

symbol function
>> 右移運算
<< 左移運算
& and 運算
| or 運算
^ xor 運算
~ not 運算

二的補數

關於二的補數,可以參考維基百科:二的補數

這邊比較需要提到的是:在二的補數系統中,某個整數 x 加上負號,會等於對該數進行 not 運算後加上 1 的值。 (-x~x + 1)

奇數判斷

1
(x & 1) == 1

將數字以二的冪次數加總, x = 2n + 2n-1 + … + 21 + 20。二的冪次中,只有 20 為奇數 1。要判斷一個整數是否為奇數時,僅需要判斷其最低位元是否為 1 即可。因此只要用 110 (00012) 對該數進行 and 運算,若結果仍為 1 即可確定該數為奇數,否則為偶數。

這邊要注意括號,由於 == 的運算優先度高於 &,若不加上括號將使運算變為 x & (1 == 1)。感謝 DJWS 前輩的指教 :)。

二的冪次

2nx

1
x << n

利用左移運算,即可輕易將整數 x 乘以 2n。若僅需要二的羃次,1 << n 即可得到 2n 該數。

x/2n

1
x >> n

利用右移運算,即可輕易將整數 x 除以 2n。需要注意的是,該除法會直接捨去浮點數部分

末端連續 n 個位元 1

1
(1 << n) - 1

1 << n 的會使得第 n 位位元為位元 1,其餘皆為位元 0。減去 12 (00…0012) 時,會使得位元中的第 0 位到第 n - 1 位的位元為位元 1,而第 n 位(含)之後的位元皆為位元 0。位元的樣子將會為 00…0011…11 共 n 個位元 1。

取 2n 的模數

1
x & ((1 << n) - 1)

前面提到 (1 << n) - 1 會得到末端連續 n 個位元 1。利用該數字與 x 進行 and 運算,此時 x 中第 0 位到第 n - 1 位的位元 1 將會被保留,而不管第 n 位(含)之後的位元為何,都將被設定為位元 0。該結果即為對 x 取 2n 的模數。

最低的位元 1

1
x & -x

最低的位元 1 為在位元表示中位於最低位的位元 1。例如: 1410 (11102) 的最低位元為 210 (00102)。

根據二的補數,-x~x + 1。因此在這先將 -x 拆開為 ~x + 1 來討論。x 中最低位的位元 1 經過 not 運算後,將在 ~x 中變為最低位的位元 0。因此對 ~x 加上 1 後,最低位的位元 0 重新變為位元 1,且其後的位元都將被設定為 0。

對任何數加上 1,在位元的意義即為將最低位的位元 0 變為位元 1,且低於該位的位元都將被設定為 0。

經過 and 運算後,x 最低的 1 位元將仍然為 1,且低於該位元的位元也都將為位元 0。另外,因為 x & ~x 會等於 0,所以 ~x 沒有改變的部分會使得高於該位元的位元也都將會是位元 0。此時該最低位的位元 1 將會被獨立出來。

是否為二的冪次

1
(x & -x) == x

此方法根據上述的最低位位元 1 而來,若該數字為二的冪次,那麼該數以位元表示時,僅會有一個位元 1。既然只會有一個位元 1,在求最低位位元 1 時,該最低位位元 1 必為該數字本身。

將第 n 個位元設定為 1

1
x | ( 1 << n )

利用第 n 個位元為 1 的二的冪次數 2n,該數字僅有第 n 個位元為 1。在利用 or 運算,即可保證第 n 個位元被設定為 1。

將第 n 個位元設定為 0

方法一

1
x & ~(1 << n)

取得第 n 個位元為 1 的數字,在對其做 not 運算。因此該數字變為僅有第 n 個位元為 0,而其他位元為 1 的數字。在以該數字與 x 做 and 運算,此時 x 的第 n 個位元將被設定為 0。

方法二

1
~((~x) | (1 << n)

先對該數字做 not 運算,以 x’ 代稱。在利用上述將第 n 個位元設定為 1 的方法,將 x’ 的第 n 個位元設定為 1。最後在對該數做一次 not 運算,由於第 n 個位元在 x’ 中被設定為 1 了,所以在做一次 not 後,將會變為 0。

整數交換

1
x^=y^=x^=y

拆開來看即為:

1
2
3
x = x ^ y;
y = y ^ x;
x = x ^ y;

這個方法基於 xor(^) 的特性:

  • 任何數對本身做 xor 運算都會等於 0
  • 任何數對 0 做 xor 運算都會等於本身

在第一次進行 xor 運算後,此時的 x 等於 x ^ y,暫時稱為 x'。第二次進行 xor 運算後 y 等於 y ^ x',將 x' 還原後的 yy ^ x ^ y。根據前面提到 xor 的特性, y 即等於 x,暫時以 y' 代稱。第三次的 xor 運算 x 又等於 x' ^ y',將 x'y' 展開後,x 等於 x ^ y ^ x,同樣根據 xor 的特性, x 會等於 y。如此一來就完成了兩個整數的交換。

消去最低位 1 位元

方法一

1
x & ( x - 1 )

x 減去 1 時,其最低位位元 1 將會被低於該位的位數拿去使用。因此在最低位位元 1 之前的位元 0 都將變為位元 1,而該最低位位元 1 將會變成位元 0。再透過 and 運算,最低位 1 位元將被消去。

方法二

1
x - ( x & -x)

利用取得最低位位元 1 的方法,將原數字直接減去該位元即可。

應用

消去最低位位元 1 再應用上可以用來計算位元 1 的數量。另外還有個經典的應用,Binary Indexed Tree 的操作上就是使用消去或最低位位元 1,有興趣可以去研究一下,真的是非常漂亮的資料結構。