Cho số nguyên, mỗi số nhỏ hơn , hãy cộng XOR của mọi cặp số. Có gần cặp nên duyệt cặp là vô vọng. Nhưng XOR không coi mỗi số là một khối: nó xử lý 60 vị trí nhị phân hoàn toàn riêng rẽ, nên bài toán thực chất là 60 bài đếm số 0 và số 1 xếp chồng nhau, mỗi bài giải bằng một lượt duyệt.
Đó là nửa thứ nhất của thao tác bit: một số nguyên là một dãy công tắc, và các phép &, |, ^ bật tắt từng công tắc một. Nửa thứ hai là nhìn chính dãy công tắc ấy như một tập hợp: giao, hợp, hiệu của hai tập thành một phép toán, còn duyệt mọi tập con của 20 phần tử thành một vòng for chạy qua số. Bài này dạy cả hai nửa, rồi dùng chúng làm nền cho quy hoạch động trên tập con ở bài 15.
Số nguyên là một dãy công tắc
Số nguyên không âm được lưu dưới dạng nhị phân, với . Ta gọi là bit thứ và đánh số từ 0, bắt đầu ở bit nhỏ nhất, tức bên phải. Số viết là , có , , , .
Ba phép toán hai ngôi dưới đây làm việc theo từng cột: bit thứ của kết quả chỉ phụ thuộc bit thứ của hai toán hạng.
a & b(AND) có bit 1 ở những cột mà cả hai bit đều là 1.a | b(OR) có bit 1 ở những cột mà ít nhất một bit là 1.a ^ b(XOR) có bit 1 ở những cột mà hai bit khác nhau.
Phép ~a (NOT) đảo mọi bit; với số có dấu, ~a bằng -a - 1, nên sau khi đảo ta thường phải AND với một mặt nạ để cắt các bit thừa phía trên.
Phép dịch x << k thêm bit 0 vào cuối, nên bằng khi không tràn. Phép dịch x >> k bỏ bit cuối, nên bằng khi . Với số âm, >> làm tròn xuống (-7 >> 1 bằng ) còn / làm tròn về 0; trong bài thi ta chỉ dịch số không âm.
Có hai bẫy mà ai cũng vấp ít nhất một lần. Bẫy thứ nhất là kiểu của hằng số: số 1 là int 32 bit, nên 1 << 40 dịch quá độ rộng của kiểu, một hành vi không xác định; gán kết quả vào biến long long cũng không cứu được vì phép dịch đã hỏng từ trước khi gán. Mặt nạ đi với số 64 bit phải viết 1LL << 40. Bẫy thứ hai là độ ưu tiên: các phép &, ^, | yếu hơn ==, !=, còn +, - mạnh hơn <<, >>. Vì vậy x & 1 == 0 được hiểu là x & (1 == 0), còn 1 << k + 1 là 1 << (k + 1). Quy tắc an toàn: bọc ngoặc quanh mọi biểu thức bit, như (x & 1) == 0.
Hỏi nhanh
Một bạn viết if (x & 1 == 0) chan = true; để đánh dấu là số chẵn. Với , biến chan có được đặt thành true không?
Do == mạnh hơn &, biểu thức là x & (1 == 0). Phép so sánh 1 == 0 cho 0, nên điều kiện là x & 0, luôn bằng 0, tức luôn sai, kể cả khi . Dấu của không liên quan. Cũng không có lỗi biên dịch: kết quả bool của phép so sánh tự đổi sang int, và C++ cho phép.
Đáng sợ hơn, x & 1 == 1 lại chạy đúng vì 1 == 1 cho 1 và x & 1 chính là bit thấp nhất. Người đã quen viết phép kiểm tra lẻ như vậy rất dễ bắt chước cho phép kiểm tra chẵn, và lỗi chỉ lộ ra khi thử thật với một số chẵn. Cách chữa là ngoặc: (x & 1) == 0.
Đọc, ghi và đếm bit. Mọi thao tác trên một bit riêng đều là AND, OR hoặc XOR với một mặt nạ (mask), số chỉ có đúng bit đó bật, 1LL << i: đọc bit (x >> i) & 1; bật x | (1LL << i); tắt x & ~(1LL << i); đảo x ^ (1LL << i); giữ bit thấp x & ((1LL << k) - 1), chính là .
Hai biểu thức quan trọng nhất xoay quanh bit 1 thấp nhất. Viết thành phần đầu , một bit 1, rồi bit 0:
Phép trừ 1 mượn từ bit 1 thấp nhất, giống trong hệ thập phân. Từ đó x & (x - 1) bằng : nó xóa bit 1 thấp nhất. Còn số âm được máy lưu theo cách gọi là bù hai: là đảo mọi bit (viết ) rồi cộng 1, nên , nên x & -x giữ riêng bit 1 thấp nhất, bằng ; giá trị này hay gọi là lowbit(x), và cây Fenwick (bài 16) di chuyển giữa các nút bằng cách cộng hoặc trừ chính nó.
Số là lũy thừa của hai khi chỉ có một bit 1, tức (x & (x - 1)) == 0. GCC có sẵn __builtin_popcountll(x) (số bit 1), __builtin_ctzll(x) (vị trí bit 1 thấp nhất) và __builtin_clzll(x) (số bit 0 đứng đầu trong 64 bit; độ dài nhị phân là 64 - __builtin_clzll(x)). Hai hàm ctz và clz không xác định khi . Chương trình sau in số bit 1, lowbit, độ dài nhị phân và kết quả kiểm tra lũy thừa của hai cho từng số dương.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll q, x;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> q;
while (q--) {
cin >> x;
cout << __builtin_popcountll(x) << ' ' << (x & -x) << ' ' << 64 - __builtin_clzll(x) << ' '
<< ((x & (x - 1)) == 0 ? "yes" : "no") << '\n';
}
}
4
13
44
64
1000000000000
3 1 4 no
3 4 6 no
1 64 7 yes
13 4096 40 no
Bản có đuôi ll là có chủ ý. Các hàm không đuôi, như __builtin_popcount, nhận unsigned int 32 bit; đưa vào đó một số long long thì 32 bit cao bị cắt âm thầm, và trình biên dịch thường không cảnh báo.
Hỏi nhanh
Với long long x = (1LL << 40) + 1;, biểu thức __builtin_popcount(x) (không có đuôi ll) cho kết quả nào?
Hàm __builtin_popcount nhận unsigned int, nên được đổi sang 32 bit bằng cách giữ lại 32 bit thấp. Bit 40 bị bỏ, chỉ còn bit 0, nên kết quả là 1. Giá trị đúng của số 64 bit là 2, và muốn có nó phải gọi __builtin_popcountll.
Đáp án 2 là kết quả ta mong đợi nhưng hàm không cho. Đáp án 41 nhầm popcount với độ dài nhị phân. Cũng không có lỗi biên dịch vì long long được ép ngầm sang unsigned int; đó chính là chỗ nguy hiểm, vì chương trình chạy bình thường và chỉ sai khi dữ liệu có số vượt .
Ví dụNuôi Đủ Số Lượng
Đề. Hộp ban đầu rỗng. Mỗi sáng được thêm bao nhiêu vi khuẩn tùy ý, mỗi tối mọi vi khuẩn chia đôi. Muốn có lúc hộp chứa đúng con; in tổng số vi khuẩn ít nhất phải tự tay thêm vào.
Lời giải. Một con thêm vào sáng ngày nào đó, sau đêm thành con. Vậy số vi khuẩn lúc dừng là tổng các lũy thừa của hai, mỗi con thêm vào góp một số hạng, và đáp án là số số hạng ít nhất để viết thành tổng các lũy thừa của hai. Viết nhị phân là cách ít nhất: hai số hạng bằng nhau luôn gộp được thành , nên một cách viết tối ưu không có số hạng lặp, tức chính là biểu diễn nhị phân. Đáp án là số bit 1 của : cần 3 con.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll x;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> x;
cout << __builtin_popcountll(x) << '\n';
}
13
3
Một số nguyên là một tập hợp
Một tập con của ứng với một số trong : phần tử thuộc tập khi bit thứ bật. Tập ứng với . Có đúng tập con và số, mỗi tập ứng với đúng một số. Số nguyên đóng vai tập hợp như vậy cũng gọi là mặt nạ (bitmask).
Mọi phép toán tập hợp trở thành một phép toán bit, và xử lý cả tập, tối đa 64 phần tử, trong một lệnh:
- giao
A & B, hợpA | B, hiệu làA & ~B, hiệu đối xứngA ^ B; - phần bù trong là
A ^ full, vớifull = (1 << n) - 1; - khi và chỉ khi
(A & B) == A; số phần tử là popcount; - phần tử thuộc khi
(A >> i) & 1; thêm hay bỏ phần tử là bật hay tắt bit.
Duyệt mọi tập con là vòng for (ll mask = 0; mask < (1 << n); mask++). Với có hơn một triệu mặt nạ; nếu mỗi mặt nạ tốn thêm để xem từng phần tử thì tổng là , vẫn nhẹ. Mảng lưu một giá trị cho mỗi mặt nạ tốn ô: số long long là 8 MB, còn số đã là 134 MB, nên thường không vượt quá 20 đến 22. Khi tập có hơn 64 phần tử, ta vẫn cần giao và hợp nhanh; lúc đó dùng bitset (bài 27) với cùng bộ phép toán.
Bài toán nhỏ: cho số và một giá trị , đếm số tập con khác rỗng có tổng bằng . Tính tổng riêng cho từng mặt nạ tốn mỗi mặt nạ. Có cách tốt hơn: tổng của mask bằng tổng của mask bỏ phần tử nhỏ nhất, cộng thêm phần tử đó. Mặt nạ mask & (mask - 1) luôn nhỏ hơn mask, nên khi duyệt mask tăng dần thì nó đã được tính xong.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, S, a[20], sum[1 << 20];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> S;
for (ll i = 0; i < n; i++) cin >> a[i];
ll cnt = 0;
for (ll mask = 1; mask < (1 << n); mask++) {
sum[mask] = sum[mask & (mask - 1)] + a[__builtin_ctzll(mask)];
if (sum[mask] == S) cnt++;
}
cout << cnt << '\n';
}
5 10
1 2 3 4 5
3
Ba tập thỏa mãn là , và . Cả bài tốn . Cách tính một mặt nạ từ mặt nạ nhỏ hơn chính là khuôn của quy hoạch động trên tập con ở bài 15.
Ví dụChọn Bộ Bài Cân Đối
Đề. Có bài, bài có độ khó . Đếm số tập gồm ít nhất hai bài có tổng độ khó trong và hiệu giữa độ khó lớn nhất với nhỏ nhất không nhỏ hơn .
Lời giải. tập, duyệt hết. Với mỗi mặt nạ, đi qua 15 bit: bit nào bật thì cộng độ khó vào tổng và cập nhật lớn nhất, nhỏ nhất; rồi kiểm ba điều kiện, kể cả popcount ít nhất 2. Tổng cộng phép. Tập một phần tử bị điều kiện hiệu loại luôn (hiệu 0 nhỏ hơn ), nhưng kiểm popcount thì không phải nghĩ.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n, l, r, x, c[15];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> l >> r >> x;
for (ll i = 0; i < n; i++) cin >> c[i];
ll ans = 0;
for (ll mask = 0; mask < (1 << n); mask++) {
if (__builtin_popcountll(mask) < 2) continue;
ll s = 0, mx = 0, mn = 1e18;
for (ll i = 0; i < n; i++)
if ((mask >> i) & 1) {
s += c[i];
mx = max(mx, c[i]);
mn = min(mn, c[i]);
}
if (l <= s && s <= r && mx - mn >= x) ans++;
}
cout << ans << '\n';
}
5 21 61 19
1 10 20 30 40
13
Duyệt các tập con của một tập
Bây giờ cho một mặt nạ và cần liệt kê mọi tập con , gọi là mặt nạ con (submask). Cách ngây thơ là cho chạy từ 0 tới rồi kiểm (s & m) == s. Cách này tốn bước, kể cả khi chỉ có hai bit bật ở vị trí 59 và 3 và chỉ có bốn tập con để duyệt.
Ý tưởng: từ một mặt nạ con , nhảy thẳng tới mặt nạ con lớn nhất nhỏ hơn bằng một phép toán:
Vì sao đúng? Viết với bit 0 ở cuối. Một mặt nạ con khác lần đầu (tính từ bit cao xuống) ở một vị trí mà có bit 1 còn có bit 0. Muốn lớn nhất thì vị trí khác nhau phải thấp nhất có thể, tức đúng là bit 1 thấp nhất của ; sau đó các bit phía dưới được tự do chọn, nên ta bật mọi bit mà cho phép. Đó chính xác là : cho , và AND với loại các bit nằm ngoài mà vẫn giữ nguyên , vì .
Bắt đầu từ rồi lặp phép nhảy, dãy giảm ngặt và đi qua mọi mặt nạ con đúng một lần, dừng ở 0. Với , dãy là như trong hình. Tập rỗng cần chú ý: nếu quên dừng ở đó, phép trả về và vòng lặp chạy mãi. Vòng ngắn for (s = m; s; s = (s - 1) & m) thì bỏ qua 0 và bạn phải xử lý nó riêng.
Mặt nạ có bit bật thì có mặt nạ con. Duyệt mọi rồi mọi mặt nạ con của nó, tổng số bước là . Lý do: mỗi cặp ứng với việc gán cho từng phần tử một trong ba trạng thái, “ngoài ”, “trong nhưng ngoài ”, “trong cả hai”. Vậy chứ không phải . Với đó là , với là (sát giới hạn), còn là , quá chậm. Vì vậy các bài kiểu “chia tập thành các nhóm, mỗi nhóm tốn ”, giải bằng , chỉ chạy được khi cỡ 16 tới 18. Chương trình dưới đếm tổng số bước với và in đúng .
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
ll steps = 0;
for (ll mask = 0; mask < (1 << n); mask++)
for (ll s = mask;; s = (s - 1) & mask) {
steps++;
if (s == 0) break;
}
cout << steps << '\n';
}
10
59049
Hỏi nhanh
Ta duyệt mọi mask từ 0 tới , và với mỗi mask duyệt mọi tập con của nó bằng vòng s = (s - 1) & mask. Với , vòng trong chạy tổng cộng khoảng bao nhiêu lần?
Mỗi cặp ứng với việc gán cho 14 phần tử một trong ba trạng thái (ngoài mask, trong mask nhưng ngoài , trong cả hai), nên có cặp.
Cách tính lấy số bit trung bình của một mặt nạ làm số mặt nạ con, nhưng số mặt nạ con là nên tăng rất nhanh theo , và trung bình thực là , không phải . Cách giả định mỗi mặt nạ có mặt nạ con, chỉ đúng với mặt nạ toàn bit 1. Cách là chi phí duyệt từng phần tử, không phải duyệt mặt nạ con.
Ví dụLiệt Kê Mặt Nạ Con
Đề. Cho có không quá 15 bit 1. In mọi mặt nạ con của theo thứ tự tăng dần, mỗi số một dòng.
Lời giải. Cho chạy từ 0 tới là bước. Vòng đi qua đúng mặt nạ con, nhưng theo thứ tự giảm dần; lưu vào mảng rồi in ngược. Vòng dừng khi vừa xử lý xong , trước khi nhảy về lại . Mọi số là ll vì tới . Với , vòng cho , in ngược thành .
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n;
vector<ll> v;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll s = n;; s = (s - 1) & n) {
v.push_back(s);
if (s == 0) break;
}
reverse(v.begin(), v.end());
for (ll s : v) cout << s << '\n';
}
10
0
2
8
10
Nghĩ theo từng bit
Đây là cách nghĩ được dùng nhiều nhất trong các bài về bit. Vì bit thứ của x & y, x | y, x ^ y chỉ phụ thuộc bit thứ của và , một bài toán chỉ gồm các phép đó tách được thành 30 hay 60 bài toán độc lập trên các số 0 và 1. Mỗi bài con dễ hơn nhiều, và đáp án ghép lại bằng cách nhân kết quả ở bit thứ với trọng số rồi cộng.
Ví dụ điển hình: tính cho số. Với mỗi bit , đặt là số phần tử có bit thứ bật và . Cặp đóng góp khi và chỉ khi hai bit khác nhau, nên có đúng cặp như vậy:
Với bốn số , tức : bit 2 bật ở hai số, bit 1 bật ở hai số, bit 0 bật ở ba số. Số cặp khác nhau là , và , nên tổng là . Kiểm trực tiếp sáu cặp: .
Cùng khuôn đó cho các phép khác. Số cặp có bit của AND bật là ; của OR là . Muốn AND, OR, XOR của một đoạn với nhiều truy vấn, ta giữ 30 mảng tổng tiền tố đếm số bit 1 của từng cột (bài 02): bit của OR bật khi số lượng trong đoạn lớn hơn 0, của AND khi số lượng bằng độ dài đoạn, của XOR khi số lượng lẻ.
Khi các số là liên tiếp thì không cần duyệt: cột lặp tuần hoàn với chu kỳ , gồm số 0 rồi số 1. Do đó số số trong có bit bật là
Thử với , : chu kỳ 4, có số từ các chu kỳ đầy, cộng số từ chu kỳ dở, tổng 5; đúng là các số .
Cách nghĩ này chỉ áp dụng cho &, |, ^, ~. Phép cộng, trừ, nhân và so sánh không tách được theo cột, vì bit thứ của còn phụ thuộc số nhớ từ các bit thấp hơn. Mục cuối sẽ cho thấy cách đưa phép cộng về XOR và AND.
Hỏi nhanh
Có 5 số . Ở bit thứ 3, đúng 2 số có bit đó bật và 3 số có bit đó tắt. Có bao nhiêu cặp để có bit thứ 3 bật?
XOR bật bit khi hai bit khác nhau, tức một số lấy từ nhóm 2 số bật và một số lấy từ nhóm 3 số tắt: cặp.
Con số 1 là , số cặp cả hai cùng bật, tức đếm cho AND. Con số 3 là , số cặp cả hai cùng tắt. Con số 10 là , tất cả các cặp, trong đó có cả những cặp hai bit bằng nhau mà XOR cho 0 ở bit này.
Ví dụTổng XOR Của Mọi Cặp
Đề. Cho số nguyên không âm . In theo modulo .
Lời giải. Đúng công thức trên: với mỗi bit từ 0 tới 59, đếm phần tử có bit bật, cộng . Đếm bit mất phép thay cho cặp. Chỗ dễ sai là tràn số: tới , vẫn vừa ll, nhưng nhân tiếp với cỡ thì vượt . Phải lấy modulo của trước khi nhân với trọng số. Với ví dụ : bit 0 cho cặp, đóng góp 2; bit 2 cho 2 cặp, đóng góp 8; tổng 10.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 1e9 + 7;
ll n, x, c[60];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 0; i < n; i++) {
cin >> x;
for (ll b = 0; b < 60; b++) c[b] += (x >> b) & 1;
}
ll ans = 0, w = 1;
for (ll b = 0; b < 60; b++) {
ans = (ans + c[b] * (n - c[b]) % MOD * w) % MOD;
w = w * 2 % MOD;
}
cout << ans << '\n';
}
3
0 1 4
10
Chọn bit từ cao xuống thấp
Bit càng cao càng nặng: lớn hơn mọi bit thấp cộng lại, vì . Hệ quả cho các bài hỏi giá trị lớn nhất của một số ghép từ các bit: ta quyết định từng bit từ cao xuống thấp, bit nào còn đặt được bằng 1 thì đặt, và không bao giờ phải hối tiếc, vì bỏ một bit cao để đổi lấy cả loạt bit thấp luôn là lỗ.
Ví dụ: cho số nhỏ hơn , tìm giá trị lớn nhất của với . Duyệt mọi cặp tốn . Cách khác: giữ ans, thử từng bit từ 29 xuống 0. Đặt c = ans | (1LL << b) rồi đếm số phần tử chứa mọi bit của c, tức (x & c) == c. Nếu có ít nhất hai phần tử như vậy thì AND của chúng chứa c, nên ta giữ ans = c.
Vì sao đúng? Gọi là khả thi nếu có ít nhất hai phần tử chứa mọi bit của . Đáp án là số khả thi lớn nhất. Giả sử các bit từ cao tới bit của ans đã trùng với . Nếu ans | (1LL << b) khả thi thì không nhỏ hơn nó, mà có cùng các bit cao, nên buộc phải có bit . Ngược lại nếu có bit thì ans | (1LL << b) chỉ gồm các bit của , nên vẫn khả thi. Vậy thử rồi giữ bit khi khả thi luôn đặt đúng bit của .
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll N = 2e5 + 5;
ll n, a[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (ll i = 0; i < n; i++) cin >> a[i];
ll ans = 0;
for (ll b = 29; b >= 0; b--) {
ll c = ans | (1LL << b), cnt = 0;
for (ll i = 0; i < n; i++)
if ((a[i] & c) == c) cnt++;
if (cnt >= 2) ans = c;
}
cout << ans << '\n';
}
5
12 8 6 14 3
12
Chạy tay: bit 3 có ba số chứa (là ) nên giữ, ans ; bit 2 có hai số chứa nên giữ, ans ; bit 1 chỉ có chứa nên bỏ; bit 0 không có số nào chứa . Đáp án , trong . Mẫu “thử bit cao, giữ nếu còn khả thi” xuất hiện rất nhiều: XOR lớn nhất của hai số (trie ở bài 20, cơ sở XOR ở bài 61), tối đa hóa AND sau một số thao tác, chọn để tối đa hóa một tổng.
Ví dụAND Lớn Nhất Sau Khi Bật Bit
Đề. Cho dãy số (tổng qua các test tới ) và . Mỗi thao tác bật một bit (vị trí 0 tới 30) của một phần tử. Với không quá thao tác, in AND lớn nhất có thể của cả dãy.
Lời giải. Bit có mặt trong AND khi mọi phần tử đều mang nó, và muốn vậy phải bật bit ở mọi phần tử còn thiếu, tốn đúng số phần tử thiếu. Chi phí của các bit độc lập với nhau, vì bật bit này không ảnh hưởng bit kia. Vậy duyệt từ 30 xuống 0: nếu số phần tử thiếu bit không vượt ngân sách còn lại thì trả và bật bit trong đáp án. Tham lam đúng vì một bit cao đáng giá hơn mọi bit thấp cộng lại. Với test đầu, bit 1 thiếu ở số 1 và bit 0 thiếu ở số 2: tốn 2 thao tác, AND bằng 3.
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll N = 2e5 + 5;
ll t, n, k, a[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> t;
while (t--) {
cin >> n >> k;
for (ll i = 0; i < n; i++) cin >> a[i];
ll ans = 0;
for (ll b = 30; b >= 0; b--) {
ll need = 0;
for (ll i = 0; i < n; i++)
if (!((a[i] >> b) & 1)) need++;
if (need <= k) {
k -= need;
ans |= 1LL << b;
}
}
cout << ans << '\n';
}
}
2
3 2
1 2 3
3 0
7 3 5
3
1
XOR và phép cộng
XOR là phép cộng từng bit theo modulo 2, không có nhớ. Bốn tính chất làm nên gần như mọi mẹo với XOR: giao hoán, kết hợp, và . Tính chất cuối nói rằng XOR là phép ngược của chính nó, nên ta chuyển vế được: khi và chỉ khi . Ba hệ quả hay dùng:
- Hai số bằng nhau triệt tiêu. Nếu trong dãy mọi giá trị xuất hiện chẵn lần, trừ đúng một giá trị xuất hiện lẻ lần, thì XOR cả dãy cho ra giá trị ấy.
- XOR có phép ngược nên có tiền tố, như tổng ở bài 02: với thì XOR của đoạn là .
- XOR của mọi phần tử trừ phần tử là , với là XOR của cả dãy.
Phép cộng khác ở chỗ có nhớ, nhưng vẫn quy về XOR và AND được. Ở cột thứ với hai bit , ta có , thử ngay được trong ba trường hợp , và . Cộng mọi cột lại:
XOR là tổng khi bỏ qua nhớ, còn AND chỉ ra các cột sinh nhớ, mỗi nhớ sang cột bên cạnh nên nhân đôi. Từ đây: ; hai vế bằng nhau khi và chỉ khi , tức hai số không chung bit nào; và hiệu luôn chẵn. Một công thức cùng loại là .
Hỏi nhanh
Hai số nguyên không âm có và . Giá trị của là bao nhiêu?
Từ ta có , nên . Cặp , thỏa: , , .
Đáp án 6 là quên hệ số 2: mỗi cột sinh nhớ có giá trị gấp đôi cột đó. Đáp án 30 là cộng thay vì trừ. Đáp án 12 là lẫn AND với XOR.
Ví dụXOR Của Một Đoạn Số
Đề. Cho . In .
Lời giải. Dùng tiền tố XOR: đặt và . Phần chung xuất hiện hai lần trong nên triệt tiêu, còn lại đúng đáp án. Tính trong : bốn số giống nhau ở mọi bit từ bit 2 trở lên, phần chung đó xuất hiện 4 lần nên triệt tiêu, còn hai bit thấp chạy qua có XOR bằng 0. Vậy mỗi khối bốn số có XOR 0, và chỉ còn là XOR của phần dở ở khối cuối. Theo là , ta có , , , . Chỗ dễ sai là : cần , mà -1 % 4 bằng chứ không phải 3, nên trả 0 riêng cho . Với ví dụ, , , đáp án .
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll A, B;
ll F(ll n) {
if (n < 0) return 0;
if (n % 4 == 0) return n;
if (n % 4 == 1) return 1;
if (n % 4 == 2) return n + 1;
return 0;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> A >> B;
cout << (F(B) ^ F(A - 1)) << '\n';
}
5 9
5
Khi đi thi
- Thấy AND, OR, XOR trên mọi cặp hay mọi đoạn với số cỡ hoặc : tách theo từng bit, giải 30 hay 60 bài đếm 0/1; cần giá trị lớn nhất hay nhỏ nhất thì quyết định bit từ cao xuống thấp.
- Luôn viết
1LL << ithay cho1 << i, bọc ngoặc mọi biểu thức bit đứng cạnh==,+,<<, và dùng__builtin_popcountll,__builtin_ctzllcho số 64 bit. - Thấy đến hoặc “chia tập thành các nhóm”: dùng mặt nạ; ước lượng trước cho duyệt mọi tập con và cho duyệt mọi tập con của mọi tập, rồi mới viết.
Tóm tắt
- Bit thứ của
a & b,a | b,a ^ bchỉ phụ thuộc bit thứ của hai toán hạng; đọc, bật, tắt, đảo bit bằng mặt nạ1LL << i, luôn bọc ngoặc. x & (x - 1)xóa bit 1 thấp nhất,x & -xgiữ riêng nó;popcount,ctz,clzcho số bit 1, vị trí bit 1 thấp nhất và độ dài nhị phân.- Tập con của phần tử là một số trong ; mặt nạ con của duyệt bằng
s = (s - 1) & mtheo thứ tự giảm dần, và duyệt mọi mặt nạ cùng mọi mặt nạ con của nó tốn . - Tách theo từng bit: tổng XOR mọi cặp là ; khi cần tối đa hóa, chọn bit từ cao xuống thấp vì lớn hơn tổng mọi bit thấp.
- XOR là phép ngược của chính nó (tiền tố XOR, chuyển vế); phép cộng quy về XOR và AND bằng .
Bài tập
Đọc thêm
- Phép toán bitVNOI WikiBài tiếng Việt đầy đủ, nói rõ hành vi không xác định của phép dịch và thứ tự ưu tiên toán tử.
- Bit manipulationcp-algorithmsCác mẹo với
n & (n - 1), đếm số bit 1 của mọi số từ 0 tới n và danh sách hàm dựng sẵn của GCC. - Submask enumerationcp-algorithmsChứng minh vòng duyệt mặt nạ con đi theo thứ tự giảm dần và cận 3^n.
- 位操作OI WikiBiểu diễn bù hai, các loại phép dịch và cách sinh mặt nạ theo số bit 1.
- ビット演算の使い方を総特集drken, QiitaTừ cờ bit và tập hợp tới bit DP và cây Fenwick, kèm nhiều đoạn code C++ ngắn.
- Bit Twiddling HacksSean Eron AndersonKho thủ thuật bit kinh điển, như đếm bit song song và tính chẵn lẻ.