Operasi bitwise adalah senjata rahasia pemrogram kompetitif. Sebuah operasi yang biasanya butuh O(log n) loop (mis. hitung bit set, cek power of two, cari MSB/LSB) dapat dilakukan dalam O(1) via instruksi CPU khusus. GCC menyediakan builtin functions (__builtin_popcount, __builtin_clz, __builtin_ctz) yang langsung compile ke instruksi hardware. Selain itu, trik-trik klasik seperti XOR swap, LSB isolasi (x & -x), dan power-of-2 check (x & (x-1)) adalah pengetahuan dasar yang penting untuk banyak algoritma (Fenwick tree, DSU bitmask, hash rolling). Pada Chapter ini kita membedah trik-trik bitwise yang paling berguna beserta implementasinya.
Aturan: Bit Manipulation Primitives #
Setiap operasi bitwise adalah O(1) pada CPU modern. Yang penting adalah identifikasi operasi mana yang efisien (builtin) vs yang butuh loop. Daftar operasi:
| Operasi | Builtin / Trik | Kompleksitas |
|---|---|---|
| Count bit 1 (popcount) | __builtin_popcount(x) | O(1) |
| Count leading zeros | __builtin_clz(x) | O(1) |
| Count trailing zeros | __builtin_ctz(x) | O(1) |
| Lowest set bit (LSB isolasi) | x & (-x) | O(1) |
| Highest set bit (MSB) | 1 << (31 - __builtin_clz(x)) | O(1) |
| Hapus LSB | x & (x - 1) | O(1) |
| Cek power of 2 | x > 0 && (x & (x-1)) == 0 | O(1) |
| Toggle bit i | x ^ (1 << i) | O(1) |
| Set bit i | x | (1 << i) | O(1) |
| Clear bit i | x & ~(1 << i) | O(1) |
| Test bit i | (x >> i) & 1 | O(1) |
| XOR swap | a^=b; b^=a; a^=b; | O(1) |
Mekanisme: Builtin Functions dan Trik Klasik #
Popcount #
__builtin_popcount(x): jumlah bit 1 di x. CPU modern punya instruksi POPCNT langsung — jauh lebih cepat dari loop manual. C++20 menyediakan std::popcount di header <bit>.
Count Leading/Trailing Zeros #
__builtin_clz(x): jumlah bit 0 sebelum MSB (most significant bit). Untukx = 1(binary0...01), clz = 31 (untuk 32-bit). Untukx = 1024 = 2^10, clz = 21.__builtin_ctz(x): jumlah bit 0 setelah LSB (least significant bit). Untukx = 8 = 0b1000, ctz = 3.
Aplikasi: bit_width(x) = 32 - clz(x) memberi jumlah bit minimum untuk representasi x. log2_floor(x) = 31 - clz(x).
LSB Isolasi: x & (-x) #
Trik sakti untuk Fenwick tree. Representasi biner -x = ~x + 1 (two’s complement). x & (-x) mengisolasi LSB. Contoh: 12 = 0b1100, -12 = 0b...10100, 12 & -12 = 0b0100 = 4.
Hapus LSB: x & (x - 1) #
x - 1 memflip LSB dari x dan semua bit trailing 0 menjadi 1. x & (x-1) menghapus LSB. Aplikasi: cek power of 2 (x & (x-1) == 0 untuk x > 0), iterasi semua bit set dalam integer.
XOR Swap #
a ^= b;
b ^= a;
a ^= b;Tukar dua variabel tanpa temporary. Setelah baris pertama a = a ^ b. Baris kedua: b = b ^ (a ^ b) = a. Baris ketiga: a = (a ^ b) ^ a = b. Warning: jangan dipakai bila a dan b adalah alias ke variabel yang sama (a ^= a menghasilkan 0).
Implementasi: Trik Bitwise #
#include <iostream>
#include <bitset>
#include <cstdint>
int main() {
// popcount: jumlah bit 1
std::cout << "Popcount (jumlah bit 1):\n";
uint32_t nums[] = {0, 1, 7, 255, 0xFFFFFFFF, 0b10110101};
for (uint32_t v : nums) {
std::cout << " popcount(" << v << ") = " << __builtin_popcount(v) << '\n';
}
// leading zeros: jumlah bit 0 di depan (sebelum MSB)
std::cout << "\nCount leading zeros (clz):\n";
for (uint32_t v : {1u, 2u, 4u, 8u, 16u, 1024u, 65536u}) {
std::cout << " clz(" << v << ") = " << __builtin_clz(v)
<< " (32 - clz = " << 32 - __builtin_clz(v) << " bit width)\n";
}
// trailing zeros: jumlah bit 0 di belakang (sebelum LSB)
std::cout << "\nCount trailing zeros (ctz):\n";
for (uint32_t v : {1u, 2u, 4u, 8u, 16u, 0b10100u, 0b11000u}) {
std::cout << " ctz(" << v << ") = " << __builtin_ctz(v) << '\n';
}
// Lowest set bit (LSB isolasi)
std::cout << "\nLowest set bit (a & -a):\n";
for (uint32_t v : {12u, 24u, 7u, 40u, 0b101000u}) {
uint32_t lsb = v & (-v);
std::cout << " LSB(" << v << ") = " << lsb << '\n';
}
// Highest set bit (1 << (31 - clz))
std::cout << "\nHighest set bit:\n";
for (uint32_t v : {12u, 24u, 7u, 40u, 0b101000u}) {
uint32_t msb = 1u << (31 - __builtin_clz(v));
std::cout << " MSB(" << v << ") = " << msb << '\n';
}
// XOR swap (tanpa variabel temp)
std::cout << "\nXOR swap:\n";
int a = 42, b = 99;
std::cout << " Sebelum: a=" << a << ", b=" << b << '\n';
a ^= b;
b ^= a;
a ^= b;
std::cout << " Sesudah: a=" << a << ", b=" << b << '\n';
// Toggle bit
std::cout << "\nToggle bit (XOR dengan 1<<i):\n";
uint32_t x = 0b1010;
std::cout << " x = " << std::bitset<8>(x) << '\n';
x ^= (1 << 1);
std::cout << " setelah toggle bit 1: " << std::bitset<8>(x) << '\n';
x ^= (1 << 3);
std::cout << " setelah toggle bit 3: " << std::bitset<8>(x) << '\n';
// Cek power of 2
std::cout << "\nPower of 2 check (x > 0 && (x & (x-1)) == 0):\n";
for (uint32_t v : {1u, 2u, 4u, 7u, 16u, 31u, 32u, 100u}) {
bool is_pow2 = v > 0 && (v & (v - 1)) == 0;
std::cout << " " << v << (is_pow2 ? " is" : " is NOT") << " power of 2\n";
}
// Bitset untuk visualisasi
std::cout << "\nBitset visualisasi:\n";
std::cout << " 255 = " << std::bitset<8>(255) << '\n';
std::cout << " 170 = " << std::bitset<8>(170) << " (checkerboard)\n";
std::cout << " 85 = " << std::bitset<8>(85) << " (checkerboard inverted)\n";
return 0;
}Output yang Dihasilkan:
Popcount (jumlah bit 1):
popcount(0) = 0
popcount(1) = 1
popcount(7) = 3
popcount(255) = 8
popcount(4294967295) = 32
popcount(181) = 5
Count leading zeros (clz):
clz(1) = 31 (32 - clz = 1 bit width)
clz(2) = 30 (32 - clz = 2 bit width)
clz(4) = 29 (32 - clz = 3 bit width)
clz(8) = 28 (32 - clz = 4 bit width)
clz(16) = 27 (32 - clz = 5 bit width)
clz(1024) = 21 (32 - clz = 11 bit width)
clz(65536) = 15 (32 - clz = 17 bit width)
Count trailing zeros (ctz):
ctz(1) = 0
ctz(2) = 1
ctz(4) = 2
ctz(8) = 3
ctz(16) = 4
ctz(20) = 2
ctz(24) = 3
Lowest set bit (a & -a):
LSB(12) = 4
LSB(24) = 8
LSB(7) = 1
LSB(40) = 8
LSB(40) = 8
Highest set bit:
MSB(12) = 8
MSB(24) = 16
MSB(7) = 4
MSB(40) = 32
MSB(40) = 32
XOR swap:
Sebelum: a=42, b=99
Sesudah: a=99, b=42
Toggle bit (XOR dengan 1<<i):
x = 00001010
setelah toggle bit 1: 00001000
setelah toggle bit 3: 00000000
Power of 2 check (x > 0 && (x & (x-1)) == 0):
1 is power of 2
2 is power of 2
4 is power of 2
7 is NOT power of 2
16 is power of 2
31 is NOT power of 2
32 is power of 2
100 is NOT power of 2
Bitset visualisasi:
255 = 11111111
170 = 10101010 (checkerboard)
85 = 01010101 (checkerboard inverted)Bedah Kode Baris-per-Baris #
__builtin_popcount(v): count bit 1. Implementasi GCC langsung compile ke instruksiPOPCNTpada CPU modern. Jauh lebih cepat dari loop manualwhile (x) { count += x & 1; x >>= 1; }.__builtin_clz(v): count leading zeros. Untuk 32-bit unsigned, hasil di [0, 31]. Undefined behavior bilav == 0— pastikanv > 0sebelum panggil.__builtin_ctz(v): count trailing zeros. Untukv = 8 = 0b1000, ctz = 3 (tiga trailing zeros). Juga UB untukv = 0.32 - __builtin_clz(v): hitung bit width minimum untuk merepresentasikanv. Mis.v = 1024 = 2^10butuh 11 bit. C++20:std::bit_width(v).v & (-v): isolasi LSB. Contoh12 = 0b1100→-12(two’s complement)= 0b...10100→12 & -12 = 0b0100 = 4. Inti dari Fenwick tree:idx += idx & (-idx)untuk navigasi ke parent.1u << (31 - __builtin_clz(v)): isolasi MSB. Mis.v = 12 = 0b1100,clz = 28,31 - 28 = 3,1 << 3 = 8 = 0b1000. Ini adalah highest power of 2 yang ≤ v.v & (v - 1): clear LSB. Mis.12 & 11 = 0b1100 & 0b1011 = 0b1000 = 8. Trik untuk iterasi bit set:while (v) { process LSB; v &= v - 1; }— loop hanya sebanyak popcount(v), bukan 32.- XOR swap: tukar
adanbtanpa temporary. Implementasinya:a ^= b→a = a ^ b.b ^= a→b = b ^ (a ^ b) = a.a ^= b→a = (a ^ b) ^ a = b. Warning: jangan pakai bilaadanbadalah reference ke variabel yang sama. x ^= (1 << i): toggle biti. Bila bit set, jadi 0. Bila clear, jadi 1. Berguna untuk DP bitmask (toggle elemen i dalam set).x > 0 && (x & (x-1)) == 0: cek power of 2.x & (x-1)menghapus LSB. Bila hasil = 0, artinya hanya ada 1 bit set → power of 2. Kondisix > 0karena0juga menghasilkan 0 dix & (x-1)tetapi bukan power of 2.- Verifikasi:
- popcount(255) = 8 (semua 8 bit set). ✓
- clz(1024) = 21 (1024 = 2^10, butuh 11 bit, 32 - 11 = 21 leading zeros). ✓
- ctz(8) = 3 (8 = 0b1000, tiga trailing zeros). ✓
- LSB(12) = 4 (0b1100, LSB = bit value 4). ✓
- MSB(12) = 8 (highest power of 2 ≤ 12). ✓
- XOR swap: 42 ↔ 99 → a=99, b=42. ✓
- Toggle bit: 0b1010 → toggle bit 1 → 0b1000 → toggle bit 3 → 0b0000. ✓
- Power of 2: 1, 2, 4, 16, 32 adalah power of 2. 7, 31, 100 bukan. ✓
Aplikasi #
Fenwick Tree #
// add(idx, val): iterasi dengan naik ke parent
for (++idx; idx <= n; idx += idx & (-idx)) bit[idx] += val;
// prefix(idx): iterasi dengan turun
for (++idx; idx > 0; idx -= idx & (-idx)) sum += bit[idx];idx & (-idx) mengisolasi LSB untuk menentukan step ke parent/child.
Iterasi Bit Set #
// Iterasi semua bit set di v, hanya loop sebanyak popcount(v) kali
while (v) {
int bit = __builtin_ctz(v);
// process bit
v &= v - 1; // clear LSB
}Lebih cepat dari for (int i = 0; i < 32; ++i) if (v & (1 << i)) yang selalu loop 32 kali.
Round Up ke Power of 2 #
uint32_t round_up_pow2(uint32_t v) {
if (v <= 1) return 1;
return 1u << (32 - __builtin_clz(v - 1));
}Berguna untuk segment tree (ukuran 2^k), hash table (size power of 2), dsb.
Hash Combine #
template<typename T>
void hash_combine(size_t& seed, const T& v) {
seed ^= std::hash<T>(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}Trik untuk combine hash beberapa value (mis. untuk unordered_map<pair<int,int>, ...>). Konstanta 0x9e3779b9 adalah golden ratio fractional part, memberi distribusi hash yang baik.
Ringkasan #
- Bitwise operations adalah O(1) di CPU modern, dengan instruksi hardware khusus.
__builtin_popcount(x): count bit 1. UB untuk x=0 di beberapa compiler.__builtin_clz(x): count leading zeros. UB untuk x=0.__builtin_ctz(x): count trailing zeros. UB untuk x=0.x & (-x): isolasi LSB. Inti dari Fenwick tree.x & (x-1): hapus LSB. Trik cek power of 2.- XOR swap: tukar tanpa temp (warning: jangan pada alias ke variabel yang sama).
- C++20 menyediakan
std::popcount,std::countl_zero,std::countr_zerodi<bit>. - Aplikasi: Fenwick tree, iterasi bit set, round-up power 2, hash combine.