【学习笔记】插头 DP
插头 DP,是一类解决网格图上连通性问题的状压 DP。
相关概念#
轮廓线:已经决策的方格和未决策方格之间的分界线。
插头:用来描述连通性,一个方格与其某一方向的相邻方格连通,则称这个方格有某个方向的插头。容易发现在轮廓线上,每个时刻都是有 个上插头与 个左插头。

如图,红线部分为 决策前后的轮廓线。
括号表示法#
对于连通性问题,需要将当前的插头的状态进行压缩用于转移。
在哈密顿回路问题中,插头一定两两配对,且不存在相交关系,因此可以用括号表示法,将当前路径的左端插头设为 (
并用 表示,右端插头设为 )
并用 表示,不含插头的位置则用 表示。
在哈密顿路径问题中,插头可能独立存在,将这样的路径 用 表示。
由于合法的括号序列个数小于 ,因此可以使用手写哈希表来减少空间,转移只和上一个方格相关,因此类似滚动数组维护两组信息即可。
状态转移#
以 模板题 为例。
注意到左插头和上插头(如果存在)转移后分别变成了上插头和左插头(如果存在),且在括号序列的位置不变,转移时只需要考虑当前的两个插头即可。
设当前决策方格的左插头为 ,上插头为 。
特判当前方格存在障碍的情况,此时 必须均为 ,转移后同样均为 。
以下转移均以当前方格以及与当前方格存在插头的方格均不存在障碍为前提。
- 若 均为 ,转移相当于新建一个连通块,当前方格需要向下向右都连通,转移后 。

- 若 不为 , 为 ,转移相当于拓展一个连通块,向下或向右有两种情况。


- 若 为 , 不为 ,同理也有两种情况。


- 若 ,转移相当于合并两个连通块,此时 对应的右括号变成左括号。

- 若 ,同理,此时 对应的左括号变成右括号。

- 若 ,二者直接合并。

- 若 ,回路闭合。

当切换一行时,注意到左插头从最后一位移至第一位,需要改变状态。
点击查看代码
int n,m;
char mp[15][15];
int ex,ey;
int head[mod+10],nxt[maxn];
int st[2][maxn],cnt[2];
ll dp[2][maxn],ans;
inline void update(int now,int s,ll val){
int p=s%mod+1;
for(int i=head[p];i;i=nxt[i]){
if(st[now][i]==s) return dp[now][i]+=val,void();
}
st[now][++cnt[now]]=s,dp[now][cnt[now]]=val;
nxt[cnt[now]]=head[p],head[p]=cnt[now];
}
int main(){
n=read(),m=read();
for(int i=1;i<=n;++i){
scanf("%s",mp[i]+1);
for(int j=1;j<=m;++j){
if(mp[i][j]=='.') ex=i,ey=j;
}
}
int now=0;
update(now,0,1);
for(int i=1;i<=n;++i){
for(int k=1;k<=cnt[now];++k) st[now][k]<<=2;
for(int j=1;j<=m;++j){
memset(head,0,sizeof(head));
now^=1;
cnt[now]=0;
for(int k=1;k<=cnt[now^1];++k){
int s=st[now^1][k];
ll val=dp[now^1][k];
int p1=s>>2*(j-1)&3,p2=s>>2*j&3;
int t=s-(p1<<2*(j-1))-(p2<<2*j);
if(mp[i][j]=='*'){
if(!p1&&!p2) update(now,s,val);
}
else{
if(!p1&&!p2){
if(mp[i+1][j]=='.'&&mp[i][j+1]=='.') update(now,t+(1<<2*(j-1))+(2<<2*j),val);
}
else if(p1&&!p2){
if(mp[i+1][j]=='.') update(now,s,val);
if(mp[i][j+1]=='.') update(now,t+(p1<<2*j),val);
}
else if(!p1&&p2){
if(mp[i+1][j]=='.') update(now,t+(p2<<2*(j-1)),val);
if(mp[i][j+1]=='.') update(now,s,val);
}
else{
if(p1==1&&p2==1){
int tmp=1;
for(int l=j+1;l<=m;++l){
if((s>>2*l&3)==1) ++tmp;
if((s>>2*l&3)==2) --tmp;
if(!tmp){
update(now,t-(1<<2*l),val);
break;
}
}
}
else if(p1==2&&p2==2){
int tmp=1;
for(int l=j-2;l>=0;--l){
if((s>>2*l&3)==2) ++tmp;
if((s>>2*l&3)==1) --tmp;
if(!tmp){
update(now,t+(1<<2*l),val);
break;
}
}
}
else if(p1==2&&p2==1) update(now,t,val);
else{
if(i==ex&&j==ey) ans+=val;
}
}
}
}
}
}
printf("%lld\n",ans);
return 0;
}
例题#
HDU-1693 Eat the Trees#
和模板题类似,可以多个回路。
Luogu-P2289 HNOI 2004 邮递员#
除特殊情况外,答案要求哈密顿回路个数的 倍,特殊情况是不存在回路,只能重复走,也就是 中有 。
注意到状态数和 大小有关,因此 取 较小值,答案需要高精度。
Luogu-P3272 SCOI 2011 地板#
思考如何定义插头,注意到一个 L
形只有一个转折点,因此可以将插头 定义为该插头对应部分尚未转折, 定义为该插头对应部分已经转折。
这样一个位置新建插头可以是两侧都有 也可以是只有一侧有 ,拓展分类讨论,合并只能是两个 。
Luogu-P3170 CQOI 2015 标识设计#
注意到 L
的形式固定,那么一定是上插头转移成左插头,所以直接 DP 状态设计成上插头位置、有无左插头以及 L
连通块个数,转移比较平凡。
Luogu-P3190 HNOI 2007 神奇游乐园#
算最大权回路,最后闭合要判断是不是轮廓线上只有这两个插头,来保证连通性。
BZOJ-2310 Park II#
回路改成路径,在陈丹琦的论文里也有提到。
定义一个“独立插头”,用 状态表示。
这样转移就不能左右括号闭合,同时增加了可以某个括号不拓展且对应另一个括号变成独立插头、独立插头和某个括号可以合并且对应另一个括号变成独立插头以及两个独立插头可以合并。
算答案一定是没有其他插头,所以只可能是有一个独立插头或两个独立插头合并。
参考资料#
-
《基于连通性状态压缩的动态规划问题》 - 陈丹琦
作者:SoyTony
出处:https://www.cnblogs.com/SoyTony/p/Learning_Notes_about_Plug_DP.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效