插头dp。

插头dp解决网格问题

轮廓线

就是格子的边

每次维护一个格子周围的状态,使之能够不重不漏的表示所有格子的状态

吃树


 

题意:求若干回路覆盖所有格子的方案数

枚举每个格子,维护包围着一个格子的轮廓线

对于每一行,已经处理过的格子轮廓线是格子的底边,要处理的格子由一条向左和向下(左边和顶边)的轮廓线包含,其他未处理的格子的轮廓线就是格子的顶边

就是这样

二进制压位,用1表示这条轮廓线有连向此点的边,0表示没有

分类讨论一个格子左方和上方有没有边与这个格子连接进行转移:

若这个格子是障碍,左方和上方都不能有边连接

对于其他情况:

若左上方都无,那么此格子是一条轮廓线的开端,像右下方连边(本格子更新完后,这个格子左上方的轮廓线变到了右下方,编号是下边的等于原来左边的,右边的等于原来上边的)

若只有左边有或只有下边有,连上后对下边或右边连边

若左上方都有,那么此格为一条轮廓线结束格子,不能往其他方向连边

for(int S=0;S<(1<<b+1);S++){//i是行,j是列
	int l=((S>>j)&1),u=((S>>j+1)&1);
	int nS=S;
	if(nS&(1<<j)) nS^=(1<<j);
	if(nS&(1<<j+1)) nS^=(1<<j+1);
	if(!mp){
		if(!l&&!u) dp[nS]+=ty[S];
	}else{
		if(!l&&!u){
			if(i<a&&j<b-1) dp[nS|(1<<j)|(1<<j+1)]+=ty[S];
		} 
		if((!l&&u)||(l&&!u)){
			if(i<a) dp[nS|(1<<j)]+=ty[S];
			if(j<b-1) dp[nS|(1<<j+1)]+=ty[S];
		}
		if(l&&u) dp[nS]+=ty[S];
	}

这是转移,如果每一行结束后,需要让最左边的那条轮廓线转移到最右边,其他的轮廓线都右移一位

memcpy(ty,dp,sizeof(ty));
for(int S=0;S<(1<<b+1);S++) dp[S]=((S&1)?0:ty[S>>1]);//一开始右边轮廓线是0,最后左边轮廓线也是0

骨牌覆盖

用1*2的骨牌覆盖完一个棋盘的方案数

还是维护轮廓线,将起点终点分类讨论,每个起点可以向右或向下连边

    for(int i=1;i<=a;i++){
			memcpy(ty,dp,sizeof(ty));memset(dp,0,sizeof(dp));
			for(int S=0;S<(1<<b);S++) dp[S<<1]=ty[S];
			for(int j=0;j<b;j++){
				memcpy(ty,dp,sizeof(ty));memset(dp,0,sizeof(dp));
				for(int S=0;S<(1<<b+1);S++){
					int nS=S;
					if(nS&(1<<j)) nS^=(1<<j);
					if(nS&(1<<j+1)) nS^=(1<<j+1); 
					bool l=(S>>j)&1,u=(S>>j+1)&1;
					if(l&&u) continue;
					if((l&&!u)||(!l&&u)) dp[nS]+=ty[S];
					if(!l&&!u) dp[nS|(1<<j)]+=ty[S],dp[nS|(1<<j+1)]+=ty[S];
				}
			}
		}

 白金元首与莫斯科

与刚才的题一样,发现要多次处理把一个格子变成障碍的方案数

直接枚举暴力时间复杂度受不了

再反着做一遍轮廓线

对于一个有障碍的格子,上下左右都不能有边,其他位置的连接应该一一对应,就能够使两种状态的轮廓线拼接成合法的情况

求轮廓线的大同小异,给出拼接的方式

	for(int i=1;i<=a;i++){
		for(int j=1;j<=b;j++){
			ll ans=0;
			if(mp[i][j]){
				printf("0 ");continue;
			}
			for(int S=0;S<(1<<b+1);S++){
				if(!((S>>j-1)&3)) ans=(ans+1ll*dp[i][j-1][S]*f[i][j+1][S]%mod)%mod;
			}//dp是正着的,第j-1位是左边轮廓线第j位是上边,f的第j-1位是下边第j位是右边
			printf("%lld ",ans);
		}putchar('\n');

[ZJOI2009]多米诺骨牌

要求任何一行和一列间都有骨牌连接

直接把这个限制也压进去时间复杂度肯定受不了

计算出来一个w[x][y][xx][yy]数组,表示是在这个范围的矩形中随便放置骨牌的方案数

看起来要枚举这个矩形的四个端点,其实枚举三个即可

枚举x,y,yy然后从x做到底边,得到w[x][y][x~a][y]

void get_ans(int _x,int _y,int _yy){
	int x=a-_x+1,y=_yy-_y+1;
	memset(mp,0,sizeof(mp)); 
	for(int i=1;i<=x;i++){
		for(int j=1;j<=y;j++) mp[i][j]=tu[_x+i-1][_y+j-1];
	}//将矩形抠出来
	for(int i=0;i<(1<<y+1);i++) dp[i]=0;
	dp[0]=1;
	for(int i=1;i<=x;i++){
		if(i!=1){
			for(int S=(1<<y+1)-1;S>=0;S--) dp[S]=((S&1)?0:dp[S>>1]);
		}
		for(int j=1;j<=y;j++){
			for(int S=0;S<(1<<y+1);S++) ty[S]=dp[S],dp[S]=0;
			for(int S=0;S<(1<<y+1);S++){
				int l=get(S,j-1),u=get(S,j);
				int nS=S-l*(1<<j-1)-u*(1<<j);
				if(mp[i][j]=='x'){
					if(!l&&!u) dp[nS]=(dp[nS]+ty[S])%mod;
					continue;
				}
				if(!l&&!u){
					dp[nS|(1<<j-1)]=(dp[nS|(1<<j-1)]+ty[S])%mod;
					dp[nS|(1<<j)]=(dp[nS|(1<<j)]+ty[S])%mod;
					dp[nS]=(dp[nS]+ty[S])%mod;//可以不放
				}
				if((l&&!u)||(!l&&u)) dp[nS]=(dp[nS]+ty[S])%mod;
			}
		}
		w[_x][_y][_x+i-1][_yy]=dp[0];
	}
}

然后考虑容斥

枚举每行之间的分割情况,然后再计算在这种情况下有多少种情况

令h[k]表示在每一行这样分割的情况下分割到第k列的情况总数

h[k]=val(0,k,S)-\sum_{j=1}^{k-1} h[j-1]*val(j,k,S)

S表示每行分割的情况

这个式子表示所有情况减去强制有分割的情况

然后再直接对于每行的情况容斥即可

	for(int S=0;S<(1<<a-1);S++){
		memset(h,0,sizeof(h));h[0]=-1;
		for(int i=1;i<=b;i++){
			for(int j=0;j<i;j++) h[i]=(h[i]-1ll*h[j]*val(j+1,i,S)%mod)%mod;
		}
		if(popcnt[S]&1) ans=(ans-h[b]+mod)%mod;
		else ans=(ans+h[b])%mod;
	}

 插头dp

发现刚才的所有情况轮廓线的状态只需用01表示,但是加上一些限制后就会使状态多种多样,我认为下面的题才算是插头dp的开始

括号序列

【模板】插头dp

多条回路变成一条回路,会有哪些变化?

1.需要将两条相邻的边合并

2.只有在结束位置才能结束一条轮廓线

然后发现只用01表示不行,因为这题需要知道哪个插头与哪个插头是联通的

引入括号序列:

0表示没有,1表示左括号,2表示右括号

1.左括号与右括号成对出现

2.在网格图上,一对匹配的左右括号中间的括号一定是匹配上的(因为如果不匹配,两条路径一定交叉,就不联通了)

因为二进制运算快,所以用4进制表示3进制

unorderer_map实测这道题会超时(可能我常数大),所以手写哈希表

推荐一种和机房大佬讨论出来的(其实完全是他看着别人的哈希表魔改的),实测开完O2非常快

struct Hash_table{
	vector<pair<int,ll> >to[mod+10];
	void init(){
		for(int i=0;i<mod;i++) to[i].clear(); 
	}
	ll &operator [] (const int &S){
		int id=S%mod;
		for(int i=0;i<to[id].size();i++){
			if(to[id][i].first==S) return to[id][i].second;
		}
		to[id].push_back({S,0});
		return to[id].back().second;
	}
}; 

也可以在记录一个数组表示状态,这样就可以遍历哈希表 

回到括号序列分类讨论

1.没有插头:若无障碍必须向下方一个左括号,向右一个右括号

2.上方和左方只有一个方向有插头:再往下方或右方推即可

3.上方和左方都是左括号:合并两个括号,找到对应的右括号,并将较靠左的改为左括号

4.上方和左方都是右括号:合并两个括号,找到对应的左括号,并将较靠左的改为右括号

5.左方右括号,右方左括号:直接合并两个括号

------3,4,5是把两条线段合并成一条线段

6.左方左括号,右方右括号:这样就是一条线段首尾相接,只能在结束位置出现,并且其他位置不能有任何括号

    dp[0]=1;
	for(int i=1;i<=a;i++){
		ty=dp,dp.init();
		for(int j=0;j<s.size();j++){
			int S=s[j];
			if(S>=(1<<b*2)) break;
			dp[S<<2]=ty[S];
		}
		for(int j=0;j<b;j++){
			ty=dp,dp.init();
			for(int h=0;h<s.size();h++){
				int S=s[h];
				int l=get(S,j),u=get(S,j+1);
				int nS=S;
				if(mp[i][j]=='*'){
					if(!l&&!u) dp[nS]+=ty[S];
					continue;
				}
				nS-=get(nS,j)*(1<<j*2);
				nS-=get(nS,j+1)*(1<<(j+1)*2);
				if(!l&&!u) dp[nS|(1<<j*2)|(2<<(j+1)*2)]+=ty[S];
				if(!l&&u){
					dp[nS|(u<<j*2)]+=ty[S],dp[nS|(u<<(j+1)*2)]+=ty[S];
				}
				if(l&&!u){
					dp[nS|(l<<j*2)]+=ty[S],dp[nS|(l<<(j+1)*2)]+=ty[S];
				}
				if(l==1&&u==1){
					int h=found(nS,j+1,2);
					dp[nS-(1<<h*2)]+=ty[S];
				}
				if(l==2&&u==2){
					int h=found(nS,j,1);
					dp[nS+(1<<h*2)]+=ty[S];
				}
				if(l==1&&u==2){
					if(i==xx&&j==yy&&!nS) ans+=ty[S];
				}
				if(l==2&&u==1){
					dp[nS]+=ty[S];
				}
			}
		}
	}

 [HNOI2004]邮递员

首先这题错题,保证长宽都是偶数,因为奇数的话可能要走两点的对角线(除了1 1的hack数据)

然后就和上面那题一样了,最后答案*2(有向图)

[HNOI2007]神奇游乐园

任意一条回路,所以终点就没限制了

注意求最大值,哈希表初始值改一下

最小表示法

[JLOI2009]神秘的生物

括号表示法要求整个图所有点都要选,因为不选和消掉都是0无法判断

就要用最小表示法了

最小表示法大意是将每一种相连的连通块染上相同的颜色,让出来的颜色序列字典序最小

需要这样几个操作

int get(int x,int i){
	return (x>>i*3)&7;
}
int cnt(int x,int i){//有几个颜色是i的
	int ans=0;
	for(int j=0;j<=a;j++) ans+=(((x>>j*3)&7)==i);
	return ans; 
}
int change(int S,int x,int y){//把x改成y颜色
	for(int i=0;i<=a;i++){
		if(get(S,i)==x) S=S-x*(1<<i*3)+y*(1<<i*3);
	}
	return S;
}
int relable(int S){//将S序列重构位最小表示
	short mp[10];
	memset(mp,0,sizeof(mp));
	int cnt=0;
	for(int i=0;i<=a;i++){
		int now=(S>>i*3)&7;
		if(!now) continue;
		if(!mp[now]) mp[now]=++cnt;
		S=S-now*(1<<i*3)+mp[now]*(1<<i*3);
	}
	return S;
}

这道题维护的轮廓线是周围的一圈格子,分类讨论即可

唯一的变动在于怎么更新状态

if(!l&&!u){
	dp[S]=max(dp[S],ty[S]);
	int to=relable(S|(7<<j*3)); //这个格子新建一个连通块
	dp[to]=max(dp[to],ty[S]+w[i][j]);
} 
if(l&&!u)  dp[S]=max(dp[S],ty[S]),dp[S|(l<<j*3)]=max(dp[S|(l<<j*3)],ty[S]+w[i][j]);
if(!l&&u){
	if(cnt(S,u)>1) dp[S-u*(1<<j*3)]=max(dp[S-u*(1<<j*3)],ty[S]);
	dp[S]=max(dp[S],ty[S]+w[i][j]);
}//这个格子合并到以前有的连通块
if(l&&u){
	int to;
	if(cnt(S,u)>1) dp[S-u*(1<<j*3)]=max(dp[S-u*(1<<j*3)],ty[S]);
	if(u<l) to=change(S,l,u);
	else to=change(S,u,l);
	dp[to]=max(dp[to],ty[S]+w[i][j]);//将两个连通块相连
}

[WC2008]游览计划

区别在于输出状态

记录每个格子的状态由上一个格子什么状态以及这一个格子什么颜色转移而来,倒推即可

自由发挥

刚才的是判断联通的两种套路方法,有些插头dp的题不需要联通这种条件,就需要自由发挥了

一般这种自由发挥的题目特点是看起来数据范围十分错误,,但是经过组合等计算后发现合法状态寥寥无几

[SCOI2011]地板

0代表没有,1代表插头还需要转弯,2表示不需要转弯

每块地板有四种长相(枚举顶点的方向),想清楚后分类讨论

	dp[0]=1;
	for(int i=1;i<=a;i++){
		ty=dp,dp.init();
		for(int h=1;h<=ty.size;h++){
			int S=ty.s[h];
			if(!((S>>b*2)&3)) dp[S<<2]=ty[S];
		}
		for(int j=1;j<=b;j++){ 
			ty=dp;dp.init();
			for(int h=1;h<=ty.size;h++){
				int S=ty.s[h];
				int l=get(S,j-1),u=get(S,j);
				int nS=S-l*(1<<(j-1)*2)-u*(1<<j*2); 
				if(mp[i][j]=='*'){
					if(!l&&!u) dp[nS]=(dp[nS]+ty[S])%mod;
					continue;
				}
				if(!l&&!u){
					dp[nS|(1<<(j-1)*2)]=(dp[nS|(1<<(j-1)*2)]+ty[S])%mod;
					dp[nS|(1<<j*2)]=(dp[nS|(1<<j*2)]+ty[S])%mod;
					dp[nS|(2<<(j-1)*2)|(2<<j*2)]=(dp[nS|(2<<(j-1)*2)|(2<<j*2)]+ty[S])%mod;//顶点靠左上
				}
				if(!l&&u==1){
					dp[nS|(1<<(j-1)*2)]=(dp[nS|(1<<(j-1)*2)]+ty[S])%mod;
					dp[nS|(2<<j*2)]=(dp[nS|(2<<j*2)]+ty[S])%mod;
				}
				if(!l&&u==2){
					dp[nS]=(dp[nS]+ty[S])%mod;//顶点靠左下
					dp[nS|(2<<(j-1)*2)]=(dp[nS|(2<<(j-1)*2)]+ty[S])%mod;
				}
				if(l==1&&!u){
					dp[nS|(2<<(j-1)*2)]=(dp[nS|(2<<(j-1)*2)]+ty[S])%mod;
					dp[nS|(1<<j*2)]=(dp[nS|(1<<j*2)]+ty[S])%mod;
				}
				if(l==1&&u==1) dp[nS]=(dp[nS]+ty[S])%mod;//顶点靠右下
				if(l==1&&u==2) continue;
				if(l==2&&!u){
					dp[nS]=(dp[nS]+ty[S])%mod;//顶点靠右上
					dp[nS|(2<<j*2)]=(dp[nS|(2<<j*2)]+ty[S])%mod;
				}
				if(l==2&&u==1) continue;
				if(l==2&&u==2) continue;
			}
		} 
	}

[CQOI2015]标识设计

再加一位表示个数,注意L只有一种长相,直接做

[NOI2010] 旅行路线

毕业题

将走的路线标号

发现除了1与a*b外,其他都与+1和-1相邻

令0为没插头,1为+1的插头,2为-1的插头

需要开两维,第一位表示插头情况,第二维是数字的情况

dp[0][0]=1;
	for(int i=1;i<=a;i++){
		for(ll S=(1<<(b+1)*2)-1;S>=0;S--){
			if(ok(S)) dp[S]=(get_to(S,0)?init:dp[S>>2]);
		} 
		for(int j=1;j<=b;j++){
			for(ll S=0;S<(1<<(b+1)*2);S++) ty[S]=dp[S],dp[S].init();
			for(ll to_S=0;to_S<(1<<(b+1)*2);to_S++){
				int to_l=get_to(to_S,j-1),to_u=get_to(to_S,j);
				
				if(!ok(to_S)) continue;
				if((to_l==1&&to_u==1)||(to_l==2&&to_u==2)) continue; 
				int n_to=to_S-(to_l<<(j-1)*2)-(to_u<<j*2);
			 	for(int h=0;h<ty[to_S].size;h++){
			 		ll S=ty[to_S].s[h];
					int w=ty[to_S][S];
			 		ll num_l=get_num(S,j-1),num_u=get_num(S,j);
			 		ll nS=S-(num_u<<j*8ll);
			 		if(!to_l&&!to_u){
			 			for(ll num=2;num<a*b;num++){
			 				if(seq[num]!=mp[i][j]) continue;
			 				(dp[n_to|(1<<(j-1)*2)|(2<<j*2)][nS|(num<<j*8ll)]+=w)%=mod;
			 				(dp[n_to|(2<<(j-1)*2)|(1<<j*2)][nS|(num<<j*8ll)]+=w)%=mod;
						}
						if(i==1||i==a||j==1||j==b){
							if(seq[1]==mp[i][j]&&(i==1||i==a||j==1||j==b)) (dp[n_to|(1<<(j-1)*2)][nS|(1ll<<j*8ll)]+=w)%=mod,(dp[n_to|(1<<j*2)][nS|(1ll<<j*8ll)]+=w)%=mod;	
						}
						if(seq[a*b]==mp[i][j]) (dp[n_to|(2<<(j-1)*2)][nS|(1ll*a*b<<j*8ll)]+=w)%=mod,(dp[n_to|(2<<j*2)][nS|(1ll*a*b<<j*8ll)]+=w)%=mod;
					}
					if(to_l&&!to_u){
						ll num=num_l+(to_l==1?1:-1);
                        if(num>a*b||num<=0) continue;
						if(num==1&&(i!=1&&i!=a&&j!=1&&j!=b)) continue;
						if(seq[num]!=mp[i][j]) continue;
						if(num==1||num==a*b) (dp[n_to][nS|(num<<j*8ll)]+=w)%=mod;
						else (dp[n_to|(to_l<<(j-1)*2)][nS|(num<<j*8ll)]+=w)%=mod,(dp[n_to|(to_l<<j*2)][nS|(num<<j*8ll)]+=w)%=mod;
						
					}
					if(!to_l&&to_u){
						ll num=num_u+(to_u==1?1:-1);
                        if(num>a*b||num<=0) continue;
						if(num==1&&(i!=1&&i!=a&&j!=1&&j!=b)) continue;
						if(seq[num]!=mp[i][j]) continue;
						if(num==1||num==a*b) (dp[n_to][nS|(num<<j*8ll)]+=w)%=mod;
						else (dp[n_to|(to_u<<(j-1)*2)][nS|(num<<j*8ll)]+=w)%=mod,(dp[n_to|(to_u<<j*2)][nS|(num<<j*8ll)]+=w)%=mod;
						
					}
					if(to_l&&to_u){
						ll num1=num_l+(to_l==1?1:-1),num2=num_u+(to_u==1?1:-1);
                        if(num1<=1||num1>=a*b||num2<=1||num2>=a*b) continue;
						if(to_u!=to_l&&num1==num2){
							if(seq[num1]!=mp[i][j]) continue;
							(dp[n_to][nS]+=w)%=mod;
						} 
					}
				}
			}
		}
	}
	ll ans=0;
	for(int i=0;i<dp[0].size;i++){
		ll S=dp[0].s[i];
		ans=(ans+dp[0][S])%mod;
	}

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值