Untuk persoalan dengan himpunan kecil (≤ 20-25 elemen), kita bisa merepresentasikan setiap himpunan sebagai integer bitmask. Bit ke-i = 1 berarti elemen ke-i ada di himpunan. Operasi himpunan (union, intersection, complement, subset check) menjadi operasi bitwise O(1). Hal ini memungkinkan DP bitmask untuk persoalan NP-hard kecil (TSP, exact cover, vertex cover exact) dan SOS DP (Sum Over Subsets) untuk prekalkulasi aggregate atas subset. Pada Chapter ini kita membedah enumerasi subset, popcount, dan SOS DP — teknik fundamental yang sering muncul di kompetisi dan interview.
Aturan: Himpunan sebagai Bitmask #
Misal himpunan S = {0, 1, 2, ..., n-1}. Setiap subset direpresentasikan sebagai integer di [0, 2^n - 1] di mana bit ke-i = 1 iff elemen i ada di subset.
Operasi standar:
| Operasi | Bitwise | Keterangan |
|---|---|---|
A ∪ B (union) | a | b | OR |
A ∩ B (intersection) | a & b | AND |
A \ B (difference) | a & ~b | AND NOT |
A ⊆ B (subset) | (a & b) == a | check subset |
| ` | A | ` (size) |
~A (complement) | ~a & ((1<<n) - 1) | mask universe |
| Lowest set bit | a & (-a) | isolasi LSB |
| Hapus LSB | a & (a - 1) | clear LSB |
Mekanisme: Enumerasi Subset dan SOS DP #
Enumerasi Semua Subset dari Mask #
Untuk mask M, iterasi semua sub ⊆ M dengan trik:
int sub = M;
do {
// process sub
sub = (sub - 1) & M;
} while (sub != M);sub = (sub - 1) & M mengurangi sub sebesar 1 lalu menjaganya tetap submask dari M. Loop berakhir saat sub kembali ke M (sudah cycle). Bila ingin exclude empty set: mulai sub = M dan berhenti saat sub == 0.
Total subset dari mask dengan k bit set = 2^k. Iterasi O(2^k).
SOS DP (Sum Over Subsets) #
Diberikan fungsi f[mask] untuk setiap mask. Hitung F[mask] = Σ f[sub] untuk semua sub ⊆ mask.
Naif: untuk setiap mask, iterasi semua subset. O(3^n) total.
SOS DP: gunakan DP dengan iterasi bit per bit.
for (int i = 0; i < K; ++i) {
for (int mask = 0; mask < (1 << K); ++mask) {
if (mask & (1 << i)) {
F[mask] += F[mask ^ (1 << i)];
}
}
}Intuisi: setelah iterasi bit ke-i selesai, F[mask] = sum dari f[sub] untuk semua sub ⊆ mask yang berbeda dari mask hanya pada bit-bit 0..i. Setelah K iterasi, F[mask] = sum untuk semua sub ⊆ mask. Kompleksitas O(K · 2^K) = O(n log n) di mana n = 2^K.
Implementasi: Enumerasi, Popcount, SOS DP #
#include <iostream>
#include <vector>
#include <algorithm>
// SOS DP (Sum Over Subsets)
// For each mask, compute sum of f[sub] for all sub that is submask of mask
std::vector<long long> sos_dp(const std::vector<long long>& f) {
int n = f.size();
int K = 0;
while ((1 << K) < n) K++;
std::vector<long long> F = f;
for (int i = 0; i < K; ++i) {
for (int mask = 0; mask < n; ++mask) {
if (mask & (1 << i)) {
F[mask] += F[mask ^ (1 << i)];
}
}
}
return F;
}
// Subset enumeration: iterasi semua submask dari mask
void enumerate_subsets(int mask) {
std::cout << "Subsets of mask " << mask << " (binary ";
for (int i = 3; i >= 0; --i) std::cout << ((mask >> i) & 1);
std::cout << "):\n ";
int sub = mask;
do {
std::cout << "(";
for (int i = 3; i >= 0; --i) std::cout << ((sub >> i) & 1);
std::cout << ") ";
sub = (sub - 1) & mask;
} while (sub != mask);
std::cout << '\n';
}
int popcount(int x) { return __builtin_popcount(x); }
int main() {
std::cout << "Popcount demo:\n";
int vals[] = {0, 1, 3, 7, 15, 255, 1023};
for (int v : vals) {
std::cout << " popcount(" << v << ") = " << popcount(v) << '\n';
}
std::cout << "\nSubset enumeration:\n";
enumerate_subsets(0b1011);
enumerate_subsets(0b1110);
std::cout << "\nSOS DP demo:\n";
std::vector<long long> f(8);
for (int i = 0; i < 8; ++i) f[i] = i; // f[mask] = mask
auto F = sos_dp(f);
for (int i = 0; i < 8; ++i) {
std::cout << " F[" << i << "] = " << F[i] << '\n';
}
std::cout << "\nVerifikasi manual untuk F[7] (mask=111):\n";
std::cout << " Subsets of 7: 0, 1, 2, 3, 4, 5, 6, 7\n";
std::cout << " Sum = 0+1+2+3+4+5+6+7 = 28\n";
return 0;
}Output yang Dihasilkan:
Popcount demo:
popcount(0) = 0
popcount(1) = 1
popcount(3) = 2
popcount(7) = 3
popcount(15) = 4
popcount(255) = 8
popcount(1023) = 10
Subset enumeration:
Subsets of mask 11 (binary 1011):
(1011) (1010) (1001) (1000) (0011) (0010) (0001) (0000)
Subsets of mask 14 (binary 1110):
(1110) (1100) (1010) (1000) (0110) (0100) (0010) (0000)
SOS DP demo:
F[0] = 0
F[1] = 1
F[2] = 2
F[3] = 6
F[4] = 4
F[5] = 10
F[6] = 12
F[7] = 28
Verifikasi manual untuk F[7] (mask=111):
Subsets of 7: 0, 1, 2, 3, 4, 5, 6, 7
Sum = 0+1+2+3+4+5+6+7 = 28Bedah Kode Baris-per-Baris #
__builtin_popcount(x): GCC builtin untuk menghitung jumlah bit 1. Sama denganstd::popcountC++20. Lebih cepat dari loop manual.enumerate_subsets: iterasi semua submask darimaskdengan triksub = (sub - 1) & mask. Awalsub = mask, akhirsub = masklagi (sudah cycle). Total 2^k iterasi di mana k = jumlah bit 1 di mask.sub = (sub - 1) & mask: trik enumerasi subset.sub - 1mengurangi 1, lalu& maskmemastikan hanya bit-bit yang ada di mask yang tetap set. Hasilnya submask berikutnya dalam urutan descending.sos_dp: inisialisasiF = f. IterasiKkali (K = bit width). Setiap iterasi, untuk setiapmaskdengan bitiset,F[mask] += F[mask ^ (1<<i)]. Ini menambah kontribusi submask yang berbeda hanya pada bit i.if (mask & (1 << i)): hanya proses mask yang punya bit i set.mask ^ (1 << i)= mask dengan bit i di-toggle (di sini berarti di-clear karena bit i pasti set).F[mask] += F[mask ^ (1 << i)]: tambah kontribusi submask tanpa bit i. Setelah K iterasi,F[mask]= sum dari semuaf[sub]untuksub ⊆ mask.- Verifikasi SOS:
f[mask] = mask(identity).F[7](mask=111, semua bit set) = sum dari f untuk semua subset =0+1+2+3+4+5+6+7 = 28. ✓F[3](mask=011) = sum dari f untuk subset{0,1,2,3}=0+1+2+3 = 6. ✓F[5](mask=101) = sum dari f untuk subset{0,1,4,5}=0+1+4+5 = 10. ✓
Aplikasi DP Bitmask #
TSP (Traveling Salesman Problem) #
Diberikan graph lengkap dengan n node, cari tour minimum yang mengunjungi semua node. DP state: dp[mask][last] = minimum cost untuk mengunjungi semua node di mask, berakhir di node last.
// dp[mask][last] = min cost to visit nodes in mask, ending at last
dp[1][0] = 0; // start at node 0
for (int mask = 1; mask < (1 << n); ++mask) {
for (int last = 0; last < n; ++last) {
if (!(mask & (1 << last))) continue;
for (int next = 0; next < n; ++next) {
if (mask & (1 << next)) continue;
int new_mask = mask | (1 << next);
dp[new_mask][next] = std::min(dp[new_mask][next],
dp[mask][last] + dist[last][next]);
}
}
}
// answer = min(dp[(1<<n)-1][last] + dist[last][0]) for all last
Kompleksitas O(n² · 2^n). Untuk n ≤ 20, ini feasible.
Vertex Cover Exact #
DP bitmask untuk minimum vertex cover: coba semua subset vertex, periksa apakah vertex cover valid (setiap edge punya endpoint di subset). O(2^n · m). Untuk n ≤ 25, feasible.
Maximum Independent Set (MIS) #
Untuk graph dengan n ≤ 40-50, meet in the middle: pecah node jadi dua bagian A dan B. Precompute MIS untuk semua subset A dan subset B. Untuk kombinasi, cek compatibility (no edge antara A dan B subset yang dipilih).
SOS DP Aplikasi #
- Count subset with property: count berapa subset
sub ⊆ maskyang memenuhi kondisi tertentu. - Subset convolution:
f * g (mask) = Σ f[sub] * g[mask \ sub]. Penting untuk Steiner tree problem, vertex cover counting. - Connected subgraph count: hitung berapa subgraph terhubung untuk setiap subset node.
Kompleksitas #
| Teknik | Kompleksitas | n Max Praktis |
|---|---|---|
| Subset enumeration per mask | O(2^k) di mana k = popcount | k ≤ 25 |
| Total enumerasi semua subset | O(3^n) | n ≤ 18 |
| SOS DP | O(n · 2^n) | n ≤ 20 |
| DP bitmask TSP | O(n² · 2^n) | n ≤ 20 |
| Exact vertex cover | O(2^n · m) | n ≤ 25 |
Ringkasan #
- Bitmask = representasi himpunan kecil sebagai integer. Operasi himpunan jadi bitwise O(1).
- Enumerasi subset:
sub = (sub - 1) & mask. Total 2^k iterasi untuk mask dengan k bit set. - Popcount:
__builtin_popcount(x)untuk count bit 1. - SOS DP: precompute
F[mask] = Σ f[sub]untuksub ⊆ mask. Kompleksitas O(n · 2^n). - DP bitmask TSP:
dp[mask][last]= min cost visit mask, end at last. O(n² · 2^n). - Batas praktis: n ≤ 20 untuk DP bitmask, n ≤ 25 untuk exact vertex cover, n ≤ 18 untuk enumerasi semua subset semua mask.