Chương 1·Bài 14

Bitwise Operations

Máy tính lưu mỗi số nguyên thành một dãy công tắc bật tắt: 1313 là 110121101_2, bốn công tắc. Các phép &, |, ^ bật tắt từng công tắc một, cột nào làm việc cột đó, nhanh như một phép cộng.

Nhìn số như vậy mở ra hai lối. Thứ nhất, nhiều bài về AND, OR, XOR trên hàng tỉ cặp số tách được thành 60 bài đếm số 0 và số 1, mỗi bài một lượt duyệt. Thứ hai, chính dãy công tắc là một tập hợp: giao, hợp, hiệu thành một phép toán, và duyệt mọi tập con của 20 phần tử chỉ là một vòng for qua 2202^{20} số. Bài này dạy cả hai, làm nền cho quy hoạch động trên tập con ở bài 15.

Dãy công tắc

Số nguyên không âm xx được lưu dưới dạng nhị phân, x=∑idi2ix = \sum_i d_i 2^i với di∈{0,1}d_i \in \{0, 1\}. Ta gọi did_i là bit thứ ii và đánh số từ 0, bắt đầu ở bit nhỏ nhất, tức bên phải. Số 13=8+4+113 = 8 + 4 + 1 viết là 110121101_2, có d0=1d_0 = 1, d1=0d_1 = 0, d2=1d_2 = 1, d3=1d_3 = 1.

Ba phép toán hai ngôi dưới đây làm việc theo từng cột: bit thứ ii của kết quả chỉ phụ thuộc bit thứ ii 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.

Bấm vào bit của a hoặc b để đảo; mỗi cột kết quả chỉ nhìn đúng cột đó.

Phép dịch x << k thêm kk bit 0 vào cuối, nên bằng x⋅2kx \cdot 2^k khi không tràn. Phép dịch x >> k bỏ kk bit cuối, nên bằng ⌊x/2k⌋\lfloor x / 2^k \rfloor khi x≥0x \ge 0. Với số âm, >> làm tròn xuống (-7 >> 1 bằng −4-4) 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 xx là số chẵn. Với x=6x = 6, biến chan có được đặt thành true không?

Đọ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ữ kk bit thấp x & ((1LL << k) - 1), chính là x mod 2kx \bmod 2^k.

Hai biểu thức quan trọng nhất xoay quanh bit 1 thấp nhất. Viết x>0x > 0 thành phần đầu PP, một bit 1, rồi kk bit 0:

x=(P 1 0…0⏟k)2,x−1=(P 0 1…1⏟k)2.x = (P\,1\,\underbrace{0 \dots 0}_{k})_2, \qquad x - 1 = (P\,0\,\underbrace{1 \dots 1}_{k})_2 .

Phép trừ 1 mượn từ bit 1 thấp nhất, giống 1000−1=9991000 - 1 = 999 trong hệ thập phân. Từ đó x & (x - 1) bằng (P 0 0…0)2(P\,0\,0 \dots 0)_2: 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: −x-x là xx đảo mọi bit (viết xˉ\bar x) rồi cộng 1, nên −x=xˉ+1=(Pˉ 1 0…0)2-x = \bar x + 1 = (\bar P\,1\,0 \dots 0)_2, nên x & -x giữ riêng bit 1 thấp nhất, bằng 2k2^k; 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ó.

Ô cam là bit 1 thấp nhất, ô vàng là các bit 0 dưới nó; bấm bit của x để đổi.

Số x>0x > 0 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 x=0x = 0. 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.

C++
#include <bits/stdc++.h>
using namespace std;
#define 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

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?

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 x≤109x \le 10^9 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 kk đêm thành 2k2^k 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 xx 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 2k+2k2^k + 2^k luôn gộp được thành 2k+12^{k+1}, 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 xx: 13=1101213 = 1101_2 cần 3 con.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define ll long long

ll x;

signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> x;
    cout << __builtin_popcountll(x) << '\n';
}
13

Số nguyên là tập hợp

Một tập con của {0,1,…,n−1}\{0, 1, \dots, n - 1\} ứng với một số trong [0,2n)[0, 2^n): phần tử ii thuộc tập khi bit thứ ii bật. Tập {0,2,3}\{0, 2, 3\} ứng với 11012=131101_2 = 13. Có đúng 2n2^n tập con và 2n2^n 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ợp A | B, hiệu A∖BA \setminus B là A & ~B, hiệu đối xứng A ^ B;
  • phần bù trong {0,…,n−1}\{0, \dots, n - 1\} là A ^ full, với full = (1 << n) - 1;
  • A⊆BA \subseteq B khi và chỉ khi (A & B) == A; số phần tử ∣A∣|A| là popcount;
  • phần tử ii thuộc AA 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 n=20n = 20 có hơn một triệu mặt nạ; nếu mỗi mặt nạ tốn thêm O(n)O(n) để xem từng phần tử thì tổng là n⋅2n≈2⋅107n \cdot 2^n \approx 2 \cdot 10^7, vẫn nhẹ. Mảng lưu một giá trị cho mỗi mặt nạ tốn 2n2^n ô: 2202^{20} số long long là 8 MB, còn 2242^{24} số đã là 134 MB, nên nn 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 n≤20n \le 20 số và một giá trị SS, đếm số tập con khác rỗng có tổng bằng SS. Tính tổng riêng cho từng mặt nạ tốn O(n)O(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.

C++
#include <bits/stdc++.h>
using namespace std;
#define 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

Ba tập thỏa mãn là {1,4,5}\{1, 4, 5\}, {2,3,5}\{2, 3, 5\} và {1,2,3,4}\{1, 2, 3, 4\}. Cả bài tốn O(2n)O(2^n). Khác biệt với cách duyệt lại từng bit, O(n⋅2n)O(n \cdot 2^n), thấy rõ khi nn lên 22:

Tổng của mọi tập con của 22 số, trung vị 5 lần chạy, i7-10850H, g++ -O2.duyệt từng bit mỗi mặt nạ223 ms · gấp 12 lầnlấy từ mask & (mask - 1)18 ms
Tổng của mọi tập con của 22 số, duyệt từng bit mỗi mặt nạ và lấy từ mặt nạ nhỏ hơn, đo trên i7-10850H, g++ -O2.

Lấy từ mặt nạ nhỏ hơn nhanh gấp khoảng 12 lầ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ó n≤15n \le 15 bài, bài ii có độ khó cic_i. Đếm số tập gồm ít nhất hai bài có tổng độ khó trong [l,r][l, r] và hiệu giữa độ khó lớn nhất với nhỏ nhất không nhỏ hơn xx.

Lời giải. 215=327682^{15} = 32768 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 15⋅215≈5⋅10515 \cdot 2^{15} \approx 5 \cdot 10^5 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 x≥1x \ge 1), nhưng kiểm popcount thì không phải nghĩ.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define 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

Mặt nạ con

Bây giờ cho một mặt nạ mm và cần liệt kê mọi tập con s⊆ms \subseteq m, gọi là mặt nạ con (submask). Cách ngây thơ là cho ss chạy từ 0 tới mm rồi kiểm (s & m) == s. Cách này tốn m+1m + 1 bước, kể cả khi mm 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 ss, nhảy thẳng tới mặt nạ con lớn nhất nhỏ hơn ss bằng một phép toán:

s′=(s−1)&m.s' = (s - 1) \mathbin{\&} m .

Vì sao đúng? Viết s=(P 1 0…0)2s = (P\,1\,0 \dots 0)_2 với kk bit 0 ở cuối. Một mặt nạ con t<st < s khác ss lần đầu (tính từ bit cao xuống) ở một vị trí mà ss có bit 1 còn tt có bit 0. Muốn tt 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 ss; sau đó các bit phía dưới được tự do chọn, nên ta bật mọi bit mà mm cho phép. Đó chính xác là (s−1)&m(s - 1) \mathbin{\&} m: s−1s - 1 cho (P 0 1…1)2(P\,0\,1 \dots 1)_2, và AND với mm loại các bit nằm ngoài mm mà vẫn giữ nguyên PP, vì s⊆ms \subseteq m.

Ô vàng ở hàng s − 1 là bit nằm ngoài m, bị AND loại đi.

Bắt đầu từ s=ms = m 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 m=10102=10m = 1010_2 = 10, dãy là 10,8,2,010, 8, 2, 0 như trong hình. Tập rỗng s=0s = 0 cần chú ý: nếu quên dừng ở đó, phép (0−1)&m(0 - 1) \mathbin{\&} m trả về mm 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ạ mm có pp bit bật thì có 2p2^p mặt nạ con. Duyệt mọi m∈[0,2n)m \in [0, 2^n) rồi mọi mặt nạ con của nó, tổng số bước là 3n3^n. Lý do: mỗi cặp s⊆ms \subseteq m ứng với việc gán cho từng phần tử một trong ba trạng thái, “ngoài mm”, “trong mm nhưng ngoài ss”, “trong cả hai”. Vậy 3n3^n chứ không phải 4n4^n. Với n=15n = 15 đó là 1,4⋅1071{,}4 \cdot 10^7, với n=18n = 18 là 3,9⋅1083{,}9 \cdot 10^8 (sát giới hạn), còn n=20n = 20 là 3,5⋅1093{,}5 \cdot 10^9, 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 ss tốn f(s)f(s)”, giải bằng dpm=min⁡s⊆m(dpm⊕s+f(s))\mathrm{dp}_m = \min_{s \subseteq m} \bigl(\mathrm{dp}_{m \oplus s} + f(s)\bigr), chỉ chạy được khi nn cỡ 16 tới 18. Chương trình dưới đếm tổng số bước với n=10n = 10 và in đúng 3103^{10}.

C++
#include <bits/stdc++.h>
using namespace std;
#define 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

Hỏi nhanh

Ta duyệt mọi mask từ 0 tới 2n−12^n - 1, 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 n=14n = 14, vòng trong chạy tổng cộng khoảng bao nhiêu lần?

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ứ bb của x & y, x | y, x ^ y chỉ phụ thuộc bit thứ bb của xx và yy, 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ứ bb với trọng số 2b2^b rồi cộng.

Ví dụ: tính ∑i<j(ai⊕aj)\sum_{i<j} (a_i \oplus a_j) cho nn số. Với mỗi bit bb, đặt c1c_1 là số phần tử có bit thứ bb bật và c0=n−c1c_0 = n - c_1. Cặp (i,j)(i, j) đóng góp 2b2^b khi và chỉ khi hai bit khác nhau, nên có đúng c1c0c_1 c_0 cặp như vậy:

∑i<j(ai⊕aj)=∑b2b c1(b) c0(b).\sum_{i<j} (a_i \oplus a_j) = \sum_{b} 2^b \, c_1(b) \, c_0(b).

Với bốn số 5,3,6,15, 3, 6, 1, tức 1012,0112,1102,0012101_2, 011_2, 110_2, 001_2: 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à 44, 44 và 33, nên tổng là 4⋅4+4⋅2+3⋅1=274 \cdot 4 + 4 \cdot 2 + 3 \cdot 1 = 27. Kiểm trực tiếp sáu cặp: 6+3+4+5+2+7=276 + 3 + 4 + 5 + 2 + 7 = 27.

Mỗi cột đếm số bit bật và tắt; cộng các cột bằng tổng XOR của mọi cặp.

Cùng khuôn đó cho các phép khác. Số cặp có bit bb của AND bật là (c12)\binom{c_1}{2}; của OR là (n2)−(c02)\binom{n}{2} - \binom{c_0}{2}. Muốn AND, OR, XOR của một đoạn [l,r][l, r] 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 bb 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à 0,1,…,n−10, 1, \dots, n - 1 liên tiếp thì không cần duyệt: cột bb lặp tuần hoàn với chu kỳ 2b+12^{b+1}, gồm 2b2^b số 0 rồi 2b2^b số 1. Do đó số số trong [0,n)[0, n) có bit bb bật là

⌊n2b+1⌋2b+max⁡(0,  (n mod 2b+1)−2b).\left\lfloor \frac{n}{2^{b+1}} \right\rfloor 2^b + \max\bigl(0,\; (n \bmod 2^{b+1}) - 2^b\bigr).

Thử với n=11n = 11, b=1b = 1: chu kỳ 4, có ⌊11/4⌋⋅2=4\lfloor 11/4 \rfloor \cdot 2 = 4 số từ các chu kỳ đầy, cộng max⁡(0,3−2)=1\max(0, 3 - 2) = 1 số từ chu kỳ dở, tổng 5; đúng là các số 2,3,6,7,102, 3, 6, 7, 10.

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ứ bb của x+yx + y 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ố a1,…,a5a_1, \dots, a_5. Ở bit thứ 3, đúng 2 số có bit đó bật và 3 số có bit đó tắt. Có bao nhiêu cặp {i,j}\{i, j\} để ai⊕aja_i \oplus a_j có bit thứ 3 bật?

Ví dụTổng XOR Của Mọi Cặp

Đề. Cho N≤3⋅105N \le 3 \cdot 10^5 số nguyên không âm Ai<260A_i < 2^{60}. In ∑i<j(Ai⊕Aj)\sum_{i<j} (A_i \oplus A_j) theo modulo 109+710^9 + 7.

Lời giải. Đúng công thức trên: với mỗi bit bb từ 0 tới 59, đếm c1c_1 phần tử có bit bb bật, cộng 2b⋅c1(N−c1)2^b \cdot c_1 (N - c_1). Đếm bit mất 60N≈1,8⋅10760N \approx 1{,}8 \cdot 10^7 phép thay cho 4,5⋅10104{,}5 \cdot 10^{10} cặp. Chỗ dễ sai là tràn số: c1c0c_1 c_0 tới 2,3⋅10102{,}3 \cdot 10^{10}, vẫn vừa ll, nhưng nhân tiếp với 2b mod p2^b \bmod p cỡ 10910^9 thì vượt 9,2⋅10189{,}2 \cdot 10^{18}. Phải lấy modulo của c1c0c_1 c_0 trước khi nhân với trọng số. Với ví dụ (0,1,4)(0, 1, 4): bit 0 cho 1⋅2=21 \cdot 2 = 2 cặp, đóng góp 2; bit 2 cho 2 cặp, đóng góp 8; tổng 10.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define 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

Bit cao trước

Bit càng cao càng nặng: 2b2^b lớn hơn mọi bit thấp cộng lại, vì 2b−1+⋯+2+1=2b−12^{b-1} + \dots + 2 + 1 = 2^b - 1. 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 nn số nhỏ hơn 2302^{30}, tìm giá trị lớn nhất của ai&aja_i \mathbin{\&} a_j với i≠ji \ne j. Duyệt mọi cặp tốn O(n2)O(n^2). Cách khác: giữ ans, thử từng bit bb từ 29 xuống 0. Đặt c = ans | (1LL << b) rồi đếm số phần tử xx 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 cc là khả thi nếu có ít nhất hai phần tử chứa mọi bit của cc. Đáp án MM là số khả thi lớn nhất. Giả sử các bit từ cao tới bit b+1b + 1 của ans đã trùng với MM. Nếu ans | (1LL << b) khả thi thì MM không nhỏ hơn nó, mà MM có cùng các bit cao, nên MM buộc phải có bit bb. Ngược lại nếu MM có bit bb thì ans | (1LL << b) chỉ gồm các bit của MM, nên vẫn khả thi. Vậy thử rồi giữ bit bb khi khả thi luôn đặt đúng bit của MM.

Ô xanh lá là bit của c mà số đó có, ô đỏ là bit thiếu; giữ bit khi còn ít nhất hai số đủ.
C++
#include <bits/stdc++.h>
using namespace std;
#define 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

Chạy tay: bit 3 có ba số chứa 100021000_2 (là 12,8,1412, 8, 14) nên giữ, ans =8= 8; bit 2 có hai số chứa 110021100_2 nên giữ, ans =12= 12; bit 1 chỉ có 1414 chứa 111021110_2 nên bỏ; bit 0 không có số nào chứa 110121101_2. Đáp án 12=12&1412 = 12 \mathbin{\&} 14, trong O(30n)O(30 n). 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 X≤KX \le K để tối đa hóa một tổng.

Ví dụAND Lớn Nhất Sau Khi Bật Bit

Đề. Cho dãy nn số ai<231a_i < 2^{31} (tổng nn qua các test tới 2⋅1052 \cdot 10^5) và k≤109k \le 10^9. 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á kk thao tác, in AND lớn nhất có thể của cả dãy.

Lời giải. Bit bb có mặt trong AND khi mọi phần tử đều mang nó, và muốn vậy phải bật bit bb ở 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 bb từ 30 xuống 0: nếu số phần tử thiếu bit bb không vượt ngân sách còn lại thì trả và bật bit bb 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.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define 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

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, x⊕0=xx \oplus 0 = x và x⊕x=0x \oplus x = 0. 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: a⊕b=ca \oplus b = c khi và chỉ khi a=b⊕ca = b \oplus c. 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 pxi=a1⊕⋯⊕ai\mathrm{px}_i = a_1 \oplus \dots \oplus a_i thì XOR của đoạn [l,r][l, r] là pxr⊕pxl−1\mathrm{px}_r \oplus \mathrm{px}_{l-1}.
  • XOR của mọi phần tử trừ phần tử ii là T⊕aiT \oplus a_i, với TT 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ứ ii với hai bit u,vu, v, ta có u+v=(u⊕v)+2 (u&v)u + v = (u \oplus v) + 2\,(u \mathbin{\&} v), thử ngay được trong ba trường hợp 0+00 + 0, 1+01 + 0 và 1+1=0+2⋅11 + 1 = 0 + 2 \cdot 1. Cộng mọi cột lại:

a+b=(a⊕b)+2 (a&b).a + b = (a \oplus b) + 2\,(a \mathbin{\&} b).

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: a+b≥a⊕ba + b \ge a \oplus b; hai vế bằng nhau khi và chỉ khi a&b=0a \mathbin{\&} b = 0, tức hai số không chung bit nào; và hiệu a+b−(a⊕b)a + b - (a \oplus b) luôn chẵn. Một công thức cùng loại là a+b=(a∣b)+(a&b)a + b = (a \mathbin{|} b) + (a \mathbin{\&} b).

Hỏi nhanh

Hai số nguyên không âm a,ba, b có a+b=18a + b = 18 và a⊕b=12a \oplus b = 12. Giá trị của a&ba \mathbin{\&} b là bao nhiêu?

Ví dụXOR Của Một Đoạn Số

Đề. Cho 0≤A≤B≤10120 \le A \le B \le 10^{12}. In A⊕(A+1)⊕⋯⊕BA \oplus (A + 1) \oplus \dots \oplus B.

Lời giải. Dùng tiền tố XOR: đặt F(n)=0⊕1⊕⋯⊕nF(n) = 0 \oplus 1 \oplus \dots \oplus n và F(−1)=0F(-1) = 0. Phần chung F(A−1)F(A - 1) xuất hiện hai lần trong F(B)⊕F(A−1)F(B) \oplus F(A - 1) nên triệt tiêu, còn lại đúng đáp án. Tính F(n)F(n) trong O(1)O(1): bốn số 4q,4q+1,4q+2,4q+34q, 4q + 1, 4q + 2, 4q + 3 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 00,01,10,1100, 01, 10, 11 có XOR bằng 0. Vậy mỗi khối bốn số có XOR 0, và F(n)F(n) chỉ còn là XOR của phần dở ở khối cuối. Theo n mod 4n \bmod 4 là 0,1,2,30, 1, 2, 3, ta có F(n)=nF(n) = n, 11, n+1n + 1, 00. Chỗ dễ sai là A=0A = 0: cần F(−1)F(-1), mà -1 % 4 bằng −1-1 chứ không phải 3, nên trả 0 riêng cho n<0n < 0. Với ví dụ, F(9)=1F(9) = 1, F(4)=4F(4) = 4, đáp án 1⊕4=51 \oplus 4 = 5.

Code đầy đủ
#include <bits/stdc++.h>
using namespace std;
#define 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

Khi đi thi

  • Thấy AND, OR, XOR trên mọi cặp hay mọi đoạn với số cỡ 2302^{30} hoặc 2602^{60}: 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 << i thay cho 1 << i, bọc ngoặc mọi biểu thức bit đứng cạnh ==, +, <<, và dùng __builtin_popcountll, __builtin_ctzll cho số 64 bit.
  • Thấy n≤16n \le 16 đến 2020 hoặc “chia tập thành các nhóm”: dùng mặt nạ; ước lượng trước n⋅2nn \cdot 2^n cho duyệt mọi tập con và 3n3^n 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ứ ii của a & b, a | b, a ^ b chỉ phụ thuộc bit thứ ii 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 & -x giữ riêng nó; popcount, ctz, clz cho số bit 1, vị trí bit 1 thấp nhất và độ dài nhị phân.
  • Tập con của nn phần tử là một số trong [0,2n)[0, 2^n); mặt nạ con của mm duyệt bằng s = (s - 1) & m theo 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 3n3^n.
  • Tách theo từng bit: tổng XOR mọi cặp là ∑b2bc1c0\sum_b 2^b c_1 c_0; khi cần tối đa hóa, chọn bit từ cao xuống thấp vì 2b2^b 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 a+b=(a⊕b)+2(a&b)a + b = (a \oplus b) + 2(a \mathbin{\&} b).

Bài tập

Đọc thêm