状压DP

状压(state compression)DP似乎不怎么常看到,其实也并不复杂。只是在一般的用整数作为状态索引 f[i]f[i]或者 f[i][j]f[i][j] 中的 ii 或者 jj 变成了用二进制作索引。这样就能方便地列举在选择问题中指数个(2n2^n)状态了。

先来熟悉一下集合运算的位运算:

集合操作

位运算

intersection A∩B

A & B

union A∪B

A | B

complementary Aᶜ

~A

An unfilled circle inside a square. The area inside the square not covered by the circle is filled with red. The borders of both the circle and the square are black.

relative complementary A\B

A & (~B)

symmetric difference A△B

A ^ B

一些操作:

state =| (1<<i) # 从状态state加入i
state & ~(1<<i) # 从状态state去掉i
state & (1<<i)  # 判断state是否包括i,或者写成(state>>i)&1

s &= s-1   # 最低位的 1 置成 0
p = s & -s # 获得最低位的1的位置(1&-1=1,2&-2=2)

s = state
while s:
    yield s # 遍历state的非空子集s
    s = (s-1) & state

热身:子集(power set)

除了前面提到的用DFS求解,还可以枚举所有用整数的二进制表示来枚举nums的所有2n2^n个子集(包括空集的power set)。然后在每个状态中判断是否各个数字被选中。

TSP

来看看经典的TSP(Traveling salesman problem):

给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并回到起始城市的最短回路。

如果城市的数量不多的话(n<10),那么我们就可以用状压DP来求解。具体地说,我们用 f[pos,state]f[\text{pos}, \text{state}] 来表示身处 pos\text{pos} ,走过 state\text{state} (二进位数字)城市后最小花费。 ff 的状态数一共有 O(n2n)O(n \cdot 2^n) 个。我们把起点叫做 ss ,求解过程:

  1. 初始化f[s,{s}]=0f[s, \{ s\}] = 0 ,其余均等于 inf\inf

  2. 状态转移f[j,state+{j}]=ministate(f[i,state]+cost [i,j])f[j, \text{state}+\{j\}] = \min_{i \in \text{state}} (f[i, \text{state}] + \text{cost }[i, j])

  3. 求解res=mini(f[i][1<<n1]+cost[i][s])\text{res} = \min_{i} (f[i][1 << n -1] + \text{cost}[i][s]) ,等于到达城市 ii且遍历了所有城市的最小花费,加上返回到起点的最小花费。

这里用二进制来表示 state\text{state} ,再用DP实现最小花费计算,就是状压DP的过程。

参考代码:

路径问题

不同路径:有顺序的放球问题

不同路径 II:二维路径数(方案数)DP

不同路径 III:状态压缩DP

分割子集

这题的解法不是DP,而需要暴力搜索所有的分割子集方法。

枚举子集在搜索过程中不会出现重复选中的元素。这里和枚举子集不一样,因为每个元素最后都要落到某一个子集,所以有可能要重头搜索见参考代码中dfs(vis, target, 0, k-1)。所以用二进制的“状态“来表示每个元素是否已被选,就可以避开那些已经考虑过的元素了。

棋盘

如何将N个皇后放置在NxN的棋盘上,并且使皇后彼此之间不能相互攻击,共有多少种摆放方案。皇后能攻击到它上下左右,以及左上左下右上右下八个方向上任意一个格子。

在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。

参考代码:

Last updated

Was this helpful?