Chương 2·Bài 27

Bitset

Một mảng bool 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 số 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 là 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.

Mảng bit đóng gói

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. Rất 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” trở thành vài phép trên tập như vậy. Khi kích thước chỉ biết lúc chạy và nhỏ hơn 64, 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;
using 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 bằng phép dịch

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, và chi phí còn O(SS/64)O(S \sqrt S / 64).

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;
using 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 qua các tầng

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;
using 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 của ma trận

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à với đồ thị của chương 3 thì đếm được tam giác khi đồ 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).

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;
using 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;
using 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

1
Cặp Gần Nhấtcanuc80k2705k≤30k \le 30 nên mỗi chuỗi là một số; khoảng cách là số bit 1 của XOR, duyệt mọi cặp với __builtin_popcount.
LQDOJ
2
Ưu Thế Mọi Lượtcanuc80k2706Có cách gán khi và chỉ khi mọi cặp tập giao nhau; mỗi tập một bitset, kiểm (s[i] & s[j]).any() cho mọi cặp.
LQDOJ
3
Đích Sau N Bướccanuc80k2708Tập vị trí có thể là bitset; mỗi bước b=(b≪ai)∨(b≪bi)b = (b \ll a_i) \lor (b \ll b_i).
LQDOJ
4
Cặp Hàng Lẻcanuc80k2710Theo từng cột, dựng bitset các hàng mang mỗi giá trị; hàng ii XOR bitset của giá trị nó vào bitset chẵn lẻ riêng, cuối cùng đếm các j>ij > i có bit 1.
LQDOJ
5
Đích Đến Vuông Góccanuc80k2714Bước lẻ đi theo trục xx, bước chẵn theo trục yy, bước đầu bắt buộc theo chiều dương; mỗi trục một bitset dịch theo ±Ai\pm A_i với gốc tọa độ đặt ở giữa.
LQDOJ
6
Tổng Gần Một Nghìncanuc80k2716Tập tổng dài 2001 bit; tìm tổng đạt được gần 1000 nhất, ưu tiên tổng lớn hơn khi bằng khoảng cách.
LQDOJ
7
Chia Đồng Xu Cân Bằngcanuc80k2717Tập tổng của mọi đồng xu; đáp án là tổng trừ hai lần tổng đạt được lớn nhất không vượt nửa tổng.
LQDOJ
8
Tập Con Chia Hếtcanuc80k2720n>mn > m thì luôn có (nguyên lý chuồng bồ câu trên tổng tiền tố); ngược lại quy hoạch động trên các số dư, O(m2)O(m^2).
LQDOJ
9
Trung Vị Các Tổng Tập Concanuc80k2721Các tổng tập con đối xứng qua nửa tổng T/2T/2; trung vị là tổng đạt được nhỏ nhất không nhỏ hơn ⌈T/2⌉\lceil T/2 \rceil.
LQDOJ
10
Phần Tử Không Cần Thiếtcanuc80k2722Sau khi sắp, tính cần thiết đơn điệu theo giá trị; tìm kiếm nhị phân phần tử, mỗi lần dựng tập tổng của các phần tử còn lại và xem có tổng nào trong [K−ai,K−1][K - a_i, K - 1].
LQDOJ
11
Đếm Mẫu Trong Chuỗi Độngcanuc80k2731Mỗi chữ cái một bitset vị trí; các vị trí bắt đầu khớp là AND của bitset chữ PjP_j dịch phải jj, đếm trong đoạn bằng dịch và count.
LQDOJ
12
Bổ Sung Cạnh Cho Tính Bắc Cầucanuc80k2738Bitset các đỉnh tới được từ mỗi đỉnh bằng BFS hoặc bao đóng; đáp án là tổng số cặp tới được trừ số cạnh đã có.
LQDOJ
13
Số Đỉnh Có Thể Tớicanuc80k2739Theo thứ tự tô pô ngược, tập tới được từ uu là {u}\{u\} hợp các tập của đỉnh kề; N=5⋅104N = 5 \cdot 10^4 bitset quá nặng thì chia đỉnh đích thành vài nhóm và chạy từng nhóm.
LQDOJ
14
Hạng Ma Trận Nhị Phâncanuc80k2749Khử Gauss trên trường 2 phần tử với mỗi hàng là một bitset, XOR hai hàng tốn M/64M / 64.
LQDOJ

Đọc thêm

  • std::bitsetcppreferenceDanh sách đầy đủ các phép và độ phức tạp.
  • BitsetsUSACO GuideBitset trong tổng con, bao đóng và đếm, kèm bài luyện.
  • bitsetOI WikiCác phép, hàm riêng của GCC và ứng dụng trong quy hoạch động, đồ thị.
  • Phép toán bitVNOI WikiNền tảng phép bit bằng tiếng Việt.