bitset

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]; // 输出 1

b[0] = 1; // 将第 0 位设置为 1
std::cout << b; // 输出 10101011

if (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; // 0000

b.set(1);    // 0010
b.set(3);    // 1010
std::cout << b << std::endl; // 输出 1010

b.reset(1);  // 1000
std::cout << b << std::endl; // 输出 1000

b.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;         // 输出 8

if (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 内部会:

  1. 定位数组索引:计算第 70 位应该在哪个整数上。70 / 64 = 1,所以它在内部数组的 internal_array[1] 上。
  2. 定位位索引:计算在该整数中的具体位置。70 % 64 = 6
  3. 执行位操作:通过位掩码和位运算(如 internal_array[1] |= (1ULL << 6);)来设置该位。

这种方式使得 bitset 能充分利用 CPU 对整数类型进行并行位操作的指令,从而获得极高的性能。

bitset 的绝大多数操作都非常快。其时间复杂度通常表示为 O(N/w),其中 Nbitset 的大小,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 中所有元素加上 y
            bs[x] <<= y; // 左移 y 位,相当于所有元素值加上 y
            bs[x] &= full; // 与 full 按位与,去掉所有 > m 的元素
        } else if (o == 2) { // 操作 2: 集合 sx 中所有元素减去 y
            bs[x] >>= y; // 右移 y 位,相当于所有元素值减去 y
            bs[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;
}
posted @ 2025-09-30 16:10  RonChen  阅读(116)  评论(0)    收藏  举报