算法知识图谱总览 / 位运算族 / bitset压位

bitset压位

P1intermediatestandardG-E-2

What · 是什么

用 bitset 把 O(n^2)/w 的操作压成位运算:Python 用 int 当 bitset(int 无限位宽+bit_count),莫队配合 bitset 加速区间统计

bitset 压位:把 0/1 布尔数组用一个整数(或 std::bitset)表示,一次位运算等价于对整个数组并行操作。C++ 用 std::bitset(库函数含 count/any/移位/位运算,底层按字长 32/64 位并行)。Python 直接用 int:`bs=0`,`bs |= 1<<k` 置位,`bs.bit_count()` 数 1,`bs << s` 整体左移 = 平移,`bs & mask` 取子集。这使背包可达性、区间统计等 O(n^2) 复杂度降为 O(n^2/64),因为 Python 大整数底层就是字长并行运算。常见应用:背包 DP(f |= f<<w)、集合包含判定、莫队+bitset 统计区间不同元素(如洛谷 P3674)。

Why · 为什么

直觉 · Intuition

背包 dp[j]=可达到 j 吗?朴素 O(n·sum)。把整个可达数组编码成一个二进制数,『加一个物品』= 把可达集合左移 w 后与原来或起来:f |= f<<w。一次大整数移位+或,在底层是按 30/64 位字并行执行的,等价于一次性完成 w 列 DP 转移,常数除以字长。

原理 · Principle

Python int 用 30 位/digit 存储(3.11 后可变),<< >> & | ^ 都是按字(digit)循环的 C 层运算,对 n 位整数 O(n/30)。所以 `f |= f<<w` 对 f 有 N 位的场景 O(N/30),而朴素 for 循环是 O(N) Python 层 → 快约 30-64 倍(再叠加 C 层优势)。C++ std::bitset 每 64 位一个 unsigned long long,库函数用 __builtin_popcountll 等。莫队+bitset:把区间元素的出现状态按值离散化后压进 bitset,莫队移动指针时 O(1) 翻转一位,回答『区间内出现的不同值』时对整个 bitset 做 & | 统计,配合分块处理(如 Ynoi 题)。

历史 · History

bitset 由 OI Wiki lang/csl/bitset 页系统介绍(含 LOJ515 模板题与手写 64 位优化);OI Wiki misc/mo-algo-with-bitset 页讲莫队配合 bitset(Ynoi2016 掉进兔子洞等)。Python 用 int 当 bitset 的技巧在社区广泛使用(int.bit_count、int.bit_length、移位),是 Python 选手实现『压位』的标准手段。

How · 实现

伪代码

# Python int 当 bitset
def subset_possible(weights, target):
    bs = 1                 # bit0 = 可达(空集)
    for w in weights:
        bs |= bs << w      # 加入物品 w
    return (bs >> target) & 1 == 1

# 常用操作
置位: bs |= 1 << k
判位: (bs >> k) & 1
数 1: bs.bit_count()
清空: bs = 0
平移: bs << k
取前 k 位: bs & ((1 << k) - 1)

Python 实现

def subset_possible(weights, target):
    bs = 1
    for w in weights:
        bs |= bs << w
    return (bs >> target) & 1 == 1


def main():
    # 背包可达性:3+5=8 可达
    assert subset_possible([3, 5, 2], 8)
    assert subset_possible([3, 5, 2], 10)
    assert not subset_possible([3, 5], 9)
    assert subset_possible([], 0)
    # int 操作
    bs = 0
    bs |= 1 << 3
    bs |= 1 << 5
    assert (bs >> 3) & 1 == 1
    assert (bs >> 4) & 1 == 0
    assert bs.bit_count() == 2
    assert (bs << 2) == ((1 << 5) | (1 << 7))
    # 大范围:10^5 位级 bitset 仍可行
    big = 1
    for _ in range(20000):
        big |= big << 1
    assert big.bit_count() == 20001
    # 与朴素对拍
    import random
    random.seed(1)
    for _ in range(30):
        ws = [random.randint(1, 10) for _ in range(8)]
        t = random.randint(0, 40)
        naive = {0}
        for w in ws:
            naive |= {x + w for x in list(naive)}
        assert subset_possible(ws, t) == (t in naive)
    print("bitset-compression OK")


if __name__ == "__main__":
    main()
PyPy 提示PyPy 的大整数实现与 CPython 不同(RPython 大整数),移位/或仍按字并行但 digit 宽度与常数可能不同;bit_count 可用。PyPy 下 int-bitset 依然远快于 Python 层循环。超大 bitset(10^6 位+)注意内存(10^6 位≈128KB,OK);连续多次移位会产生临时大 int,PyPy 的分配策略可能造成 GC 压力,必要时复用变量。
时间复杂度一次位运算 O(n/30)~O(n/64)(n 为 bitset 位数);背包 DP 总 O(#items·sum/64);莫队+bitset 见具体题
空间复杂度O(n) 位(Python int O(n/30) digit)
注意 · 局限int 当 bitset 只能做『并行位级操作』,若要随机访问单元素仍 O(1)((bs>>k)&1);但不能像 list 那样对某一位就地改而不新建大对象(bs|=1<<k 每次创建新 int)。莫队+bitset 需要离散化与处理多重值(用出现次数编码),实现较复杂。Python int-bitset 的移位对超大 int 会分配新内存,连续大量移位时注意性能。bitset 本质仍是布尔运算,无法维护计数(出现次数>1)除非额外处理。

易错点

例题

OJ题号难度考点链接
LibreOJ5153「LibreOJ β Round #2」贪心只能过样例:背包可达性用 f|=f<<(j*j),OI Wiki bitset 页模板题https://loj.ac/problem/515
LuoguP36746小清新人渣的本愿:莫队+bitset 判定区间内存在差/和/积关系,OI Wiki 莫队配合 bitset 习题https://www.luogu.com.cn/problem/P3674
步骤迷你图(节点 = 算法步骤 · 边 = 流程 · 绿色 = 起始步骤)
上游 · 本知识点依赖(前置/变体/组合/共现)

信源

✓ = 已核验 · △ = 未核验