【动态规划】状态压缩DP(状压dp)
还在更新ing
一、引入
在动态规划状态设计中,若状态是一个集合,例如 \(S=\) { \(1,0,1,1,0\) } ,则表示第 \(1、2、4\) 个节点被选中(从右往左对应 \(0 \sim 4\) 号节点)。若集合的大小不超过 \(N\) ,则集合中的每个元素都是小于 \(K\) 的正整数,可以把这个集合看作一个 \(N\) 位 \(K\) 进制数,以一个 \([0,K^N-1]\) 的十进制整数作为 DP 状态。可以将 \(S=\) { \(1,0,1,1,0\) } 看作一个 \(5\) 位二进制数 \(10110\) ,其对应的十进制数为 \(21\) 。
这种将集合作为整数记录状态的一类算法叫作状态压缩 DP 。在状态压缩 DP 中,状态的设计直接决定了程序的效率或者代码长短。我们需要根据问题分析本质,才能更好地找出恰当的状态表示、状态转移方程和边界条件。
二、二进制 & 位运算
尽管用了一个十进制数据储存二进制状态,当因为操作系统是二进制的,所以在编译器中也可采用位运算解决这个问题。
1、基本位操作运算符
在状态压缩 DP 中广泛运用位运算操作,常见的位运算如下 :
A |
B |
~A (差) |
\(A \& B\)(与) | \(A | B\)(或) | A ^ B (异或) |
---|---|---|---|---|---|
0 | 0 | 1 | 0 | 0 | 0 |
0 | 1 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 0 | 1 | 1 |
1 | 1 | 0 | 1 | 1 | 0 |
如果 \(A,B\) 为整数,则可看成长度为 \(32\) 位的两个二进制数各个位分别做上面操作。
例如:
\(A=10 = (1010)_B,B=12 = (1100)_B\) 时,
\(A \& B = 8 = (1000)_B\)
\(A | B = 14 = (1110)_B\)
\(A\) ^ \(B = 6 (0110)_B\)
~ \(A = -11 (1...10101)_B\)
另外,还有两个位移操作运算符左移(<<)
、右移(>>)
。
如:A<<B
,表示 \(A\) 的所有位都向左移动 \(B\) 个,后面 \(B\) 位用 \(0\) 补充。
注意,对二进制数,位数高低为从右到左依次为 \(0\) 位、\(1\) 位、\(2\) 位 \(\ldots\) 。
所以我们可以认为 A<<B
等价于 \(A \times 2B\);A>>B
等价于 \(\frac {A}{2B}\)。
2,常见的操作有:
去掉最后一位 ( \(101101 \to 10110\) ) x >> 1
在最后加一个 \(0\) ( \(101101 \to 1011010\) ) x << 1
在最后加一个 \(1\) ( \(101101 \to 1011011\) ) x << 1 | 1
把最后一位变成 \(1\) ( \(101100 \to 101101\) ) x | 1
把最后一位变成 \(0\) ( \(101101 \to 101100\) ) x | 1 - 1
最后一位取反 ( \(101101 \to 101100\) ) x ^ 1
把右数第 \(k\) 位变成 \(1\) ( \(101001 \to 101101,k=3\) ) x | (1 << (k - 1))
把右数第 \(k\) 位变成 \(0\) ( \(101101 \to 101001,k=3\)) x & not (1 << (k - 1))
右数第 \(k\) 位取反 ( \(101001 \to 101101,k=3\) ) x ^ (1 << (k - 1))
取末三位 ( \(1101101 \to 101\) ) x & 7
取末 \(k\) 位 ( \(1101101 \to 1101,k=5\) ) x & (1 << k - 1)
取右数第 \(k\) 位 ( \(1101101 \to 1,k=4\) ) x >> (k - 1) & 1
把末 \(k\) 位变成 \(1\) ( \(101001 \to 101111,k=4\) ) x | (1 << k-1)
末 \(k\) 位取反 ( \(101001 \to 100110,k=4\) ) x ^ (1 << k-1)
把右边连续的 \(1\) 变成 \(0\) ( \(100101111 \to 100100000\) ) x & (x + 1)
把右起第一个 \(0\) 变成 \(1\) ( \(100101111 \to 100111111\) ) x | (x + 1)
把右边连续的 \(0\) 变成 \(1\) ( \(11011000 \to 11011111\) ) x | (x - 1)
取右边连续的 \(1\) ( \(100101111 \to 1111\) ) (x ^ (x + 1)) >> 1
3、表示集合(状态压缩)
有时我们用 \(32\) 位的整型数的各个位表示一个最多 \(32\) 个元素的集合,第 \(i\) 位为 \(1\) 表示第 \(i\) 个元素在集合中,为 \(0\) 则表示不在集合中。
常见的操作有:
并集操作 A | B
交集操作 A & B
集合的差 A & ~ B
补集 -1 ^ A
( \(-1\) 为二进制数 \(111...1\))
加入第 \(i\) 个元素 A = A | (1 << (i - 1))
删除第 \(i\) 个元素 A = A & ~ (1 << (i - 1))
判断第 \(i\) 个元素 (A & (1 << (i - 1))) != 0
4、lowbit
求最低位的 \(1\) 的位置,即著名的 lowbit 问题。
lowbit(int x) {
return x&(-x);
}
三、实现流程
状态压缩 DP 大可分为两类 :
- 棋盘式(基于连通性)DP
- 集合式 DP
状态压缩dp三部曲:
- 考虑如何状态压缩
- 确定状态表示和状态转移方
- 根据实际问题确定筛选条件
例题引入
著名的旅行商问题( Traveling Salesman Problem,TSP )指一个旅行商从一个城市出发,经过每一个城市一次且只有一次回到原来的地方,要求经过的距离最短。
TSP 问题是一个 NP 难题,目前没有多项式时间的高效算法。若采用搜索+剪枝,则该算法的时间复杂度为 \(O(n!)\) ,数据量大时,这种方法无法解决,可以尝试采用动态规划解决。
假设已访问的节点集合为 \(S\) (将源点 \(0\) 当作未被访问的节点,因为从 \(0\) 出发,所以要回到 \(0\) ),当前所在的节点为 \(u\) 。
I. 状态表示
\(f_{S,u}\) 表示已经访问的节点集合为 \(S\) ,从 \(u\) 出发走完所有剩余节点回到源点的最短距离。
II. 状态转移方程
若当前 \(u\) 的邻接节点 \(v\) 未被访问,则 \(f_{S,u}\) 由两部分组成,第 \(1\) 部分是 \(u\) 到 \(v\) 的边值,第 \(2\) 部分是从 \(v\) 出发走完所有剩余节点再回到源点的最短距离。访问完 \(v\) 之后,已访问的节点集合变为 \(S \cap\) { \(v\) } ,若 \(u\) 有多个未被访问的邻接点 \(v\) ,则取最小值。
III. 临界条件
\(f_{(1<<n)-1,0}=0\) ,表示若所有节点都被访问,则此时已经没有剩余节点,从 \(0\) 节点出发走完所有剩余节点回到源点的最短距离为 \(0\) 。
Code
void Traveling() { //计算f[S][u]
f[(1<<n)-1][0]=0; //1<<n一定要加括号(别问我为什么)
for(int S=(1<<n)-2;S>=0;S--) {
for(int u=0;u<n;u++) {
for(int v=0;v<n;v++) {
if((u!=0&&!(S>>u&1))||w[u][v]==inf) continue; //可以加约束条件,不加太多状态
if(!(S>>v&1)&&f[S][u]>f[(S|1<<v)][v]+w[u][v]) {
f[S][u]=f[(S|1<<v)][v]+w[u][v];
path[S][u]=v; //记录后继节点
}
}
}
}
}
训练 1 :[USACO06NOV] Corn Fields G
源自 洛谷 P1879 [USACO06NOV] Corn Fields G
题目描述
农场主 \(\rm John\) 新买了一块长方形的新牧场,这块牧场被划分成 \(M\) 行 \(N\) 列 \((1 \le M \le 12; 1 \le N \le 12)\),每一格都是一块正方形的土地。 \(\rm John\) 打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。
遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是 \(\rm John\) 不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。
\(\rm John\) 想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)
输入格式
第一行:两个整数 \(M\) 和 \(N\),用空格隔开。
第 \(2\) 到第 \(M+1\) 行:每行包含 \(N\) 个用空格隔开的整数,描述了每块土地的状态。第 \(i+1\) 行描述了第 \(i\) 行的土地,所有整数均为 \(0\) 或 \(1\) ,是 \(1\) 的话,表示这块土地足够肥沃,\(0\) 则表示这块土地不适合种草。
输出格式
一个整数,即牧场分配总方案数除以 \(100,000,000\) 的余数。
样例 #1
样例输入 #1
2 3
1 1 1
0 1 0
样例输出 #1
9
题解
I. 分析题意
本题求在值为 \(1\) 的土地上种上植草地,每个草地的上下左右相邻的土地上不能有草地,求所有的种植方案数。