2025.6.1讲课文件2(bitset)

std::bitset 在信息学竞赛中确实是一个非常强大的工具,它不仅仅是存储0和1的容器,更重要的是它能够利用CPU的位运算指令,极大地加速某些类型的计算。通常,一次位运算可以同时处理32个或64个布尔值,这就是 bitset 速度优势的来源(常数因子 $ 1/w $,其中 $ w $ 通常是64)。

以下是一些 bitset 在竞赛中的经典应用场景和例题:

bitset** 的核心优势与应用思路:**

  1. 状态压缩与集合表示:当需要表示一个较大集合(元素编号不大)的子集,或者进行状态压缩DP时,如果状态可以用一个二进制串表示,bitset 是比 intlong long 更大范围的选择。
  2. 布尔矩阵运算/图邻接矩阵:对于稠密图,或需要频繁进行行/列间位运算(如求交集、并集)时,bitset 可以高效表示邻接矩阵的一行或一列。
  3. 优化暴力/枚举:某些情况下,暴力枚举可以通过 bitset 的批量操作来加速。
  4. 0-1背包可行性/可达性问题:判断某些值是否可以通过组合得到。
  5. 字符串匹配/模式查找:特定情况下的字符串问题可以转换为位运算。

例题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 之间有边。
    对于两个点 uv,它们的共同邻居集合可以通过 adj[u] & adj[v] 得到。这两个 bitset 的按位与操作会得到一个新的 bitset,其中为1的位 k 就表示 k 同时是 uv 的邻居。
    共同邻居的数量就是 (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 能到 jdp[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. 用例题1的方法,计算出数组 $ A $ 中所有可能的子集和,存入 bitset<MAX_T + 1> sums_A;
    2. 同样地,计算出数组 $ B $ 中所有可能的子集和,存入 bitset<MAX_T + 1> sums_B;
    3. 现在需要检查是否存在 s_as_b 使得 sums_A[s_a] == 1, sums_B[s_b] == 1s_a + s_b = T
      这等价于检查是否存在 s_a 使得 sums_A[s_a] == 1sums_B[T - s_a] == 1
    4. 如何用 bitset 高效检查?
      创建一个 sums_B_rev_shiftedsums_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]
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,就可以考虑。

一般建议和注意事项:

  1. 空间bitset<N> 占用 $ N/8 $ 字节。如果 $ N $ 非常大 (如 $ 10^7, 10^8 $),空间可能会成为瓶颈。例如 bitset<10^7> 约1.2MB,bitset<10^8> 约12MB,都是可以接受的。但多个这样的 bitset 数组就要小心。
  2. 时间:虽然单个操作快,但如果 $ N $ 极大,即使是 $ O(N/w) $ 也可能慢。关键是找到能显著减少操作次数的场景。
  3. _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
// }
  1. 多重背包的二进制优化 + bitset:对于多重背包问题,如果物品数量 $ c_i $ 较大,可以先用二进制拆分将每种物品拆成 $ \log c_i $ 个权值为 $ w_i \cdot 2^k $ 的0-1物品,然后再用 bitset 处理0-1背包的可行性。

bitset 是一个非常有用的工具,尤其是在处理涉及大量布尔状态、集合运算或特定数值可达性问题时。多练习相关题目,就能体会到它的威力。

posted @ 2025-06-01 11:04  左边之上  阅读(86)  评论(0)    收藏  举报