CC++ & Algorithm

bitset位集合

困难5
语言版本:C++
概述:用一个小小的“位袋子”来节约内存,高速处理多个“是/否”类型的数据。

用bitset位集合高效管理“是/否”状态

你是不是经常遇到这样的场景:记录一周哪几天要早起跑步、游戏里哪些关卡已经解锁、班级里谁今天没交作业……这些“是/否”的问题,如果用 bool 数组记录,每个 bool 占1个字节(8位),其实只用了其中1位,太浪费了。C++ 的 bitset 就像一个小巧的“位袋子”,把多个“是/否”信息压缩到位级别,1个字节就能装8个开关,让你既省内存又能高速批量处理。

什么是位集合?

想象你有一个长方形的盒子,里面整齐地排着8个小格子(代表8个二进制位),每个格子可以涂成黑色(1)或白色(0)。这个盒子就是 bitset,它能帮你记录8个“是/否”状态。比如用第0位表示周一要跑步,第1位表示周二……这样7天的计划只用1个字节就装下了,而 bool 数组需要7个字节。

bitset 的位数在编译时就固定了(比如 bitset<8> 就是8位),你不能在程序运行中随意改变它的大小。但好处是它内部用最紧凑的方式存储,并且支持各种位运算(与、或、异或、移位),让你一行代码就能同时处理所有位。

创建和初始化 bitset

使用 bitset 前要包含头文件 <bitset>。它可以有几种创建方式:

  1. 默认全0bitset<8> flags; 创建一个8位集合,所有位初始为0。
  2. 用二进制字符串初始化bitset<8> week("10101010"); 字符串长度必须等于位数,从左到右对应高位到低位(最左边是最高位)。
  3. 用无符号整数初始化bitset<8> num(170); (170的二进制是10101010) 整数会转换成对应的二进制位。

注意:下标从最右边开始,第0位是最低位(最右边)。当你用 cout 输出时,显示的顺序是高位在左,低位在右,和日常书写习惯一致。

常用操作:像数组一样访问和修改

bitset 支持用 [] 操作符像数组一样访问每一位。因为位是连续的,你可以直接赋值、读取:

#include <iostream>
#include <bitset>
using namespace std;

int main() {
    bitset<8> flags;   // 8位,初始全0
    
    // 设置周一到周四要跑步
    flags[0] = 1;      // 周一(第0位)
    flags[1] = 1;      // 周二(第1位)
    flags[2] = 1;      // 周三(第2位)
    flags[3] = 1;      // 周四(第3位)
    
    cout << "flags = " << flags << endl;   // 输出: 00001111 (第0~3位是1)
    cout << "周五是否要跑步?" << flags[4] << endl;  // 0(不需要)
    
    // 翻转某一位
    flags.flip(3);     // 把第3位(周四)从1变成0
    cout << "修改后flags = " << flags << endl;  // 00000111
    
    // 统计1的个数
    cout << "一周有" << flags.count() << "天要跑步" << endl; // 3
    return 0;
}

位运算:同时处理所有状态

这是 bitset 最强大的地方——你可以对两个大小相同的 bitset 进行位运算,结果也是一个 bitset,每一位独立运算。这就像同时给全班同学批改作业,而不是一个个来。

生活中的例子:老师有一份“今天交作业名单”和一份“今天迟到名单”,用两个 bitset 表示(1表示交作业/迟到),然后可以用位运算找出“既交了作业又迟到的同学”(与运算)、“交了作业或迟到的同学”(或运算)等。

#include <iostream>
#include <bitset>
using namespace std;

int main() {
    // 假设班级有8位同学,编号0~7
    bitset<8> homework;   // 交作业情况
    bitset<8> late;       // 迟到情况
    
    homework[0] = 1;      // 同学0交了作业
    homework[2] = 1;
    homework[4] = 1;
    homework[7] = 1;
    
    late[1] = 1;          // 同学1迟到了
    late[2] = 1;
    late[5] = 1;
    
    cout << "交作业: " << homework << endl;  // 10010101
    cout << "迟到的: " << late << endl;      // 01001010
    
    // 找出既交作业又迟到的同学(位与)
    bitset<8> both = homework & late;
    cout << "既交作业又迟到: " << both << endl;  // 00010100 → 只有同学2
    cout << "人数: " << both.count() << endl;    // 1
    
    // 找出交作业或迟到的同学(位或)
    bitset<8> either = homework | late;
    cout << "交作业或迟到: " << either << endl; // 11011111
    cout << "人数: " << either.count() << endl; // 6
    
    // 找出交了作业但没迟到的同学(位与之后取反)
    bitset<8> only_homework = homework & (~late);
    cout << "只交作业没迟到: " << only_homework << endl; // 10010101 & 10110101 = 10010101? 注意~late
    // 实际: ~late = 10110101, homework & ~late = 10010101 & 10110101 = 10010101(同学0、4、7)
    return 0;
}

移位操作:像移动队列一样移动位

bitset 支持左移 << 和右移 >>,效果和整数的移位一样。这在处理需要整体移动的场景中有用,比如模拟一个滑动窗口。

bitset<8> a("11001100");
cout << "a << 2 = " << (a << 2) << endl; // 00110000 (高位移出,低位补0)
cout << "a >> 3 = " << (a >> 3) << endl; // 00011001 (低位移出,高位补0)

其他好用的小功能

  • .any():检查是否有任何一位是1(返回 true/false
  • .none():检查是否所有位都是0
  • .all():检查是否所有位都是1(C++11起支持)
  • .set():将所有位设为1(可以传入参数 pos 单独设置某一位)
  • .reset():将所有位设为0(可以传入参数 pos 单独复位某一位)
  • .flip():翻转所有位(可以传入参数 pos 单独翻转某一位)
  • .test(pos):检查第 pos 位是否为1,如果越界会抛出 out_of_range 异常(而 [] 不检查越界)
  • .to_ulong() / .to_ullong():将 bitset 转换成 unsigned long / unsigned long long(如果位数超过会报错)
  • .to_string():转换成二进制字符串

常见错误与避坑指南

  1. 下标从0开始,且0是最低位
    新手常把 bitset<8>("10101010") 的第0位当作最左边的1,其实第0位是最右边的0。输出 bitset 时高位在左,所以 "10101010" 的第0位其实是0(最右边)。建议画个位索引图避免混淆。

  2. bitset 大小在编译时固定,不能改变
    如果你需要动态大小的位集合,比如要根据输入决定位数,请使用 vector<bool>(虽然它不完全符合标准容器的要求,但可以动态调整)。bitset 适合固定位数的场景。

  3. 位运算必须两个 bitset 类型相同(位数相同)
    不同大小的 bitset 不能直接进行 &| 等运算,编译器会报错。

  4. to_ulong() 或 to_ullong() 时可能溢出
    如果 bitset 的位数超过 unsigned longunsigned long long 的位数(通常64位),转换会抛出 overflow_error 异常。这时可以用 to_string() 先转成字符串。

  5. 不要试图对 bitset 进行逻辑运算(&&, ||)
    bitset 并没有定义 &&|| 操作符。如果你要判断“所有位都是1”之类的条件,请用 .all() 方法。

完整示例:用 bitset 管理游戏关卡的解锁情况

假设一个游戏有16个关卡(编号0~15),每通过一关就解锁下一关。我们用 bitset<16> 记录解锁状态,并演示如何判断某关是否能玩、如何一次性解锁多关。

#include <iostream>
#include <bitset>
using namespace std;

int main() {
    // 16个关卡,初始只解锁第0关(新手关)
    bitset<16> unlocked;   // 初始全0
    unlocked[0] = 1;       // 第0关解锁
    
    cout << "初始解锁状态: " << unlocked << endl;
    cout << "已解锁关卡数: " << unlocked.count() << endl;
    
    // 玩家通过了第0关,解锁第1关和第2关(一次性解锁)
    unlocked.set(1);       // 解锁第1关
    unlocked.set(2);       // 解锁第2关
    cout << "通过第0关后: " << unlocked << endl;
    
    // 检查第3关是否解锁
    if (unlocked.test(3)) {
        cout << "可以玩第3关" << endl;
    } else {
        cout << "第3关未解锁" << endl;
    }
    
    // 假设玩家通关了第1关和第2关,解锁第3、4、5关
    unlocked |= bitset<16>("0000000000111000");  // 位或运算,解锁3,4,5
    cout << "解锁更多关卡: " << unlocked << endl;
    
    // 判断是否所有关卡都解锁了
    if (unlocked.all()) {
        cout << "恭喜!所有关卡解锁!" << endl;
    } else {
        cout << "还有" << (16 - unlocked.count()) << "关未解锁" << endl;
    }
    
    // 翻转已解锁状态(用于某些彩蛋模式)
    unlocked.flip();
    cout << "翻转后状态(彩蛋模式): " << unlocked << endl;
    
    return 0;
}

输出示例:

初始解锁状态: 0000000000000001
已解锁关卡数: 1
通过第0关后: 0000000000000111
第3关未解锁
解锁更多关卡: 0000000000111111
还有10关未解锁
翻转后状态(彩蛋模式): 1111111111000000

相关知识点延伸

如果你还想了解更多位操作相关的知识,可以看看:

  • vector<bool>:能动态调整大小的位集合,但它的内部实现是一个特化版本,访问方式稍有不同(返回的不是真正的引用),适合需要动态位数的场景。
  • 位域(Bit-field):C++结构体中可以定义位域,用于精确控制每个成员占用的位数,适合在结构体中存放多个小标志。
  • 位运算基础&(与)、|(或)、^(异或)、~(取反)、<<(左移)、>>(右移)是计算机底层的强大工具,理解它们能帮你写出更高效的代码。
  • std::bitset 的局限性:当位数超过几百甚至几千时,bitset 仍然很高效(内部用多个整数实现)。但如果需要频繁插入/删除位,可以考虑 std::vector<bool> 或自建数据结构。

bitset 是一个小而精致的工具,在你需要管理多个“是/否”状态的场景中,它能帮你写出清晰、快速、省内存的代码。现在就试试用它记录下你的运动计划吧!

例题精讲

1单选题

下列关于C++标准库中bitset的描述,哪一项是正确的?

Abitset的大小可以在运行时动态指定
Bbitset的模板参数必须是整型常量表达式
Cbitset的默认构造函数将所有位初始化为随机值
Dbitset支持以字符串形式直接输出所有位,但字符串长度可以小于模板参数
2单选题

对于bitset<8> bs("10101010"); 执行bs.count()后返回的值是多少?

A8
B4
C2
D1
3判断题

bitset<32> bs; bs.set(40); 这段代码可以正常执行,将第40位(从0开始)设置为1。

4填空题
以下代码使用bitset判断一个无符号整数x的二进制表示中是否有奇数个1。请补全空白处的表达式。

#include <bitset>
#include <iostream>
using namespace std;

bool hasOddParity(unsigned int x) {
    bitset<sizeof(unsigned int)*8> bs(x);
    return ___;
}
5填空题
下面代码用两个bitset表示两个集合,求它们的并集并输出。请补全空白处的代码。

#include <bitset>
#include <iostream>
using namespace std;

int main() {
    bitset<10> setA("1010101010");  // 偶数位为1
    bitset<10> setB("0101010101");  // 奇数位为1
    bitset<10> unionSet = setA ___ setB;
    cout << unionSet << endl;  // 期望输出:1111111111
    return 0;
}