好学易懂 从零开始的插头DP(二)

2023-11-07 01:00

本文主要是介绍好学易懂 从零开始的插头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(二)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/360190

相关文章

上海邀请赛 A题目 HDU 5236(dp)

先求出没有ctrl+s的时候构造长度为i的期望f[i] 。然后枚举保存的次数,求出最小即可。 #include<cstdio>#include<cstdio>#include<cmath>#include<queue>#include<stack>#include<string>#include<cstring>#include<iostream>#include<map>

poj 3160 Father Christmas flymouse 强连通+dp

首先我们可以确定的是,对于val值小于0的节点都变成0.   假设一个集合内2个房间都能任意到达,那么我就可以吧集合内的所有点的价值都取到,并且可以达到任一点。实际上集合内的每个点是相同的,这样的集合就是一个强连通分量。 那么我们就可以用tarjin算法进行强连通缩点, 最后形成一个dag的图。在dag的图上面进行dp。可以先用拓扑排序后dp。或者建反响边记忆化搜索 。 VIEW

秋招突击——6/22——复习{区间DP——加分二叉树,背包问题——买书}——新作{移除元素、实现strStr()}

文章目录 引言复习区间DP——加分二叉树个人实现 背包问题——买书个人实现参考实现 新作移除元素个人实现参考思路 找出字符串中第一个匹配项的下标个人实现参考实现 总结 引言 今天做了一个噩梦,然后流了一身汗,然后没起来,九点多才起床背书。十点钟才开始把昨天那道题题目过一遍,然后十一点才开始复习题目,为了不耽误下午的时间,所以这里的就单纯做已经做过的题目,主打一个有量,不在学

Android从零开始搭建MVVM架构(5)—— LifeCycle详解

1.Lifecycle简介 为什么要使用lifecycle? activity 和fragment 是有声明周期的,有时候,我们的很多操作需要写在声明周期的方法中,比如,下载,文件操作等,这样很多情况下回导致,我们在activity中的声明周期方法中写越来越多的代码,activity或者fragment 越来越臃肿,代码维护越来越困难。 使用lifecycle就可以很好的解决这类问题。 lifec

Android从零开始搭建MVVM架构(4)——LiveData

LiveData 介绍 Livedata 是 Google 推荐的 Android 架构组件之一,是一个存放可被观察的数据持有类,有生命周期感知功能,解决了android开发者需要去手动处理生命周期的痛点。 比如当我们使用 Retrofit+Rxjava处理接口回调数据时,需要考虑activity 或 fragment 生命周期,以解决 onStop 或 onDestory之后回调数据的问题。现

Android从零开始搭建MVVM架构(3)——ViewModel

ViewModel类是被设计用来以可感知生命周期的方式存储和管理 UI 相关数据,ViewModel中数据会一直存活即使 activity configuration发生变化。 ViewModel有什么优势? 1.数据持久化 activity 在销毁重建时,之前我们可以用 activity 的onSaveInstanceState()机制保存和恢复数据,但缺点很明显,onSaveInstan

React+TS 从零开始教程(2):简中简 HelloWolrd

源码链接:https://pan.quark.cn/s/c6fbc31dcb02 这一节,我们来见识React+TS的威力,开始上手开发第一个组件,什么组件呢? 当然是简中简的 HelloWolrd组件啦。 在src下创建一个components,然后新建Hello.tsx 为什么是tsx呢,这个目的就是告诉编译器,我这个文件是支持jsx语法的,如果遇到你看不懂的标签,就当作React Ele

从零开始学数据结构系列之第三章《平衡二叉树基础概念》

文章目录 前言什么是平衡二叉树往期回顾 前言 ​   在前面的学习过程中,我们了解到二叉排序树可以在一定程度上提高查找(搜索)的效率,但仍然会出现特殊情况,让二叉排序树失效。例如,将序列{1,2,3,4,5,6}中的元素依次插入到二叉排序树中,会得到右斜树,这就相当于一个单链表了,搜索效率降低为O(n)。   于是在 1962 年,一个姓 AV 的大佬(G. M. Ade

动态规划DP--斐波那契数、爬楼梯、使用最小花费爬楼梯等示例代码

动态规划DP 文章目录 动态规划DP509. 斐波那契数70. 爬楼梯746. 使用最小花费爬楼梯62. 不同路径63. 不同路径II343.整数拆分 509. 斐波那契数 509. 斐波那契数 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) =

从零开始搭建一个酷炫的个人博客

效果图 一、搭建网站 git和hexo准备 注册GitHub本地安装Git绑定GitHub并提交文件安装npm和hexo,并绑定github上的仓库注意:上述教程都是Windows系统,Mac系统会更简单! 域名准备 购买域名,买的是腾讯云域名,购买完成之后的域名管理解析域名域名备案 二、优化网站 使用的Fluid主题,Hexo Fluid 用户手册 增加图床,图片可以放在g