Chương 2·Bài 27

Bitset

Cho một dãy đồng xu và hỏi: những tổng nào tạo được? Ta giữ một mảng bool, thêm mỗi đồng xu thì duyệt cả mảng một lượt. Mảng dài nn dùng nn byte, và mỗi phép trên nó đi qua từng phần tử.

bitset của C++ gói 64 phần tử vào một từ máy, nên AND, OR, XOR, dịch và đếm bit 1 trên cả mảng chỉ tốn khoảng n/64n / 64 phép. Thuật toán không đổi, độ phức tạp vẫn O(n2)O(n^2) hay O(nS)O(nS). Nhưng chia cho 64 thường là khác biệt giữa quá thời gian và chạy trong một giây: 30003=2,7⋅10103000^3 = 2{,}7 \cdot 10^{10} chia 64 còn khoảng 4⋅1084 \cdot 10^8 phép trên từ máy.

Bitset

bitset<N> b là NN bit, kích thước NN phải là hằng số lúc biên dịch; đặt theo giới hạn lớn nhất của đề. b[i] đọc hay gán một bit, b.set(), b.reset() bật hay tắt hết. Giữa hai bitset cùng kích thước có &, |, ^, ~; b << k dịch mọi bit lên chỉ số cao hơn kk vị trí, bit vượt quá N−1N - 1 bị bỏ. b.count() đếm số bit 1, b.any() hỏi có bit 1 nào không, b._Find_first() và b._Find_next(i) nhảy tới bit 1 kế tiếp (hai hàm này riêng của GCC). Mọi phép trên cả bitset tốn O(N/64)O(N / 64).

Bitset là một tập con của {0,…,N−1}\{0, \dots, N - 1\}: AND là giao, OR là hợp, count là kích thước. Nhiều bài “đếm phần tử chung”, “tập các tổng đạt được”, “tập các đỉnh tới được” thành vài phép trên tập như vậy. Tập nhỏ hơn 64 phần tử thì một số ll cùng các phép bit (bài 14) là đủ; bitset dành cho tập lớn hơn.

Ví dụNăm Chuỗi Kết Quả

Đề. Hai chuỗi nhị phân A,BA, B dài n≤105n \le 10^5. In AA AND BB, AA OR BB, AA XOR BB, NOT AA, NOT BB, mỗi kết quả một dòng nn ký tự.

Lời giải. Đọc hai chuỗi vào hai bitset<100000>, bit ii ứng với ký tự ii, rồi dùng thẳng các toán tử. NOT đảo cả những bit từ nn trở đi, nên khi in chỉ in nn bit đầu. Không dùng hàm to_string, vì nó in bit cao trước và in đủ cả 10510^5 ký tự.

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

const ll N = 1e5;
ll n;
string s, t;
bitset<N> a, b;

void out(bitset<N> x) {
    for (ll i = 0; i < n; i++) cout << x[i];
    cout << '\n';
}

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

    cin >> n >> s >> t;
    for (ll i = 0; i < n; i++) {
        a[i] = s[i] == '1';
        b[i] = t[i] == '1';
    }
    out(a & b);
    out(a | b);
    out(a ^ b);
    out(~a);
    out(~b);
}
6
101100
110010

Tổng con

Quy hoạch động tổng con (bài 12) giữ ok[s]\mathrm{ok}[s] là có chọn được tập con tổng ss không; thêm vật nặng xx thì ok′[s]=ok[s]∨ok[s−x]\mathrm{ok}'[s] = \mathrm{ok}[s] \lor \mathrm{ok}[s - x]. Cả mảng ok\mathrm{ok} là một bitset bb, và công thức trên chính là b |= b << x: dịch xx tạo các tổng có dùng vật mới, OR giữ lại các tổng cũ. Không phải lo duyệt ss giảm dần như khi viết bằng mảng, vì b << x được tính từ bản cũ của bb trước khi OR.

Mỗi vật: dịch cả bitset rồi OR với bản cũ; ô xanh là các tổng đạt được.

Với nn vật và tổng tới SS, chi phí là O(nS/64)O(nS / 64). Khi nhiều vật cùng khối lượng, tách mỗi nhóm cc vật giống nhau thành các gói 1,2,4,…1, 2, 4, \dots và phần dư như trong bài cái túi giới hạn. Tổng các khối lượng khác nhau không quá SS thì chỉ có O(S)O(\sqrt S) khối lượng khác nhau, chi phí còn O(SS/64)O(S \sqrt S / 64).

Thử với 2000 vật, tổng tới 100 nghìn:

Tổng con: 2000 vật, tổng tới 100 nghìn, trung vị 5 lần chạy, i7-10850H, g++ -O2.bitset8,3 msmảng bool89 ms · gấp 11 lần
Tổng con với 2000 vật, đo trên i7-10850H, g++ -O2.

Bitset nhanh hơn mảng bool cỡ 11 lần.

Hỏi nhanh

b đang biểu diễn tập tổng {0,4}\{0, 4\}. Sau b |= b << 4, tập tổng là gì?

Ví dụCác Tổng Có Thể

Đề. n≤100n \le 100 đồng xu, giá trị tới 1000, tổng không quá 10510^5. In số tổng dương tạo được và liệt kê chúng tăng dần.

Lời giải. bitset<100001> b với b[0] = 1, mỗi đồng xu một lần b |= b << x. Số tổng dương là b.count() - 1, bỏ tổng 0. Chi phí 100⋅105/64≈1,6⋅105100 \cdot 10^5 / 64 \approx 1{,}6 \cdot 10^5 phép trên từ máy.

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

const ll S = 1e5;
ll n;
bitset<S + 1> b;

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

    cin >> n;
    b[0] = 1;
    for (ll i = 1; i <= n; i++) {
        ll x;
        cin >> x;
        b |= b << x;
    }
    cout << b.count() - 1 << '\n';
    for (ll s = 1; s <= S; s++)
        if (b[s]) cout << s << ' ';
    cout << '\n';
}
3
2 3 7

Truy vết

Bitset chỉ nói tổng nào đạt được, không nói đạt bằng cách nào. Muốn in nghiệm, giữ lại bitset của từng tầng: n+1n + 1 bitset dài SS tốn nS/8nS / 8 byte, với n=100n = 100 và S=104S = 10^4 chỉ khoảng 125 KB. Rồi đi từ tầng cuối về, hoặc từ tầng đầu tới, mỗi bước chọn một lựa chọn mà phần còn lại vẫn đạt được.

Muốn nghiệm nhỏ nhất theo thứ tự từ điển, quyết định từ vật đầu tiên trở đi, và với mỗi vật thử lựa chọn nhỏ trước. Để biết “phần còn lại vẫn đạt được”, dựng bitset theo hậu tố: sufi\mathrm{suf}_i là tập tổng tạo được từ các vật i,…,ni, \dots, n.

Ví dụHai Mặt, Một Tổng

Đề. N≤100N \le 100 thẻ, thẻ ii chọn mặt H giá trị AiA_i hoặc mặt T giá trị BiB_i, mỗi giá trị tới 100. Chọn mặt cho mọi thẻ để tổng bằng S≤104S \le 10^4; in No nếu không được, ngược lại Yes và chuỗi H/T nhỏ nhất theo thứ tự từ điển.

Lời giải. sufN+1={0}\mathrm{suf}_{N+1} = \{0\} và sufi=(sufi+1≪Ai)∨(sufi+1≪Bi)\mathrm{suf}_i = (\mathrm{suf}_{i+1} \ll A_i) \lor (\mathrm{suf}_{i+1} \ll B_i): mỗi thẻ bắt buộc chọn một mặt, nên không OR với bản cũ. S∉suf1S \notin \mathrm{suf}_1 thì in No. Ngược lại, với phần tổng còn thiếu cc (ban đầu SS), thẻ ii chọn H nếu c≥Aic \ge A_i và c−Ai∈sufi+1c - A_i \in \mathrm{suf}_{i+1}, không thì chọn T. Với ví dụ, thẻ 1 chọn H còn thiếu 9, đạt được bằng 2+72 + 7.

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

const ll S = 1e4 + 1;
ll n, s, A[105], B[105];
bitset<S> suf[105];

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

    cin >> n >> s;
    for (ll i = 1; i <= n; i++) cin >> A[i] >> B[i];
    suf[n + 1][0] = 1;
    for (ll i = n; i >= 1; i--) suf[i] = (suf[i + 1] << A[i]) | (suf[i + 1] << B[i]);
    if (!suf[1][s]) {
        cout << "No\n";
        return 0;
    }
    string res;
    ll c = s;
    for (ll i = 1; i <= n; i++) {
        if (c >= A[i] && suf[i + 1][c - A[i]]) {
            res += 'H';
            c -= A[i];
        } else {
            res += 'T';
            c -= B[i];
        }
    }
    cout << "Yes\n" << res << '\n';
}
3 10
1 5
2 4
3 7

AND hai hàng

Ma trận nhị phân n×nn \times n lưu mỗi hàng thành một bitset. Số cột mà hai hàng i,ji, j cùng có bit 1 là (r[i] & r[j]).count(), tốn n/64n / 64 thay vì nn. Duyệt mọi cặp hàng: O(n3/64)O(n^3 / 64), với n=3000n = 3000 là khoảng 2⋅1082 \cdot 10^8 phép trên từ máy, chạy dưới một giây.

Cùng cách đó kiểm hai tập có giao nhau, và đếm tam giác của đồ thị cho bằng ma trận kề (ô (u,v)(u, v) bằng 1 khi có cạnh nối uu với vv). Trên đồ thị có hướng không chu trình còn dựng được tập đỉnh tới được từ mỗi uu: nó là OR các tập của các đỉnh kề với uu, tính theo thứ tự tô pô ngược (bài 32).

Số cột chung của mọi cặp hàng, bảng 1500 × 1500, trung vị 5 lần chạy, i7-10850H, g++ -O2.bitset, AND rồi count174 msmảng bool, duyệt từng cột1253 ms · gấp 7,2 lần
Đếm cột chung của mọi cặp hàng trong bảng 1500 × 1500, đo trên i7-10850H, g++ -O2.

Ở đây bitset nhanh hơn khoảng 7 lần, ít hơn con số 64 vì trình biên dịch đã tự tối ưu vòng lặp bool. Dù vậy chênh lệch ấy vẫn đủ để qua giới hạn thời gian.

Hai hàng cam được AND với nhau; số bit 1 của kết quả là số cột chung. Sửa bảng để thử.

Hỏi nhanh

Đếm tam giác trên đồ thị 30003000 đỉnh bằng ba vòng lặp lồng nhau tốn khoảng 4,5⋅1094{,}5 \cdot 10^9 phép. Thay vòng trong cùng bằng (r[i] & r[j]).count() thì còn khoảng bao nhiêu phép trên từ máy?

Ví dụBốn Góc Đen

Đề. Bảng N×NN \times N với N≤3000N \le 3000, mỗi ô đen hoặc trắng. Đếm số hình chữ nhật (hai hàng khác nhau, hai cột khác nhau) có bốn góc đều đen.

Lời giải. Cố định cặp hàng i<ji < j; gọi cc là số cột mà cả hai hàng đều đen. Mỗi cặp cột trong cc cột đó cho một hình chữ nhật, tức (c2)\binom{c}{2}. Tính cc bằng AND hai hàng rồi count. Có 4,5⋅1064{,}5 \cdot 10^6 cặp hàng, mỗi cặp 3000/64≈473000 / 64 \approx 47 từ máy. Đáp án tới (30002)2≈2⋅1013\binom{3000}{2}^2 \approx 2 \cdot 10^{13}, cần ll.

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

const ll N = 3000;
ll n;
bitset<N> r[N];

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

    cin >> n;
    for (ll i = 0; i < n; i++) {
        string s;
        cin >> s;
        for (ll j = 0; j < n; j++) r[i][j] = s[j] == '1';
    }
    ll ans = 0;
    for (ll i = 0; i < n; i++)
        for (ll j = i + 1; j < n; j++) {
            ll c = (r[i] & r[j]).count();
            ans += c * (c - 1) / 2;
        }
    cout << ans << '\n';
}
4
1100
1110
0110
1100

Ví dụBa Đỉnh Khép Kín

Đề. Đồ thị vô hướng N≤3000N \le 3000 đỉnh cho bằng ma trận kề. Đếm số tam giác.

Lời giải. Với mỗi cạnh (i,j)(i, j), i<ji < j, số đỉnh thứ ba kề cả hai là (r[i] & r[j]).count(). Mỗi tam giác có ba cạnh nên được đếm ba lần; chia 3 ở cuối. Đường chéo bằng 0 nên ii và jj không tự đếm vào phần giao.

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

const ll N = 3000;
ll n;
bitset<N> r[N];

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

    cin >> n;
    for (ll i = 0; i < n; i++) {
        string s;
        cin >> s;
        for (ll j = 0; j < n; j++) r[i][j] = s[j] == '1';
    }
    ll ans = 0;
    for (ll i = 0; i < n; i++)
        for (ll j = i + 1; j < n; j++)
            if (r[i][j]) ans += (r[i] & r[j]).count();
    cout << ans / 3 << '\n';
}
5
01100
10110
11010
01100
00000

Khi đi thi

  • Thấy O(n3)O(n^3) hay O(nS)O(nS) với nn cỡ vài nghìn mà chỉ có phép logic trên tập: thử bitset, chia 64 thường đủ. Ước lượng n3/64n^3 / 64 hoặc nS/64nS / 64 trước khi viết.
  • Kích thước bitset là hằng số lúc biên dịch: đặt theo giới hạn lớn nhất, nhớ +1+1 khi cần chỉ số SS; mảng nhiều bitset lớn thì tính bộ nhớ kN/8kN / 8 byte.
  • Dịch trái làm mất bit vượt kích thước, đúng ý khi chỉ quan tâm tổng không quá SS; NOT bật cả các bit thừa ngoài nn, đừng count sau NOT mà không lọc.

Tóm tắt

  • bitset<N> gói 64 bit một từ máy; AND, OR, XOR, dịch, count tốn O(N/64)O(N / 64).
  • Tổng con: b |= b << x mỗi vật, O(nS/64)O(nS / 64).
  • Truy vết: giữ bitset từng tầng (hoặc theo hậu tố để lấy nghiệm nhỏ nhất theo từ điển).
  • Ma trận nhị phân lưu theo hàng; AND hai hàng đếm phần chung, cho các bài cặp hàng, tam giác, bao đóng bắc cầu trong O(n3/64)O(n^3 / 64).

Bài tập

Đọc thêm