【Coel.学习笔记】插头动态规划
前言
某些状态压缩动态规划(如网格覆盖问题)需要在某些位置记录联通性信息,这种就被称为“插头”动态规划(下文简称插头 DP)。此类问题在陈丹琦(又是她)在集训队论文《基于连通性状态压缩的动态规划问题》中首次被总结。
插头 DP 通常使用轮廓线解决,本质上是状态压缩的一个方法。
例题讲解
插头 DP 知识点并不难,但分类讨论很多。这也是插头 DP 很多黑题的原因。
【模板】插头dp
洛谷传送门
给定一个 的棋盘,某些格子上有障碍,试求出哈密顿回路个数。。
解析:考虑朴素的状态压缩做法。记 表示当前走过点的状态为 ,到达点 时的回路个数,那么走到 时就可以转移到 的答案。时间复杂度为 。
由于整张图是一个棋盘,所以求解可以得到优化。我们首先把转移方式改成按格子转移,这时转移过的部分和没转移的部分就会有一条分界线,也就是轮廓线。对于一个格子而言,其上下左右四个方向(即“插头”)只能选择两个,一个作为出边,一个作为入边。那么按照每格进行状态转移,可能的结果只有 种。
此时,我们需要记录哪几个边属于同一个连通块,因为回路的本质就是联通整个棋盘的连通块。记录连通块有两种方法:最小表示法和括号表示法。
最小表示法需要遍历所有出边,当找到一个没有标记的连通块时进行标号,然后从头继续遍历,直到所有连通块都被标记过。当出边不存在时,记作 。
括号表示法的原理基于回路的性质,由于经过轮廓线的边必然满足两两配对且不存在交叉,所以联系括号序列,用 表示左括号(即入边), 表示右括号(即出边), 表示没有连边,那么每一个配对的括号组就可以表示当前能够连通的括号状态。括号表示法的值域为 ,所以更新状态更加方便。
记 表示已经处理到的格子为 ,轮廓线状态为 时的回路个数,则转移可能如下(为方便,用二元组 替代 ):
- 为一个障碍物,则当 时不可更新(因为障碍物不可能连边),否则保持状态不变;
- 不是障碍物(下同)且 ,则必然会利用到相邻格子之外的边(即除掉 之后剩下的边);
- ,则剩下的两条边一个连上,一个不连,做一个枚举即可;
- ,同理进行枚举;
- ,显然 要连起来,但此时两个连通块会连起来,所以另一边的两个插头需要调整,即前一个改成 ;
- ,同样需要微调,把后一个变成 ;
- ,直接相连即可得到合法结果,无需调整;
- ,此时 必然在同一个连通块中,所以这种情况只会发生在回路最后一个格子上,在此进行方案计数即可。
由于直接存状态的数量级为 ,而实际上可能的状态很少(因为不能交叉等性质会大大消除可能的状态个数),所以我们考虑用哈希表存状态,并且用上滚动数组。STL 和 pb_ds 的哈希表效率都很一般,需要手写哈希表。存储状态则需要用一个数组存下每次滚动的结果。
总的时间复杂度为 ,其中 为状态个数,与 同阶,但除去不合法方案后其实很小。
代码看着很长,实际上大部分都在分类讨论,所以也不怎么难写。
#include <iostream>
#include <cstring>
const int maxn = 5e4 + 10, mod = 1e6 + 3;
typedef long long ll;
int n, m, dx, dy, cur;
int M[20][20], Q[2][maxn], cnt[maxn];
int Hash[2][mod];
ll v[2][mod], res;
using namespace std;
int getHash(int cur, int x) { //查探法哈希
int t = x % mod;
while (Hash[cur][t] != -1 && Hash[cur][t] != x)
if (++t == mod) t = 0;
return t;
}
inline void insPlug(int cur, int st, ll w) { //插入插头
int t = getHash(cur, st);
if (Hash[cur][t] == -1) {
Hash[cur][t] = st, v[cur][t] = w;
Q[cur][++cnt[cur]] = t;
} else v[cur][t] += w;
}
inline int getState(int st, int k) { return st >> k * 2 & 3; } //得到四进制下第 k 位数字(三进制比较难写)
inline int setState(int k, int v) { return v * (1 << k * 2); } //构造四进制第 k 位为 v 的数字
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
char s[25];
cin >> (s + 1);
for (int j = 1; j <= m; j++)
if (s[j] == '.') M[i][j] = 1, dx = i, dy = j;
}
memset(Hash, -1, sizeof(Hash));
insPlug(cur, 0, 1);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= cnt[cur]; j++)
Hash[cur][Q[cur][j]] <<= 2; //先把队列里的状态左移一位(二进制下两位)
for (int j = 1; j <= m; j++) {
int lst = cur;
cur ^= 1, cnt[cur] = 0;
memset(Hash[cur], -1, sizeof(Hash[cur]));
for (int k = 1; k <= cnt[lst]; k++) {
int st = Hash[lst][Q[lst][k]];
ll w = v[lst][Q[lst][k]];
int x = getState(st, j - 1), y = getState(st, j);
if (!M[i][j]) { //可能1:有障碍物
if (!x && !y) insPlug(cur, st, w);
} else if (!x && !y) { //可能2:x=y=0
if (M[i + 1][j] && M[i][j + 1]):
insPlug(cur, st + setState(j - 1, 1) + setState(j, 2), w);
} else if (!x && y) { //可能3:x=0,y!=0
if (M[i][j + 1]) insPlug(cur, st, w);
if (M[i + 1][j]) insPlug(cur, st + setState(j - 1, y) - setState(j, y), w);
} else if (x && !y) { //可能4:x!=0,y=0
if (M[i][j + 1]) insPlug(cur, st - setState(j - 1, x) + setState(j, x), w);
if (M[i + 1][j]) insPlug(cur, st, w);
} else if (x == 1 && y == 1) { //可能5:x=y=1
for (int u = j + 1, s = 1;; u++) {
int curState = getState(st, u);
if (curState == 1) s++;
else if (curState == 2)
if (--s == 0) { //调整插头
insPlug(cur, st - setState(j - 1, x) - setState(j, y) - setState(u, 1), w);
break;
}
}
} else if (x == 2 && y == 2) { //可能6:x=y=2
for (int u = j - 2, s = 1;; u--) {
int curState = getState(st, u);
if (curState == 2) s++;
else if (curState == 1)
if (--s == 0) { //调整插头
insPlug(cur, st - setState(j - 1, x) - setState(j, y) + setState(u, 1), w);
break;
}
}
} else if (x == 2 && y == 1) //可能7:x=2,y=1
insPlug(cur, st - setState(j - 1, x) - setState(j, y), w);
else if (i == dx && j == dy) res += w; //可能8:x=1,y=2
/*此时所有情况都排掉了,所以只要判断是不是到终点*/
}
}
}
cout << res;
return 0;
}
插头 DP 的应用基本还是在哈密顿回路上,所以下面的几道题都和哈密顿回路有关。
[HNOI2004]邮递员
洛谷传送门
给定一个 的棋盘,棋盘上没有障碍物,求有向哈密顿回路的个数。
解析:其实就是去掉障碍物的限制,然后无向变有向而已,答案乘以二即可。另外这题没有让取模而且棋盘范围比较大,需要写高精度(虽然 __int128
就够了)。
代码和上一个大同小异,就不放了。
[HNOI2007]神奇游乐园
洛谷传送门
给定一个 的棋盘,每个格子带有一个权值,求权值最大的哈密顿回路。,权值可能为负数。
解析由于问题只是把方案计数换成了最优化,所以模型上没太大区别。唯一的不同在于,当 时这个格子可以走也可以不走(上一题中每个格子都要走),所以要分开插头。
/*前面部分没什么区别(除了把 insPlug 的加法改成取 max)*/
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
memset(Hash, -1, sizeof(Hash));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> a[i][j];
insPlug(cur, 0, 0);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= cnt[cur]; j++)
Hash[cur][Q[cur][j]] <<= 2;
for (int j = 1; j <= m; j ++ ) {
int lst = cur;
cur ^= 1, cnt[cur] = 0;
memset(Hash[cur], -1, sizeof(Hash[cur]));
for (int k = 1; k <= cnt[lst]; k++) {
int st = Hash[lst][Q[lst][k]], w = v[lst][Q[lst][k]];
int x = getSt(st, j - 1), y = getSt(st, j);
if (!x && !y) {
insPlug(cur, st, w);
if (i < n && j < m)
insPlug(cur, st + setSt(j - 1, 1) + setSt(j, 2), w + a[i][j]);
} else if (!x && y) {
if (i < n) insPlug(cur, st + setSt(j - 1, y) - setSt(j, y), w + a[i][j]);
if (j < m) insPlug(cur, st, w + a[i][j]);
} else if (x && !y) {
if (i < n) insPlug(cur, st, w + a[i][j]);
if (j < m) insPlug(cur, st - setSt(j - 1, x) + setSt(j, x), w + a[i][j]);
} else if (x == 1 && y == 1) {
for (int u = j + 1, s = 1;; u++) {
int z = getSt(st, u);;
if (z == 1) s++;
else if (z == 2)
if (--s == 0) {
insPlug(cur, st - setSt(j - 1, x) - setSt(j, y) - setSt(u, 1), w + a[i][j]);
break;
}
}
} else if (x == 2 && y == 2) {
for (int u = j - 2, s = 1;; u--) {
int z = getSt(st, u);
if (z == 2) s++;
else if (z == 1)
if (--s == 0) {
insPlug(cur, st - setSt(j - 1, x) - setSt(j, y) + setSt(u, 1), w + a[i][j]);
break;
}
}
} else if (x == 2 && y == 1) {
insPlug(cur, st - setSt(j - 1, x) - setSt(j, y), w + a[i][j]);
} else if (st == setSt(j - 1, x) + setSt(j, y))
gma(res, w + a[i][j]);
}
}
}
cout << res;
return 0;
}
[SCOI2011]地板
洛谷传送门
给定一个 (原文为 ,这里改成符合习惯的写法)的棋盘,某些格子上有障碍。试求出用若干个 L 型地板铺满整个棋盘的方案数对 取模的结果。L 型地板的定义参见原题、
解析:与模板题相比,这道题换成了 L 型地板。看起来很复杂,但仔细一想,L 型地板其实就是限制为有且只有一个拐弯点。根据这一点,我们同样可以用一个三进制表示插头状态: 表示没有插头, 表示插头为直线, 表示插头为拐弯点。
接下来继续分类讨论就行了,这里限于篇幅我比较懒没有放出来,请读者参考代码画图思考。
另外为了让状态个数少一些,从而节省哈希表空间,我们保持 ,具体来说当 的时候翻转一下即可。
/*insPlug 换成加法取模*/
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
memset(Hash, -1, sizeof(Hash));
for (int i = 1; i <= n; i++) {
cin >> (s + 1);
for (int j = 1; j <= m; j++)
if (s[j] == '_')
a[i][j] = 1, dx = i, dy = j;
}
if (n < m) {
swap(n, m), swap(dx, dy);
for (int i = 1; i <= n; i++)
for (int j = 1; j < i; j++)
swap(a[i][j], a[j][i]);
}
insPlug(cur, 0, 1);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= cnt[cur]; j++)
Hash[cur][Q[cur][j]] <<= 2;
for (int j = 1; j <= m; j++) {
int lst = cur;
cur ^= 1, cnt[cur] = 0;
memset(Hash[cur], -1, sizeof(Hash[cur]));
for (int k = 1; k <= cnt[lst]; k++) {
int st = Hash[lst][Q[lst][k]], w = v[lst][Q[lst][k]];
int x = getSt(st, j - 1), y = getSt(st, j);
if (!a[i][j]) { //有障碍物,状态不变
if (!x && !y) insPlug(cur, st, w);
} else if (!x && !y) { //x=y=0
if (a[i][j + 1]) insPlug(cur, st + setSt(j, 1), w); //右边插头
if (a[i + 1][j]) insPlug(cur, st + setSt(j - 1, 1), w); //下面插头
if (a[i][j + 1] && a[i + 1][j])
insPlug(cur, st + setSt(j - 1, 2) + setSt(j, 2), w); //两个都插头
} else if (!x && y == 1) { //x=0,y=1
if (a[i + 1][j]) insPlug(cur, st + setSt(j - 1, y) - setSt(j, y), w); //右边插头
if (a[i][j + 1]) insPlug(cur, st + setSt(j, 1), w); //下面插头
} else if (x == 1 && !y) { // x=1,y=0
if (a[i][j + 1]) insPlug(cur, st - setSt(j - 1, x) + setSt(j, x), w); //下面插头
if (a[i + 1][j]) insPlug(cur, st + setSt(j - 1, 1), w); // 右边插头
} else if (!x && y == 2) { // x=0,y=2
if (i == dx && j == dy) (res += w) %= mod; //到达边界
else if (a[i + 1][j]) insPlug(cur, st + setSt(j - 1, y) - setSt(j, y), w);
insPlug(cur, st - setSt(j, y), w); // 下面插头
} else if (x == 2 && !y) {
if (i == dx && j == dy) (res += w) %= mod; // 到达边界
else if (a[i][j + 1]) insPlug(cur, st - setSt(j - 1, x) + setSt(j, x), w);
insPlug(cur, st - setSt(j - 1, x), w); // 下边插头
} else if (x == 1 && y == 1) { // x=1,y=1
if (i == dx && j == dy) (res += w) %= mod; // 到达边界
insPlug(cur, st - setSt(j - 1, x) - setSt(j, y), w); //其余都是不合法方案,想一想为什么
}
}
}
}
cout << res;
return 0;
}
本文作者:Coel's Blog
本文链接:https://www.cnblogs.com/Coel-Flannette/p/16773742.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步