비트마스킹이란
작은 원소 집합(보통 \(n \le 20\))을 정수 하나의 이진 비트로 표현하는
기법입니다. \(i\)번째 비트가 1이면 "원소 \(i\)를 포함", 0이면 "미포함"입니다.
집합 연산을 비트 연산으로 바꾸면 매우 빠르고, 상태 압축 DP의 토대가 됩니다.
예를 들어 \(\{0, 2, 3\}\)은 이진수 \(1101_2 = 13\)입니다.
핵심 연산 사전
| 의도 | 식 | 설명 |
|---|---|---|
| \(i\) 포함? | mask & (1 << i) |
0이 아니면 포함 |
| \(i\) 추가 | mask \| (1 << i) |
OR로 켠다 |
| \(i\) 제거 | mask & ~(1 << i) |
AND NOT으로 끈다 |
| \(i\) 토글 | mask ^ (1 << i) |
XOR로 뒤집는다 |
| 합집합 | a \| b |
|
| 교집합 | a & b |
|
| 차집합 | a & ~b |
a에서 b를 뺀다 |
| 원소 개수 | popcount(mask) |
켜진 비트 수 |
| 전체 집합 | (1 << n) - 1 |
n개 모두 1 |
1 << i를 쓸 때 \(i \ge 31\)이면 int가 넘칩니다. 이때는 1LL << i로 써야
합니다 — 가장 흔한 함정입니다.
모든 부분집합을 순회
크기 \(n\)인 집합의 모든 부분집합은 \(0\)부터 \(2^n - 1\)까지의 정수에 대응합니다.
for (int mask = 0; mask < (1 << n); mask++) {
// mask가 하나의 부분집합
}
원소 \(i\)가 켜져 있는지 보며 부분집합의 내용을 읽습니다. 전체 순회는 \(O(2^n)\).
어떤 부분집합의 "부분집합들"만 순회
mask의 부분집합만 효율적으로 도는 유명한 관용구가 있습니다.
for (int sub = mask; sub > 0; sub = (sub - 1) & mask) {
// sub는 mask의 부분집합 (공집합 제외)
}
모든 mask에 대해 이 루프를 합치면 전체 비용이
$$ \sum_{mask} 2^{\text{popcount}(mask)} = 3^n $$
이 됩니다. 각 원소가 "양쪽에 없음 / 큰 집합에만 / 양쪽에"의 세 가지 상태를
갖기 때문입니다. 부분집합 DP의 단골 복잡도가 \(O(3^n)\)인 이유입니다.
복잡도 감각
| 패턴 | 복잡도 |
|---|---|
| 전체 부분집합 순회 | \(O(2^n)\) |
| 부분집합의 부분집합 순회 | \(O(3^n)\) |
| 비트 하나 단위 처리 | \(O(2^n \cdot n)\) |
\(2^{20} \approx 10^6\), \(3^{20} \approx 3.5 \times 10^9\) 정도이니, \(n\)이
20을 넘으면 비트마스킹은 보통 무리입니다. 다음 강의에서 구현 디테일과 응용을
봅니다.