插头DP记录

news/2025/1/15 19:57:43/文章来源:https://www.cnblogs.com/Claimo0611/p/18673649

AAA黑题批发。

这个东西好像设问还挺广泛的,做到哪写到哪吧。

得先了解一下轮廓线dp定义。

概念

设问广泛但是总体来说是连通性相关状压dp的一类设计方法。

骨牌覆盖问题

比如说,最简单的,问你 \(n*m\) 的棋盘格里能放多少 \(1*2\) 的骨牌。

考虑把一个节点分为上下左右四个插头,从上往下,从左往右地逐格dp方案。这些插头的作用是:两个插头可以拼到一起形成一个骨牌,进而终态方案不能存在落单的插头(要不然就是 1*1 的骨牌碎片了hh)。

然后dp的过程是这样的。(来自luogu题解)

这些下箭头和那个右箭头从左到右编号为 \(1...m+1\),蓝格子是当前考虑放置的格子 \((i,j)\),关乎这个格子的两个插头分别是格子旁边的右插头 \(j\) 和格子上面的下插头 \(j+1\),这张图展示的是插头的编号,实际转移中只有穿过当前轮廓线的插头才是切实存在的。

考虑状态设计 \(dp_{i,j,S}\) 表示到格子 \((i,j)\) ,当前轮廓线上的插头存在状态 \(S\),因为当前每个插头只有存在 1 和不存在 0 所以用二进制数存储(以后会有更多进制)。

上面说到关乎当前格子的两个插头在完成当前格子的转移后格子会右移,此时右插头的编号就会变。

可以看到先前位于 \(j-1\) 格的右插头 \(6\) 变成了下插头,位于 \(j\) 格的下插头 \(7\) 变成了新状态的右插头。转移的时候就要用到这种插头变化规律来直接用存好的进制数操作状态。

现在分讨如何转移。

显然对于本题来讲,右插头和下插头同时出现的情况是不合法的(相当于左侧和上侧的骨牌叠加了),只有出现单个或者一个不出现的情况。

先讨论一个都没出现的情况,因为格子肯定是要放满的所以这个格子一定是一个骨牌的起点,可以考虑放置一个右插头或者下插头,相当于 \(dp_{i,j,S}\rightarrow dp_{i,j,modify(S)}\)

如果只有右插头相当于这个格子是用来补全那个插头的,新的状态就要把原先状态的右插头扣掉。

如果只有上插头情况是一样的。

每一行转移完后右插头就会归零,此时上次转移的所有状态左移一位才是当前层对应的用于转移的状态,可以想一想。

这种放骨牌的问题普遍比较简单。

白金元首与莫斯科

这个题就是说去放骨牌,然后有障碍,而且没障碍的点可以不放。

先考虑说假如只有障碍这道题怎么做。还是和例子一样的分讨。

如果当前格子是一个障碍,则它不应该出现右插头或者上插头。此时状态 \(S\) 和新状态一致。

if(!a[i][j]){if(!p1&&!p2)Add(f[i][j][S],val);}

接下来讨论格子不为障碍的情况。

如果当前格子不存在右插头或下插头,则有:放置右插头或下插头,或不放置三种方案。不放置则状态不变。

如果要放置一个右插头,回顾上面给的图,放置之后进入下个状态时那个右插头编号应恰好为 \(j+1\),所以新状态 \(s=S+pw_j\)

如果要放置下插头则根据图,新状态中下插头编号为 \(j\)\(s=S+pw_{j-1}\)。进而得到代码。

				else if(!p1&&!p2){Add(f[i][j][S],val);if(a[i][j+1]){int s=S+pw[j];Add(f[i][j][s],val);}if(a[i+1][j]){int s=S+pw[j-1];Add(f[i][j][s],val);}}

如果现在只有上插头,则操作之后上插头消去,可以列表分析前后插头的存在情况(因为之后的题插头操作还挺复杂的)。

j-1 j
S 0 1
s 0 0

进而只需减去 \(pw_j\) 编号插头即可转移。

只有右插头的情况是同理的。

				else if(!p1&&p2){int s=S-pw[j];	Add(f[i][j][s],val);}else if(p1&&!p2){int s=S-pw[j-1];Add(f[i][j][s],val);}

那就有一个不要脑子的做法就是 \(O(nm)\) 枚举军队所在位置然后跑那么多次dp,那肯定是死了。

考虑优化这个过程,假设现在转移到了军队的格子,则答案应由轮廓线以上和轮廓线以下两部分答案 \(f_{s1},g_{s2}\) 组成,由计数原理对答案的贡献是乘积的形式。然而 \(s1,s2\) 能配对的充要条件是 \(s1\) 的每个轮廓处下插头与 \(s2\) 一一对应。

进而先正常dp出 \(f_{i,j,S}\),然后反向地 dp出一个对应于 \(f\) 状态的补集答案 \(g_{i,j,s}\),注意此时插头也应该是一一对应的,然后就会比较绕,得自己画图理解。

最终的对于一个 \(i,j\) 所有的合法状态 \(\{S\}\) 应当是扣去 \(j\)\(j-1\) 两位的全 1 序列的所有子集。放一下代码。

#include<bits/stdc++.h>
#define MAXN 19
#define N (1<<18)+5
#define ll long long
using namespace std;
const int mod=1e9+7;
int n,m,a[MAXN][MAXN];
int f[MAXN][MAXN][N],g[MAXN][MAXN][N];
int pw[MAXN];
inline void Add(int &x,int y){x=x+y>mod?x+y-mod:x+y;
}
int ans[MAXN][MAXN];
signed main(){pw[0]=1;for(int i=1;i<MAXN;i++)pw[i]=pw[i-1]<<1;scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[i][j]),a[i][j]^=1;int toT=(1<<(m+1))-1;f[0][m][0]=1;//正着dpfor(int i=1;i<=n;i++){for(int j=0;j<=toT;j++)f[i][0][j<<1]=f[i-1][m][j];for(int j=1;j<=m;j++){for(int S=0;S<=toT;S++){int p1=(S>>(j-1))&1,p2=(S>>(j))&1,val=f[i][j-1][S];if(!val)continue;if(!a[i][j]){if(!p1&&!p2)Add(f[i][j][S],val);}else if(!p1&&!p2){Add(f[i][j][S],val);if(a[i][j+1]){int s=S+pw[j];Add(f[i][j][s],val);}if(a[i+1][j]){int s=S+pw[j-1];Add(f[i][j][s],val);}}else if(!p1&&p2){int s=S-pw[j];	Add(f[i][j][s],val);}else if(p1&&!p2){int s=S-pw[j-1];Add(f[i][j][s],val);}}}}g[n+1][1][0]=1;//倒着dp,这块真挺绕的。
//因为转移方向完全相反并且转移状态又是基于正着的状态配对的,所以乍一看会觉得状态转移很怪,得画图仔细想。for(int i=n;i>=1;i--){for(int j=0;j<=toT;j++)Add(g[i][m+1][j>>1],g[i+1][1][j]);for(int j=m;j>=1;j--){for(int S=0;S<=toT;S++){int p1=(S>>(j-1))&1,p2=(S>>j)&1,val=g[i][j+1][S];if(!val)continue;if(!a[i][j]){if(!p1&&!p2)Add(g[i][j][S],val);}else if(!p1&&!p2){Add(g[i][j][S],val);if(a[i][j-1]){int s=S+pw[j-1];Add(g[i][j][s],val);}if(a[i-1][j]){int s=S+pw[j];Add(g[i][j][s],val);}}else if(!p1&&p2){int s=S-pw[j];Add(g[i][j][s],val);}else if(p1&&!p2){int s=S-pw[j-1];Add(g[i][j][s],val);//	printf("g %lld %lld %lld=%lld\n",i,j,s,g[i][j][s]);}}}}for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){if(!a[i][j])continue;int S=toT^pw[j-1]^pw[j];for(int s=S;s;s=(s-1)&S)Add(ans[i][j],(ll)f[i][j-1][s]*g[i][j+1][s]%mod);Add(ans[i][j],(ll)f[i][j-1][0]*g[i][j+1][0]%mod);}}for(int i=1;i<=n;i++){for(int j=1;j<=m;j++)printf("%d ",ans[i][j]);printf("\n");}return 0;
}

一条回路与多条回路问题

Eat the Trees

这块才开始正经插头dp,分讨就会开始复杂。

仍然讨论当前格相邻的右插头和上插头。

当前格为障碍时:只有右上插头都不存在才能转移

不为障碍时:

右上插头都不存在时:

由于本题要求全部被覆盖,则此时必须新建一个回路即连通分量,在状态上,相当于同时新建了两个插头。

				else if(!p1&&!p2){	int s=S+pw[j]+pw[j-1];dp[now][j][s]+=val;}

只存在右插头时:

这个时候相当于延续右插头的路径,且上插头不存在,所以可以选择向右或向下走。这个时候就要列表了:

想往右走,先前的右插头对应 \(j-1\),放置后右插头对应 \(j\) 且右插头所在格没有下插头。

j-1 j
S 1 0
s 0 1

也就是说当减去 \(j-1\) 插头并加上 \(j\) 插头才能由 \(S\) 变为 \(s\)

想往下走,先前的右插头对应 \(j-1\),放置后下插头对应 \(j-1\) 且右插头所在格没有右插头(什。不难发现此时状态是不变的。

另外,能走向那个方向的前提是那个点不是障碍点。

				else if(p1&&!p2){if(a[i+1][j]){int s=S;dp[now][j][s]+=val;}if(a[i][j+1]){int s=S-pw[j-1]+pw[j];dp[now][j][s]+=val;}}

只存在下插头的推导留给自己当习题活动大脑真的不是懒得码字吗,只放代码。

				else if(!p1&&p2){if(a[i+1][j]){int s=S-pw[j]+pw[j-1];dp[now][j][s]+=val;}if(a[i][j+1]){int s=S;dp[now][j][s]+=val;}}

两个插头同时存在。

这道题中这种情况时合法且必须要存在的,因为我们是在从上往下从左往右dp,所以这种情况等效于闭合了当前回路的一部分。闭合后两个插头全部抵消,减去即可。

				else if(p1&&p2){int s=S-pw[j]-pw[j-1];dp[now][j][s]+=val;}

没有找到其他的题...

一条回路

【模板】插头 DP

一条回路总要比多条回路简单的...吗?

实则不然,因为要保证最终只存在一个联通块,所以在上文分讨的基础上,两个插头都存在的时候就不能无脑合并了。板子放在这个位置是有原因的

引理:对于穿过轮廓线的从左到右的四个不一定连续的插头 \(a,b,c,d\),若 \(ac\) 联通则 \(bd\) 一定不联通。 要不然路径就重了。

既然如此,对于当前轮廓线上的若干插头,一个已确定的合法方案大概长成这样:

发现这样的关系很像一个括号序列,进而地,将已确定的插头分为左括号和右括号。dp的过程就是一个建立并最终完成合并括号序列的过程。

然后看括号序列如何维护出一条回路。

显然除了两个插头都存在的情况,其他情况的处理可以直接照搬上一题。

将插头的二进制表示改为四进制,012分别表示没有插头,左括号和右括号,现在讨论当前格子右插头和下插头 \(p1,p2\) 的状态。

\(p1=p2=1\)

相当于是两个左括号,此时可以把两个左括号拼接到一起(直接删),这之后内层括号序列的右括号会失效转而成为外层括号序列的左括号。找内层右括号可以用模拟栈暴力实现。找到之后进行一个删右加左的操作。

\(p1=p2=2\)

相当于两个右括号,合并然后向左找内层左括号同理。

\(p1=2,p2=1\)

这会相当于把两个括号序列拼起来了,然后捏掉中间的两个括号。

\(p1=1,p2=2\)

根据转移的特点,这样的操作一定是一次回路的闭合,但是肯定不能想闭合就闭合,毕竟点是要走遍的。所以找到转移中最后的合法点,当且仅当在该点时能对答案做出贡献。

关于实现

四进制对空间的消耗相当大,考虑用hashtable映射掉这些状态。具体看代码。

#include<bits/stdc++.h>
#define int long long
#define MAXN 15
#define N 300005
#define L(i) (pw[i])
#define R(i) (2*pw[i])
#define At(x,i) ((x)*pw[i])
int n,m;
char txt[MAXN];
int a[MAXN][MAXN],ex,ey;
int pw[MAXN];
int now,dp[2][N],sta[2][N],tot[2];
int ans;
struct Hash_Table{int h[N],nxt[N];const int mod=299989;inline void add(int x,int val){int u=x%mod+1;for(int i=h[u];i;i=nxt[i]){int v=sta[now][i];if(x==v){dp[now][i]+=val;return ;}}nxt[++tot[now]]=h[u],h[u]=tot[now];sta[now][tot[now]]=x,dp[now][tot[now]]=val;}inline void  reset(){memset(h,0,sizeof(h)),tot[now]=0;}
}H;
signed main(){pw[0]=1;for(int i=1;i<=12;i++)pw[i]=pw[i-1]<<2;scanf("%lld%lld",&n,&m);for(int i=1;i<=n;i++){scanf("%s",txt+1);for(int j=1;j<=m;j++){a[i][j]=txt[j]=='.';if(a[i][j])ex=i,ey=j;}}tot[now]=dp[now][1]=1,sta[now][1]=0;for(int i=1;i<=n;i++){for(int j=1;j<=tot[now];j++)sta[now][j]<<=2;for(int j=1;j<=m;j++){int lst=now;now^=1;H.reset();for(int k=1;k<=tot[lst];k++){int S=sta[lst][k];int p1=(S>>((j-1)*2))%4,p2=(S>>(j*2))%4,val=dp[lst][k];if(!a[i][j]){//有障碍 if(!p1&&!p2)H.add(S,val);}else if(!p1&&!p2){//新联通块 if(a[i+1][j]&&a[i][j+1]){int s=S+L(j-1)+R(j);H.add(s,val);}}else if(!p1&&p2){//下/右 if(a[i][j+1]){/*右,向右的插头是会换的原本在j-1是.,j是下,向右之后就是j处有右插头,和原先j处有上插头是一样表示的 */int s=S;H.add(s,val);}if(a[i+1][j]){//下的话j的右插头就没了,但是这一格子有下插头(对应到j-1位)。int s=S-At(p2,j)+At(p2,j-1);H.add(s,val);	}}else if(p1&&!p2){//转移方向是一致的 if(a[i][j+1]){//现在向右的话原本j有右插头现在没了,然后多了j-1(当前格)的下插头 int s=S+At(p1,j)-At(p1,j-1);H.add(s,val);}if(a[i+1][j]){//这时候右插头变成现在的下插头了,表示不变,往下走了右插头就没了,但是p2本身就不存在,所以仍不变。int s=S;H.add(s,val); }}else if(p1==1&&p2==1){/*两个左括号,半个回形,合并,合并完这俩左括号都没了。 但是这样的话p2的左括号对应的右括号就不是右括号而是左括号了,p1仍可以和p2或者别的配对所以还是右括号 */int cnt=1;for(int l=j+1;l<=m;l++){if((S>>(2*l))%4==1)++cnt;if((S>>(2*l))%4==2)--cnt;if(!cnt){int s=S-L(j-1)-L(j)-R(l)+L(l);H.add(s,val);break;}}}else if(p1==2&&p2==2){/*两个右括号,还是合并,然后p1左括号失效变成右括号。 */int cnt=-1;for(int l=j-2;l>=0;l--){if((S>>(2*l))%4==1)++cnt;if((S>>(2*l))%4==2)--cnt;if(!cnt){int s=S-R(j-1)-R(j)-L(l)+R(l);H.add(s,val);break;}} }else if(p1==2&&p2==1){/*左边右括号右边左括号就直接合并,两个括号序列捏成一个了。 */int s=S-R(j-1)-L(j);H.add(s,val);}else if(i==ex&&j==ey)ans+=val;}}}printf("%lld\n",ans);return 0;
} 

双倍经验

bzoj3125

考虑一下 -| 的路径怎么处理,其他的就是板子。

对于 - 相当于所有下插头的目标点既不能是障碍点也不能是 -,转移的时候只能考虑右插头的情况。对于 | 是类似的,板子改一改就能过。

神奇游乐园

插头dp也可以用来解决最优化问题。具体地,设 \(dp_{i,j,S}\) 表示到格点 \((i,j)\) 状态为 \(S\) 时的最优方案,注意只要是延伸插头,当前格点都是必选的(因为有插头相当于先前钦定了必须走...),除非是新建联通分量的时候可以不选。

正在施工。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/869761.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

03_LaTeX之文档元素

在知道了如何输入文字后,在本章了解一个结构化的文档所依赖的各种元素——章节、目录、列表、图表、交叉引用、脚注等等。目录03_\(\LaTeX{}\) 之文档元素章节和目录章节标题目录文档结构的划分标题页交叉引用脚注和边注特殊环境列表对齐环境引用环境摘要环境代码环境表格列格…

THREE.js学习笔记6——Geometries

这一小节学习THREE.js中的物理模型。 什么是geometry?(英文解释,翻译为中文就看不懂了,直接看英语吧)Composed of vertices (point coordinates in 3D spaces)and faces (triangles that join those vertices to create a surface) Can be used for meshes but also for par…

第三节 回归实战

数据处理超参:人为指定不能改变测试数据只有x没有标签y 训练数据拆分,82开,作训练集和验证集(验证模型好坏),模型训练不是一路上升的过程,训练几次验证一次,最好的模型save下来 one-hot独热编码 猪(1 0 0) 狗(0 1 0) 猫(0 0 1) def get_feature_importance(feature_data, label…

Windows git bash 文字显示/斜杠开头数字

前言全局说明Windows git bash 文字显示/斜杠开头数字一、说明 详细介绍:https://zhuanlan.zhihu.com/p/133706032二、问题三、解决方法 git config --global core.quotepath false免责声明:本号所涉及内容仅供安全研究与教学使用,如出现其他风险,后果自负。参考、来源: h…

DDR 带宽的计算与监控

DDR 带宽(Double Data Rate Bandwidth)是指 DDR 内存在一秒内可以传输的数据量,通常以 GB/s(Gigabytes per second) 为单位。它是衡量内存系统性能的重要指标,直接影响系统的数据吞吐能力。 1.如何计算 DDR 带宽 计算 DDR 理论带宽的公式为: DDR主频 * 位宽 = 理论带宽其…

1.15

尽力了,之前的粗心导致现在要改很多以前的坑,明天再继续

中考英语优秀范文-热点话题-传统文化-006 Welcome to Chinese Summer Camp 欢迎参加中国夏令营

1 写作要求 假定你是李华,你校今年暑假将为外国学生举办一场汉语夏令营活动(Chinese Summer Camp)。请你根据下面海报的内容,用英语给你的笔友David写一封电子邮件,介绍本次活动并邀请他参加。词数80左右。 Welcome to Chinese Summer Camp Time:July 18th—July 28th, 2…

机器人

本文来自博客园,作者:Traktorea,转载请注明原文链接:https://www.cnblogs.com/kdsmyhome/p/18673586

使用Nginx实现前端映射到公网IP后端内网不映射公网.250115

一、场景: 系统移动端需要映射到公网,但是后端地址不能映射出去 qbpm.xxxx.cn 系统解析内网IP qmbpm.xxxx.cn 移动端解析公网IP 二、思路: 移动端前端公网端口放出80 443端口 移动端后端映射到内网后端地址qbpm.xxxx.cn:8443 三、解决方法: vim nginx.confserver {listen 8…

Qml 中实现任意角为圆角的矩形

在 Qml 中,矩形(Rectangle)是最常用的元素之一。 然而,标准的矩形元素仅允许设置统一的圆角半径。 在实际开发中,我们经常需要更灵活的圆角设置,例如只对某些角进行圆角处理,或者设置不同角的圆角半径。 本文将介绍如何通过自定义 Qml 元素实现一个任意角可为圆角的矩形…

【附源码】JAVA在线投票系统源码+SpringBoot+VUE+前后端分离

学弟,学妹好,我是爱学习的学姐,今天带来一款优秀的项目:在线投票系统源码 。 本文介绍了系统功能与部署安装步骤,如果您有任何问题,也请联系学姐,偶现在是经验丰富的程序员! 一. 系统演示 系统测试截图系统视频演示https://githubs.xyz/show/340.mp4二. 系统概述【 系统…

Python Playwright学习笔记(一)

一、简介 1.1Playwright 是什么? 它是微软在 2020 年初开源的新一代自动化测试工具,其功能和 selenium 类似,都可以驱动浏览器进行各种自动化操作。 1.2、特点是什么支持当前所有的主流浏览器,包括 chrome、edge、firefox、safari; 支持跨平台多语言:支持Windows、Linux、…