轮廓线dp小结

介绍

本质上是状压dp,所以是用在一类数据范围很小的题中,一般是求方案数之类的。

正题

这个东西大家有时候也管他叫插头dp,个人认为插头dp只是轮廓线dp的一种,只是把轮廓线上的状态设计成插头而已,事实上也可以不是插头,确实也有这样的题。

不过插头dp可能更广为人知,这里就以一道经典题作为例题:

给出一张网格图,每个位置是.**表示这个位置不可用,你要在可用的位置上设计一条哈密顿回路,经过所有可用点。链接

不知道这道题的话还是看一眼原题比较好qwq。

先说轮廓线是什么东西,说清楚也很简单,就是下面的红线部分:
在这里插入图片描述
其中,绿色的两段称为关键段,转移了一次后就变成了右图的样子。

不难发现,上面的部分是已经dp过的部分,而下面的部分是仍未dp的。每一段上如果有一条边从已处理部分延伸到未处理部分,则称这条边为插头,插头分两类:如果这条边是他所在路径的起点,那么插头类型为 1 1 1,如果是终点,则为 2 2 2。没有插头,就是 0 0 0

注意到由于这个哈密顿回路不可能有边相交,所以 1 , 2 1,2 1,2 之间是类似括号匹配的,即在轮廓线上,一个 1 1 1 往右找第一个没有 1 1 1 和他匹配的 2 2 2 一定是和自己匹配的,也就是和自己是同一条路径。

然后就可以开始转移了,设左边的绿色段的状态为 b 1 b_1 b1,上面的绿色短的状态为 b 2 b_2 b2

先上一张图,这样下面好讲,也许你看了就知道怎么转移了:
在这里插入图片描述
接下来就是分类讨论,顺序和上图对应:

b 1 = 0 , b 2 = 0 b_1=0,b_2=0 b1=0,b2=0 时,由于每个位置都要被覆盖,所以肯定要新建一条路径,变成 b 1 = 1 , b 2 = 2 b_1=1,b_2=2 b1=1,b2=2

b 1 , b 2 b_1,b_2 b1,b2 其中一个是 0 0 0 时,也就是有一条边伸到了这个格子里,那么可能从下面伸出去,也可能从右边伸出去,讨论一下即可。

b 1 = 2 , b 2 = 1 b_1=2,b_2=1 b1=2,b2=1 时,这两条路径就直接连起来。

b 1 = 1 , b 2 = 1 b_1=1,b_2=1 b1=1,b2=1 时,这两条路径也要连起来,但是后面就会有一组 2 , 2 2,2 2,2 的插头,需要将 b 2 b_2 b2 对应的路径结尾的插头类型改成 1 1 1,这个暴力 O ( n ) O(n) O(n) 往后找即可。

b 1 = 2 , b 2 = 2 b_1=2,b_2=2 b1=2,b2=2 时类似,将 b 1 b_1 b1 对应的路径起点的插头类型改成 2 2 2 即可。

最后一种情况, b 1 = 1 , b 2 = 2 b_1=1,b_2=2 b1=1,b2=2,这时这个哈密顿回路就封闭起来了,只能在最后下面最右边的格子连。

然后这状态三进制不好存,直接四进制即可,状态数很多是没用的,再用哈希压缩一下即可。

代码如下(磕代码的过程是少不了的qwq,不要放弃呀):

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define ll long long

int n,m,mp[20][20],ex,ey,bin[20];
int sta[2][300010],tot[2],cur=0;//滚动数组
int last[300010],ne[300010];ll f[2][300010],ans=0;
const int hash_val=299997;
void add(int x,ll val){
    
    
	int hv=x%hash_val;
	for(int i=last[hv];i;i=ne[i])
		if(sta[cur][i]==x){
    
    f[cur][i]+=val;return;}
	ne[++tot[cur]]=last[hv];last[hv]=tot[cur];
	sta[cur][tot[cur]]=x;f[cur][tot[cur]]=val;
}
void solve(){
    
    
	bin[0]=1;for(int i=1;i<=12;i++)bin[i]=bin[i-1]<<2;
	sta[0][tot[0]=1]=0;f[cur][1]=1;
	for(int i=1;i<=n;i++){
    
    
		for(int j=1;j<=tot[cur];j++)sta[cur][j]<<=2;//换到下一行时,状态要左移,玩一玩就懂了
		for(int j=1;j<=m;j++){
    
    
			cur^=1;memset(last,0,sizeof(last));tot[cur]=0;
			for(int p=1;p<=tot[cur^1];p++){
    
    
				int now=sta[cur^1][p],x=(now>>((j-1)<<1))%4,y=(now>>(j<<1))%4;
				ll &F=f[cur^1][p];
				if(!mp[i][j]){
    
    //如果这个位置不能放,那么就不能有插头指向这个格子,否则这个状态就不合法
					if(!x&&!y)add(now,F);
				}else if(!x&&!y){
    
    
					if(mp[i][j+1]&&mp[i+1][j])add(now+bin[j-1]+(bin[j]<<1),F);
				}else if(!x){
    
    
					if(mp[i][j+1])add(now,F);
					if(mp[i+1][j])add(now-bin[j]*y+bin[j-1]*y,F);
				}else if(!y){
    
    
					if(mp[i+1][j])add(now,F);
					if(mp[i][j+1])add(now-bin[j-1]*x+bin[j]*x,F);	
				}else if(x==2&&y==1){
    
    
					add(now-(bin[j-1]<<1)-bin[j],F);
				}else if(x==1&&y==1){
    
    
					int c=1;for(int k=j+1;k<=m;k++){
    
    
						if((now>>(k<<1))%4==1)c++;
						if((now>>(k<<1))%4==2)c--;
						if(!c){
    
    add(now-bin[j-1]-bin[j]-bin[k],F);break;}
					}
				}else if(x==2&&y==2){
    
    
					int c=1;for(int k=j-2;k>=0;k--){
    
    
						if((now>>(k<<1))%4==2)c++;
						if((now>>(k<<1))%4==1)c--;
						if(!c){
    
    add(now-(bin[j]<<1)-(bin[j-1]<<1)+bin[k],F);break;}
					}
				}else if(i==ex&&j==ey)ans+=F;
			}
		}
	}
}

int main()
{
    
    
	scanf("%d %d",&n,&m);
	static char s[20];
	for(int i=1;i<=n;i++){
    
    
		scanf("%s",s+1);
		for(int j=1;j<=m;j++){
    
    
			mp[i][j]=(s[j]=='.');
			if(s[j]=='.')ex=i,ey=j;
		}
	}
	solve();
	printf("%lld\n",ans);
}

然而还有一个简单一些的,区别在于可以有多个回路而不一定是一个。

这样就不需要记录插头的类型,只在乎插头的有无,这样只需要记录 0 , 1 0,1 0,1,二进制即可。

转移时, 00 → 11 , 10 → 01 , 10 , 01 → 01 , 10 , 11 → 00 00\to 11,10\to01,10,01\to01,10,11\to 00 0011,1001,10,0101,10,1100

膜一下神兔,就可以得到一个很简洁的代码,思想是这个思想,但可以巧妙利用位运算:

#include <cstdio>
#define maxn 15
#define FOR(i,a,b) for(int i=(a);i<=(b);i++)

int T,n,m,a[maxn][maxn];
long long f[2][1<<maxn];

int main()
{
    
    
	scanf("%d",&T);while(T--)
	{
    
    
		scanf("%d %d",&n,&m);int s=1<<m+1;
		FOR(i,1,n)FOR(j,1,m)scanf("%d",&a[i][j]);
		FOR(i,0,s)f[1][i]=0;f[1][0]=1;
		FOR(i,1,n)FOR(j,1,m){
    
    
			FOR(k,0,s)f[0][k]=j>1?f[1][k]:k&1?0:f[1][k>>1];
			FOR(k,0,s)if(!a[i][j])f[1][k]=k>>(j-1)&3?0:f[0][k];
				else f[1][k]=f[0][k^3<<(j-1)]+((k>>(j-1)&1)^(k>>j&1)?f[0][k]:0);
		}
		printf("%lld\n",f[1][0]);
	}
}

还有一道好题,不是插头dp,但是是轮廓线dp:这里

另外一道练习题

猜你喜欢

转载自blog.csdn.net/a_forever_dream/article/details/110147998