本文主要是介绍好学易懂 从零开始的插头DP(二),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
好学易懂 从零开始的插头DP(二)
前情提要
上篇文章里,我们解决了一个例题,了解了,从回路模型转换到插头模型的一些性质和优点。知道了只要按规则放置插头,就可以保证都是闭合回路。现在,让我们对例题做一些改变,之前是可以多个闭合回路,如果现在要求只能有一条闭合回路呢?
备注:本文引用了一些大佬论文的图片和文字。
例题的变化
洛谷P5056 【模板】插头DP
给出 n×m 的方格,有些格子不能铺线,其它格子必须铺,形成一个闭合回路。问有多少种铺法? ( 1 < = n , m < = 12 ) (1<=n,m<=12) (1<=n,m<=12)
那么,在上一题的基础上,我们怎么保证只有一条回路呢?或者说我们怎么知道这是哪一个回路呢?
从下意识地反应开始——最小表示法
每一个回路都是一个连通块,很容易联想到并查集。我们给每个回路编号,应该就可以知道是属于哪个回路的了。
让我们回到上一篇文章的这个图,当时,我们记录了每个插头的有无,现在我们还要额外记录这些插头属于哪个回路,记录每个插头的一个编号。
稍加思考可以发现,对于一个轮廓线(图中红色的线),它上面一定有偶数个插头。因为是回路,要一去一回。那m+1个插头,至多属于(m+1)/2个回路,那么用一个(m+3)/2进制数可以状态压缩表示。
我们用dp[i][j][S][mask]表示一个状态,其中(i,j)为格子坐标,S为插头的有无。新增加的mask记录每个插头属于哪个回路。很容易发现,可以把S合并到mask里面去,只要这一位是0就是不存在插头,否则就存在。
当然我们知道,编号需要制定一个规则,使得编号唯一。我们不妨设从上到下,从左到右,第一个必须走的格点所在的回路为1号回路,第二个必须走且没被标记的为2号回路,以此类推。当两个回路合并的时候,下一个新的回路选择最小的未被使用的数字标记。
这就是最小表示法。
进一步的优化——括号匹配法
当然,我们发现,上面的下意识地反应,虽然也有可操作性,但是太麻烦了。毕竟这个mask确实有点大,而且我们最后明明只有一个回路,很多标记最后是浪费的。让我们重新审视轮廓线上的插头,希望能发现些有用的性质。
1:之前,我们发现,对于一个轮廓线,它上面一定有偶数个插头。因为是回路,要一去一回。更准确的说,对于一个回路,有且仅有两个插头,否则他们目前一定还是两个不同的回路。
“仔细观察上面的图,可以发现轮廓线上方是由若干条互不相交的路径构成的,而每条路径的两个端口恰好对应了轮廓线上的两个插头! 一条路径上的所有格子对应的是一个连通块,而每条路径的两个端口对应的两个插头是连通的而且不与其他任何一个插头连通.”
2:轮廓线上从左到右4个插头a, b, c, d,如果a, c连通,并且与b不连通,那么b, d一定不连通.
“证明:反证法,如果a, c连通,b, d连通,那么轮廓线上方一定至少存在一条a到c的路径和一条b到d的路径.如图,两条路径一定会有交点,不妨设两条路径相交于格子P,那么P既与a, c连通,又与b, d连通,可以推出a, c与b, d连通,矛盾,得证.”
观察上面两个性质,成对匹配,不会交叉,很自然就会联想到括号匹配。我们将一个回路的左侧的插头标记为左括号,右侧的插头标记为右括号,一种合法的插头情况,不就是一种合法的括号匹配嘛?
学过状态压缩的你一定可以轻松的表示出状态,这里我们用0表示没插头,1表示左括号,2表示右括号。从而用一个三进制数表示出了mask。当然,实际应用的时候,四进制会比较好,因为可以位运算。提取出每一位也会更快。
这就是括号匹配法。
如何实现——哈希
很明显,这题里,括号匹配法比最小表示法优秀一些。但是,对于这个数据范围来说,枚举所有状态也需要12124^13,达到了1e10的复杂度,显然超标了,怎么办呢?
我们知道mask是描述括号匹配的状态的,但是括号匹配合法数是一个卡特兰数,这里显然没有4^13这么多的有用状态,我们做一个哈希映射,仅保留有用的状态。(代码里的insert函数)模数挂表就行了,模数这里取得是299987(学大佬的)
状态转移
做过上一道题目,此时我们对于状态转移,应该驾轻就熟了吧。和上一个的区别是不再是有无插头,而是左括号右括号,变成了三进制的状态,建议先自己思考下。图片挡内容大法。
0:如果当前格点不能走,一个括号都不能有。
1:如果左侧和上方都没有括号。那么伸出去的两个插头分别标记为左括号和右括号,相互匹配。
2:如果是左括号+没括号,类似之前那个题目。把当前格子伸出去的那个插头标记为左括号。右括号+没括号类似。
3:如果是没括号+左括号,类似之前那个题目。把当前格子伸出去的那个插头标记为左括号。没括号+右括号类似。
4:如果左侧和上方都有括号那么,自然要把这两个括号去掉。但是与前一题不同:括号分左括号和右括号,这里还需要额外的分类讨论。
(a):都是左括号,那么后方最近的两个右括号,他们与这两个左括号匹配。为了保证删掉这两个左括号后依旧括号匹配,要把右方第一个右括号改成左括号。
(b):都是右括号,与(a)类似,前方最近的两个左括号,与这两个右括号匹配。为了保证删掉这两个右括号后依旧括号匹配,要把前方第一个左括号改成右括号。
(c):右括号加左括号,直接删去即可。因为前方第一个左括号,匹配这个右括号,后方第一个右括号,匹配这个左括号。现在这两对括号标记的回路合并了,直接删去能表示连通性且括号匹配。
(d):左括号加右括号,表示形成回路,因为只能有一条回路,所有只有在最后一个可走的点处可以合拢。
代码
大题思路已经出来了,剩下的就是实践了,这里提供一份AC代码。
代码如下:
#include<stdio.h>
#include<iostream>
#include<cstring>
using namespace std;
const long long hs=299987;
long long n,m,ex,ey,now,last,ans;
long long a[13][13],head[300000],next[2<<24],que[2][2<<24],val[2][2<<24],cnt[2],inc[13];
void init()
{scanf("%lld%lld",&n,&m);for (int i=1;i<=n;i++){for (int j=1;j<=m;j++){char ch=getchar();while (ch!='*'&&ch!='.') ch=getchar();if (ch!='.') a[i][j]=0;else {a[i][j]=1;ex=i;ey=j;}}}inc[0]=1;for(int i=1;i<=13;i++){inc[i]=inc[i-1]<<2;}
}
inline void insert(long long bit,long long num)
{long long u=bit%hs+1;for(int i=head[u];i;i=next[i]){if(que[now][i]==bit){val[now][i]+=num;return;}}next[++cnt[now]]=head[u];head[u]=cnt[now];que[now][cnt[now]]=bit;val[now][cnt[now]]=num;
}
void solve()
{cnt[now]=1; val[now][1]=1; que[now][1]=0;for(int i=1;i<=n;i++){for(int j=1;j<=cnt[now];j++){que[now][j]<<=2;}for(int j=1;j<=m;++j){memset(head,0,sizeof(head));last=now; now^=1;cnt[now]=0;for(int k=1;k<=cnt[last];++k){long long bit=que[last][k],num=val[last][k];long long b1=(bit>>((j-1)*2))%4,b2=(bit>>(j*2))%4;if(!a[i][j]){if(!b1&&!b2) insert(bit,num);}else if(!b1&&!b2){if(a[i+1][j]&&a[i][j+1]) insert(bit+inc[j-1]+inc[j]*2,num);}else if(!b1&&b2){if(a[i][j+1]) insert(bit,num);if(a[i+1][j]) insert(bit-inc[j]*b2+inc[j-1]*b2,num);}else if(b1&&!b2){if(a[i+1][j]) insert(bit,num);if(a[i][j+1]) insert(bit-inc[j-1]*b1+inc[j]*b1,num);}else if(b1==1&&b2==1){int flag=1;for(int l=j+1;l<=m;++l){if((bit>>(l*2))%4==1) flag++;if((bit>>(l*2))%4==2) flag--;if(!flag){insert(bit-inc[j]-inc[j-1]-inc[l],num);break;}}}else if(b1==2&&b2==2){int flag=1;for(int l=j-2;l>=0;--l){if((bit>>(l*2))%4==1) flag--;if((bit>>(l*2))%4==2) flag++;if(!flag){insert(bit-inc[j]*2-inc[j-1]*2+inc[l],num);break;}}}else if(b1==2&&b2==1) insert(bit-inc[j-1]*2-inc[j],num);else if(i==ex&&j==ey) ans+=num;}}}
}
int main()
{init();solve();printf("%lld\n",ans);return 0;
}
パソコンの前のこの努力しているかっこいい姿は誰ですか。そう 私 です
这篇关于好学易懂 从零开始的插头DP(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!