我的坟头应该开满玫瑰吗?|

YYYmoon

园龄:1年粉丝:20关注:41

插头dp

(真的太佩服了……陈丹琦在高中期间不仅发明了cdq分治,还发明了插头dp。

前置

插头dp实际上是状压的一种,一般用于处理路径连通性问题

轮廓线:已决策状态和未决策状态的分界线。

插头:一个格子某个方向的插头存在,表示这个格子在这个方向与相邻格子相连。

多条回路

例题:P5074 Eat the Trees

轮廓线长度是 m+1,包含 m 个横向分界和1个纵向分界。我们只需要对这 m+1 条分界线维护它们上面有无插头即可。令 dpi,j,s 表示考虑完格子 (i,j) ,轮廓线状态为 s 的方案数。

转移的时候分类讨论,从当前格子左/上插头的存在情况推出右/下插头的存在情况。

考虑行间转移。结束状态会在最右侧边界,注意要保证它没有插头。删掉这个分界后加入下一行最开头的分界线即可。最后答案为 fn,m,0

有前人经验:插头dp=巨型分讨

一条回路

以下是真正的模板题:P5056 [模板] 插头 DP

此时我们必须要求“一条回路”,跟整体的连通性有关,那么仅仅记录轮廓线上是否存在插头已经不够用了。所以我们需要新方法来记录轮廓线状态转移。

1.记录状态

1.1 最小表示法:

所有的障碍格子记为0,第一个非障碍格子和他连通的所有格子记为1,再找第一个未标记的非障碍格子和与它联通的格子记为2,重复操作直至所有格子标记完毕。

形式化的,我们对每一个连通块编号,每一个连通块的标号就是当前已有连通块的个数+1,所有连通块内的点的编号都和该连通块编号相同

1.2 括号表示法:

【性质】轮廓线上从左到右四个插头 a,b,c,d ,若 a,c 连通且与 b 不连通,那么 b,d 一定不连通。(这个性质对所有棋盘模型的问题都适用

即,“两两匹配”,“不会交叉”,容易想到括号匹配。

将轮廓线上每一条路径穿过它处,左边的插头标记为左括号,右边的插头标记为右括号,则左右括号一定一一对应。

于是我们就可以用三进制,0表示无插头,1表示左括号插头,2表示右括号插头,记录下所有的轮廓线信息。

注意到,括号表示法只对路径/回路等问题适用;连通块问题只能用最小表示法。

对这两种表示方法,我们一般都把它的状态表示为 2x 进制数,这样我们就可以用位运算了!

且一般来说,我们的状态总数都会很大,但括号序列实际上并不会有太多合法状态,所以可以用哈希表来存储所有dp值。还得写滚动数组。

2.转移状态

x 为第 j 位上的状态,y 为第 j+1 位的状态

其实和多条路径的分讨比较类似,但是需要注意的是我们需要讨论 x,y 具体的值去转移。和上面同理的是,每个点上必然有一个入插头和一个出插头。

建议多画几张图理解。

  • (i,j) 不能铺线,则只转移无插头的情况

  • x=y=0,必须新建一对插头,p=1,q=2

  • x=0y=0,则只能有下或右插头,且这个插头只能作为上一状态的延伸,此时把 (j,j+1) 的状态分别向 (x,y)(y,x) 转移即可。

  • x0,y0,我们只能合并这两个插头,因为不能再有延伸了。但还是需要对 x,y 具体的值分讨。

    • x=1,y=2,这两个插头合并会形成回路。但对于一条回路的题目,只有在 (n,m) 处形成这种转移。

    • x=2,y=1,两个连通块被合并成了一个。

    • x=1,y=1,我们将两个左端点合并了,为了保证括号序列的合法性,那么原来 y 对应的右端点就会变成新连通块的左端点。我们需要找到这个点并修改。

    • x=2,y=2,同上。

(括号表示法)[模板] 插头dp
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e5+5,inf=2e9,p=9973;
int n,m,a[15][15],px,py;
struct hashtable{
	int h[p],nxt[maxn],tot,s[maxn],dp[maxn];
	void clear(){
		tot=0;
		for(int i=0;i<p;i++) h[i]=0;
	}
	void ins(int st,int val){
		int x=st%p;
		for(int i=h[x];i;i=nxt[i]){
			if(s[i]==st){
				dp[i]+=val;
				return ;
			}
		}
		nxt[++tot]=h[x],s[tot]=st,dp[tot]=val,h[x]=tot;
	}
}f[2];
int getw(int s,int i){
	return (s>>((i-1)<<1))&3;
}
int setw(int s,int i,int v){
	return s^((getw(s,i)^v)<<((i-1)<<1));
}
int getl(int s,int i){
	int res=0;
	for(int p=i;p>0;p--){
		int x=getw(s,p);
		if(x==2) res++;
		else if(x==1) res--;
		if(!res) return p;
	}
	return 0;
}
int getr(int s,int i){
	int res=0;
	for(int p=i;p<=m+1;p++){
		int x=getw(s,p);
		if(x==1) res++;
		else if(x==2) res--;
		if(!res) return p;
	}
	return 0;
}
signed main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++){
		string s; cin>>s;
		for(int j=1;j<=m;j++){
			a[i][j]=(s[j-1]=='.')?1:0;
			if(a[i][j]) px=i,py=j;
		}
	}
	int lst=0,now=1,ans=0;
	f[lst].ins(0,1);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			for(int k=1;k<=f[lst].tot;k++){
				int s=f[lst].s[k],v=f[lst].dp[k];
				int x=getw(s,j),y=getw(s,j+1);
				if(!a[i][j]){
					if(!x&&!y) f[now].ins(s,v);
				}
				else{
					if(!x&&!y) f[now].ins(setw(setw(s,j,1),j+1,2),v);
					else if(!x||!y){
						f[now].ins(s,v);
						f[now].ins(setw(setw(s,j,y),j+1,x),v);
					}
					else{
						int nxt=setw(setw(s,j,0),j+1,0);
						if(x==1&&y==2){
							if(i==px&&j==py&&!nxt) ans+=v;//最后一个格子,统计答案 
						}
						else if(x==2&&y==1) f[now].ins(nxt,v);
						else if(x==1&&y==1){
							nxt=setw(nxt,getr(s,j+1),1);
							f[now].ins(nxt,v);
						}
						else{
							nxt=setw(nxt,getl(s,j),2);
							f[now].ins(nxt,v);
						}
					}
				}
			}
			swap(lst,now);
			f[now].clear();
		}
		for(int k=1;k<=f[lst].tot;k++){
			int s=f[lst].s[k],v=f[lst].dp[k];
			if(!getw(s,m+1)) f[now].ins(s<<2,v);
		}
		swap(lst,now);
		f[now].clear();
	}
	printf("%lld",ans);
	return 0;
}

例题

跟板子差不多的题

  • [bzoj3125]CITY

  • [HNOI2004]邮递员

  • [HNOI2007]神奇游乐园

[SCOI2011]地板

其实这题跟回路的题也是类似的,只不过它算是“路径”

dp的题目重点都在于状态设计,把插头未转弯设为1,转过弯设为2,无插头设为0

转移分讨类似

「CQOI2015」标识设计

和上一题很像,只不过这题是固定方向的。

审题:放且仅放三个“L”……

我们不妨把当前已有的L的数量加入我们dp的转移式子,就可以统计了

额外需要注意的是,位运算会炸int,需要写成 1ll<<x 这样!!!

[BZOJ2310] ParkII

[HNOI2007]神奇游乐园 的升级版

区别:一条路径,每个点最多经过一次

考虑到对于路径,特殊的点在于它有起点和终点,即有两个格子是只经过一次的。同样我们可以单独用一个四进制数来表示这样格子的数量,加入dp的维度中,继续讨论。

写着写着你就会发现:完蛋了,和起点和终点相连的格子不能确定是左还是右括号!!只能新加一个状态3记录和起点终点相连的了。。

太难绷,调了两个晚上,发现是少考虑一种情况:对于中途给加起点/终点的,要把它所配的另一个括号改成独立插头!!!

[Wc2008]游览计划

这题就是一个要求连通块的题了,不能简单地用括号序列维护,这就是要用最小表示法了。

另外,它也是斯坦纳树的经典例题。斯坦纳树是这样一类问题:带边权无向图上有几个(一般是越10个)关键点,要求选择一些边使得这些点在同一个连通块内,同时要求所选的边的边权和最小。

最小生成树是在给定的点集和边中寻求最短网络使所有点连通。而最小斯坦纳树允许在给定点外增加额外的点,使生成的最短网络开销最小。 —— oi-wiki

斯坦纳树也是一种状压dp,但在这道题上比插头dp最小表示法简单了很多。(摆了,偷懒了,写斯坦纳树了。(逃

f[s][i][j] 表示选择关键点的集合为s,钦定根节点为(i,j)

斯坦纳树的dp转移有两个:

  1. 合并子树(枚举子集,在根节点合并) f[s][i][j]=min(f[s][i][j],f[t][i][j]+f[st][i][j]a[i][j])

  2. 找根(注意不是“换根”) f[s][i][j]=min(f[s][i][j],f[s][x][y]+dis((i,j),(x,y)))

Q: 为什么是合并子树而不能直接单点去加?

A:注意到我们转移时并没有全部记录路径,所以在我们转移时不能确定这个单点离整个连通块最近的距离。

那么为什么不能随意置换根的位置,从而找到离某个单点最近的距离?

在“找根”这一步,我们只是考虑了它的根还可以延伸到哪里;而当前最优方案,并不能把这个连通块内的所有点为根时的贡献都更新到。

如果还是不理解,手模一下可以发现:如果有一个节点,想从单点转移,度数大于2时,直接就寄了。。。

本文作者:YYYmoon

本文链接:https://www.cnblogs.com/YYYmoon/p/18705337

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   YYYmoon  阅读(18)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起