【插头DP】【学习笔记】
【插头DP】【学习笔记】
Tips:
虽然插头Dp模板是黑的,但其实算法并不难理解,用到的只是轮廓线dp+哈希表而已,比较复杂的是讨论多种情况的转移和位运算,但封装几个函数以后,代码也十分简单了。
模板
Solution
- 首先考虑状压dp,考虑需要哪些状态,如果仅仅知道每个格子是否有向下伸出的插头是不够的,例如
同样是向下伸出4个插头,前者是合法的,后者是非法的(只允许有一个闭合回路)
因此,必须要知道插头之间的配对情况。一般有最小表示法和括号表示法。
最小表示法就是从左到右按照顺序给左边的插头标号为1,2,···,n,并给对应的右插头标成同样的数,对于无插头标为0
括号表示法是有限制条件的,本题中的插头情况一定在任意一个时刻满足,插头两两配对,且不存在交叉,因此可以用括号表示法用一个3进制数表示,其中0表示无插头,1表示左插头,2表示右插头。为了方便利用位运算,通常把它当成4进制计算 - 如果按行转移的话,若直接枚举相邻的两行判断能否转移,时间是不够的,但如果枚举一行计算它能转移到哪些行又太复杂。考虑轮廓线dp
- 设f[i][j][s]表示枚举到第i行,第j列,轮廓线状态为s,且满足之前的格子全部合法的方案数(合法指每个需要填的格子,一定填,且度为2,不需要填的一定不填,除最后一个能填的格子外,不存在已经封口的闭合回路)
- 分类讨论(这个太复杂需要图来说明,就看别人博客吧)
- 优化:首先前两维可以滚动,而且发现每一层合法状态很少,所以考虑刷表法,只转移合法的状态,可以用个数据结构将每一层合法的状态存起来,每次遍历这一层所有的合法状态,同时推出下一层所有的合法状态,计算出对应的方案数。因为可能有的状态从好几个状态转移而来,所以用大小与状态数接近的哈希表来维护,复杂度期望是线性的。注意到f数组可以省略掉,直接用hash表进行转移即可。当然这个hash也是滚动的,要开两层。
代码细节
- 哈希表对N取模后,值有可能为0,为了方便,hd数组下标从0开始,因此其初值要赋成-1,遍历时用~i来判断。
- 只有在最后一个合法格子才能封口统计答案,因此需要提前找到这个点的坐标ex,ey
- 每计算完一行,状态是:
但转移到下一层需要用的是:
显然这两个侧边始终都是0,原先是0······,现在是······0,相当于所有原先的状态在4进制下左移一格,每计算完一行,以此更新当前状态即可。 - 用两个变量表示上层状态lst,这层状态cur,写起代码更直观些。
- 封装mk(i,k)函数表示生成一个在第i为k,其余为0的4进制数,方便转移。
- 封装get(s,k)函数返回4进制数s的第k位,也是方便转移
- 封装update(cur,s,w)表示将第cur层的hash表中状态为s的方案数加w。
- 注意在找与某个插头对应的另一插头时,要用sum计数
- mp[i][j]用来表示第i行j列的格子是否要放,对于不在棋盘上的格子要默认为不放(即用1表示放,0表示不放)
Code
#include<bits/stdc++.h> using namespace std; #define int long long inline int read() { int x=0,w=1;char ch=getchar(); while((ch<'0'||ch>'9')&&ch!='-') ch=getchar(); if(ch=='-') {w=-1;ch=getchar();} while(ch>='0'&&ch<='9') {x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} return x*w; } inline void write(int x) { if(x<0) putchar('-'),x=~(x-1); if(x>9) write(x/10); putchar('0'+x%10); } const int N=50000; int n,m,mp[20][20]; int hd[2][N],cnt[2]; struct node{ int nxt,s,w; }e[2][N]; int ex,ey; char str[20]; void update(int cur,int s,int w) { int pos=s%N; for(int i=hd[cur][pos];~i;i=e[cur][i].nxt) { if(e[cur][i].s==s){ e[cur][i].w+=w;return; } } e[cur][++cnt[cur]].nxt=hd[cur][pos];e[cur][cnt[cur]].s=s;e[cur][cnt[cur]].w=w;hd[cur][pos]=cnt[cur]; } int get(int s,int pos) { return s>>pos+pos&3; } int mk(int pos,int k) { return k*(1<<pos+pos); } int res; void print(int x) { for(int i=0;i<=m;++i) write(x&3),x>>=2; } signed main() { n=read();m=read(); for(int i=1;i<=n;++i){ scanf("%s",str+1); for(int j=1;j<=m;++j) if(str[j]=='.') mp[i][j]=1,ex=i,ey=j; } memset(hd,-1,sizeof hd); int cur=0;update(cur,0,1); for(int i=1;i<=n;++i) { for(int j=1;j<=cnt[cur];++j) e[cur][j].s<<=2; for(int j=1;j<=m;++j) { int lst=cur;cur^=1;memset(hd[cur],-1,sizeof hd[cur]);cnt[cur]=0; for(int k=1;k<=cnt[lst];++k) { int s=e[lst][k].s,w=e[lst][k].w; int x=get(s,j-1),y=get(s,j);//cout<<i<<" "<<j<<" "<<x<<" "<<y<<" ";print(s);puts(""); if(mp[i][j]==0){ if(!x&&!y) update(cur,s,w); continue; } if(!x&&!y){ if(mp[i][j+1]&&mp[i+1][j]) update(cur,s+mk(j-1,1)+mk(j,2),w); continue; } if(!x&&y){ if(mp[i][j+1]) update(cur,s,w); if(mp[i+1][j]) update(cur,s-mk(j,y)+mk(j-1,y),w); continue; } if(!y&&x){ if(mp[i+1][j]) update(cur,s,w); if(mp[i][j+1]) update(cur,s-mk(j-1,x)+mk(j,x),w); continue; } if(x==1&&y==1){ for(int g=j+1,sum=1;g<=m;++g) { if(get(s,g)==2){ sum--;if(sum) continue; update(cur,s-mk(j-1,x)-mk(j,y)-mk(g,1),w);break; } if(get(s,g)) sum++; } continue; } if(x==2&&y==2){ for(int g=j-2,sum=1;g;--g){ if(get(s,g)==2) sum++; else if(get(s,g)){ sum--;if(sum) continue; update(cur,s-mk(j-1,x)-mk(j,y)+mk(g,1),w);break; } } continue; } if(x-1){ update(cur,s-mk(j-1,x)-mk(j,y),w);continue; } if(ex==i&&ey==j){ res+=w; } } } } write(res); return 0; } /* 3 3 ... .*. ... */
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效