位元運算

Math & BitsPriority 3 of 5 — Worth knowing — usually a variant of a must-know patternWorth knowing 更新於 Sep 19, 2026
Section priorityPriority 5 of 5 — Must know — expect it in almost every loopMust knowPriority 4 of 5 — High value — a gap here costs you roundsHigh valuePriority 3 of 5 — Worth knowing — usually a variant of a must-know patternWorth knowingPriority 2 of 5 — Niche — read once, revisit only if a company is known to askNicheMarked on the sections that carry it — unmarked sections are background/reference.

範圍 — 整數在位元層次上是怎麼被表示的(二補數、固定位寬、位移),以及建立在其上的各種運算與技巧:遮罩、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

參考資料

0) 電腦科學基礎 Priority 5 of 5 — Must know — expect it in almost every loop

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

# 事實 它解釋了什麼
1 一個數字是「數字 × 位值」,而二進位的位值就是 2 的次方 用手讀寫二進位/十六進位
2 int 是一個固定寬度的 32 位元盒子,滿了會繞回來 溢位、MIN_VALUE、為什麼需要遮罩
3 負數是用二補數存的 ~x = -x-1x & -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) 二補數 — 負數是怎麼存的 Priority 5 of 5 — Must know — expect it in almost every loop

規則-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_VALUEMath.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 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
# 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 裡都一樣:

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 ≤ 約 202^20 ≈ 10^6);
  • 所有遮罩加總起來,遮罩的總數是 3^n 而不是 4^n — 這正是 §2-1sub = (sub - 1) & mask 迴圈跑得動的原因。

0-9) 標準函式庫的輔助函式 — 值 vs 索引 Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern

每個語言都內建這些函式,而拿錯那一個是無聲的 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 的封閉形式 Priority 4 of 5 — High value — a gap here costs you rounds

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) 無分支寫法 Priority 3 of 5 — Worth knowing — usually a variant of a must-know pattern

一個式子做掉大部分的事:對非負的 intx >> 310;對負的則是 -1(全部都是 1)§0-3)。 全 1 的遮罩 AND 起來是保留,全 0 的遮罩 AND 起來是丟掉 — 分支就是這樣變成算術的。

目的 式子 為什麼成立
ab 正負號相反嗎? (a ^ b) < 0 XOR 的符號位元剛好在兩個符號位元不同時是 1 — 而且不像 a * b < 0 會溢位
abs(x) (x ^ (x >> 31)) - (x >> 31) x ≥ 0(x ^ 0) - 0 = xx < 0(x ^ -1) - (-1) = ~x + 1 = -x
0 / -1 表示 x 的正負號 x >> 31 把符號位元塗滿全部 32 位
x2^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) 清掉最低的那個 1x-1 會從尾端的 0 一路借位,所以在那一位和它以下,兩者都不一致
x & -x 只留下最低的那個 1-x = ~x + 1,所以在那一位以上,兩者互補
x & (x + 1) 清掉尾端連續的 1x & (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 x2 的冪 或 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 = 0a ^ 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 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
// 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 回顧)

mask0 跑到 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 DPn 很小(≤ 約 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 層巢狀搜尋。 訣竅是不要再去追蹤你正在填哪一個桶,只追蹤:

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] > targetbreak(不是 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 = 2target = 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,這裡不重複。

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

  1. 對負的 intwhile (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 << ii >= 31 時溢位 — 要用 1L << i(也別忘了 Java 會把位移量取低 5 位, 所以 1 << 32 == 1)。(§0-2§0-5
  5. 把 32 位元的迴圈原封不動搬到 Python — Python 永遠不會溢位,所以每一步都要 & 0xFFFFFFFF,最後還要把結果轉回有號數。(§0-6