插头dp
前置芝士
状压dp。
什么是插头dp
插头dp实际上是一种特殊的状压dp,特殊在他是一个格点一次转移的
插头dp怎么做
众所周知,dp分为三个部分,状态、转移和优化,以下皆以P5056【模板】插头 DP为例;
状态设置
在说清楚插头dp状态是什么之前,我们需要先知道插头和轮廓线是什么。
轮廓线
由于插头dp按照格点转移,已被转移的点和未被转移的点中间具有一条折线,这条线就是轮廓线
插头
其实每道题中插头的定义不尽相同,但是他们都有一个统一的特点,表示这条边相邻的两个方块的关系。
在这里,插头指的是这个点是否需要与他下/右边的相连,如图(图是我画的,比较丑)
其中蓝色表示一种方案,红色表示轮廓线,我们注意到红线上有的边被蓝线经过,有的没有,这些就是插头(本图共有5个插头)。
这些被经过的点的插头表示需要连接,没被经过则表示不必连接。
好的,现在我们已经知道了插头和轮廓线的基本定义,那么插头dp作为状压dp的一个分支,应该将什么标记进状态里呢?
不难想到应该状压轮廓线上的插头状态。
我们依然以上图为例,在上图中我们把需要连接定义为1,反之定义为0。那么轮廓线上的状态即为10111。
(友情提示,如果你是初学者的话,为了方便调试,建议将唯一一个横着的单独拎出来记成一维,具体见我后面放的代码)
但是如果单纯按照这种方式进行dp的话,不难发现可能出现最终连的是多个连通块的情况。
例如
我们现在的问题转化成了如何让避免这种状态出现。
可以很明显的感觉到在不改变状态的情况下这是极其困难的。
我们考虑改变状态:
注意到一个插头一定会与另外一个插头相连,且顺序固定,即不会出现两个插头在不消失的情况下换位置的情况。
我们可以用括号序来进行状压,若一个插头在与他相连的插头前面,则标记为1,否则标记为2,若没有插头则标为0
则
该图的轮廓线状态变为10212
至此状态部分彻底结束
转移
转移其实并不复杂,对于每个点,暴力分类其左边和上边的状态即可,这里举个例子:(配图太困难了,就挑了几个配)
如果当前枚举到的状态这个点上面和左边都是0,那么这个点一定会新建两个插头,将下插头标称1,右插头标称2
如果当前遇到的情况左0上不是0,这个点可以直接从上面向下连(下插头同上插头,右插头为0),或者在这个点拐个弯(右插头同上插头,下插头为0)
和
左非0上0同理
左1上1或左2上2,这两种情况都需要找到与其原来配对的插头并修改其权值。
例如
原来的轮廓线状态为01122,在连接完之后应将与上插头连接的改为1,即00012
左2上1,这种情况直接连即可
左1上2,这种情况回事一个连通块独立,不合法,只会在统计答案是用到
现在转移就都好了,但是如果这样写的话会TLE+MLE,这时候就要到第三步了
优化
优化运用的是hashtable优化dp,我们将状态的数字对一个质数取模,存在一个邻接表中,询问时直接遍历整个hashtable即可,听起来比较抽象,实际上非常好写,直接看代码就行了
#include<bits/stdc++.h>
#define int long long
#define inf 0x3f3f3f3f3f3f3f3f
#define N 15
#define M 600005
#define Mod 590027
using namespace std;
struct po{
int k,val;
};
struct Hash_Table{ //hashtable优化
int he[M],ne[M*5],cnt;
po v[M*5];
void init()
{
cnt=0;
for(int i=0;i<Mod;i++)
he[i]=0;
return ;
}
void insert(int k,int val)
{
int K=k%Mod;
for(int i=he[K];i;i=ne[i])
{
if(v[i].k==k)
{
v[i].val+=val;
return ;
}
}
++cnt;
ne[cnt]=he[K];
he[K]=cnt;
v[cnt]=po{k,val};
}
}g[2][3];//和文中所说一样,将横向插头拎出来单开一维
char s[15],num[15][15],Fl[15][15];
int Ans=0,P[15],T=0,n,m;
int getf(int k,int id)
{
return (k/P[id-1])%3;
}
void Solve(int x)
{
for(int i=1;i<=m;i++)
{
T^=1;
g[T][0].init();
g[T][1].init();
g[T][2].init();
if(num[x][i]==1)
{
for(int j=0;j<Mod;j++)
for(int J=g[T^1][0].he[j];J;J=g[T^1][0].ne[J])
{
po I=g[T^1][0].v[J];
if(getf(I.k,i)==0)
g[T][0].insert(I.k,I.val);
}
}
else //大力分讨
{
for(int j=0;j<Mod;j++)
{
for(int J=g[T^1][0].he[j];J;J=g[T^1][0].ne[J])
{
po I=g[T^1][0].v[J];
if(getf(I.k,i)==0)
{
g[T][2].insert(I.k+P[i-1],I.val);
}
if(getf(I.k,i)==1)
{
g[T][0].insert(I.k,I.val);
g[T][1].insert(I.k-P[i-1],I.val);
}
if(getf(I.k,i)==2)
{
g[T][0].insert(I.k,I.val);
g[T][2].insert(I.k-2*P[i-1],I.val);
}
}
for(int J=g[T^1][1].he[j];J;J=g[T^1][1].ne[J])
{
po I=g[T^1][1].v[J];
if(getf(I.k,i)==0)
{
g[T][1].insert(I.k,I.val);
g[T][0].insert(I.k+P[i-1],I.val);
}
if(getf(I.k,i)==1)
{
int Cnt=1,k=i;
while(Cnt!=0)//找到配对的另一个
{
k++;
if(getf(I.k,k)==1)
Cnt++;
else if(getf(I.k,k)==2)
Cnt--;
}
g[T][0].insert(I.k-P[i-1]-P[k-1],I.val);
}
if(getf(I.k,i)==2)//这里就是文中说的不合法的地方,只在统计答案是用到
{
if(Fl[x][i])//表示最后一个非障碍节点
if(I.k==2*P[i-1])
Ans+=I.val;
}
}
for(int J=g[T^1][2].he[j];J;J=g[T^1][2].ne[J])
{
po I=g[T^1][2].v[J];
if(getf(I.k,i)==0)
{
g[T][2].insert(I.k,I.val);
g[T][0].insert(I.k+2*P[i-1],I.val);
}
if(getf(I.k,i)==1)
{
g[T][0].insert(I.k-P[i-1],I.val);
}
if(getf(I.k,i)==2)
{
int Cnt=1,k=i;
while(Cnt!=0)
{
k--;
if(getf(I.k,k)==1)
Cnt--;
else if(getf(I.k,k)==2)
Cnt++;
}
g[T][0].insert(I.k-2*P[i-1]+P[k-1],I.val);
}
}
}
}
}
return ;
}
void solve()
{
g[0][0].insert(0,1);
for(int i=1;i<=n;i++)
{
Solve(i);
g[T][1].init();
g[T][2].init();
}
return ;
}
signed main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);
for(int j=1;j<=m;j++)
num[i][j]=s[j]=='*';
}
int x=n,y=m;
while(1) //寻找最后一个非障碍节点
{
if(num[x][y]==1)
{
y--;
if(y==0)
{
x--;
y=m;
}
}
else
{
Fl[x][y]=1;
break;
}
}
P[0]=1;
for(int i=1;i<=m;i++)
P[i]=P[i-1]*3;
solve();
printf("%lld ",Ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架