什么是状态压缩DP
DP,即动态规划,传统的动态规划都是基于整数的, 比如背包问题。定义状态 d p [ i ] [ j ] dp[i][j] dp[i][j]:背包容量为 j j j时前 i i i件物品的最大收益。这里 i i i取整数。而对于状态压缩型的DP:动态规划是基于集合的,但是我们使用二进制将这个集合压缩为一个整数表示,这个过程就是状态压缩。
常用位运算
状态压缩DP过程常常用到位运算来模拟对集合的操作:
运算符 | 运算 | 结果 |
---|---|---|
& | 1011 & 1000 | 1000 |
| | 1011 | 0100 | 1111 |
~ | ~1011 | 0100 |
^ | 1001 ^ 1111 | 0110 |
例题一: 旅行商问题
-
定义 d p [ S ] [ v ] dp[S][v] dp[S][v]: 当已拜访节点集合为 S S S,且当前位置为 v v v时,回到位置0还需要经过的最短路径。
-
目标态 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]:拜访所有城市再回到0所经过的最短路径。
-
状态转移:
- u ∉ S u \notin S u∈/S: 说明不可重复经过一点。
- 实例:
d
p
[
0100
]
[
1
]
=
m
i
n
{
d
p
[
0110
]
[
2
]
+
2
,
d
p
[
0101
]
[
3
]
+
2
}
dp[0100][1] = min\{dp[0110][2] + 2, dp[0101][3] + 2\}
dp[0100][1]=min{dp[0110][2]+2,dp[0101][3]+2}:过程如下
-
更新策略: 由状态转移方程式可知, 所有的 d p [ S ] [ v ] dp[S][v] dp[S][v]都由 d p [ S ’ ] [ u ] ( S ’ > S ) dp[S^’][u](S^’>S) dp[S’][u](S’>S)转移而来,故我们按照 S S S递减更新。
-
初始化: d p [ 2 n − 1 ] [ 0 ] = 0 dp[2^n-1][0] = 0 dp[2n−1][0]=0:已经全部拜访,并且当前位置为 0 0 0,因此还需经过路径长度为0.
-
核心代码:
void solve(int n)
{
/*
n: 需要拜访的城市数量
road[i][j]: 城市i到城市j的距离,若没有路,则为INF。
dp[i][j]: 当前到达城市节点集合为i, 且当前位于城市j,剩余的路程长度。
*/
// 初始化dp数组
for(int i=0; i<1<<n; i++)
for(int j=0; j<n; j++)
dp[i][j] = inf;
dp[(1<<n)-1][0] = 0;
for(int i=(1<<n)-2; i>=0; i--)
{
for(int v=0; v<n; v++)
{
for(int u=0; u<n; u++)
{
// 使用移位运算和按位与判断元素是否存在于集合
if(!(i >> u & 1))
{
// 使用按位或运算模拟集合求并
// 由于没有路初始化为inf,因此没有判断v和u间是否存在路。
dp[i][v] = min(dp[i][v], dp[i | (1 << u)][u] + road[v][u]);
}
}
}
}
}
例题二:Traveling by Stagecoach(Poj 2686)
- 定义 d p [ T ] [ v ] dp[T][v] dp[T][v]: 当前所剩票集合为 T T T,且位于城市 v v v, 要到达 b b b还需要的最小花费。
- 目标态 d p [ 2 n − 1 ] [ a ] dp[2^n-1][a] dp[2n−1][a]: 从 a a a出发,且有题目给定的票数,到达 b b b的最小花费。
- 状态转移:
d p [ T ] [ v ] = m i n { d p [ T ∖ t ] [ u ] + r o a d [ v ] [ u ] / t } , t ∈ T , u ∈ N v ( N v 表 示 节 点 v 的 邻 居 结 点 ) dp[T][v] = min \{dp[T\setminus t][u] + road[v][u] / t\}, t \in T, u \in N_v(N_v表示节点v的邻居结点) dp[T][v]=min{dp[T∖t][u]+road[v][u]/t},t∈T,u∈Nv(Nv表示节点v的邻居结点) - 更新策略:由状态转移方程式可知, T T T是用 T ’ ( T ’ < T ) T^’(T^’<T) T’(T’<T)更新的, 因此按 T T T递增更新。
- 初始化:
- d p [ ∗ ] [ b ] = 0 dp[*][b] = 0 dp[∗][b]=0, 已经到达b,还需花费为0。
- d p [ 0 ] [ x ] = i n f , x ! = b dp[0][x] = inf, x != b dp[0][x]=inf,x!=b: 若不是终点,且没有票,则无法到达,置inf。
- 核心代码:
void solve(int n)
{
/*
1代表还有该票
0代表没有该票
dp[i][j]: 剩下车票状态i,现在在城市j到达b还需要的花费
*/
for(int i=0; i<=m; i++){
dp[0][i] = INF;
}
for(int i=0; i< (1<<n); i++)
dp[i][b] = 0;
for(int s=1; s<1<<n; s++)
{
for(int v=1; v<=m; v++)
{
for(int u=1; u<=m; u++)
{
if(grad[v][u] != INF)
{
for(int t=0; t<n; t++)
{
if(s >> t & 1)
dp[s][v] = min(dp[s][v], dp[s & ~(1 << t)][u] + grad[v][u] / T[t]);
}
}
}
}
}
}
例题三:铺砖问题(Poj 2411)
此题比较开脑洞,参考了网上解法才算弄明白。简单说明一下解题思路。
-
编码:
对铺好的砖进行编码,(1):横放,则两个格都为1
。(2)竖放,则上面格为0
,下面格为1
。 可以证明编码可铺装方法是一一对应的的 -
递推:
- 根据编码,我们知道: (1): 铺砖的上一排和下一排一定有对应关系,必须按一定规则才算合法。(2):最后一排砖一定全为1。
- 假设我们已经知道: (1)倒数第二排所有编码对应的谱砖方法总数。 (2)最后一排对应编码的所有倒数第二排合法编码。 那我们就能够得到总数, 即为所有倒数第二排合法编码总数之和。(如下图,
2
×
2
2 \times 2
2×2的矩阵, 我们知道最后一排全一对应的合法倒数第二排为
11
11
11和
00
00
00, 且它们对应的铺砖方法总数分别为
1
1
1和
1
1
1, 因此总数量为
1
+
1
=
2
1+1=2
1+1=2)
而要求倒数第二排的数量,我们又需要倒数第三排的数量以及对应合法关系,因此逐层递推。
-
求解对应关系:
如何简便求解对应的合法关系? 考虑两排格子。对第二排的当前格子,我们有三种铺放方式:(1): 右铺。(2)不铺。(3)上铺
这样对下一排的编码方式进行深度优先搜索,我们就可以求出所有的上下两排对应合法编码。具体的,先将两排(top, down)都初始化为0。
(1)右铺:则 t o p = ( t o p < < 2 ) ∣ 3 top = (top << 2 ) | 3 top=(top<<2)∣3, d o w n = ( d o w n < < 2 ) ∣ 3 down = (down << 2) | 3 down=(down<<2)∣3
(2)上铺:则 t o p = ( t o p < < 1 ) top = (top << 1) top=(top<<1), d o w n = ( d o w n < < 1 ) ∣ 1 down = (down << 1) | 1 down=(down<<1)∣1
(3)不铺: 则 t o p = ( t o p < < 1 ) ∣ 1 top = (top << 1) | 1 top=(top<<1)∣1., d o w n = ( d o w n < < 1 ) down = (down << 1) down=(down<<1)
可以证明若不是刚好铺完长度为 m m m的铺法,则是不合法的, 反之则合法:
方便起见,我们人为设置第0排为全一,并将其数量置为1,因为这样设置第0排符合第一排的设定,因为第一排不可上铺。
现在回到求解动态规划的步骤:
- 定义 d p [ i ] [ E ] dp[i][E] dp[i][E]: 第 i i i行的编码为 E E E时,前 i i i行所有铺砖方法的总数。
- 目标态: d p [ n ] [ 2 m − 1 ] dp[n][2^m-1] dp[n][2m−1], 最后一行全为1的铺法总数。
- 状态转移: d p [ i ] [ E d o w n ] = ∑ d p [ i − 1 ] [ E t o p ] , E t o p ∈ N , 其 中 N 为 所 有 和 E d o w n 合 法 匹 配 的 上 一 行 dp[i][E_{down}] = \sum dp[i-1][E_{top}], E_{top} \in N, 其中N为所有和E_{down}合法匹配的上一行 dp[i][Edown]=∑dp[i−1][Etop],Etop∈N,其中N为所有和Edown合法匹配的上一行。
- 更新策略: 按 i i i从小到大更新。
- 初态: d p [ 0 ] [ 2 m − 1 ] = 1 dp[0][2^m-1] = 1 dp[0][2m−1]=1, 其余 d p [ 0 ] [ ∗ ] = 0 dp[0][*] = 0 dp[0][∗]=0
- AC代码:
#include<iostream>
#include<cstring>
using namespace std;
typedef long long ll;
const int MAX = 1 << 13;
int ok_for_top_down[MAX][2];
//ok_for_top_down[i][0]表示第i个合法上下排的上
//ok_for_top_down[i][0]表示第i个合法上下排的下排
ll dp[12][MAX];
int n, m;
ll cnt;
void dfs_get_all_ok(int c, int top, int down)
{
// 不合法
if(c > m)
return;
// 合法
if(c == m)
{
ok_for_top_down[cnt][0] = top;
ok_for_top_down[cnt][1] = down;
cnt++;
}
// 右铺
dfs_get_all_ok(c+2, (top << 2) | 3, (down << 2) | 3);
// 上铺
dfs_get_all_ok(c+1, (top << 1), (down << 1)|1);
// 不铺
dfs_get_all_ok(c+1, (top << 1) | 1, (down << 1));
}
int main()
{
while(cin >> n >> m)
{
if(n + m == 0)
break;
if(n < m)
swap(n, m);
cnt = 0;
dfs_get_all_ok(0, 0, 0);
memset(dp, 0, sizeof(dp));
dp[0][(1<<m)-1] = 1;
for(int i=1; i<=n; i++)
{
for(int k=0; k<cnt; k++)
{
int top = ok_for_top_down[k][0];
int down = ok_for_top_down[k][1];
dp[i][down] += dp[i-1][top];
}
}
cout << dp[n][(1<<m)-1] << endl;
}
return 0;
}