2025.6.1讲课文件2(bitset)
std::bitset 在信息学竞赛中确实是一个非常强大的工具,它不仅仅是存储0和1的容器,更重要的是它能够利用CPU的位运算指令,极大地加速某些类型的计算。通常,一次位运算可以同时处理32个或64个布尔值,这就是 bitset 速度优势的来源(常数因子 $ 1/w $,其中 $ w $ 通常是64)。
以下是一些 bitset 在竞赛中的经典应用场景和例题:
bitset** 的核心优势与应用思路:**
- 状态压缩与集合表示:当需要表示一个较大集合(元素编号不大)的子集,或者进行状态压缩DP时,如果状态可以用一个二进制串表示,
bitset是比int或long long更大范围的选择。 - 布尔矩阵运算/图邻接矩阵:对于稠密图,或需要频繁进行行/列间位运算(如求交集、并集)时,
bitset可以高效表示邻接矩阵的一行或一列。 - 优化暴力/枚举:某些情况下,暴力枚举可以通过
bitset的批量操作来加速。 - 0-1背包可行性/可达性问题:判断某些值是否可以通过组合得到。
- 字符串匹配/模式查找:特定情况下的字符串问题可以转换为位运算。
例题1:0-1背包/可达性统计 (子集和问题)
- 问题类型:判断给定物品能否凑出特定重量,或统计能凑出的所有不同重量的个数。
- 题目描述 (简化版):
有 $ N $ 个物品,每个物品有一个重量 $ w_i $。问用这些物品(每种物品至多用一次)能够凑出的所有不同重量中,有多少个不同的重量值(不包括0)?或者问是否能凑出重量 $ S \(? \) N \le 100 $,每个 $ w_i \le 1000 $。总重量最大 $ 100 \times 1000 = 10^5 $。 - 思路:
定义一个bitset<MAX_SUM + 1> dp;,其中dp[j] = 1表示重量j可以被凑出,dp[j] = 0表示不能。
初始时,只有重量0可以凑出(不选任何物品),所以dp[0] = 1。
然后遍历每个物品 $ w_i $:
对于一个已经可以凑出的重量j(即dp[j] == 1),如果再放入物品 $ w_i $,那么新的可凑出重量就是j + w_i。
这等价于将dp中所有为1的位向左移动 $ w_i $ 位,然后和原来的dp取并集。
操作:dp |= (dp << w_i);
这个操作非常高效,因为<<和|都是bitset的内建高效操作。
整个过程的时间复杂度是 $ O(N \cdot MAX_SUM / w) $,其中 $ w $ 是机器字长(通常是64)。 - 核心代码:
#include <iostream>
#include <vector>
#include <numeric> // for std::accumulate (optional for max_sum)
#include <bitset>
const int MAX_TOTAL_WEIGHT = 100 * 1000; // N * max_w_i
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n;
std::cin >> n;
std::vector<int> weights(n);
int current_max_sum = 0; // Dynamically track max possible sum if not fixed
for (int i = 0; i < n; ++i) {
std::cin >> weights[i];
current_max_sum += weights[i];
}
// If MAX_TOTAL_WEIGHT is too large, use current_max_sum
// For this problem, MAX_TOTAL_WEIGHT is fixed and small enough.
std::bitset<MAX_TOTAL_WEIGHT + 1> dp;
dp[0] = 1; // Base case: sum 0 is achievable with no items
for (int w : weights) {
dp |= (dp << w);
}
// To count how many distinct sums are achievable (excluding 0)
int achievable_sums_count = 0;
for (int i = 1; i <= current_max_sum; ++i) { // Iterate up to actual max sum
if (dp[i]) {
achievable_sums_count++;
}
}
// Or simply: achievable_sums_count = dp.count() - 1; (if dp[0] is the only one to exclude)
std::cout << "Number of distinct achievable sums (excluding 0): " << (dp.count() - 1) << std::endl;
// Example: Check if a specific sum S is achievable
// int S_to_check = 7;
// if (S_to_check <= MAX_TOTAL_WEIGHT && dp[S_to_check]) {
// std::cout << "Sum " << S_to_check << " is achievable." << std::endl;
// } else {
// std::cout << "Sum " << S_to_check << " is not achievable." << std::endl;
// }
return 0;
}
- 相关题目: 洛谷 P1174 宇宙“虫洞” (这题是分组背包+01背包判断可行性,其中01背包部分可以用bitset) 或者更纯粹的子集和问题。很多背包问题求方案数不能用bitset,但求可行性或个数(如果值域不大)就可以。
例题2:图论中的应用 (如统计共同邻居、传递闭包等)
- 问题类型:处理图的邻接关系,特别是涉及多个点集的交并操作。
- 题目描述 (简化版 - 统计两点间的共同邻居):
给定一个 $ N $ 个点 $ M $ 条边的无向图。对于所有点对 $ (u, v) \(,你需要快速计算它们有多少个共同的邻居。或者,找出共同邻居最多的点对。 \) N \le 2000 \(,\) M $ 任意。 - 思路:
使用bitset<N> adj[N];来存储图的邻接矩阵。adj[i][j] = 1表示点i和点j之间有边。
对于两个点u和v,它们的共同邻居集合可以通过adj[u] & adj[v]得到。这两个bitset的按位与操作会得到一个新的bitset,其中为1的位k就表示k同时是u和v的邻居。
共同邻居的数量就是(adj[u] & adj[v]).count()。
遍历所有点对 $ (u, v) $ (其中 $ u < v $ 以免重复) 的时间复杂度是 $ O(N^2) $。每次计算共同邻居是 $ O(N/w) $。
总时间复杂度为 $ O(N^3/w) $。对于 $ N=2000 \(,\) 2000^3 / 64 \approx 1.25 \times 10^8 $,可能需要卡常或题目有特殊限制,但比 $ O(N^3) $ 好得多。如果只是对特定少数点对查询,则非常快。 - 核心代码 (寻找共同邻居最多的点对):
#include <iostream>
#include <vector>
#include <bitset>
#include <algorithm> // For std::max
const int MAXN = 2005; // Max number of nodes
std::bitset<MAXN> adj[MAXN];
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int n, m;
std::cin >> n >> m;
for (int k = 0; k < m; ++k) {
int u, v;
std::cin >> u >> v;
// Assuming 0-indexed nodes, if 1-indexed, subtract 1
// u--; v--;
adj[u].set(v);
adj[v].set(u); // For undirected graph
}
int max_common_neighbors = -1;
int best_u = -1, best_v = -1;
for (int u = 0; u < n; ++u) {
for (int v = u + 1; v < n; ++v) {
// Calculate common neighbors for pair (u, v)
std::bitset<MAXN> common = adj[u] & adj[v];
int current_common_count = common.count();
if (current_common_count > max_common_neighbors) {
max_common_neighbors = current_common_count;
best_u = u;
best_v = v;
}
}
}
if (best_u != -1) {
std::cout << "Pair (" << best_u << ", " << best_v
<< ") has the most common neighbors: "
<< max_common_neighbors << std::endl;
} else {
std::cout << "No pairs or no common neighbors found." << std::endl;
}
return 0;
}
- 相关题目:
- 统计三角形数量: 一个经典问题。对于每条边 $ (u,v) $,其与 $ u,v $ 的共同邻居 $ k $ 构成一个三角形 $ (u,v,k) $。总的三角形数量是 \(\sum_{(u,v) \in E} (adj[u] \& adj[v]).count() / 3\)。(因为每个三角形会被每条边算一次)。复杂度 $ O(M \cdot N/w) $。洛谷P2315
- 传递闭包 (Floyd-Warshall变种):
dp[i][j]=1表示i能到j。dp[i] |= dp[k]如果dp[i][k]=1(即dp[i].test(k)为真)。for k=0..N-1: for i=0..N-1: if(dp[i][k]): dp[i] |= dp[k];复杂度 $ O(N^3/w) $。洛谷P4306 - Codeforces 1381C - Prefix Flip (Hard Version) (这是一个字符串问题,但最终用bitset维护操作集合) - 可能过于复杂。
- AtCoder Regular Contest 082 D - Fennec VS. Snuke (博弈论,但可能某些子问题分析中会用到)
- 洛谷 P5023 [NOIP2018 提高组] 填数游戏 (Station): 这题的其中一个解法涉及对路径的表示和交集,可以用
bitset优化。
例题3:Meet-in-the-Middle / 数据匹配 / 约束满足
- 问题类型:将问题分成两半,分别计算可能的状态/值,然后合并结果。
- 题目描述 (简化版 - 两集合元素和):
给定两个数组 $ A $ 和 $ B $,长度分别为 $ N_A, N_B $。是否存在一个来自 $ A $ 中元素的子集和 $ S_A $ 和一个来自 $ B $ 中元素的子集和 $ S_B $,使得 $ S_A + S_B = T \(? \) N_A, N_B \le 20 $ (如果这样,可以直接暴力枚举子集)。如果 $ N_A, N_B $ 稍大,比如到40-50,但元素值或目标 $ T $ 不太大时,bitset可以参与。
更适合bitset的场景:$ N_A, N_B $ 可能较大 (e.g. 100),但元素值很小,目标和 $ T $ 也不太大 (e.g., $ 10^5 $)。 - 思路:
- 用例题1的方法,计算出数组 $ A $ 中所有可能的子集和,存入
bitset<MAX_T + 1> sums_A;。 - 同样地,计算出数组 $ B $ 中所有可能的子集和,存入
bitset<MAX_T + 1> sums_B;。 - 现在需要检查是否存在
s_a和s_b使得sums_A[s_a] == 1,sums_B[s_b] == 1且s_a + s_b = T。
这等价于检查是否存在s_a使得sums_A[s_a] == 1且sums_B[T - s_a] == 1。 - 如何用
bitset高效检查?
创建一个sums_B_rev_shifted。sums_B_rev_shifted[s_a] = 1当且仅当sums_B[T - s_a] = 1。
这可以通过将sums_B翻转(sums_B_rev[i] = sums_B[MAX_T - i]),然后再进行一次移位得到。
sums_B_rev_shifted = (sums_B_rev >> (MAX_T - T))(大致思路,精确移位和范围要小心)。
然后检查(sums_A & sums_B_rev_shifted).any()是否为真。一个更直接的构造sums_B_target[k] = sums_B[T-k]:
- 用例题1的方法,计算出数组 $ A $ 中所有可能的子集和,存入
std::bitset<MAX_T + 1> sums_B_target;
for (int s_b = 0; s_b <= T; ++s_b) {
if (sums_B[s_b] && (T - s_b >= 0)) {
sums_B_target[T - s_b] = 1;
}
}
// Then check if (sums_A & sums_B_target).any()
这个构造 sums_B_target 的过程是 $ O(T) $。bitset 的与操作和 any() 是 $ O(T/w) $。
总复杂度是 $ O((N_A+N_B) \cdot MAX_VAL_SUM / w + T) $。
- 核心代码 (示意):
#include <iostream>
#include <vector>
#include <bitset>
const int MAX_SUM_PER_SET = 50000; // Example max sum from one set
const int TARGET_T = 70000; // Example target sum
// Function to compute all possible subset sums
std::bitset<MAX_SUM_PER_SET + 1> get_subset_sums(const std::vector<int>& arr) {
std::bitset<MAX_SUM_PER_SET + 1> sums;
sums[0] = 1;
int current_max = 0;
for (int x : arr) {
if (x > 0 && x <= MAX_SUM_PER_SET) { // Ensure x is positive and within bounds
sums |= (sums << x);
current_max += x;
current_max = std::min(current_max, MAX_SUM_PER_SET); // cap current_max
}
}
return sums;
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
// Example arrays (replace with actual input)
std::vector<int> A = {1, 5, 10, 2, 8};
std::vector<int> B = {3, 7, 4, 11, 6};
// Ensure elements and sums fit within MAX_SUM_PER_SET for individual bitsets
// And TARGET_T is the overall target.
std::bitset<MAX_SUM_PER_SET + 1> sums_A = get_subset_sums(A);
std::bitset<MAX_SUM_PER_SET + 1> sums_B = get_subset_sums(B);
bool possible = false;
// Iterate through sums from A. For each s_a, check if T - s_a is in sums_B.
// This loop iterates up to TARGET_T or MAX_SUM_PER_SET, whichever is smaller for s_a
for (int s_a = 0; s_a <= std::min(TARGET_T, MAX_SUM_PER_SET); ++s_a) {
if (sums_A[s_a]) {
int required_s_b = TARGET_T - s_a;
if (required_s_b >= 0 && required_s_b <= MAX_SUM_PER_SET && sums_B[required_s_b]) {
possible = true;
std::cout << "Found: s_a = " << s_a << ", s_b = " << required_s_b << std::endl;
break;
}
}
}
if (possible) {
std::cout << "It's possible to achieve sum " << TARGET_T << std::endl;
} else {
std::cout << "It's not possible to achieve sum " << TARGET_T << std::endl;
}
return 0;
}
- 相关题目:
- 很多 "子集和" 加上一些额外约束的题目。
- CF 455C - Civilization: 这题主要是并查集维护直径,但如果涉及一些小的状态集合判断,bitset可能有应用场景。
- AtCoder ABC215G - Dist Max 2: (曼哈顿距离转切比雪夫距离后,可能涉及二维数据结构或扫描线,如果值域小,某些判断可以用bitset)。
- 一般如果题目要求从两个集合中各选一些东西(或一个),组合起来满足特定数值条件,且数值范围适合
bitset,就可以考虑。
一般建议和注意事项:
- 空间:
bitset<N>占用 $ N/8 $ 字节。如果 $ N $ 非常大 (如 $ 10^7, 10^8 $),空间可能会成为瓶颈。例如bitset<10^7>约1.2MB,bitset<10^8>约12MB,都是可以接受的。但多个这样的bitset数组就要小心。 - 时间:虽然单个操作快,但如果 $ N $ 极大,即使是 $ O(N/w) $ 也可能慢。关键是找到能显著减少操作次数的场景。
_Find_first()** 和 **_Find_next(pos):这两个非标准但通常可用的成员函数(GCC)可以用来高效遍历bitset中为1的位。_Find_first()找到第一个1的位置,_Find_next(pos)找到pos之后第一个1的位置。这比手动循环检查每一位快得多,尤其是在稀疏bitset中。
// Example using _Find_first and _Find_next
// for (int i = bs._Find_first(); i < bs.size(); i = bs._Find_next(i)) {
// // Process bit i which is set
// }
- 多重背包的二进制优化 + bitset:对于多重背包问题,如果物品数量 $ c_i $ 较大,可以先用二进制拆分将每种物品拆成 $ \log c_i $ 个权值为 $ w_i \cdot 2^k $ 的0-1物品,然后再用
bitset处理0-1背包的可行性。
bitset 是一个非常有用的工具,尤其是在处理涉及大量布尔状态、集合运算或特定数值可达性问题时。多练习相关题目,就能体会到它的威力。

浙公网安备 33010602011771号