位元運算
範圍 — 整數在位元層次上是怎麼被表示的(二補數、固定位寬、位移),以及建立在其上的各種運算與技巧:遮罩、XOR 恆等式、最低位 1、子集列舉,還有 bitmask DP。 另見:bit_manipulation_examples.md — 支撐這些技巧的十四道詳解題;math.md — 不靠位元的數值操作;combinatorics_math_patterns.md — 計數;dp.md — bitmask DP 所屬的完整 DP 目錄。
LeetCode 題目清單
總覽
位元運算直接操作整數的二進位表示。因為每個運算都只是一道 CPU 指令,位元技巧能把許多
O(n) 的掃描變成 O(1) 的算術,也能讓一個小整數當成最多 32/64 個旗標的緊湊集合
(bitmask)。
關鍵性質
- 時間複雜度:每個位元運算
O(1);整個字組的掃描是O(number of bits)≈O(32) - 空間複雜度:
O(1)— 一個遮罩重複使用同一個整數,取代陣列或集合 - 核心想法:用
&|^~<<>>讀取/翻轉個別位元;XOR 會讓成對的值互相抵消(a ^ a = 0) - 什麼時候用:配對/抵消類的題目、算 1 的個數、子集列舉(bitmask)、判斷 2 的次方、不用
+做加法、把旗標打包進一個數字
速查表 — 一定要背起來的技巧 Priority 5 of 5 — Must know — expect it in almost every loop
| 目標 | 表達式 |
|---|---|
測試第 i 位是不是 1 |
(x >> i) & 1 |
把第 i 位設為 1 |
x | (1 << i) |
把第 i 位清成 0 |
x & ~(1 << i) |
翻轉第 i 位 |
x ^ (1 << i) |
| 取出最低位的 1 | x & -x |
| 清掉最低位的 1 | x & (x - 1) |
| 是不是 2 的次方? | x > 0 && (x & (x - 1)) == 0 |
| 是不是偶數? | (x & 1) == 0 |
| XOR 自我抵消 | a ^ a = 0, a ^ 0 = a |
參考資料
- LeetCode — Bit Manipulation card
- bit_manipulation.md
- leetcode-easy-bitwise-xor-summary
- bit VS byte VS char — 各種位寬,附 Java 範例
- Python operators — 運算子優先順序表
0) 電腦科學基礎 Priority 5 of 5 — Must know — expect it in almost every loop
幾乎每一個看起來「很巧妙」的位元技巧,都只是機器如何儲存整數的直接後果。 五個事實就講完了 — 把它們學起來,下面的技巧就不再需要硬背。
| # | 事實 | 它解釋了什麼 |
|---|---|---|
| 1 | 一個數字是「數字 × 位值」,而二進位的位值就是 2 的次方 | 用手讀寫二進位/十六進位 |
| 2 | int 是一個固定寬度的 32 位元盒子,滿了會繞回來 |
溢位、MIN_VALUE、為什麼需要遮罩 |
| 3 | 負數是用二補數存的 | ~x = -x-1、x & -x、負數的 >> |
| 4 | 位移就是乘以/除以 2 的次方 | <<、>>、>>> 和它們的邊界狀況 |
| 5 | 一個 int 的各個位元就是 32 個項目的子集 | bitmask、子集列舉、bitmask DP |
0-1) 位值、二進位與十六進位
一個 X 進位數字的實際數值,由每一位的數字和它的位置共同決定
(參考):
123.45 (base 10) = 1*10^2 + 2*10^1 + 3*10^0 + 4*10^-1 + 5*10^-2
720.5 (base 8) = 7*8^2 + 2*8^1 + 0*8^0 + 5*8^-1
1011 (base 2) = 1*2^3 + 0*2^2 + 1*2^1 + 1*2^0 = 8 + 0 + 2 + 1 = 11
要讀二進位,就把帶著 1 的那些位值加起來;要寫二進位,就一直除以 2、由下往上記下餘數。 整個轉換就只有這樣。
十六進位就是四個一組的二進位。 一個十六進位數字 = 4 個位元(一個 nibble),
所以一個 32 位元的 int 剛好是 8 個十六進位數字 — 這也是遮罩都寫成這種形式的原因:
1011 1101 -> B D -> 0xBD = 11*16 + 13 = 189
each nibble maps to one hex digit — no arithmetic needed
0xFFFFFFFF = 32 ones 0x7FFFFFFF = 31 ones = Integer.MAX_VALUE
2 的次方值得背熟 — 它們同時會以「題目限制」和「遮罩」兩種身分出現:
n |
4 | 8 | 10 | 16 | 20 | 31 | 32 |
|---|---|---|---|---|---|---|---|
2^n |
16 | 256 | 1024 | 65536 | 約一百萬 | 2 147 483 648 | 4 294 967 296 |


0-2) 固定寬度 — int 是一個 32 位元的盒子
| Java 型別 | 寬度 | 範圍 |
|---|---|---|
byte |
8 bits | -128 … 127 |
char |
16 bits | 0 … 65535(無號) |
int |
32 bits | -2^31 … 2^31 - 1 |
long |
64 bits | -2^63 … 2^63 - 1 |
- 第
i位帶著位值2^i。在int裡,第 31 位是符號位,所以真正能用的大小只有 31 位 — 這就是為什麼那麼多解法都在跑for i in 0..31。 - 任何超出盒子的運算都會無聲地繞回來:
Integer.MAX_VALUE + 1 == Integer.MIN_VALUE。 題目寫「假設結果可以用 32 位元整數表示」時,其實就是在告訴你這正是它要考的邊界狀況。 1 << 31已經是負數了。想要2^31這個數值時,要寫1L << 31。
0-3) 二補數 — 負數是怎麼存的 Priority 5 of 5 — Must know — expect it in almost every loop
規則:-x 是以 ~x + 1 的形式儲存的 — 每個位元都翻轉,然後加一。
等價地說,-x 的位元樣式就是無號數 2^32 - x。
(traces are 8-bit for space; a real int is the same picture, 32 wide)
5 = 0000 0101
~5 = 1111 1010 flip every bit
-5 = 1111 1011 ... + 1
check: 5 + (-5) = 1 0000 0000 -> the carry falls out of the box, leaving 0 ✓
為什麼用這種表示法,而不是「符號位 + 大小」?因為這樣一個加法器就能處理兩種符號 —
不管運算元是正是負,a + b 都走同一組電路,而且零只有一種寫法。
三個直接推論:
| 事實 | 會在哪裡遇到 |
|---|---|
~x == -x - 1 |
在沒有無號型別的語言裡改寫 ~ |
| 最高位是 1 ⇔ 負數 | for i in 0..31 那種逐位掃描的迴圈 |
x >> 31 不是 0(非負)就是 -1(負) |
無分支的 abs、取出符號 |
會咬人的不對稱:範圍裡的負數比正數多一個,所以 Integer.MIN_VALUE 沒有對應的正數 —
-Integer.MIN_VALUE 和 Math.abs(Integer.MIN_VALUE) 算完都還是 Integer.MIN_VALUE。
這一個值正是 LC 29(Divide Two Integers)藏起來的測資;在取負號之前就要先處理掉它。
0-4) 為什麼 x & (x - 1) 和 x & -x 有效
兩者都是從二補數直接推出來的。推導過一次,就再也不用去背哪個是哪個:
x = 0101 1000 lowest set bit is bit 3
x - 1 = 0101 0111 borrowing flips that 1 to 0, and every 0 below it to 1
-x = 1010 1000 = ~x + 1
above bit 3 at bit 3 below bit 3
x vs x-1 : identical 1 vs 0 complementary
x vs -x : complementary 1 vs 1 both 0
x & (x-1) = 0101 0000 above survives; bit 3 and everything under it AND to 0
x & (-x) = 0000 1000 above ANDs to 0; only bit 3 survives
所以 x & (x - 1) 的意思是**「丟掉最低位的 1」**(把它包成迴圈 → Brian Kernighan 的
popcount,見 §1-3);x & -x 的意思是
「只留下最低位的 1」(這也是 Fenwick tree 的前進規則 — 見
binary_indexed_tree.md)。
0-5) 位移:左移、算術右移、邏輯右移
| 運算子 | 名稱 | 補進來的是 | 效果 |
|---|---|---|---|
x << n |
左移 | 右邊補 0 | x * 2^n;被擠出最高位的位元會消失 |
x >> n |
算術右移 | 複製符號位 | floor(x / 2^n) |
x >>> n |
邏輯右移(只有 Java 有) | 補 0 | 把整個位元樣式當成無號數 |
-8 >> 1 = -4 1111 1000 -> 1111 1100 sign preserved
-8 >>> 1 = 2147483644 1111 1000 -> 0111 1100 sign bit treated as just another bit
什麼時候需要 >>>:任何要走完一個可能為負的 int 全部 32 個位元的迴圈 —
LC 190(Reverse Bits)、LC 191(Number of 1 Bits)、LC 338。
用 >> 的話,負數會永無止盡地補進 1,while (x != 0) 永遠不會結束。
另外兩條會讓人意外的規則(上面都驗證過了):
- Java 會把位移量取低 5 位:
1 << 32等同1 << 0,也就是1— 不是0。 要嘛改移long(取低 6 位),要嘛把位移拆成兩次。 +比<<更緊:x << 1 + 2其實是x << 3。見 §0-7。
0-6) 在這件事上 Python 不是 Java Priority 5 of 5 — Must know — expect it in almost every loop
Python 的整數是任意精度的,行為上就像有無限多個符號位。
沒有盒子,所以沒有東西會溢位 — 也沒有 >>>,因為根本沒有一個「最高位」可以停。
Java(int,32 位元) |
Python(無上限) | |
|---|---|---|
1 << 31 |
-2147483648(撞到符號位了) |
2147483648 |
Integer.MAX_VALUE + 1 |
繞回 MIN_VALUE |
就繼續變大 |
-1 >> 100 |
-1 |
-1(無限多個符號位) |
| 邏輯右移 | x >>> n |
沒有 — 要自己用遮罩模擬 |
~5 |
-6 |
-6(一樣) |
所以一段倚賴 32 位元繞回行為的 Python 迴圈,必須自己把盒子做出來:
# python
MASK = 0xFFFFFFFF # keep only the low 32 bits
INT_MAX = 0x7FFFFFFF # 2^31 - 1
def to_signed(x):
"""read a masked 32-bit pattern back as a signed Python int"""
return x if x <= INT_MAX else ~(x ^ MASK)
這正是為什麼 LC 371(Sum of Two Integers)在 Python 看起來比 Java 醜那麼多:
進位迴圈其實是同樣的三行,但每一步都得 & MASK,最後的結果還得用 to_signed 轉回來。
經驗法則:在 Python 裡,只要
x可能為負,就用for i in range(32): (x >> i) & 1逐位掃描,而不要用while x:— 那個while不會停。
0-7) 優先順序 — 全部加上括號 Priority 4 of 5 — High value — a gap here costs you rounds
由緊到鬆 — 這條鏈在 C、Java、Python 裡都一樣:
~ -> * / % -> + - -> << >> -> & -> ^ -> |
這個順序會造成兩種出錯:
x << 1 + 2 parses as x << (1 + 2) -> x << 3, not (x << 1) + 2
a ^ b + 1 parses as a ^ (b + 1)
比較運算子則是三個語言唯一不一致的地方:C 和 Java 把 == 塞在 >> 和 & 中間,
Python 卻把它放在 | 下面。所以 x & 1 == 0 在三個語言裡是三件不同的事:
| 語言 | x & 1 == 0 實際被解讀成 |
結果 |
|---|---|---|
| C / C++ | x & (1 == 0) → x & 0 |
無聲地永遠是 0 |
| Java | x & (1 == 0) → int & boolean |
編譯錯誤(bad operand types) |
| Python | (x & 1) == 0 |
正確 — 在 Python 裡比較運算子綁得比 & 更鬆 |
不要去賭你現在在哪個語言裡。就寫 (x & 1) == 0。
0-8) 一個 bitmask 就是一個集合
最後一塊基礎:n 個項目的所有子集,和整數 0 … 2^n - 1 是一對一對應的。
看懂這件事之後,「bitmask」就不需要再多解釋了 — 每一個集合操作都只是一道指令。
| 集合語言 | 位元語言 |
|---|---|
S = {} / S = {0..n-1} |
0 / (1 << n) - 1 |
i ∈ S |
(mask >> i) & 1 |
S ∪ {i} / S \ {i} / 翻轉 i |
mask | (1<<i) / mask & ~(1<<i) / mask ^ (1<<i) |
A ∪ B / A ∩ B / A \ B |
a | b / a & b / a & ~b |
A ⊆ B |
(a & b) == a |
A ∩ B = ∅ |
(a & b) == 0 |
|S| |
Integer.bitCount(mask) / bin(mask).count("1") |
在 n 個項目內取補集 |
mask ^ ((1 << n) - 1) |
兩個計數事實能告訴你 bitmask 解法塞不塞得進題目的限制:
- 子集有
2^n個,所以O(2^n · n)的 DP 需要n ≤ 約 20(2^20 ≈ 10^6); - 把所有遮罩加總起來,子遮罩的總數是
3^n而不是4^n— 這正是 §2-1 裡sub = (sub - 1) & mask迴圈跑得動的原因。
0-9) 標準函式庫的輔助函式 — 值 vs 索引 Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern
每個語言都內建這些函式,而拿錯那一個是無聲的 bug,不是編譯錯誤。造成這件事的分界是:
有些函式回傳的是值(一個只有單一位元被設起來的模式),有些回傳的是索引(0…31)。
兩者永遠不能互換。
x = 0b101000 (= 40)
Integer.highestOneBit(x) = 0b100000 = 32 <- a VALUE
Integer.lowestOneBit(x) = 0b001000 = 8 <- a VALUE (exactly x & -x)
Integer.numberOfTrailingZeros(x) = 3 <- an INDEX
31 - Integer.numberOfLeadingZeros(x) = 5 <- an INDEX
下面這兩欄只有在 x >= 0 時才等價 — 原因見下面的「負數」小節。
| 目的 | Java(x >= 0) |
Python(x >= 0) |
|---|---|---|
| popcount(數 1 的個數) | Integer.bitCount(x) |
x.bit_count()(3.10+),否則 bin(x).count("1") |
| 最低的 1 — 值 | Integer.lowestOneBit(x) |
x & -x |
| 最低的 1 — 索引 | Integer.numberOfTrailingZeros(x) |
(x & -x).bit_length() - 1 |
| 最高的 1 — 值 | Integer.highestOneBit(x) |
1 << (x.bit_length() - 1) |
最高的 1 — 索引 = floor(log2 x) |
31 - Integer.numberOfLeadingZeros(x) |
x.bit_length() - 1 |
| 反轉全部 32 個位元 | Integer.reverse(x) |
沒有 — 自己跑迴圈(LC 190) |
| 把位元印出來 | Integer.toBinaryString(x) |
bin(x),或 format(x, "032b") |
| 解析二進位字串 | Integer.parseInt(s, 2) |
int(s, 2) |
負數 — 兩欄從哪裡開始不一致
Java 的這些函式讀的是32 位元的二補數樣式,所以符號位只是「又一個位元」。
Python 的 bit_length() 和 bit_count() 讀的是絕對值,根本沒有符號位
(§0-6)。而且各列壞掉的程度並不一樣:
x = -19 Java Python
popcount 30 3 <- DIVERGES (Java counts the sign bits)
lowest set bit (value) 1 1 <- agrees
lowest set bit (index) 0 0 <- agrees
highest set bit (value) -2147483648 16 <- DIVERGES (Java: always the sign bit)
highest set bit (index) 31 4 <- DIVERGES (Java: always 31)
最低位的那幾列活得下來,因為 x & -x 在兩個語言裡是同一套算術。
最高位那幾列和 popcount 則不行:對任何負的 int,Java 的最高位 1 就是符號位,
所以值永遠是 Integer.MIN_VALUE,索引永遠是 31。
如果你需要在 Python 裡得到 Java 的答案,要先遮成 32 位元(x & 0xFFFFFFFF)。
零的情況也是沿著「值 vs 索引」這條線分開的 — 而這正是會咬人的地方:
value helpers -> Integer.highestOneBit(0) = 0
Integer.lowestOneBit(0) = 0
raw counts -> Integer.numberOfTrailingZeros(0) = 32 <- a count, not a position
Integer.numberOfLeadingZeros(0) = 32
derived index -> 31 - Integer.numberOfLeadingZeros(0) = -1
(0).bit_length() - 1 (Python) = -1
那些計數是誠實的 — 0 裡面確實有 32 個 0 位元。bug 是在你把一個計數當成位置來用的那一刻
才出現的,而且兩個方向壞的方式不一樣:尾端 0 的計數在上面那張表裡就是最低位 1 的索引,
所以它會給你 32,而 1 << 32 會被 Java 取回 1
(§0-5)— 得到的是一個錯的答案,
而不是一個崩潰。前導 0 的計數則是透過 31 - … 轉成索引,所以它會給你 -1,
而 1 << -1 會依語言不同而丟例外或位移 31 位。
所以要擋的不是「每一次計數呼叫之前」,而是「把計數當成位置用之前,先擋掉 x == 0」。
Integer.bitCount(0) 和那兩個回傳值的函式,完全不需要防護。
Python 的 log2 是 bit_length(),不是 math.log2。 浮點數的尾數只有 53 個位元,
所以一旦整數需要的精度超過這個範圍,四捨五入就會給你差一:
# python
# IDEA: floor(log2 x) == x.bit_length() - 1, exactly, for every x > 0
x = (1 << 53) - 1
x.bit_length() - 1 # 52 <- correct
int(math.log2(x)) # 53 <- WRONG: log2 rounded up to 53.0
x = (1 << 64) - 1
x.bit_length() - 1 # 63 <- correct
int(math.log2(x)) # 64 <- WRONG
手寫的版本也要會。「不要用 Integer.bitCount,把 1 的個數數出來」不是刁難 —
那就是 LC 191 的全部。x &= (x - 1) 迴圈在 §1-3;
先把函式庫的呼叫講出來,然後再把迴圈寫出來。
0-10) XOR 前綴,以及 0..n 的封閉形式 Priority 4 of 5 — High value — a gap here costs you rounds
XOR 就是把進位丟掉的加法(§0-4), 所以每一個前綴和的技巧都有一個 XOR 版的雙胞胎 — 而且 XOR 那個更簡單, 因為 XOR 是自己的反運算,不需要減法那一步。
用前綴做區間 XOR
pre[0] = 0
pre[i+1] = pre[i] ^ a[i]
a[l] ^ a[l+1] ^ ... ^ a[r] = pre[r+1] ^ pre[l]
為什麼是 ^ 而不是 -:l 之前的每一個元素,在 pre[r+1] ^ pre[l] 裡都出現兩次,
左右各一次,所以自己把自己消掉了。用「和」的時候你必須做減法;用 XOR 的時候,
同一個運算子就把自己還原了。
# python
# IDEA: pre[i+1] = XOR of a[0..i]; any range XOR is then one operation
# time = O(n) build + O(1) per query, space = O(n)
def build(a):
pre = [0] * (len(a) + 1)
for i, v in enumerate(a):
pre[i + 1] = pre[i] ^ v
return pre
def range_xor(pre, l, r): # inclusive [l, r]
return pre[r + 1] ^ pre[l]
O(1) 求出 0..n 的 XOR
每一個對齊的四連塊都會自己消掉,這讓整個前綴塌縮成一次對 n % 4 的查表:
4k = ...00
4k+1 = ...01 (4k) ^ (4k+1) = 1 <- differ only in bit 0
4k+2 = ...10
4k+3 = ...11 (4k+2) ^ (4k+3) = 1 <- differ only in bit 0
1 ^ 1 = 0 <- so each aligned quadruple vanishes
只有最後一個完整區塊之後的尾巴會留下來:
n % 4 |
0 ^ 1 ^ … ^ n |
|---|---|
0 |
n |
1 |
1 |
2 |
n + 1 |
3 |
0 |
# python
# IDEA: aligned blocks of 4 cancel; only n % 4 decides what is left
# time = O(1), space = O(1)
def xor_to(n): # XOR of 0..n (identical to 1..n, since 0 changes nothing)
return [n, 1, n + 1, 0][n % 4]
def xor_range(l, r): # XOR of l..r
return xor_to(r) ^ xor_to(l - 1)
xor_to 同時也涵蓋了 1..n — XOR 進一個 0 什麼都不會改變
(§0-8 有這條恆等式:a ^ 0 = a)。
會在哪裡出現:任何「一段區間的 XOR」查詢;以及在 LC 268(Missing Number)裡,
用來取代 x1 = 1 ^ 2 ^ … ^ n 那個迴圈的 O(1) 寫法。LC 2683(Neighboring Bitwise XOR)
則是把前綴的想法反過來跑。LC 1310 — XOR Queries of a Subarray 是前綴 XOR 最標準的練習題;
這個 repo 目前還沒有它的解答。
0-11) 無分支寫法 Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern
一個式子做掉大部分的事:對非負的 int,x >> 31 是 0;對負的則是 -1(全部都是 1)
(§0-3)。
全 1 的遮罩 AND 起來是保留,全 0 的遮罩 AND 起來是丟掉 — 分支就是這樣變成算術的。
| 目的 | 式子 | 為什麼成立 |
|---|---|---|
a 和 b 正負號相反嗎? |
(a ^ b) < 0 |
XOR 的符號位元剛好在兩個符號位元不同時是 1 — 而且不像 a * b < 0 會溢位 |
abs(x) |
(x ^ (x >> 31)) - (x >> 31) |
x ≥ 0 → (x ^ 0) - 0 = x;x < 0 → (x ^ -1) - (-1) = ~x + 1 = -x |
用 0 / -1 表示 x 的正負號 |
x >> 31 |
把符號位元塗滿全部 32 位 |
x 是 2^k 的倍數嗎? |
(x & ((1 << k) - 1)) == 0 |
低 k 位就是餘數 |
把 x 往下取整到 2^k 的倍數 |
x & ~((1 << k) - 1) |
把餘數那幾位清掉 |
abs 一樣會踩到 MIN_VALUE 的陷阱,和 Math.abs 完全一樣:兩者都會原封不動回傳
Integer.MIN_VALUE,因為它沒有對應的正數
(§0-3)。改成無分支寫法救不了你。
絕對不要寫出去的那一個 — XOR 交換
// java
// IDEA: swap without a temporary. Correct ONLY when the two operands are distinct.
a ^= b; b ^= a; a ^= b;
這是最經典的「你看,不用暫存變數」的答案,而它會在兩個運算元指向同一個位置時,把值毀掉:
swap(arr, i, j) with i == j, arr[i] = 7
arr[i] ^= arr[j] -> 7 ^ 7 = 0 both names point at the same slot
arr[j] ^= arr[i] -> 0 ^ 0 = 0
arr[i] ^= arr[j] -> 0 ^ 0 = 0 the 7 is gone
任何有可能呼叫到 swap(i, i) 的 partition 步驟 — 好幾種標準的 quicksort 和荷蘭國旗
partition 都會 — 都會被它無聲地清成 0。用一個暫存變數。
那個多出來的變數從來就不是瓶頸,也從來沒有面試官因為你把它省掉而加分。
0-12) 自己推導一次
§0 的重點是:這些都是推得出來的結果,不是要背的單字。把右邊那一欄遮起來, 用二補數和位值把每一條重建一次;做得到的話,就算哪天把這份文件裡的技巧全忘光, 你也能重新推回來。
每一列都假設 x >= 0,這也是它們在題目裡出現的樣子。其中在負數上會直接失去意義的,
是格雷碼和「相鄰的 1」那兩條 — 在 Python 裡 (-1) ^ (-1 >> 1) 是 0、
(-1) & (-1 >> 1) 是 -1,因為符號位是無窮多個
(§0-6)。
| 式子 | 它做什麼,以及從哪裡來 |
|---|---|
x & (x - 1) |
清掉最低的那個 1 — x-1 會從尾端的 0 一路借位,所以在那一位和它以下,兩者都不一致 |
x & -x |
只留下最低的那個 1 — -x = ~x + 1,所以在那一位以上,兩者互補 |
x & (x + 1) |
清掉尾端連續的 1 — x & (x - 1) 的鏡像 |
x | (x + 1) |
設起最低的那個 0 — 進位會一路傳到第一個 0 |
x ^ (x >> 1) |
x 的格雷碼 — 相鄰的碼只差一個位元(LC 89) |
x & (x >> 1) |
不為 0 ⇔ x 有兩個相鄰的 1 |
(x >> i) & 1 |
讀第 i 位 — 把它移到第 0 位,再把其他位遮掉 |
x ^ ((1 << n) - 1) |
在 n 位內取補數 — 跟一整排 1 做 XOR,每一位都被翻轉 |
(x & (x - 1)) == 0 |
x 是 2 的冪 或 0 — 最多只有一個 1;要排除 0 得再加上 x > 0 |
x >> 31 |
0 或 -1 — 符號位元塗滿整個寬度 |
用這張表要誠實,有兩條規則:先把式子做什麼講出來,再去對答案,
以及講出哪一個輸入會讓它壞掉。(x & (x - 1)) == 0 會把 0 也算進去,
是最多人只答對一半的那一條。
1) 核心運算
1-1) 六個運算子
| 運算子 | 名稱 | 規則 | 範例(4-bit) |
|---|---|---|---|
& |
AND | 兩邊都是 1 才是 1 | 0110 & 1010 = 0010 |
| |
OR | 任一邊是 1 就是 1 | 0110 | 1010 = 1110 |
^ |
XOR | 兩邊不同才是 1 | 0110 ^ 1010 = 1100 |
~ |
NOT | 每個位元都翻轉(~x = -x - 1) |
~0110 = ...1001 |
<< |
左移 | 尾端補 n 個 0 → x * 2^n |
0011 << 1 = 0110 |
>> |
右移 | 丟掉低位的 n 個位元 → x // 2^n |
0110 >> 1 = 0011 |
XOR 恆等式(許多 LC 題目的核心):
a ^ a = 0、a ^ 0 = a, XOR 具交換律與結合律 → 把整份清單 XOR 起來,出現偶數次的值全部抵消, 只剩下出現奇數次的那一個。
1-2) 單一位元的技巧(附程式碼)
// java
int testBit(int x, int i) { return (x >> i) & 1; } // 1 if bit i is set, else 0
int setBit(int x, int i) { return x | (1 << i); } // force bit i to 1
int clearBit(int x, int i) { return x & ~(1 << i); } // force bit i to 0
int toggleBit(int x, int i){ return x ^ (1 << i); } // flip bit i
int lowestSetBit(int x) { return x & -x; } // isolate lowest 1-bit
int clearLowestBit(int x) { return x & (x - 1); } // turn OFF lowest 1-bit
# python
def test_bit(x, i): return (x >> i) & 1 # 1 if bit i is set, else 0
def set_bit(x, i): return x | (1 << i) # force bit i to 1
def clear_bit(x, i): return x & ~(1 << i) # force bit i to 0
def toggle_bit(x, i): return x ^ (1 << i) # flip bit i
def lowest_set_bit(x): return x & -x # isolate lowest 1-bit
def clear_lowest(x): return x & (x - 1) # turn OFF lowest 1-bit
1-3) 計算 1 的個數(population count)
核心想法:x & (x - 1) 會清掉最低位的 1(原因見 §0-4),
所以迴圈每個 1 只跑一次(Brian Kernighan 演算法)→ 是 O(popcount) 而不是 O(32)。
// java
// IDEA: each `x &= (x - 1)` removes exactly one set bit
public int countBits(int x) {
int count = 0;
while (x != 0) {
x &= (x - 1); // clear lowest set bit
count++;
}
return count;
// built-in: Integer.bitCount(x)
}
# python
# IDEA: each `x &= (x - 1)` removes exactly one set bit
def count_bits(x):
count = 0
while x:
x &= (x - 1) # clear lowest set bit
count += 1
return count
# built-in: bin(x).count("1")
視覺追蹤 — count_bits(12),12 = 1100:
x = 1100 x & (x-1) = 1100 & 1011 = 1000 count = 1
x = 1000 x & (x-1) = 1000 & 0111 = 0000 count = 2
x = 0000 stop → 2 set bits
1-4) 沿著位元「欄」做計數 — LC 461 / LC 477
模式:不要對數字兩兩配對跑迴圈,改成對 32 個位元位置跑迴圈,問每一欄各貢獻多少。
這能把很多看起來像 O(n^2) 的問題壓成 O(32n)。
核心想法:在位元位置 i,若有 ones 個數字在該位是 1、n - ones 個是 0,
那麼在這一位上剛好有 ones * (n - ones) 對數字不同。把 32 個位置加總即可。
// java
// LC 461 - Hamming Distance (the 2-number base case)
// IDEA: differing bits of x and y are exactly the set bits of x ^ y
// time = O(popcount), space = O(1)
class Solution {
public int hammingDistance(int x, int y) {
int diff = x ^ y, count = 0;
while (diff != 0) {
diff &= (diff - 1); // clear lowest set bit
count++;
}
return count; // built-in: Integer.bitCount(x ^ y)
}
}
// LC 477 - Total Hamming Distance (all pairs)
// IDEA: per bit column, ones * (n - ones) pairs differ there
// time = O(32 * N), space = O(1)
class Solution2 {
public int totalHammingDistance(int[] nums) {
int n = nums.length, total = 0;
for (int i = 0; i < 32; i++) {
int ones = 0;
for (int x : nums) ones += (x >> i) & 1; // count 1s in column i
total += ones * (n - ones); // each 1 pairs with each 0
}
return total;
}
}
# python
# LC 461 - Hamming Distance
# time = O(popcount), space = O(1)
class Solution(object):
def hammingDistance(self, x, y):
diff, count = x ^ y, 0
while diff:
diff &= diff - 1 # clear lowest set bit
count += 1
return count # built-in: bin(x ^ y).count("1")
# LC 477 - Total Hamming Distance
# IDEA: per bit column, ones * (n - ones) pairs differ there
# time = O(32 * N), space = O(1)
class Solution2(object):
def totalHammingDistance(self, nums):
n, total = len(nums), 0
for i in range(32):
ones = sum((x >> i) & 1 for x in nums) # count 1s in column i
total += ones * (n - ones)
return total
為什麼可行 — [4, 14, 2] = 00100, 01110, 00010:
bit column : 0 1 2 3
ones : 0 2 2 1 n = 3
zeros : 3 1 1 2
pairs : 0*3 2*1 2*1 1*2 -> 0 + 2 + 2 + 2 = 6
1-5) 把 bitmask 當成字元集合 — LC 318 Priority 4 of 5 — High value — a gap here costs you rounds
模式:小寫字母的集合只需要 26 個位元,所以整個單字可以壓成一個 int。
接著所有集合問題都變成單一指令:
| 集合問題 | 位元表達式 |
|---|---|
| 兩個單字有共同字母嗎? | (maskA & maskB) != 0 |
| 兩者互斥嗎? | (maskA & maskB) == 0 |
| 字母的聯集 | maskA | maskB |
| 有幾個相異字母? | Integer.bitCount(mask) / bin(mask).count("1") |
| 單字裡有重複字母嗎? | 建構過程中檢查:(mask & bit) != 0 |
這把每一對都要花 O(len) 的字串比較,換成 O(1) 的 AND。
// java
// LC 318 - Maximum Product of Word Lengths
// IDEA: encode each word's letters as a 26-bit mask; two words share no letter iff (mA & mB) == 0
// time = O(N * L + N^2), space = O(N)
class Solution {
public int maxProduct(String[] words) {
int n = words.length;
int[] mask = new int[n];
for (int i = 0; i < n; i++) {
for (char c : words[i].toCharArray()) {
mask[i] |= 1 << (c - 'a'); // add letter c to the set
}
}
int best = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if ((mask[i] & mask[j]) == 0) { // disjoint letter sets
best = Math.max(best, words[i].length() * words[j].length());
}
}
}
return best;
}
}
# python
# LC 318 - Maximum Product of Word Lengths
# IDEA: encode each word's letters as a 26-bit mask; disjoint iff (mA & mB) == 0
# time = O(N * L + N^2), space = O(N)
class Solution(object):
def maxProduct(self, words):
masks = []
for w in words:
m = 0
for c in w:
m |= 1 << (ord(c) - ord('a')) # add letter c to the set
masks.append(m)
best = 0
for i in range(len(words)):
for j in range(i + 1, len(words)):
if masks[i] & masks[j] == 0: # no shared letter
best = max(best, len(words[i]) * len(words[j]))
return best
變形 A — 逐步累積互斥遮罩的聯集(LC 1239)
變化點:不是只挑兩個互斥的單字,而是貪婪地把所有可達的聯集都長出來。
維護一份可達遮罩清單;一個單字只有在 cur & m == 0 時才能加進某個遮罩。
// java
// LC 1239 - Maximum Length of a Concatenated String with Unique Characters
// IDEA: keep every reachable "union of disjoint words" mask; answer = max popcount
// time = O(2^N * 26), space = O(2^N)
class Solution {
public int maxLength(List<String> arr) {
List<Integer> masks = new ArrayList<>();
masks.add(0); // empty selection
int best = 0;
for (String s : arr) {
int m = 0;
boolean dup = false;
for (char c : s.toCharArray()) {
int bit = 1 << (c - 'a');
if ((m & bit) != 0) { dup = true; break; } // word itself repeats a letter
m |= bit;
}
if (dup) continue;
// iterate BACKWARDS over the snapshot so newly added masks aren't reused this round
for (int i = masks.size() - 1; i >= 0; i--) {
int cur = masks.get(i);
if ((cur & m) != 0) continue; // overlap -> can't concatenate
masks.add(cur | m);
best = Math.max(best, Integer.bitCount(cur | m));
}
}
return best;
}
}
# python
# LC 1239 - Maximum Length of a Concatenated String with Unique Characters
# IDEA: keep every reachable "union of disjoint words" mask; answer = max popcount
# time = O(2^N * 26), space = O(2^N)
class Solution(object):
def maxLength(self, arr):
masks, best = [0], 0 # 0 = empty selection
for s in arr:
m, dup = 0, False
for c in s:
bit = 1 << (ord(c) - ord('a'))
if m & bit: # word itself repeats a letter
dup = True
break
m |= bit
if dup:
continue
for cur in list(masks): # snapshot, so this word is used at most once
if cur & m: # overlap -> can't concatenate
continue
masks.append(cur | m)
best = max(best, bin(cur | m).count("1"))
return best
變形 B — 把固定寬度的符號打包成滾動的 int key(LC 187)
變化點:字母表只有 4 個符號(A C G T),所以每個字元只需要 2 個位元,
10 個字元的視窗就是一個 20 位元的整數。用 hash = ((hash << 2) | code) & mask 滑動視窗
— 這是 O(1) 的滾動 key,不必每一步都去雜湊一段 10 字元的子字串。
// java
// LC 187 - Repeated DNA Sequences
// IDEA: 2 bits per base -> a 10-char window is one 20-bit int; roll it with shift + mask
// time = O(N), space = O(N)
class Solution {
public List<String> findRepeatedDnaSequences(String s) {
int L = 10, n = s.length();
List<String> res = new ArrayList<>();
if (n <= L) return res;
int mask = (1 << (2 * L)) - 1; // keep only the low 20 bits
int hash = 0;
Map<Integer, Integer> seen = new HashMap<>();
for (int i = 0; i < n; i++) {
hash = ((hash << 2) | "ACGT".indexOf(s.charAt(i))) & mask; // push new base, drop oldest
if (i >= L - 1) {
int c = seen.getOrDefault(hash, 0) + 1;
seen.put(hash, c);
if (c == 2) res.add(s.substring(i - L + 1, i + 1)); // report once
}
}
return res;
}
}
# python
# LC 187 - Repeated DNA Sequences
# IDEA: 2 bits per base -> a 10-char window is one 20-bit int; roll it with shift + mask
# time = O(N), space = O(N)
class Solution(object):
def findRepeatedDnaSequences(self, s):
L, n = 10, len(s)
if n <= L:
return []
code = {'A': 0, 'C': 1, 'G': 2, 'T': 3}
mask = (1 << (2 * L)) - 1 # keep only the low 20 bits
h, seen, res = 0, {}, []
for i, c in enumerate(s):
h = ((h << 2) | code[c]) & mask # push new base, drop the oldest
if i >= L - 1:
seen[h] = seen.get(h, 0) + 1
if seen[h] == 2: # report each repeat exactly once
res.append(s[i - L + 1:i + 1])
return res
更多字母遮罩的練習(同樣的 26 位元編碼,沒有新技術): LC 1255(Maximum Score Words Formed by Letters)、LC 2135(Count Words Obtained After Adding a Letter)、LC 1684(Count the Number of Consistent Strings —
word & ~allowed == 0)。
2) Bitmask DP
bitmask 讓一個整數代表一組已走訪/已選取的項目(第 i 位是 1 ⇔ 項目 i 在集合裡)。
當 DP 狀態需要記錄「我用掉了 ≤ 約 20 個項目中的哪個子集」時,遮罩本身就是狀態 —
這讓指數級的子集問題能在 O(2^n · n) 內跑完。
2-1) 子集列舉(LC 78 回顧)
讓 mask 從 0 跑到 2^n − 1,就會不重不漏地走過每一個子集;用位元測試挑出成員
(見詳解題第 12 節)。幾個好用的遮罩慣用寫法:
# python
mask & (1 << i) # is item i in the subset?
mask | (1 << i) # add item i
mask & ~(1 << i) # remove item i
bin(mask).count("1") # size of the subset
sub = (sub - 1) & mask # enumerate all SUB-masks of `mask` (classic trick)
2-2) TSP 型的 bitmask DP(Held–Karp)
旅行推銷員家族是 bitmask DP 的代表題:dp[mask][i] = 一條恰好走訪 mask 中所有城市
且目前停在城市 i 的路徑的最小成本。
state : dp[mask][i] mask = set of visited cities, i = current city
trans : dp[mask | (1<<j)][j] = min( dp[mask][i] + dist[i][j] ) for j not in mask
answer: min over i of dp[FULL][i] (+ dist[i][start] for a cycle)
time : O(2^n · n^2) space : O(2^n · n)
// java
// Held–Karp TSP skeleton: dp[mask][i] = min cost visiting `mask`, ending at city i
int tsp(int[][] dist) {
int n = dist.length, FULL = (1 << n) - 1;
int[][] dp = new int[1 << n][n];
for (int[] row : dp) Arrays.fill(row, Integer.MAX_VALUE / 2);
dp[1][0] = 0; // start at city 0, only it visited
for (int mask = 1; mask <= FULL; mask++) {
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) == 0) continue; // i must be in mask
for (int j = 0; j < n; j++) {
if ((mask & (1 << j)) != 0) continue; // j must NOT be visited yet
int next = mask | (1 << j);
dp[next][j] = Math.min(dp[next][j], dp[mask][i] + dist[i][j]);
}
}
}
int ans = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) ans = Math.min(ans, dp[FULL][i] + dist[i][0]); // close cycle
return ans;
}
什麼時候該想到 bitmask DP:
n很小(≤ 約 20,讓2^n還算得動),而且狀態是 「我用過/走訪過哪個子集」。相關 LC:847(Shortest Path Visiting All Nodes)、 1349(Maximum Students Taking Exam)、691(Stickers to Spell Word)、526(Beautiful Arrangement)。
2-3) 「一次填滿一個桶」的 bitmask DP — LC 698 Priority 5 of 5 — Must know — expect it in almost every loop
模式:切成 k 個相等群組的題目,看起來需要 k 層巢狀搜尋。
訣竅是不要再去追蹤你正在填哪一個桶,只追蹤:
state : dp[mask] = how full the CURRENT bucket is, given `mask` items are already placed
(-1 = mask unreachable)
key : sum(mask) is fixed by the mask, so the bucket index is implied —
every time the running bucket hits `target` it wraps to 0 and a new bucket starts
trans : dp[mask | (1<<i)] = (dp[mask] + nums[i]) % target, allowed iff dp[mask] + nums[i] <= target
answer: dp[FULL] == 0 (all items used AND the last bucket closed exactly)
time : O(2^n · n) space : O(2^n)
核心想法:% target 正是讓「開始下一個桶」這個轉移完全免費的關鍵 — 不需要為桶的計數器多開一個狀態維度。
真正有用的剪枝:把 nums 由小到大排序,然後一旦 dp[mask] + nums[i] > target 就
break(不是 continue)— 後面的每個元素都更大,一樣會失敗。
// java
// LC 698 - Partition to K Equal Sum Subsets
// IDEA: dp[mask] = fill level of the current bucket; % target rolls over to the next bucket
// time = O(2^n * n), space = O(2^n)
class Solution {
public boolean canPartitionKSubsets(int[] nums, int k) {
int sum = 0;
for (int x : nums) sum += x;
if (sum % k != 0) return false; // can't split evenly
int target = sum / k, n = nums.length;
Arrays.sort(nums); // ascending -> enables the `break` prune
if (nums[n - 1] > target) return false; // one item already overflows a bucket
int FULL = (1 << n) - 1;
int[] dp = new int[1 << n];
Arrays.fill(dp, -1); // -1 = state not reachable
dp[0] = 0; // nothing placed, empty bucket
for (int mask = 0; mask <= FULL; mask++) {
if (dp[mask] < 0) continue; // unreachable
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) continue; // item i already used
if (dp[mask] + nums[i] > target) break; // sorted -> all later items fail too
int next = mask | (1 << i);
if (dp[next] < 0) {
dp[next] = (dp[mask] + nums[i]) % target; // == 0 -> bucket closed, start next
}
}
}
return dp[FULL] == 0; // every item used and the final bucket landed exactly on target
}
}
# python
# LC 698 - Partition to K Equal Sum Subsets
# IDEA: dp[mask] = fill level of the current bucket; % target rolls over to the next bucket
# time = O(2^n * n), space = O(2^n)
class Solution(object):
def canPartitionKSubsets(self, nums, k):
total = sum(nums)
if total % k: # can't split evenly
return False
target, n = total // k, len(nums)
nums.sort() # ascending -> enables the `break` prune
if nums[-1] > target: # one item already overflows a bucket
return False
FULL = (1 << n) - 1
dp = [-1] * (1 << n) # -1 = state not reachable
dp[0] = 0 # nothing placed, empty bucket
for mask in range(FULL + 1):
if dp[mask] < 0:
continue
for i in range(n):
if mask & (1 << i): # item i already used
continue
if dp[mask] + nums[i] > target:
break # sorted -> all later items fail too
nxt = mask | (1 << i)
if dp[nxt] < 0:
dp[nxt] = (dp[mask] + nums[i]) % target # 0 -> bucket closed
return dp[FULL] == 0
視覺追蹤 — nums = [1,2,2,3](已排序),k = 2,target = 4。
第 i 位 = nums[i] 已被使用;這裡只畫出成功的那條路徑(迴圈其實也會填其他遮罩):
mask 0000 dp=0 place nums[0]=1 -> dp[0001] = 1
mask 0001 dp=1 place nums[3]=3 -> dp[1001] = (1+3) % 4 = 0 (bucket closed!)
mask 0011 dp=3 place nums[2]=2 -> 3+2 = 5 > 4 -> break (dead branch)
mask 1001 dp=0 place nums[1]=2 -> dp[1011] = 2
mask 1011 dp=2 place nums[2]=2 -> dp[1111] = (2+2) % 4 = 0
dp[FULL] == 0 -> TRUE ([1,3] and [2,2])
變形 — 同一個模板,k 寫死(LC 473)
變化點:LC 473(Matchsticks to Square)就是 k = 4 的 LC 698,其他完全一樣。
變形 — 遮罩當成遊戲狀態,而不是 DP 表(LC 464)
變化點:在 LC 464(Can I Win)裡,遮罩代表「1..maxChoosable 中哪些數字已被拿走」,
遞迴是 minimax 而不是求成本:win(mask) 為 true 的條件是存在某個沒用過的 i,
它要嘛立刻達到總和,要嘛讓對手落入必敗狀態 !win(mask | (1 << (i-1)))。
只用 mask 做記憶化 — 剩下的總和已經被它隱含決定了。
先用 maxChoosable * (maxChoosable + 1) / 2 < desiredTotal 剪枝 → 誰都贏不了。
更多 bitmask DP 練習:LC 1125(Smallest Sufficient Team — 集合覆蓋,
dp[skillMask])、 LC 980(Unique Paths III — 已走訪格子的遮罩)、LC 864(Shortest Path to Get All Keys — BFS 狀態 =(cell, keyMask))、LC 1494(Parallel Courses II — 用sub = (sub - 1) & mask列舉目前可修課程集合的子遮罩)。
不是 bitmask DP,但和位元相鄰:LC 421(Maximum XOR of Two Numbers in an Array)和 LC 1707(Maximum XOR With an Element From Array)是用**二元/XOR 字典樹(Trie)**解的 — 請看
trie.md,這裡不重複。
Worked Examples
Nineteen problems live in bit_manipulation_examples.md, grouped by which property of the bit operators they lean on:
| Group | The property | Problems |
|---|---|---|
| XOR — cancelling pairs | x ^ x == 0, so anything paired disappears |
LC 136, 137, 260, 268 |
| Counting & transforming bits | x & (x-1) clears the lowest set bit |
LC 191, 338, 190, 231 |
| Arithmetic without arithmetic | XOR is addition without carry; AND finds the carry | LC 371, 67, 29 |
| Enumerating & constructing | an integer is a subset, and counting up visits every one | LC 78, 89, 201 |
| Bit-field surgery | build a mask, clear the field, OR the new bits in | CtCI 5.1–5.7 |
總結
Pick the technique from the question
| The problem says… | Reach for | Section |
|---|---|---|
| “every element appears twice except one” | XOR the whole array | XOR — cancelling pairs |
“count the 1 bits” / “for every i in 0..n” |
x & (x-1) loop, or DP on i >> 1 |
§1-3 |
| “sum over all pairs” of a bit property | loop the 32 bit columns, not the pairs | §1-4 |
| lowercase words, “share a letter” / “unique characters” | a 26-bit letter mask | §1-5 |
| a small fixed alphabet + a sliding window | pack k bits per symbol, roll with << and a mask |
Variation B |
“choose a subset”, n ≤ ~20 |
dp[mask], bitmask DP |
§2 |
“partition into k equal groups” |
dp[mask] = fill level, % target |
§2-3 |
“XOR of a subarray” / “XOR of 1..n”, asked repeatedly |
XOR prefix array, or the n % 4 closed form |
§0-10 |
“add / divide without + or /” |
XOR = sum, AND = carry | Arithmetic without arithmetic |
“set / clear / replace bits i..j” |
a 111..000..111 mask, then OR the field in |
Bit-field surgery |
| “next number with the same number of 1s” | flip the rightmost non-trailing zero, repack the ones | Bit-field surgery |
| “maximum XOR of two numbers” | binary trie — see trie.md | — |
讓位元運算題掛掉的五個 bug
- 對負的
int跑while (x != 0) x >>= 1— 算術右移會一直補 1 進來。 Java 要用>>>,Python 要改成for i in range(32)。(§0-5、§0-6) - 少了括號 — 要寫
(x & 1) == 0,絕不要寫x & 1 == 0。(§0-7) Integer.MIN_VALUE沒有對應的正數 —Math.abs和一元-算完都還是它本身, 所以「先取負再相除」會無聲地壞掉。(§0-3)1 << i在i >= 31時溢位 — 要用1L << i(也別忘了 Java 會把位移量取低 5 位, 所以1 << 32 == 1)。(§0-2、§0-5)- 把 32 位元的迴圈原封不動搬到 Python — Python 永遠不會溢位,所以每一步都要
& 0xFFFFFFFF,最後還要把結果轉回有號數。(§0-6)