跳转至

状态压缩动态规划 (Bitmask DP)

状态压缩动态规划(简称状压 DP)是利用整型的二进制位来紧凑表示离散集合的选取状态(通常 \(N \le 20\))的高阶 DP 技术。它将指数级组合状态压缩至单个整型变量中,极大加速了状态的转移、判重与记忆化。


1. 集合状态表示与位运算原子操作

设全集为 \(U = \{0, 1, 2, \dots, n - 1\}\),任意子集 \(S \subseteq U\) 均可唯一映射为一个 \(n\) 位的非负整数 \(mask \in [0, 2^n - 1]\),其中第 \(i\) 位为 \(1\) 表示元素 \(i\) 被选入集合中。

高频位运算速查

操作目标 位运算表达式 含义解释
判断元素存在 (mask >> i) & 1 检查第 \(i\) 位是否为 1
添加元素 mask | (1 << i) 将第 \(i\) 位置为 1
删除元素 mask & ~(1 << i) 将第 \(i\) 位置为 0
翻转元素状态 mask ^ (1 << i) 0 变 1,1 变 0
提取最低位 1 mask & (-mask) 提取 lowbit(最低非零位)
清除最低位 1 mask & (mask - 1) 消去最低位的 1
集合大小 __builtin_popcount(mask) 统计二进制中 1 的个数
全集掩码 (1 << n) - 1 \(n\) 位全部置 1

2. 子集遍历核心技巧与 \(\mathcal{O}(3^n)\) 复杂度严格证明

在许多划分与拼装问题中,需要对每个状态 \(mask\) 枚举它的所有非空子集 \(sub\)

2.1 高效子集枚举代码

// 遍历 mask 的全部非空子集 sub (按降序排列)
for (int sub = mask; sub > 0; sub = (sub - 1) & mask) {
    // sub 必定是 mask 的严格子集
}
  • 核心机理sub - 1 会将 \(sub\) 最低位的 \(1\) 翻转为 \(0\),并将其后的所有 \(0\) 翻转为 \(1\);随后通过 & mask,强制屏蔽掉那些不属于母集合 \(mask\) 的多余位,从而在不遗漏任何子集的前提下步进到下一个合法子集。

2.2 复杂度证明:二项式定理与 \(\mathcal{O}(3^n)\)

表面上看,外层枚举 \(mask\)\(2^n\) 种可能,内层枚举子集又有 \(2^n\) 种可能,总复杂度容易被误判为 \(\mathcal{O}(4^n)\)。 然而,并非所有 \(mask\) 都有 \(2^n\) 个子集!

严格数学证明

设母集合 \(mask\) 中包含 \(k\) 个元素(即有 \(k\) 个二进制位为 1),其子集数量恰好为 \(2^k\)。 在所有 \(2^n\) 个状态中,大小为 \(k\) 的集合数量为二项式系数 \(\binom{n}{k}\)。因此全状态子集遍历的总操作次数为:

\[ \sum_{k=0}^{n} \binom{n}{k} 2^k \]

根据二项式定理 \((x + y)^n = \sum_{k=0}^{n} \binom{n}{k} x^{n-k} y^k\),令 \(x = 1, y = 2\)

\[ \sum_{k=0}^{n} \binom{n}{k} 1^{n-k} 2^k = (1 + 2)^n = \mathbf{3^n} \]

总时间复杂度是严格的 \(\mathcal{O}(3^n)\),这对于 \(n \approx 12 \sim 15\) 的数据规模而言极具实战杀伤力!


3. 旅行商问题 (TSP) 状态机模型

给定 \(n\) 个城市及彼此间的距离矩阵 \(dist[u][v]\),求解访问完所有城市且每个城市恰好访问一次的最短路径。

状态定义与转移

  • 状态设计:定义 \(dp[mask][u]\) 表示已经访问过的城市集合为 \(mask\),且当前所处的最后一个城市为 \(u\) 的最短路径长度;
  • 状态转移方程:枚举下一个即将访问的城市 \(v\)(需满足 \((mask >> v) \& 1 == 0\)):
\[ dp[mask \mid (1 \ll v)][v] = \min\Big( dp[mask \mid (1 \ll v)][v], \; dp[mask][u] + dist[u][v] \Big) \]
#include <vector>
#include <algorithm>

const int INF = 1e9;

int tsp(int n, const std::vector<std::vector<int>>& dist) {
    // dp[mask][u]
    std::vector<std::vector<int>> dp(1 << n, std::vector<int>(n, INF));

    // 起点初始化 (假设从城市 0 出发)
    dp[1 << 0][0] = 0;

    for (int mask = 1; mask < (1 << n); ++mask) {
        for (int u = 0; u < n; ++u) {
            if (dp[mask][u] >= INF) continue;

            // 寻找下一个未访问的城市 v
            for (int v = 0; v < n; ++v) {
                if (!(mask & (1 << v))) {
                    int next_mask = mask | (1 << v);
                    dp[next_mask][v] = std::min(dp[next_mask][v], dp[mask][u] + dist[u][v]);
                }
            }
        }
    }

    int ans = INF;
    int full_mask = (1 << n) - 1;
    for (int u = 0; u < n; ++u) {
        ans = std::min(ans, dp[full_mask][u]);
    }
    return ans;
}
  • 时间复杂度\(\mathcal{O}(n^2 \cdot 2^n)\)
  • 空间复杂度\(\mathcal{O}(n \cdot 2^n)\)

4. LeetCode 经典真题精选与题解提示

  • LeetCode 847 - 访问所有节点的最短路径 (Shortest Path Visiting All Nodes) 困难

    提示

    状压 + 双重 BFS

    • 图中有环且允许重复经过节点。
    • 状态由二元组 (u, mask) 唯一标识:当前在节点 \(u\) 且已访问集合为 \(mask\)
    • 由于边权均为 1,将所有单点 (i, 1 << i) 同时入队作为多源起点,运行 BFS,首次弹出 mask == (1 << n) - 1 的步数即为全局最短步数。
  • LeetCode 943 - 最短超串 (Find the Shortest Superstring) 困难

    提示

    字符串重叠度转化为有向图 TSP

    • 预处理两两字符串 \(S_i\)\(S_j\) 拼接时能重叠的最大字符数作为图的权值。
    • 运行标准状压 TSP 求出重叠字符最多的遍历顺序,最后通过记录的转移前驱路径还原拼接出的最短超串。
  • LeetCode 698 - 划分为k个相等的子集 (Partition to K Equal Sum Subsets) 中等

    提示

    状压记忆化搜索 + 贪心剪枝

    • 目标是拼出 \(k\) 个和为 \(target\) 的桶。
    • 状态定义:dp[mask] 记录选入集合 \(mask\) 时的当前未满桶余数 current_sum % target。记忆化搜索避免重复搜索无效状态;降序排序优先处理大数可极大加速剪枝。
  • LeetCode 1655 - 分配重复整数 (Distribute Repeating Integers) 困难

    提示

    顾客集合子集枚举 \(\mathcal{O}(3^m)\)

    • 顾客数量 \(m \le 10\),状态压缩 \(mask \in [0, 2^m)\) 表示已被满足的顾客子集。
    • 预处理每个子集所需要的总整数数量;
    • 外层枚举可用的频次数字,内层按 for (int sub = mask; sub; sub = (sub - 1) & mask) 枚举当前数字能够打包满足的新顾客子集。严格 \(\mathcal{O}(n \cdot 3^m)\)
  • LeetCode 1349 - 参加考试的最大学生数 (Maximum Students Taking Exam) 困难

    提示

    行间不冲突状压 DP

    • 考试座位受限于本行不能相邻、左前与右前不能看抄。
    • 每一行可以容纳的考生分布压缩为一个二进制掩码;
    • \(dp[i][mask]\) 表示第 \(i\) 行状态为 \(mask\) 时前 \(i\) 行的最大容纳学生数,仅依赖第 \(i-1\) 行的不冲突掩码进行状态转移。