进阶算法2: 状压dp
状压 DP
一、原理介绍
状态压缩动态规划(状压 DP) 是一种将集合或状态用二进制位进行压缩表示的动态规划方法。它适用于 维度较小(通常 n≤20)但需要记录多个元素“选/不选”或“存在/不存在”等信息的组合优化问题。
通过将每个元素映射到一个二进制位,可以用一个整数表示一个子集或状态,从而在 DP 中进行高效的转移。
常用位运算操作
| 操作 | 代码 | 含义 |
|---|---|---|
| 将第 $i$ 位设为 1 | mask | (1 << i) |
选中元素 i |
| 将第 $i$ 位设为 0 | mask & ~(1 << i) |
移除元素 i |
| 检查第 $i$ 位是否为 1 | mask & (1 << i) |
判断元素 i 是否存在 |
| 翻转第 $i$ 位 | mask ^ (1 << i) |
切换元素 i 的状态 |
| 检查是否有连续的 1 | mask & (mask >> 1) |
若结果为 0 则无相邻的 1(常用于棋盘问题) |
| 去掉最低位的 1 | mask & (mask - 1) |
lowbit 操作,常用于枚举子集 |
常见模型
- 棋盘/矩阵上的不相邻放置:每行状态用二进制表示放置位置,转移需满足本行无相邻、与上一行无上下相邻、且不与障碍冲突。
- 集合覆盖/选数问题:用一个整数表示已选取的集合,DP 逐步扩大集合,计算最小代价或最大收益。
- 排列/顺序相关的状压:
dp[mask][i]表示已走过的集合为mask,最后停在i的最优值(如 TSP 问题)。 - 区间覆盖:用状压表示已使用的物品(如羊圈),转移时考虑用某个物品覆盖连续的一段,更新状态。
时间复杂度
通常为 O(n·2ⁿ) 或 O(n²·2ⁿ),要求 n 一般不超过 20~22。
二、例题解析
例题1:宠物袋问题
题目描述
给定一个 N×M 的网格,某些格子有食物(不能放宠物)。宠物不能放在相邻(上下左右)的格子。求最多能放多少宠物。
输入格式
第一行 N, M(1≤M≤10,1≤N≤30)。接下来 N 行,每行 M 个数,1 表示该格有食物(不可用),0 表示空地。
输出格式
一个整数,表示最多可放的宠物数。
样例输入
1 | 2 3 |
样例输出
1 | 1 |
分析
由于 M 很小,我们可以用二进制表示每行的放置状态。
- 用
row[i]记录第 i 行的障碍情况:若第 j 列为 1,则第 j 位为 1(不可放置)。 - 预处理所有行内无相邻宠物的状态:若
(state & (state >> 1)) == 0,则状态合法。 - 定义
dp[i][state]:前 i 行,第 i 行状态为 state 时能放的最多宠物数。 - 转移时,除了本行合法、不与障碍冲突,还需满足与上一行状态 pre 无上下相邻:
state & pre == 0。 - 边界:第一行直接初始化;最终答案为
max(dp[N-1][state])。
代码实现
1 | n, m = map(int, input().split()) |
例题2:羊圈问题
题目描述
有 m 头羊排成一列,第 i 头羊有 p_i 的概率逃跑。现有 n 种羊圈,第 i 种可以框住连续的 l_i 只羊,使其无法逃跑。羊圈不一定全部使用,也不一定按顺序使用。求合理安排羊圈后,能跑掉的羊的数量的期望的最小值。
输入格式
第一行 n, m。
第二行 n 个整数,表示每个羊圈可以覆盖的连续羊的数量 l1, l2, …, ln。
第三行 m 个浮点数,表示每头羊逃跑的概率 p1, p2, …, pm。
输出格式
一个浮点数,表示最小期望值(保留两位小数)。
样例输入
1 | 3 10 |
样例输出
1 | 1.00 |
分析
羊圈的使用情况可以用一个 n 位的二进制数表示,第 k 位为 1 表示使用了第 k 种羊圈。
状态定义:
dp[i][mask]表示处理完前 i 只羊,已经使用羊圈集合 mask 时,逃跑数量的最小期望。转移有两种:
不放羊圈:第 i 只羊不被覆盖,逃跑概率贡献 p_i。即
dp[i][mask] = dp[i-1][mask] + p_i放置某种羊圈:若 mask 中包含第 k 种羊圈,且该羊圈可以覆盖以 i 结尾的长度 l_k 的区间,则
dp[i][mask] = min(dp[i][mask], dp[i - l_k]
这表示用一个羊圈一口气覆盖了 [i - l_k + 1, i] 这些羊,它们都不会逃跑。
最终答案为
dp[m][(1 << n) - 1],即所有羊圈都被考虑后的最小值。
代码实现
1 | n, m = map(int, input().split()) |
三、解决状压 DP 的常用技巧
预处理合法状态
很多问题(如棋盘放置)中,大量状态本身就不合法(例如有相邻 1),可以提前筛选并存储在数组中,减少无效枚举。预处理状态间的转移关系
对于每个合法状态,可预先找出所有能转移到它的前驱状态(如上一行状态),避免在 DP 循环中重复判断,大幅降低常数。滚动数组优化空间
状压 DP 的空间通常是 O(第一阶段大小 × 2ⁿ),当第一阶段很大时(例如 N 很大但状态只依赖前一层),可用滚动数组将空间降为 O(2ⁿ)。注意二进制位数
通常 n 不超过 20,开数组时大小为1 << n,勿混淆 n 与 m。Python 中注意1 << n可能很大,需结合题目内存限制。边界与初始化
设计 DP 状态时,通常起点为dp[0][0] = 0,其他为 -∞ 或 +∞,确保未定义状态不会影响转移。结合其他算法
状压 DP 常与 图论、概率期望、排列组合 等结合,需要综合设计状态表示。
四、总结
状态压缩 DP 的核心在于用二进制位表达集合元素的存在性,从而在 O(2ⁿ) 的状态空间中进行最优子结构的递推。它小巧、精妙,是解决小规模组合优化问题的利器。
熟练位运算、预处理技巧以及常见模型(棋盘放置、集合覆盖、TSP 等)后,遇到类似问题便能快速构思出 DP 状态和转移方程。