C++标准库中的bitset是一个常被低估的工具。很多开发者习惯了用int或long long配合移位和按位与或非来处理位运算,代码写起来繁琐且容易越界出错。而bitset将固定长度的二进制位封装成一个类型,既支持类似数组的下标访问,又重载了完整的位运算符,还提供了统计、转换等实用成员函数,是处理状态压缩、标志位管理和大规模布尔数据的首选方案。本文将系统讲解bitset的使用方法与进阶技巧。

bitset基础:构造与核心操作
bitset定义在<bitset>头文件中,模板参数是位的个数,且必须是编译期常量。它的底层通常按机器字长分组存储,一个64位的字可以装下64个布尔位,相比vector<bool>或bool数组,内存占用缩小到原来的八分之一。构造方式有四种:默认全0、用无符号整数值初始化、用字符串初始化以及拷贝构造。
#include <bitset>
#include <iostream>
using namespace std;
int main() {
bitset<8> b1; // 00000000
bitset<8> b2(0x5A); // 01011010
bitset<8> b3(string("1010")); // 00001010
bitset<8> b4(b3); // 拷贝构造
cout << b2 << endl; // 直接输出二进制字符串
b2[0] = 1; // 下标访问,注意0是最低位
b2.set(3); // 将第3位置1
b2.reset(1); // 将第1位清0
b2.flip(2); // 翻转第2位
cout << b2 << " count=" << b2.count() << endl;
return 0;
}需要特别注意下标方向:b[0]对应的是最低位(最右边),这与我们平时手写二进制的阅读顺序一致,但与字符串初始化的顺序相反。字符串"1010"中最左边的字符对应最高位。这是新手最容易踩的坑之一,建议写代码时用cout << b << endl;打印确认。
set、reset、flip三个函数都支持无参调用,分别表示全部置1、全部清0、全部取反。count()返回1的个数,size()返回总位数,test(i)带边界检查地访问某一位,越界时抛出out_of_range异常,而b[i]越界是未定义行为。在需要健壮性的场景下优先用test。
位运算符与批量位查询技巧
bitset重载了&、|、^、~、<<、>>以及对应的复合赋值运算符,语义与普通整数位运算完全一致,但作用范围扩展到了整个位串。这意味着可以一次性对几百上千个位做按位运算,编译器和库会自动生成基于字长的向量化指令,性能远高于逐位循环。
#include <bitset>
#include <iostream>
using namespace std;
int main() {
bitset<32> a(0b1100);
bitset<32> b(0b1010);
auto c = a & b; // 按位与
auto d = a | b; // 按位或
auto e = a ^ b; // 按位异或
auto f = ~a; // 按位取反
auto g = a << 2; // 左移2位
cout << c << "\n" << d << "\n" << e << "\n";
cout << g << endl;
// 批量查询:all any none
bitset<4> x(0b1010);
cout << x.any() << " " // 是否存在1,输出1
<< x.all() << " " // 是否全为1,输出0
<< x.none() << "\n"; // 是否全为0,输出0
return 0;
}三个批量查询函数非常实用:any()判断是否存在至少一个1,none()判断是否全为0,all()判断是否全为1。如果用手写位运算实现同样的功能,需要写循环逐位判断,而bitset直接调用一次即可,内部通常被优化成少数几条指令。此外还有to_ulong()和to_ullong()用于转换回整数,to_string()用于转成字符串,但要注意位数超过64时调用整数转换会抛出overflow_error异常。
移位运算在bitset上同样安全,超出边界的位会被丢弃,不存在整数移位的未定义行为问题。另外,两个不同长度的bitset不能直接运算,模板参数不同就是不同类型,需要先统一长度,这也是使用时的一个约束。
状态压缩与集合表示的实战应用
bitset最大的用武之地是状态压缩动态规划和集合运算。比如经典的旅行商问题(TSP),用一个bitset表示已访问的城市集合,判断某城市是否在集合中只需一次下标访问,将城市加入集合只需一次set调用。当城市数量不超过20时,一个bitset<20>就能枚举所有子集状态。
#include <bitset>
#include <iostream>
using namespace std;
int main() {
const int N = 5;
// 判断子集:sub是否是super的子集
bitset<N> super(string("11101"));
bitset<N> sub(string("10101"));
bool isSub = ((super | sub) == super);
cout << "is subset: " << isSub << endl;
// 枚举一个集合的所有子集
bitset<4> s(0b1011);
for (int mask = s.to_ulong(); ; mask = (mask - 1) & s.to_ulong()) {
bitset<4> cur(mask);
cout << cur << "\n";
if (mask == 0) break;
}
return 0;
}判断子集的经典技巧是(super | sub) == super,如果sub的所有1位都被super覆盖,按位或之后结果不变。枚举子集则利用了(mask - 1) & mask这个位运算技巧,每次减1会把最低位的1变成0、其后的0变成1,再与原集合按位与就得到下一个更小的子集,时间复杂度只有O(3的n次方)级别而非暴力枚举的O(2的n次方乘n)。
在图论中,bitset常用来做可达性传递闭包。设adj[i]表示点i能直接到达的点集,则adj[i] | adj[j]的不断迭代就能求出所有间接可达关系,配合Floyd算法框架,复杂度从O(n三次方)降为O(n三次方除以机器字长),当n等于1000时性能提升可达数十倍,这是手写邻接矩阵难以企及的。
手写位运算常用技巧速查
即使有了bitset,整数字面上的位运算技巧仍然必须掌握,它们在面试和底层编程中出现频率极高。以下是最常用的一批模板,其中x为非负整数。
#include <iostream>
using namespace std;
int main() {
int x = 44; // 二进制 101100
// 1. 判断奇偶:最低位是否为1
bool odd = x & 1;
// 2. 取最低位的1:lowbit
int lowbit = x & (-x); // 结果为 4
// 3. 清除最低位的1
int y = x & (x - 1); // 结果为 40
// 4. 判断x是否为2的幂
bool pow2 = x && !(x & (x - 1));
// 5. 将第n位置1、清0、翻转、测试
int n = 3;
int setN = x | (1 << n);
int clearN = x & ~(1 << n);
int flipN = x ^ (1 << n);
bool hasN = x & (1 << n);
// 6. 交换两个数(不用临时变量)
int a = 5, b = 9;
a ^= b; b ^= a; a ^= b;
cout << lowbit << " " << y << " " << pow2 << endl;
return 0;
}这些技巧的原理值得逐一理解。x & (-x)之所以能取最低位的1,是因为负数按补码存储,等于按位取反再加1,除了最低位的1及其右侧,其余位在取反后恰好与原数互补,按位与后只剩最低位的1。x & (x - 1)清除最低位的1则是减1操作只影响最低位1以下的位,这个技巧还常用来统计1的个数:循环执行清除操作直到x变为0,循环次数就是1的个数。
lowbit运算配合树状数组是算法竞赛的标配,x ^ (1 << n)翻转特定位在状态切换中也很常见。需要注意的是,所有涉及1 << n的写法,当n大于等于31时要写成1LL << n或1ULL << n,否则移位溢出是未定义行为,这是工程中真实发生过的线上事故来源。
bitset与其他方案的对比与选型建议
与vector<bool>相比,bitset的优势在于运算符直接可用且性能更好,劣势是长度必须编译期确定。vector<bool>虽然也做了位压缩,但它没有重载按位与或非,做集合运算只能手写循环。如果位数运行时才确定,C++标准之外的boost::dynamic_bitset或者按64位分块手写是常见替代方案。
| 方案 | 长度可变 | 位运算支持 | 内存 | 适用场景 |
|---|---|---|---|---|
| bitset | 否 | 完整运算符 | 最紧凑 | 位数已知、集合运算密集 |
| vector<bool> | 是 | 无 | 紧凑 | 动态布尔标志 |
| 手写整数移位 | 否 | 原生 | 受限64位 | 少量标志位 |
综合来看,当位数在编译期可确定且需要频繁做整体位运算时,bitset是毫无争议的最佳选择;位数不超过64时直接用uint64_t配位运算技巧更轻量;位数动态变化时再考虑动态位集方案。掌握bitset与基础位运算技巧的组合使用,能在状态压缩、集合判定、大规模布尔筛选等场景中同时获得简洁性和性能,是C++开发者值得精进的基本功。
C++ bitset位运算位操作技巧修改时间:2026-09-01 00:39:22