(轮廓线)插头 DP
出自陈丹琦的《基于连通性状态压缩的动态规划问题》。
一般基于棋盘(方格表)模型。
【(轮廓线)插头 DP】
如果有简单点的例题就好了,但没有找到,那么直接拿插头 DP 模板题吧。
给定一个方格表,有一些格子放了障碍物,求用一条回路恰好经过所有格子的方案数。
。
连模板都是黑色
【插头】
因为要研究格子之间的连通性,而每个格子只会与上下左右四个格子连通,所以可以想象在每个格子里有一个中枢;然后这个中枢有上下左右四个可能的插头。
如果一个格子有对应方向的插头,就表示这个格子要和对应方向的相邻格子连通。
再观察到题目中回路的条件。在一条回路中,每个格子的度恰好为
【连通性的表示】
对于这种数据范围很小的,我们根据以往的经验,应该会想到状压 DP(事实上插头 DP 也只是对状压的优化)。
如果是常见的状压 DP,只需要用状态记录每一行每个格子是否有向下的插头即可;但是这个问题还要考虑格子之间的连通性,因此还要额外压缩一维状态用来记录连通性。
问题来了:怎么把连通性压缩为一个数?
论文中提到三种表示方法,分别是最小表示法、最左表示法和括号表示法。括号表示法暂且按下不表。
最小表示法和最左表示法,其实都是把同一个连通块的元素赋予同一个编号。
-
最小表示法:循环
,如果 有编号,跳过;如果 是障碍物,赋值编号为 ;否则从 搜索一遍,把所有与它连通的都赋予一个新编号。 -
最左表示法:如果是障碍物,编号
;否则编号为它所在连通块最靠左的格子编号。
因为最小表示法比较好写,比较常用,下面都用最小表示法了。
【尝试状压 DP】
看到棋盘、
(注意
在状态转移的时候,枚举
我们发现,如果一个格子没有向下的插头,它与其他格子的连通性是没有记录的意义的:因为和它连通必然用到其他格子。所以只需要记录插头的连通情况即可。
而因为我们只记录有插头的地方,所以可以把
【轮廓线/插头 DP】
状压 DP 是一行一行递推的,有没有可能可以按格子递推呢?当然可能。而这就是轮廓线 DP。
轮廓线:把已递推的格子和未递推的格子分开的线。
轮廓线 DP 要求每次可能对下方产生影响的,都是 "边界" 上的格子;而形成回路,显然只有边界上的格子可以连接。注意轮廓线长度为
省略一些过程,轮廓线 DP 的状态描述为:
那我们把普通状压改成轮廓线获得了什么?
首先显而易见的,空间从
普通状压 DP 需要从一个状态
(其实把这称作插头 DP 是不太妥当的,但大概因为插头 DP 不上轮廓线做不了吧)
【还能优化?括号表示法】
上面没提到的括号表示法,在这里讲。
我们观察到一个性质:如果有在轮廓线上顺次排列的四个插头
由此联想到括号序列:如果把没有插头的位置标记为空,把一个连通分量在轮廓线上靠左的插头位置标记为左括号,靠右的插头标记为右括号,沿着轮廓线,可以得到一个合法括号序列。
注意不可能一个连通块里有三个插头,因为最后所有插头都连通了,这三个插头就会形成一个回路套回路,不符合题意;也不可能有一个插头不与其他连通。
(这里说回路套回路而不是说多个回路,是因为有的题目是多个回路,但都是简单回路)
而根据括号序列,我们也能还原出轮廓线的状态。
那么我们就可以把原来的不知道多少进制压缩而成的状态,变成稳定的三进制压缩了。
【实现的方法】
看了上面的分析:简直太恐怖了!
首先就是怎么存储一个高进制/三进制的状态:这道题
主要有两种方法:循环和记忆化广搜。记忆化广搜一般会比循环快两三倍,因为循环实际上有很多无效状态,但是如果是用广搜主动拓展出来的肯定是合法状态。
滚动数组优化:因为
另外,用括号表示法比不用大概又快两三倍。
有一个点要特别注意:第
点击查看代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 50000, M = N * 2 + 7;
int n, m, edx, edy;
int g[20][20]; //是否是障碍物
int q[2][N] = {{}}, cnt[2] = {}; //队列,和队列目前长度
int h[2][M] = {{}}; //哈希表
ll dp[2][M] = {{}}; //方案数
//状态存储:越靠左越高位
int find(int cur, int x) { //在哈希表的cur版本里找x的位置
int t = x % M;
while (h[cur][t] != -1 && h[cur][t] != x) {
t++;
if (t == M)
t = 0;
}
return t;
}
void insrt(int cur, int stt, ll w) { //在cur哈希表里给stt状态累计w的方案书
int t = find(cur, stt);
if (h[cur][t] == -1) { //第一次访问这个状态
h[cur][t] = stt, dp[cur][t] = w;
q[cur][++cnt[cur]] = t; //放进更新队列里
}
else
dp[cur][t] += w;
}
int get(int stt, int k) { // 求stt的第k个格子的状态,四进制的第k位数字
return stt >> k * 2 & 3;
}
int mk(int k, int dp) { // 构造四进制的第k位数字为dp的数
return dp * (1 << k * 2);
}
ll ans = 0;
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
string str;
cin >> str;
for (int j = 0; j < m; j++)
if (str[j] == '.') {
g[i][j + 1] = 1;
edx = i, edy = j + 1;
}
}
memset(h, -1, sizeof h);
int cur = 0;
insrt(cur, 0, 1);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= cnt[cur]; j++)
h[cur][q[cur][j]] <<= 2; //从上一行的状态变成下一行
for (int j = 1; j <= m; j++) {
int lst = cur;
cur ^= 1, cnt[cur] = 0;
memset(h[cur], -1, sizeof h[cur]);
for (int k = 1; k <= cnt[lst]; k++) {
int stt = h[lst][q[lst][k]];
ll w = dp[lst][q[lst][k]];
int x = get(stt, j - 1), y = get(stt, j);
if (!g[i][j]) { //障碍物
if (!x && !y) //不能通向障碍物
insrt(cur, stt, w);
}
else if (!x && !y) { //两边闭合,必须向另外两边
if (g[i + 1][j] && g[i][j + 1]) //另外两边不能是障碍物
insrt(cur, stt + mk(j - 1, 1) + mk(j, 2), w);
}
else if (!x && y) { //左边闭合,上面不闭
if (g[i][j + 1]) //上右
insrt(cur, stt, w);
if (g[i + 1][j]) //上下
insrt(cur, stt + mk(j - 1, y) - mk(j, y), w);
}
else if (x && !y) { //上面闭合,左边不闭
if (g[i][j + 1])
insrt(cur, stt - mk(j - 1, x) + mk(j, x), w);
if (g[i + 1][j])
insrt(cur, stt, w);
}
else if (x == 1 && y == 1) { //都不闭合,都是左括号
for (int u = j + 1, s = 1; true; u++) {
int z = get(stt, u);
if (z == 1)
s++;
else if (z == 2) {
if (--s == 0) {
insrt(cur, stt - mk(j - 1, x) - mk(j, y) - mk(u, 1), w);
break;
}
}
}
}
else if (x == 2 && y == 2) { //都是右括号
for (int u = j - 2, s = 1; true; u--) {
int z = get(stt, u);
if (z == 2)
s++;
else if (z == 1) {
if (--s == 0) {
insrt(cur, stt - mk(j - 1, x) - mk(j, y) + mk(u, 1), w);
break;
}
}
}
}
else if (x == 2 && y == 1) //一右一左
insrt(cur, stt - mk(j - 1, x) - mk(j, y), w);
else if (i == edx && j == edy) //一左一右,只发生在全部闭合处
ans += w;
}
}
}
cout << ans << endl;
return 0 ;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!