std::bitset
是 C++ 标准库中的一个类模板,用于高效地处理固定大小的位序列。它就像一个“位”的数组,但提供了比普通数组更丰富的操作接口,并且在空间上进行了优化。
当需要处理一组布尔标志、进行位掩码操作或实现某些对位操作要求较高的算法时,bitset
是一个非常强大和方便的工具。
要使用 bitset
,首先需要包含相应的头文件:
#include <bitset>
bitset
的声明需要在一个尖括号内指定其大小(即位的数量),这个大小必须是一个编译时常量。
// 声明一个大小为 32 的 bitset,所有位都被初始化为 0
std::bitset<32> b1; // b1: 00000000000000000000000000000000// 使用一个无符号整数来初始化,整数的二进制表示会被用来设置 bitset 的位
// 注意:如果整数的二进制位数超过 bitset 的大小,高位会被截断
std::bitset<8> b2(42); // 42 的二进制是 101010,所以 b2 是 00101010// 使用字符串来初始化
// 可以是 '0' 和 '1' 组成的字符串
std::bitset<8> b3(std::string("1101")); // b3: 00001101// 也可以只使用字符串的一部分
// 从索引 2 开始,取 4 个字符
std::bitset<8> b4(std::string("10110101"), 2, 4); // "1101" -> b4: 00001101
[]
:像数组一样访问特定位,可以读取也可以修改。注意: bitset
的索引是从右到左的,第 0 位在最右边。
std::bitset<8> b("10101010");
std::cout << b[0]; // 输出 0 (最右边的位)
std::cout << b[1]; // 输出 1b[0] = 1; // 将第 0 位设置为 1
std::cout << b; // 输出 10101011if (b[7]) { // 检查第 7 位 (最左边的位)std::cout << "Position 7 is 1" << std::endl;
}
bitset
提供了一些修改位的成员函数。
set()
:将所有位都设置为 1。set(pos)
:将第pos
位设置为 1。reset()
:将所有位都设置为 0。reset(pos)
:将第pos
位设置为 0。flip()
:翻转所有位(0 变 1,1 变 0)。flip(pos)
:翻转第pos
位。
std::bitset<4> b; // 0000b.set(1); // 0010
b.set(3); // 1010
std::cout << b << std::endl; // 输出 1010b.reset(1); // 1000
std::cout << b << std::endl; // 输出 1000b.flip(); // 0111
std::cout << b << std::endl; // 输出 0111
bitset
提供了一些查询状态的成员函数。
count()
:返回值为 1 的位的数量。size()
:返回bitset
的总大小。any()
:检查是否有任何一位是 1。none()
:检查是否所有位都是 0。all()
:检查是否所有位都是 1。
std::bitset<8> b("00010010");
std::cout << "Count of 1s: " << b.count() << std::endl; // 输出 2
std::cout << "Size: " << b.size() << std::endl; // 输出 8if (b.any()) {std::cout << "Contains at least one 1." << std::endl;
}
if (b.none()) {// 不会执行
}std::bitset<4> all_ones("1111");
if (all_ones.all()) {std::cout << "All bits are 1." << std::endl;
}
bitset
重载了常见的位运算符,可以方便地在两个大小相同的 bitset
之间进行运算。
&
(与)|
(或)^
(异或)~
(非)<<
(左移)>>
(右移)
std::bitset<4> b1("1010");
std::bitset<4> b2("0110");std::cout << "b1 & b2: " << (b1 & b2) << std::endl; // 0010
std::cout << "b1 | b2: " << (b1 | b2) << std::endl; // 1110
std::cout << "b1 ^ b2: " << (b1 ^ b2) << std::endl; // 1100
std::cout << "~b1: " << (~b1) << std::endl; // 0101
std::cout << "b1 << 1: " << (b1 << 1) << std::endl; // 0100 (左移一位,右边补0)
std::cout << "b1 >> 1: " << (b1 >> 1) << std::endl; // 0101 (右移一位,左边补0)
std::bitset
的高效源于其巧妙的内部实现。它并不是直接操作单个位,而是将位序列打包存储在一个 unsigned long
(或 unsigned long long
) 类型的数组中。
例如,一个 std::bitset<100>
在 64 位系统上,内部可能是一个包含 2 个 unsigned long long
的数组。
- 第一个
unsigned long long
存储第 0 到 63 位。 - 第二个
unsigned long long
存储第 64 到 99 位。
当执行 b.set(70)
这样的操作时,bitset
内部会:
- 定位数组索引:计算第 70 位应该在哪个整数上。
70 / 64 = 1
,所以它在数组的第 1 个元素上。 - 定位位索引:计算在该整数中的具体位置。
70 % 64 = 6
。 - 执行位操作:通过位掩码和位运算(如
internal_array[1] |= (1ULL << 6);
)来设置该位。
这种方式使得 bitset
能充分利用 CPU 对整数类型进行并行位操作的指令,从而获得极高的性能。
bitset
的绝大多数操作都非常快。其时间复杂度通常表示为 O(N/w),其中 N
是 bitset
的大小,w
是机器的字长(通常是 32 或 64)。这是因为很多操作可以一次性处理 w
个位。
-
O(1) 操作:
- 访问和修改单个位:
[]
,set(pos)
,reset(pos)
,flip(pos)
。这些操作只需要进行几次整数和位运算即可定位,是常数时间复杂度。
- 访问和修改单个位:
-
O(N/w) 操作:
- 整体操作:
set()
,reset()
,flip()
。 - 位运算:
&
,|
,^
,~
。 - 查询:
count()
,any()
,none()
,all()
。
这些操作需要遍历内部的整个整数数组,对每个整数执行一次操作,因此其速度与N/w
成正比。对于count()
,现代 CPU 通常有popcount
这样的专用指令,可以极快地计算一个整数中 1 的数量,进一步提升了性能。
- 整体操作:
例题:B3695 集合运算 3
本题要求对集合进行多种操作,包括修改集合元素和查询集合关系。如果直接用数组维护集合元素,对于修改操作(加/减一个值),需要遍历整个集合,时间复杂度为 \(O(|s_x|)\)。对于查询操作,也需要 \(O(|s_x| + |s_y|)\) 的时间。在 \(q\) 次操作下,这种方法会超时。
注意到集合元素的范围 \([1, m]\) 并不大(\(m \le 30000\)),可以考虑使用一种与值域相关的数据结构。std::bitset
是一个理想的选择。可以用 bitset
来表示一个集合,如果整数 \(k\) 在集合中,则 bitset
的第 \(k\) 位为 1,否则为 0。
使用 bitset
后,各种集合操作可以高效地通过位运算实现:
- 交集 (\(A \cap B\)):对应两个
bitset
的 按位与 (&
) 操作。 - 并集 (\(A \cup B\)):对应两个
bitset
的 按位或 (|
) 操作。 - 对称差 (\(A \Delta B\)):对应两个
bitset
的 按位异或 (^
) 操作。 - 元素全体加 \(y\):相当于
bitset
向左移位 (<<
) \(y\) 位。 - 元素全体减 \(y\):相当于
bitset
向右移位 (>>
) \(y\) 位。
std::bitset
的位运算和移位操作都非常快,其复杂度与 bitset
的大小和机器字长有关,通常为 \(O(m/w)\),其中 \(w\) 是机器字长(如 64)。
参考代码
#include <cstdio>
#include <bitset>
using namespace std;
const int N = 30005; // 根据 m 的最大值 30000 设定
bitset<N> bs[N], full, tmp; // bs[i] 存储集合 s_i, full 用于操作后保留 [1, m] 范围内的数, tmp 是临时变量
int main()
{int n, m, q;scanf("%d%d%d", &n, &m, &q);// 初始化 full bitset,将 1 到 m 的位置为 1,用于限制元素范围for (int i = 1; i <= m; i++) full[i] = 1;// 读入 n 个集合for (int i = 1; i <= n; i++) {int c; scanf("%d", &c);while (c--) {int s; scanf("%d", &s);bs[i][s] = 1; // 将集合中的元素 s 在 bitset 的对应位置为 1}}// 处理 q 次操作while (q--) {int o, x, y; scanf("%d%d%d", &o, &x, &y);if (o == 1) { // 操作 1: 集合 sx 中所有元素加上 ybs[x] <<= y; // 左移 y 位,相当于所有元素值加上 ybs[x] &= full; // 与 full 按位与,去掉所有 > m 的元素} else if (o == 2) { // 操作 2: 集合 sx 中所有元素减去 ybs[x] >>= y; // 右移 y 位,相当于所有元素值减去 ybs[x] &= full; // 去掉可能由高位移入的 1 (虽然 bitset 移入的是 0,但这是一个好习惯)} else if (o == 3) { // 操作 3: 查询交集大小tmp = bs[x] & bs[y]; // 交集对应按位与printf("%d\n", (int)tmp.count()); // .count() 返回 bitset 中 1 的个数} else if (o == 4) { // 操作 4: 查询并集大小tmp = bs[x] | bs[y]; // 并集对应按位或printf("%d\n", (int)tmp.count());} else { // 操作 5: 查询对称差大小tmp = bs[x] ^ bs[y]; // 对称差对应按位异或printf("%d\n", (int)tmp.count());}}return 0;
}