位元運算

數學與位元運算優先度 3/5 — 值得會 — 多半是必備模式的變形值得會 更新於 Oct 9, 2026
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。

範圍 — 整數在位元層次上是怎麼被表示的(二補數、固定位寬、位移),以及建立在其上的各種運算與技巧:遮罩、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 的次方、不用 + 做加法、把旗標打包進一個數字

速查表 — 一定要背起來的技巧 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

目標 表達式
測試第 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

參考資料

0) 電腦科學基礎 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

幾乎每一個看起來「很巧妙」的位元技巧,都只是機器如何儲存整數的直接後果。 五個事實就講完了 — 把它們學起來,下面的技巧就不再需要硬背。

# 事實 它解釋了什麼
1 一個數字是「數字 × 位值」,而二進位的位值就是 2 的次方 用手讀寫二進位/十六進位
2 int 是一個固定寬度的 32 位元盒子,滿了會繞回來 溢位、MIN_VALUE、為什麼需要遮罩
3 負數是用二補數存的 ~x = -x-1、x & -x、負數的 >>
4 位移就是乘以/除以 2 的次方 <<、>>、>>> 和它們的邊界狀況
5 一個 int 的各個位元就是 32 個項目的子集 bitmask、子集列舉、bitmask DP

0-1) 位值、二進位與十六進位

一個 X 進位數字的實際數值,由每一位的數字和它的位置共同決定 (參考):

text
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 個十六進位數字 — 這也是遮罩都寫成這種形式的原因:

text
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) 二補數 — 負數是怎麼存的 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

規則:-x 是以 ~x + 1 的形式儲存的 — 每個位元都翻轉,然後加一。 等價地說,-x 的位元樣式就是無號數 2^32 - x。

text
(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 有效

兩者都是從二補數直接推出來的。推導過一次,就再也不用去背哪個是哪個:

text
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 把整個位元樣式當成無號數
text
-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 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

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
# 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) 優先順序 — 全部加上括號 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

由緊到鬆 — 這條鏈在 C、Java、Python 裡都一樣:

text
~   ->   * / %   ->   + -   ->   << >>   ->   &   ->   ^   ->   |

這個順序會造成兩種出錯:

text
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 索引 優先度 3/5 — 值得會 — 多半是必備模式的變形

每個語言都內建這些函式,而拿錯那一個是無聲的 bug,不是編譯錯誤。造成這件事的分界是: 有些函式回傳的是值(一個只有單一位元被設起來的模式),有些回傳的是索引(0…31)。 兩者永遠不能互換。

text
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)。而且各列壞掉的程度並不一樣:

text
                        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 索引」這條線分開的 — 而這正是會咬人的地方:

text
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
# 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 的封閉形式 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

XOR 就是把進位丟掉的加法(§0-4), 所以每一個前綴和的技巧都有一個 XOR 版的雙胞胎 — 而且 XOR 那個更簡單, 因為 XOR 是自己的反運算,不需要減法那一步。

用前綴做區間 XOR

text
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
# 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 的查表:

text
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
# 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) 無分支寫法 優先度 3/5 — 值得會 — 多半是必備模式的變形

一個式子做掉大部分的事:對非負的 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
// java
// IDEA: swap without a temporary. Correct ONLY when the two operands are distinct.
a ^= b;  b ^= a;  a ^= b;

這是最經典的「你看,不用暫存變數」的答案,而它會在兩個運算元指向同一個位置時,把值毀掉:

text
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
// 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
# 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
// 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
# 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:

text
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
// 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
# 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:

text
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 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

模式:小寫字母的集合只需要 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
// 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
# 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
// 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
# 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
// 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
# 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
# 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 的路徑的最小成本。

text
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
// 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 優先度 5/5 — 必備 — 幾乎每一輪面試都會出現

模式:切成 k 個相等群組的題目,看起來需要 k 層巢狀搜尋。 訣竅是不要再去追蹤你正在填哪一個桶,只追蹤:

text
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
// 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
# 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] 已被使用;這裡只畫出成功的那條路徑(迴圈其實也會填其他遮罩):

text
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,這裡不重複。

範例詳解

十九道題目放在 bit_manipulation_examples.md, 依照它們倚賴位元運算子的哪個性質分組:

分組 用到的性質 題目
XOR — 抵消成對元素 x ^ x == 0,所以任何成對的東西都會消失 LC 136, 137, 260, 268
計數與轉換位元 x & (x-1) 清掉最低位的 1 LC 191, 338, 190, 231
不用算術做算術 XOR 是不帶進位的加法;AND 找出進位 LC 371, 67, 29
列舉與建構 一個整數就是一個子集,往上數就能走遍所有子集 LC 78, 89, 201
位元欄位手術 建一個遮罩、清掉該欄位、再把新位元 OR 進去 CtCI 5.1–5.7

總結

從題目敘述挑出該用的技巧

題目說… 就用 章節
「每個元素都出現兩次,只有一個例外」 把整個陣列 XOR 起來 XOR — 抵消成對元素
「數 1 的個數」/「對 0..n 的每個 i」 x & (x-1) 迴圈,或對 i >> 1 做 DP §1-3
對某個位元性質「所有配對加總」 跑 32 個位元欄,不要跑配對 §1-4
小寫單字、「有共同字母」/「字元不重複」 26 位元的字母遮罩 §1-5
固定的小字母表 + 滑動視窗 每個符號打包 k 位元,用 << 加遮罩滾動 變形 B
「選一個子集」,n ≤ 約 20 dp[mask],bitmask DP §2
「切成 k 個總和相等的群組」 dp[mask] = 目前桶的填充量,配 % target §2-3
反覆問「某個子陣列的 XOR」/「1..n 的 XOR」 XOR 前綴陣列,或 n % 4 的封閉形式 §0-10
「不用 + 或 / 做加法/除法」 XOR 是和,AND 是進位 不用算術做算術
「設定/清除/取代第 i..j 位元」 一個 111..000..111 遮罩,再把欄位 OR 進去 位元欄位手術
「1 的個數相同的下一個數字」 翻轉最右邊的非尾端 0,再重新排好那些 1 位元欄位手術
「兩個數字的最大 XOR」 二元字典樹 — 見 trie.md —

讓位元運算題掛掉的五個 bug

  1. 對負的 int 跑 while (x != 0) x >>= 1 — 算術右移會一直補 1 進來。 Java 要用 >>>,Python 要改成 for i in range(32)。(§0-5、§0-6)
  2. 少了括號 — 要寫 (x & 1) == 0,絕不要寫 x & 1 == 0。(§0-7)
  3. Integer.MIN_VALUE 沒有對應的正數 — Math.abs 和一元 - 算完都還是它本身, 所以「先取負再相除」會無聲地壞掉。(§0-3)
  4. 1 << 31 是符號位,而且 1 << 32 == 1 — 1 << 31 就是 Integer.MIN_VALUE, 當作第 31 位的遮罩仍然正確,但它不是正數 2^31;Java 會把 int 的位移量取低 5 位、 long 的取低 6 位(1L << 64 == 1),所以 i 在 32…63,或需要 2^31 這個數值時,要用 1L << i。(§0-2、§0-5)
  5. 把 32 位元的迴圈原封不動搬到 Python — Python 永遠不會溢位,所以每一步都要 & 0xFFFFFFFF,最後還要把結果轉回有號數。(§0-6)