- Mọi số nguyên trong máy tính đều lưu ở dạng nhị phân: bit thứ
k(đếm từ phải sang trái, bắt đầu từ0) mang trọng số2k. - Sáu toán tử bit cơ bản gồm:
AND (&),OR (|),XOR (^),NOT (~), dịch trái(<<)và dịch phải(>>). - Biểu thức
x & (x - 1)xóa đi bit1thấp nhất củax— dùng để kiểm tra lũy thừa của 2 trongO(1)và đếm số bit1. - Trong C++, luôn đặt ngoặc đơn khi so sánh kết quả phép bit (như
(a & b) == 0) và dùng1LL << kkhi dịch từ 31 bit trở lên (k >= 31) để tránh tràn số 32-bit.
Cách máy tính biểu diễn số nguyên ở hệ nhị phân
Bên trong bộ nhớ máy tính, mọi số nguyên đều được lưu bằng dãy các công tắc điện tử chỉ có hai trạng thái: tắt (0) và bật (1), gọi là các bit (binary digit). Khi viết một số không âm ở hệ nhị phân (cơ số 2), ta đánh số vị trí các bit từ phải sang trái, bắt đầu từ k = 0:
- Bit thứ
0(bit ngoài cùng bên phải — Least Significant Bit) có trọng số20 = 1. - Bit thứ
1có trọng số21 = 2; bit thứ2có trọng số22 = 4; tổng quát bit thứkmang giá trị2k.
Ví dụ với x = 13, vì 13 = 8 + 4 + 0 + 1 = 1·2³ + 1·2² + 0·2¹ + 1·2⁰, nên biểu diễn nhị phân 4 bit của 13 là 1101. Trong đó các bit ở vị trí k = 0, 2, 3 đang bật (1), còn bit ở vị trí k = 1 đang tắt (0). Quy ước đánh số từ phải sang trái này là nền tảng của mọi công thức thao tác bit cũng như thuật toán lũy thừa nhị phân.
Sáu toán tử bit (Bitwise Operators) cơ bản
Khác với toán tử logic (&&, ||, !) chỉ xét tính đúng/sai của cả con số, phép toán bit (bitwise operators) đặt hai số nguyên thẳng cột theo dạng nhị phân rồi tính toán độc lập trên từng vị trí bit. Cả C++ và Python đều dùng chung 6 toán tử sau:
1. Bảng chân trị của AND (&), OR (|), XOR (^) và NOT (~)
| Bit A | Bit B | A & B (AND) |
A | B (OR) |
A ^ B (XOR) |
~A (NOT) |
|---|---|---|---|---|---|
0 |
0 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
1 |
1 |
1 |
1 |
0 |
0 |
1 |
1 |
0 |
1 |
1 |
1 |
1 |
0 |
0 |
Ý nghĩa trực quan của từng phép toán:
- AND (
&— Phép lọc): Chỉ bằng1khi cả hai bit cùng bằng1. Dùng để giữ lại các bit ở vị trí có mặt nạ1và ép về0ở các vị trí khác. Ví dụ:13 & 11là1101₂ & 1011₂ = 1001₂ = 9. - OR (
|— Phép bật): Bằng1khi ít nhất một trong hai bit bằng1. Dùng để bật bit từ0lên1mà không ảnh hưởng tới các bit khác. Ví dụ:13 | 11là1101₂ | 1011₂ = 1111₂ = 15. - XOR (
^— Phép đảo / loại trừ): Bằng1khi hai bit khác nhau, bằng0khi hai bit giống nhau. Phép XOR có tính giao hoán, kết hợp,a ^ 0 = avà đặc biệt là tính tự triệt tiêua ^ a = 0. Nhờ đó, nếu một mảng có mọi phần tử xuất hiện chẵn lần trừ duy nhất một số xuất hiện lẻ lần, việc XOR toàn bộ mảng sẽ tìm ra ngay số đó trongO(n)thời gian vàO(1)bộ nhớ. - NOT (
~— Phép đảo toàn bộ bit): Lật mọi bit0thành1và1thành0. Do số nguyên có dấu lưu theo mã bù 2 (two's complement), trong cả C++ và Python ta luôn có~x = -x - 1(ví dụ~0 = -1,~5 = -6).
2. Hai phép dịch bit: Dịch trái (<<) và Dịch phải (>>)
- Dịch trái
x << k(vớik >= 0): Đẩy các bit củaxsang tráikô và điềnkbit0vào bên phải. Mỗi bước dịch trái nhân đôi giá trị, nênx << kbằngx × 2k(khi không tràn số). Đặc biệt,1 << ktạo ra số chỉ có duy nhất bit thứkbằng1, tức2k. - Dịch phải
x >> k: Đẩy các bit củaxsang phảikô, làmkbit cuối bên phải rơi mất. Vớix ≥ 0,x >> ktương đương phép chia nguyên⌊x / 2k⌋(ví dụ13 >> 2biến1101₂thành11₂ = 3).
Sáu mẹo thao tác bit kinh điển
Khi kết hợp 6 toán tử trên với mặt nạ (1 << k) (với k >= 0), ta điều khiển được từng bit riêng lẻ trong O(1). Hình dưới đây minh họa các thao tác trên số x = 13 (1101₂):
| Thao tác | Công thức | Giải thích cơ chế | Ví dụ với x = 13 (1101₂) |
|---|---|---|---|
1. Kiểm tra bit thứ k |
(x >> k) & 1 |
Đưa bit thứ k về vị trí 0 rồi & 1 để lấy giá trị (0 hoặc 1). |
(13 >> 2) & 1 -> 1 |
2. Bật bit thứ k lên 1 |
x | (1 << k) |
Mặt nạ 1 << k (k >= 0) chỉ có bit k bằng 1; phép OR ép bit k của x lên 1. |
13 | (1 << 1) -> 15 (1111₂) |
3. Tắt bit thứ k về 0 |
x & ~(1 << k) |
Mặt nạ ~(1 << k) (k >= 0) toàn 1 trừ vị trí k bằng 0; phép AND xóa bit k. |
13 & ~(1 << 2) -> 9 (1001₂) |
4. Đảo bit thứ k |
x ^ (1 << k) |
Vì b ^ 1 = ~b, XOR với 1 << k (k >= 0) lật ngược đúng bit thứ k. |
13 ^ (1 << 0) -> 12 (1100₂) |
5. Xóa bit 1 thấp nhất |
x & (x - 1) |
Phép x - 1 lật bit 1 thấp nhất thành 0 và các bit bên phải thành 1; & xóa sạch phần đó. |
13 & 12 = 12 (1100₂) |
6. Lấy bit 1 thấp nhất |
x & (-x) |
Trong mã bù 2, -x = ~x + 1 chỉ giữ lại đúng bit 1 thấp nhất khi & với x. |
12 & (-12) = 4 (0100₂) |
Từ mẹo số 5 (x & (x - 1)), ta có hai ứng dụng rất phổ biến:
- Kiểm tra lũy thừa của 2 trong
O(1): Một số nguyên dương là lũy thừa của 2 (1, 2, 4, 8, ...) khi và chỉ khi nó chỉ có duy nhất một bit1. Do đóx > 0 && (x & (x - 1)) == 0kiểm tra chính xác tính chất này. - Đếm số lượng bit
1(Brian Kernighan): Vòng lặpwhile (x > 0) { x &= (x - 1); dem++; }nhảy thẳng qua từng bit1, chỉ mất số bước đúng bằng số bit1củax. Ngoài ra, bạn có thể dùng hàm gắn sẵn__builtin_popcountll(x)trong C++ hoặcx.bit_count()trong Python 3.10+.
#include <iostream>
using namespace std;
// 1. Kiểm tra bit thứ k của x (trả về 1 hoặc 0)
int kiemTraBit(long long x, int k) {
return (x >> k) & 1LL;
}
// 2. Bật bit thứ k của x lên 1
long long batBit(long long x, int k) {
return x | (1LL << k);
}
// 3. Tắt bit thứ k của x về 0
long long tatBit(long long x, int k) {
return x & ~(1LL << k);
}
// 4. Đảo trạng thái bit thứ k của x (0 thành 1, 1 thành 0)
long long daoBit(long long x, int k) {
return x ^ (1LL << k);
}
// 5. Kiểm tra x có phải lũy thừa của 2 không (2^0, 2^1, 2^2, ...)
bool laLuyThuaCua2(long long x) {
return x > 0 && (x & (x - 1)) == 0;
}
// 6. Đếm số lượng bit 1 của x theo thuật toán Brian Kernighan
int demBit1(long long x) {
int dem = 0;
while (x > 0) {
x &= (x - 1); // xóa bit 1 thấp nhất ở mỗi bước
dem++;
}
return dem;
}
// 7. Lấy giá trị của bit 1 thấp nhất (2^k nhỏ nhất chia hết x)
long long bit1ThapNhat(long long x) {
return x & (-x);
}
int main() {
long long x = 13; // 13 ở hệ nhị phân là 1101 (bit 0, 2, 3 bằng 1; bit 1 bằng 0)
cout << kiemTraBit(x, 2) << " " << kiemTraBit(x, 1) << "\n"; // 1 0
cout << batBit(x, 1) << "\n"; // 15 (1111)
cout << tatBit(x, 2) << "\n"; // 9 (1001)
cout << daoBit(x, 0) << "\n"; // 12 (1100)
cout << laLuyThuaCua2(16) << " " << laLuyThuaCua2(18) << "\n"; // 1 0
cout << demBit1(x) << "\n"; // 3
cout << bit1ThapNhat(12) << "\n"; // 4 (1100 -> 0100)
return 0;
}
def kiem_tra_bit(x, k):
"""Kiểm tra bit thứ k của x (trả về 1 hoặc 0)."""
return (x >> k) & 1
def bat_bit(x, k):
"""Bật bit thứ k của x lên 1."""
return x | (1 << k)
def tat_bit(x, k):
"""Tắt bit thứ k của x về 0."""
return x & ~(1 << k)
def dao_bit(x, k):
"""Đảo trạng thái bit thứ k của x (0 thành 1, 1 thành 0)."""
return x ^ (1 << k)
def la_luy_thua_cua_2(x):
"""Kiểm tra x có phải lũy thừa của 2 không (trả về 1 hoặc 0)."""
return 1 if (x > 0 and (x & (x - 1)) == 0) else 0
def dem_bit_1(x):
"""Đếm số lượng bit 1 của x nguyên không âm bằng mẹo xóa bit 1 thấp nhất."""
dem = 0
while x > 0:
x &= x - 1
dem += 1
return dem
def bit_1_thap_nhat(x):
"""Lấy giá trị của bit 1 thấp nhất."""
return x & (-x)
x = 13 # 13 ở hệ nhị phân là 1101
print(kiem_tra_bit(x, 2), kiem_tra_bit(x, 1)) # 1 0
print(bat_bit(x, 1)) # 15
print(tat_bit(x, 2)) # 9
print(dao_bit(x, 0)) # 12
print(la_luy_thua_cua_2(16), la_luy_thua_cua_2(18)) # 1 0
print(dem_bit_1(x)) # 3
print(bit_1_thap_nhat(12)) # 4
Chủ đề này nằm trong Lecture 02: Bit Manipulation (Thao tác Bit) của Khóa Orange. Xem đề cương
Duyệt tập con bằng kỹ thuật Bitmask
Khi tập hợp có n phần tử nhỏ (n ≤ 20), ta có thể dùng một số nguyên mask từ 0 đến 2n - 1 (tức (1 << n) - 1 với n > 0) để mã hóa một tập con: nếu bit thứ i của mask bằng 1 thì phần tử a[i] được chọn, ngược lại bằng 0 là không chọn.
Ví dụ với n = 3 phần tử [a₀, a₁, a₂], 8 giá trị mask từ 0 (000₂) đến 7 (111₂) biểu diễn trọn vẹn 8 tập con: mask = 0 là tập rỗng ∅, mask = 5 (101₂, bật bit 0 và 2) là {a₀, a₂}, còn mask = 7 (111₂) là {a₀, a₁, a₂}. Nhờ vậy, việc duyệt mọi tập con để đếm số tập con có tổng bằng S chỉ cần hai vòng lặp đơn giản thay vì viết đệ quy quay lui:
#include <iostream>
#include <vector>
using namespace std;
// Đếm số tập con của mảng a có tổng đúng bằng S bằng kỹ thuật Bitmask
int demTapConCoTong(const vector<int>& a, long long S) {
int n = (int)a.size();
int soTapCon = 0;
// Duyệt mọi trạng thái mask từ 0 đến 2^n - 1
for (int mask = 0; mask < (1 << n); mask++) {
long long tong = 0;
for (int i = 0; i < n; i++) {
// Nếu bit thứ i của mask bật (bằng 1) thì chọn phần tử a[i]
if ((mask >> i) & 1) {
tong += a[i];
}
}
if (tong == S) {
soTapCon++;
}
}
return soTapCon;
}
int main() {
cout << demTapConCoTong({3, 5, 7, 8, 10}, 15) << "\n"; // 3 ({5,10}, {7,8}, {3,5,7})
cout << demTapConCoTong({2, 4, 6}, 5) << "\n"; // 0
cout << demTapConCoTong({1, 2, 3, 4}, 6) << "\n"; // 2 ({2,4}, {1,2,3})
return 0;
}
def dem_tap_con_co_tong(a, s):
"""Đếm số tập con của danh sách a có tổng đúng bằng s bằng kỹ thuật Bitmask."""
n = len(a)
so_tap_con = 0
# Duyệt mọi trạng thái mask từ 0 đến 2^n - 1
for mask in range(1 << n):
tong = 0
for i in range(n):
if (mask >> i) & 1:
tong += a[i]
if tong == s:
so_tap_con += 1
return so_tap_con
print(dem_tap_con_co_tong([3, 5, 7, 8, 10], 15)) # 3
print(dem_tap_con_co_tong([2, 4, 6], 5)) # 0
print(dem_tap_con_co_tong([1, 2, 3, 4], 6)) # 2
Độ phức tạp của thuật toán là O(n · 2n) thời gian và O(1) bộ nhớ phụ, chạy tốt trong 1 giây khi n ≤ 20. Xem thêm bài nên học Python hay C++ để biết cách tối ưu tốc độ vòng lặp.
Lỗi hay gặp
- Thứ tự ưu tiên toán tử trong C++ (
==cao hơn&,^,|). Nếu bạn viếtif (a & b == 0), trình biên dịch C++ sẽ tínhb == 0trước rồi mới lấyađem&với kết quả đó! Bắt buộc phải bọc ngoặc đơn:if ((a & b) == 0). - Tràn số 32-bit khi dịch
1 << kvớik >= 31trong C++. Hằng số1mặc định thuộc kiểuint32-bit có dấu, nên dịch từ 31 bit trở lên sẽ gây tràn số (Undefined Behavior) dù biến nhận kết quả làlong long. Khi làm việc với bit thứ 31 đến 62, luôn viết1LL << k(k >= 31). - Duyệt Bitmask khi
nquá lớn. Khin = 30,230 ≈ 109trạng thái sẽ bị quá thời gian (TLE). Vớinkhoảng 30–40, hãy chia đôi mảng làm hai nửa 15–20 phần tử, duyệt Bitmask mỗi nửa rồi ghép bằng tìm kiếm nhị phân (Meet in the Middle).
Luyện ngay trên Big-O Coding
Thực hành ngay các bài toán thao tác bit và duyệt tập con Bitmask trên hệ thống Big-O Coding:
Câu hỏi thường gặp
Toán tử & (Bitwise AND) khác gì với && (Logical AND)?
Toán tử && xét mỗi vế khác 0 hay bằng 0 để trả về true/false và có tính ngắt mạch. Ngược lại, & tính AND trên từng cặp bit của hai số nguyên: 4 && 2 bằng true (1), nhưng 4 & 2 (100₂ & 010₂) bằng 0.
Làm sao kiểm tra số chẵn lẻ bằng phép toán bit?
Dùng biểu thức (x & 1): bằng 0 là số chẵn, bằng 1 là số lẻ. Cách này kiểm tra trực tiếp bit cuối (2⁰) nên chạy nhanh và đúng với cả số nguyên âm.
Vì sao x & -x lại lấy được đúng giá trị bit 1 thấp nhất của x?
Trong mã bù 2, -x = ~x + 1. Phép đảo ~x lật bit 1 thấp nhất thành 0 và các bit 0 bên phải thành 1; khi cộng 1, phần bên phải tràn nhớ và trả đúng bit thấp nhất đó về 1, còn phần bên trái bị đảo ngược nên triệt tiêu hết khi & với x.
Khóa Orange: làm chủ thuật toán nâng cao như Graph, DP tối ưu, Segment Tree, HLD để giải bài Hard. 16 buổi online, C++/Java/Python, học thử buổi 1 miễn phí.


