插头DP学习笔记
插头 DP
插头 DP 是在状态压缩 DP 的基础上用于解决一些需要处理连通性问题(比如求棋盘上的哈密顿回路)。
要了解插头 DP ,首先先回到普通的状压 DP。状压 DP 的一类经典解法就是设 DP 数组
有些时候,这种逐行枚举的办法可能会发现状态并不好转移,而插头 DP 最为典型的做法是用逐格枚举代替普通状压 DP 的逐行枚举。
为了能够清楚的说明插头 DP,需要引入两个概念:轮廓线、插头。
轮廓线是当前已经进行转移了的格子与未进行转移格子的分界线,是一条将原来的方格图划分成为两部分的折线。
插头是一个格子所具有的状态,可以具有上下左右四个插头,表示与上下左右四个格子有连接。
接下来,结合模板题,将会讲解这一神奇的 DP。
【模板】插头dp
题目背景
ural 1519
陈丹琦《基于连通性状态压缩的动态规划问题》中的例题。
题目描述
给出
输入格式
第一行,两个整数,分别代表
从第二行到第 *
和 .
的字符串,*
表不能铺线,.
表必须铺。
输出格式
输出一行一个整数,表示总方案数。
样例 #1
样例输入 #1
4 4
**..
....
....
....
样例输出 #1
2
样例 #2
样例输入 #2
4 4
....
....
....
....
样例输出 #2
6
提示
数据规模与约定
- 对于
的数据,保证 。
Solution
这道题的要求是求出这个棋盘的哈密顿回路,如果采用普通的状压 DP 来转移状态,会发现很难转移,所以用插头 DP 来解决。
首先需要知道的是,因为需要构成回路,那么每一个格子都会具有两个插头,一个进、一个出。这一性质十分重要。
插头 DP 是在轮廓线上进行 DP 。设 DP 数组
对于轮廓线上的状态,轮廓线会将可能的回路切割成为两半,并且在轮廓线上会有这些回路留下的插头,那么因为要形成回路,留下的插头一定是偶数个并且成对出现(可以把理解为一个插头用于进入当前路径,另一个插头用于离开当前路径)。并且更重要的是这些插头的匹配是满足括号匹配的(我不会证明,但是可以感性认识下,如果
既然已经知道了轮廓线的编码方式,那么就可以来讨论转移的问题了。插头 DP 的转移的情况较多,接下来将逐个分析,为了方便表述,将当前格子的左侧的插头记作
-
当前格子为障碍物
如果当前格子为障碍物,那么能转移到当前格子的状态只能是当
与 两个都不存在(即 ),此时的轮廓线的状态将不发生改变,直接转移至当前格子。 -
左侧插头与上侧插头都不存在(
)既然当前格子上侧与左侧插头都不存在,并且为了为了满足一个格子有两个插头这一性质,当前格子将会具有右插头和下插头,并需要用这两个插头来更新轮廓线状态。
-
仅存在左侧插头(
)仅存在左插头并且不存在上插头,那么当前格子就可以具有下插头或者是右插头,分开讨论并转移这两种状态即可。
-
仅存在上侧插头(
)与仅存在左插头的情况相同,当前格子可以就有下插头或者是右插头,分开讨论并转移。
-
当前格子的上侧插头与左侧插头都为
( )理解作有两个需要进入当前格子的插头,因为一个格子只能具有两个插头,所以这个格子就只会具有左和上插头。因为要构成回路,所以就需要将上插头对应的那个插头的值改为
,可以理解成为将这组插头表示的路径颠倒方向。因为轮廓线在这个格子更新后已经不再与这两个插头有关(这两个插头已经被接上,所以不存在在轮廓线上了),所以轮廓线的状态需要将这两个插头删除,并且将上插头对应的那个插头的值改为 。 -
当前格子的上侧插头与左侧插头都为
( )这种情况与都为
的情况也类似,因为需要接上这两个插头,所以需要将左插头代表的路径反向,即需要向左找到与左插头对应的插头并将值改为 。 -
当前格子的左侧插头值为
,上侧插头值为 ( )即从上侧进入当前格子并从左侧离开,不需要对任何插头进行操作,直接删除这两个插头就可以了(同样是因为这两个插头不存在于轮廓线上了)。
-
当前格子的左侧插头值为
,上侧插头值为 ( )从左侧进入当前格子并从上侧离开,因为向上走会回到原来的位置,所以这一种情况是用于最后一个格子来构成回路用的,当到达了最后一个格子轮廓线状态满足这种情况,那么这种状态就是一种合法的回路方案,计算进最终答案就行了。
在代码实现的时候,因为状态数量是比较零散的,直接开数组存储可能存在浪费空间的情况,所以可以用哈希表来优化空间。并且如果每到达一个新格子就要枚举所有可能情况的话,时间开销又会比较大,所以可以记录下可能的状态,在需要枚举的时候直接继承上一个格子的结果,这样就可以省下一些时间。
其他有一些小技巧,此题存储的轮廓线状态由
开始前先预处理了一个
Code
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
using namespace std;
template<typename T> void read(T &k)
{
k=0;T flag=1;char b=getchar();
while (!isdigit(b)) {flag=(b=='-')?-1:1;b=getchar();}
while (isdigit(b)) {k=k*10+b-48;b=getchar();}
k*=flag;
}
int n,m;
bool mp[15][15];
int ex,ey;//记录下最后一个非障碍物的格子的坐标,并在这个格子统计答案
int bits[30];
int pre=1,cur=0;//pre表示上一行存储的位置,cur表示当前行存储的位置,当进入新的一行的时候就交换这两个值来达到滚动数组的目的
const int _SIZE=6e5,MOD=590027;
struct HASH_TABLE{//链表实现哈希表
int pre,to;
}H[_SIZE+5];
int tot[2],ptr[_SIZE+5],at=0;
int state[2][_SIZE+5],dp[2][_SIZE+5];//存储状态和dp数组
int Fans=0;//统计最终答案
void modify(int sta,int val)//查看hash表内是否存在键值sta
{
int key=sta%MOD;//计算表头
for (int i=ptr[key];i;i=H[i].pre)//遍历当前表头的所有sta
if (state[cur][H[i].to]==sta)//存在
{
dp[cur][H[i].to]+=val;//说明在dp数组中出现过,直接将val加入
return;
}
tot[cur]++;//如果没有的话就添加进当前表头
state[cur][tot[cur]]=sta;//存状态(类似映射表)
dp[cur][tot[cur]]=val;
H[++at].pre=ptr[key];//链表的处理
H[at].to=tot[cur];
ptr[key]=at;
}
void init() {for (int i=1;i<=25;i++) bits[i]=(i<<1);}//初始化bits数组
void PlugDP()//插头DP
{
tot[cur]=1,dp[cur][1]=1;//初始化边界条件
state[cur][1]=0;
for (int i=1;i<=n;i++)
{
for (int j=1;j<=tot[cur];j++) state[cur][j]<<=2;//先将轮廓线下移,即将所有插头向后走一位
for (int j=1;j<=m;j++)
{
at=0;mem(ptr,0);//新的格子,清空hash表
swap(pre,cur);//滚动
tot[cur]=0;
int nowsta,isD,isR,nowans;
for (int k=1;k<=tot[pre];k++)
{
nowsta=state[pre][k],nowans=dp[pre][k];//先存储当前状态以及答案
isD=(nowsta>>bits[j])%4,isR=(nowsta>>bits[j-1])%4;//获取左插头和上插头
if (!mp[i][j])//障碍物
{
if ((!isD) && (!isR)) modify(nowsta,nowans);//判断插头情况,只有两个插头都不存在才是合法的
}
else if ((!isD) && (!isR))//两个插头都不存在
{
if (mp[i+1][j] && mp[i][j+1]) modify(nowsta+(1<<bits[j-1])+2*(1<<bits[j]),nowans);//一个插头设置为1,一个设置为2
}
else if (isR && (!isD))//仅存在左插头
{
if (mp[i+1][j]) modify(nowsta,nowans);//插头的位置不会发生改变
if (mp[i][j+1]) modify(nowsta-isR*(1<<bits[j-1])+isR*(1<<bits[j]),nowans);//插头位置右移一位,即删除原来位置并加入原来右侧位置
}
else if (isD && (!isR))//仅存在上插头
{
if (mp[i][j+1]) modify(nowsta,nowans);//同样不发生改变
if (mp[i+1][j]) modify(nowsta-isD*(1<<bits[j])+isD*(1<<bits[j-1]),nowans);//插头位置左移一位
}
else if (isD==1 && isR==1)//两个插头都为1
{
int cnt=1;
for (int l=j+1;l<=m;l++)//向右找对应的2
{
if ((nowsta>>bits[l])%4==1) cnt++;
else if ((nowsta>>bits[l])%4==2) cnt--;
if (!cnt)//找到匹配的了
{
modify(nowsta-(1<<bits[j-1])-(1<<bits[j])-(1<<bits[l]),nowans);//将l位置设为1,也就是将2减去1
break;
}
}
}
else if (isD==2 && isR==2)//两个插头都为2
{
int cnt=1;
for (int l=j-2;l;l--)//向左找对应的1
{
if ((nowsta>>bits[l])%4==1) cnt--;
else if ((nowsta>>bits[l])%4==2) cnt++;
if (!cnt)
{
modify(nowsta-2*(1<<bits[j-1])-2*(1<<bits[j])+(1<<bits[l]),nowans);//将插头去除,插头l加1变成2
break;
}
}
}
else if (isD==1 && isR==2)//上1左2
modify(nowsta-2*(1<<bits[j-1])-(1<<bits[j]),nowans);//直接删除这两个插头
else if (isR==1 && isD==2 && i==ex && j==ey)//上2左1,必须到达最后一个点才是合法的
Fans+=nowans;//统计入最终答案
}
}
}
}
signed main()
{
read(n),read(m);
for (int i=1;i<=n;i++)
{
char st[15];scanf("%s",st+1);
for (int j=1;j<=m;j++)
if (st[j]=='.') ex=i,ey=j,mp[i][j]=1;
}
init();
PlugDP();
printf("%lld\n",Fans);
return 0;
}
推荐题目: Luogu P5074 多回路问题
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步