Một mảng bool dài dùng 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 phép. Thuật toán không đổi, độ phức tạp vẫn là hay , 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: chia 64 còn khoảng phép trên từ máy.
Mảng bit đóng gói
bitset<N> b là bit, kích thước 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 vị trí, bit vượt quá 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 .
Bitset là một tập con của : 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 dài . In AND , OR , XOR , NOT , NOT , mỗi kết quả một dòng ký tự.
Lời giải. Đọc hai chuỗi vào hai bitset<100000>, bit ứng với ký tự , rồi dùng thẳng các toán tử. NOT đảo cả những bit từ trở đi, nên khi in chỉ in bit đầu. Không dùng hàm to_string, vì nó in bit cao trước và in đủ cả ký tự.
#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
100000
111110
011110
010011
001101
Tổng con bằng phép dịch
Quy hoạch động tổng con (bài 12) giữ là có chọn được tập con tổng không; thêm vật nặng thì . Cả mảng là một bitset , và công thức trên chính là b |= b << x: dịch 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 giảm dần như khi viết bằng mảng, vì b << x được tính từ bản cũ của trước khi OR.
Với vật và tổng tới , chi phí là . Khi nhiều vật cùng khối lượng, tách mỗi nhóm vật giống nhau thành các gói 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á thì chỉ có khối lượng khác nhau, và chi phí còn .
Hỏi nhanh
b đang biểu diễn tập tổng . Sau b |= b << 4, tập tổng là gì?
b << 4 là , các tổng có dùng vật mới; OR với cho . Vật mới chỉ được dùng một lần, nên 12 (dùng hai vật nặng 4 mới) không xuất hiện; là quên OR với bản cũ.
Ví dụCác Tổng Có Thể
Đề. đồng xu, giá trị tới 1000, tổng không quá . 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í phép trên từ má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
7
2 3 5 7 9 10 12
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: bitset dài tốn byte, với và 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ố: là tập tổng tạo được từ các vật .
Ví dụHai Mặt, Một Tổng
Đề. thẻ, thẻ chọn mặt H giá trị hoặc mặt T giá trị , mỗi giá trị tới 100. Chọn mặt cho mọi thẻ để tổng bằng ; 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. và : mỗi thẻ bắt buộc chọn một mặt, nên không OR với bản cũ. thì in No. Ngược lại, với phần tổng còn thiếu (ban đầu ), thẻ chọn H nếu và , không thì chọn T. Với ví dụ, thẻ 1 chọn H còn thiếu 9, đạt được bằng .
#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
Yes
HHT
AND hai hàng của ma trận
Ma trận nhị phân lưu mỗi hàng thành một bitset. Số cột mà hai hàng cùng có bit 1 là (r[i] & r[j]).count(), tốn thay vì . Duyệt mọi cặp hàng: , với là khoảng 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ề (ô bằng 1 khi có cạnh nối với ). 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 : nó là OR các tập của các đỉnh kề với , tính theo thứ tự tô pô ngược (bài 32).
Hỏi nhanh
Đếm tam giác trên đồ thị đỉnh bằng ba vòng lặp lồng nhau tốn khoảng 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òng trong cùng duyệt đỉnh, còn AND và count trên bitset chỉ duyệt khoảng từ máy. Có cặp đỉnh, tổng khoảng . Độ phức tạp vẫn , chỉ hằng số nhỏ đi 64 lần.
Ví dụBốn Góc Đen
Đề. Bảng với , 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 ; gọi là số cột mà cả hai hàng đều đen. Mỗi cặp cột trong cột đó cho một hình chữ nhật, tức . Tính bằng AND hai hàng rồi count. Có cặp hàng, mỗi cặp từ máy. Đáp án tới , cần ll.
#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
4
Ví dụBa Đỉnh Khép Kín
Đề. Đồ thị vô hướng đỉnh cho bằng ma trận kề. Đếm số tam giác.
Lời giải. Với mỗi cạnh , , 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 và không tự đếm vào phần giao.
#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
2
Khi đi thi
- Thấy hay với cỡ vài nghìn mà chỉ có phép logic trên tập: thử bitset, chia 64 thường đủ. Ước lượng hoặc 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ớ khi cần chỉ số ; mảng nhiều bitset lớn thì tính bộ nhớ 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á ; NOT bật cả các bit thừa ngoài , đừng
countsau 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,counttốn .- Tổng con:
b |= b << xmỗi vật, . - 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 .
Bài tập
__builtin_popcount.(s[i] & s[j]).any() cho mọi cặp.count.Đọ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.