DP 系列(六):状压 DP

本讲定位:前五讲的状态都能用”几个数”描述——下标、容量、区间端点、子树根。本讲面对一类新问题:必须记住”选了哪些”,而这份名单没法压缩成几个数。解决办法是把整个集合塞进一个二进制整数,让”集合”也能当数组下标——这就是状态压缩(状压)DP。学完本讲,旅行商类、棋盘放置类问题都有了统一框架。

前置知识:专题一(记忆化→递推、依赖决定顺序)、专题五(“缺信息就加维”,第 5 节呼应);位运算基础(本讲第 2 节会集中复习)。

标注 ⭐ 的小节为进阶内容,第一遍学可以跳过。


1. 为什么需要状压:当”选了哪些”绕不过去

看这道题(洛谷 P1433 吃奶酪):平面上有 $n \le 15$ 块奶酪,从原点 $(0,0)$ 出发把它们全部吃完,问走过的路程最短是多少。

按 DP 的老套路想:走到中途,未来的决策取决于什么?

第一条好办,一个编号就行。第二条麻烦了——试着压缩它:只记”还剩几块没吃”行不行?不行。同样是”剩 3 块”,剩的是家门口的 3 块还是天边的 3 块,后面要走的路完全不同。未来真的依赖”具体是哪些”,这个信息一位都省不掉。

那就别省了,直接把集合存进状态。关键观察:

一个 $n$ 元集合的子集,恰好对应一个 $n$ 位二进制数:第 $i$ 位是 $1$ 表示元素 $i$ 在集合里。集合就是一个 $[0, 2^n)$ 的整数,当然能当数组下标。

代价是状态数 $2^n$——所以状压题的 $n$ 都很小。看到 $n \le 15 \sim 20$(或棋盘一边 $\le 10$ 出头),就要条件反射地想到状压。 这是数据范围反推算法的典型案例:$2^{15} = 32768$,乘上一两个 $n$ 的系数正好卡在 $10^7 \sim 10^8$ 量级。


2. 位运算工具箱

状压 DP 的全部”集合语言”就这几句,务必烂熟:

集合含义 位运算写法
全集 $\{0,1,\ldots,n-1\}$ (1 << n) - 1
判断 $i$ 是否在 $S$ 中 S >> i & 1
把 $i$ 加入 $S$ S | (1 << i)
把 $i$ 移出 $S$ S & ~(1 << i)
集合大小(1 的个数) __builtin_popcount(S)
取最低位的 1(lowbit) S & -S
判断 $S$ 中有无相邻元素 S & (S << 1)(非零即有)
两集合是否相交 S & T(非零即相交)
$S$ 是否是 $T$ 的子集 (S & T) == S

两个高频坑,先打预防针:

  1. 运算符优先级:== 比 & 优先级高,S & 1 == 0 实际是 S & (1 == 0),恒为 0。位运算表达式一律多加括号。
  2. 移位溢出:1 << i 是 int,$i \ge 31$ 就溢出了,要写 1LL << i。本讲 $n \le 20$ 没事,但省选题里集合配合 long long 计数时常踩这个坑。