• 插头DP


    前言

    学习完来自陈丹琦的《基于连通性状态压缩的动态规划问题》后来做个总结。

    算法简介

    插头 DP 就是一种基于连通性状态压缩的动态规划。

    也就是说,在状态中需要记录若干个元素的联通情况。

    插头 DP 有两个比较明显的特征:

    1. 以方格表为背景

    2. 状态总数为指数级,故数据范围在 (10) 左右。

    详细的内容会结合着下面的例题来讲。

    例题选讲

    下面提供几道经典的题目:

    例题1:BZOJ1814: Formula 1

    (Large{ ext{Description:}})

    给定一个 (m imes n) 的棋盘,有的格子中存在障碍,求经过所有非障碍格子的哈密顿回路个数。

    数据范围:(2leq m,nleq 12)

    (Large{ ext{Solution:}})

    这题怎么做呢?

    首先想到搜索,但是时间复杂度是 (O((mn)!)) 的,显然不太行……

    于是考虑状态压缩!

    下面先介绍两个基本概念:轮廓线、插头。

    • 轮廓线:已决策格子与未决策格子的分界线;(下图中的红线)
    • 插头:当前格子在插头方向与相邻格子相连。(下图中的箭头)

    我们来从上到下,从左到右逐格转移。

    考虑如下图所示的红色轮廓线向蓝色轮廓线转移时该怎么做。

    (f(i,j,S)) 表示刚刚转移完 ((i,j)),插头状态为 (S) 的方案总数。

    那么想一下 (S) 应该如何表示?

    最小表示法!

    我们用一个 (n+1) 个数来表示 (S),具体如下:

    • 轮廓线上的这一个格子如果无插头,该位为 (0)
    • 轮廓线上的这一个格子如果有插头,该位与和这个格子联通的格子标记相同数字。

    其中的非零数字从 (1) 开始取,每次取未取的最小数字。

    但这么做在这题中依旧不够优秀,还有没有办法进行优化呢?

    括号匹配序列!

    为什么会这么想呢?

    由于插头不会两两交叉且是两两匹配的,我们很自然地想到括号匹配问题。

    接下来我们考虑用括号匹配序列来表示这个 (S),具体如下:

    用一个三进制数来设置状态:

    • (0):无插头状态
    • (1):左括号插头
    • (2):右括号插头

    就下图而言,它表示出来的 (S) 是这样子的:((##)() o 100212)

    直到这里,我们把插头 DP 的基本概念学清楚了!

    现在我们考虑怎么转移 (f(i,j,S))

    其实每次转移就是把当前决策格子的左插头改成下插头,上插头改成右插头。

    于是我们把 ((i,j)) 的插头状态分类讨论一下。

    1. 没有上插头和左插头,那么变成有下插头和右插头。

    这种情况相当于构成一个新的连通块。

    以下图为例,(S={1,0,0,0,0,2})

    由于有一条直线会穿过当前格,那么它是怎么被穿过的呢?

    一定是这个格子多了下插头和右插头对吧。

    此时的 (S'={1,0,0,1,2,2})

    2. 上插头和左插头中恰好只有 1 个。

    此时相当于延续原来的连通分量。

    什么意思呢?

    我们同样举下图为例。

    转移前,(S={1,0,0,0,2,0})

    那么当前格有两种决策:继续向下,向右拐。

    而两种决策后得到的 (S') 分别为:

    • 向下:(S'={1,0,0,2,0,0})
    • 向右:(S'={1,0,0,0,2,0})

    我们发现其实这并不改变原来的括号匹配情况。

    简单来讲,直接不改变 (S) 就行了。

    3. 同时有上插头和左插头。

    那么此时就相当于在这个格子结束,合并两个联通分量。

    但实际上这个非常麻烦,我们再分成 4 类进行讨论。

    3.1. 上插头和左插头均为 (

    我们发现它影响的不只是这两个插头了,那怎么办呢?

    考虑枚举这一位后的插头情况,直到遇见第一个不匹配成功的 ())

    此时把它变成 (() 即可。

    这么做为什么是对的呢?我们看一下下面的图。

    原先,(S={0,1,1,2,0,2})

    当我们把当前格合并后,如果只是把 (1 o 0),那么 (S'={0,0,0,2,0,2})

    这样是不匹配的!

    所以我们要找到右边第一个未匹配的 ()),并使 () o ()

    在这里,我们让 (S'={0,0,0,1,0,2}) 即可。

    3.2. 上插头和左插头均为 )

    同上,读者自行推导不难。

    3.3. 上插头为 (,左插头为 )

    我们考虑由于这两个必定会合起来,变成两个 (#)

    直接这么做有没有问题呢?

    显然没有。

    如上图所示,(S={1,0,0,2,1,2},S'={1,0,0,0,0,2})

    3.4. 上插头为 ),左插头为 (

    这种情况只有在下面都是障碍方格时才可能会出现,特判一下即可。

    对于刚刚提出来的例题,我们像上面这样转移即可。

    时间复杂度:(O(3^{m+1}nm)),相比于搜索还是优化了非常多!

    但其实它还有一点点的小常数可以优化。

    在我们存 (S) 的时候其实是用 (3) 进制存的,这样没有用位运算,显然不够快。

    于是我们干脆把它变成用 (4) 进制存。

    这样虽然多了一个无用的数字,但可以用位运算实现,实测效率会高很多!

    (Large{ ext{Code:}})

    #include<bits/stdc++.h>
    #define Re register
    using namespace std;
    
    typedef long long LL;
    
    const int N=15;
    const LL Hash=299987;
    
    LL n,m,X,Y,inc[N],ans;
    
    int mp[N][N];
    
    LL now,lst;
    
    LL hd[Hash+5],nxt[1<<25],que[2][1<<25],val[2][1<<25],cnt[2];
    
    inline void insert(LL bit,LL num)
    {
    	LL u=bit%Hash+1;
    	for(Re LL i=hd[u];i;i=nxt[i])
    	{
    		if(que[now][i]==bit)
    		{
    			val[now][i]+=num;
    			return;
    		}
    	}
    	cnt[now]++; 
    	nxt[cnt[now]]=hd[u];
    	hd[u]=cnt[now];
    	que[now][cnt[now]]=bit;
    	val[now][cnt[now]]=num;
    }
    
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(Re LL i=1;i<=n;i++)
    	{
    		char s[21];
    		scanf("%s",s+1);
    		for(Re LL j=1;j<=m;j++)
    		{
    			if(s[j]=='.')
    			{
    				mp[i][j]=1;
    				X=i,Y=j;
    			}
    		}
    	}
    	inc[0]=1;
    	for(Re LL i=1;i<=13;i++)
    	{
    		inc[i]=inc[i-1]<<2;
    	}
    	cnt[0]=1;
    	val[0][1]=1;
    	que[0][1]=0;
    	for(Re LL i=1;i<=n;i++)
    	{
    		for(Re LL j=1;j<=cnt[now];j++)
    		{
    			que[now][j]<<=2;
    		}
    		for(Re LL j=1;j<=m;j++)
    		{
    			memset(hd,0,sizeof hd);
    			lst=now;
    			now^=1;
    			cnt[now]=0;
    			for(Re LL k=1;k<=cnt[lst];k++)
    			{
    				LL bit,num,b1,b2;
    				bit=que[lst][k];
    				num=val[lst][k];
    				b1=(bit>>((j-1)<<1))%4;
    				b2=(bit>>(j<<1))%4;
    				if(!mp[i][j])
    				{
    					if(!b1&&!b2)
    					{
    						insert(bit,num);
    					}
    				}
    				else 
    				{
    					if(!b1&&!b2)
    					{
    						if(mp[i+1][j]&&mp[i][j+1])
    						{
    							insert(bit+inc[j-1]+(inc[j]<<1),num);
    						}
    					}
    					if(!b1&&b2)
    					{
    						if(mp[i][j+1])
    						{
    							insert(bit,num);
    						}
    						if(mp[i+1][j])
    						{
    							insert(bit-inc[j]*b2+inc[j-1]*b2,num);
    						}
    					}
    					if(b1&&!b2)
    					{
    						if(mp[i+1][j])
    						{
    							insert(bit,num);
    						}
    						if(mp[i][j+1])
    						{
    							insert(bit-inc[j-1]*b1+inc[j]*b1,num);
    						}
    					}
    					if(b1==1&&b2==1)
    					{
    						LL K=1;
    						for(Re LL l=j+1;l<=m;l++)
    						{
    							if((bit>>(l<<1))%4==1)
    							{
    								K++;
    							}
    							if((bit>>(l<<1))%4==2)
    							{
    								K--;
    							}
    							if(!K)
    							{
    								insert(bit-inc[j]-inc[j-1]-inc[l],num);
    								break;
    							}
    						}
    					}
    					if(b1==2&&b2==2)
    					{
    						LL K=1;
    						for(Re LL l=j-2;l>=0;l--)
    						{
    							if((bit>>(l<<1))%4==1)
    							{
    								K--;
    							}
    							if((bit>>(l<<1))%4==2)
    							{
    								K++;
    							}
    							if(!K)
    							{
    								insert(bit-(inc[j]<<1)-(inc[j-1]<<1)+inc[l],num);
    								break;
    							}
    						}
    					}
    					if(b1==2&&b2==1)
    					{
    						insert(bit-(inc[j-1]<<1)-inc[j],num);
    					}
    					if(i==X&&j==Y)
    					{
    						ans+=num;
    					}
    				}
    			}
    		}
    	}
    	printf("%lld",ans);
    	return 0;
    }
    

    例题2:Eat the Trees

    (Large{ ext{Description:}})

    给出 (n imes m) 的方格表,有些格子不能铺线,其它格子必须铺,可以形成多个闭合回路。问有多少种铺法?

    数据范围:(2leq nleq 12)

    (Large{ ext{Solution:}})

    这题与之前的例题类似,只是在这题中,它可以形成多条回路。

    正因为如此,所以我们没必要用括号序列。

    为什么呢?

    因为括号序列是为了防止回路提前闭合才使用的,而这题不需要。

    那就直接存轮廓线上有的插头即可。

    现在我们来考虑如何转移状态。

    1. 左 0 上 0 ( o) 右 1 下 1,即加了个转角。

    2. 左 1 上 1 ( o) 右 0 下 0,即闭合了一段回路。

    3. 左 0 上 1 ( o) 右 1 下 0 或 右 0 下 1,即向右转弯或继续向下。

    4. 左 1 上 0 ( o) 右 1 下 0 或 右 0 下 1,即继续向下或向右转弯。

    上述的情况 (3,4) 本质相同,可以归为一类。

    这样就完成了本题。

    #include<bits/stdc++.h>
    #define Re register
    using namespace std;
    
    typedef long long LL;
    
    const int N=15;
    
    int T,n,m,mp[N][N];
    
    LL dp[2][1<<20];
    
    inline int rd()
    {
    	char ch=getchar();
    	int x=0,f=1;
    	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    	return x*f;
    }
    
    inline void change(int& cur,int pos,int val)
    {
    	if(val==1) cur=cur|(1<<(pos-1));
    	else cur=cur&(~(1<<(pos-1)));
    }
    
    int main()
    {
    	scanf("%d",&T);
    	while(T--)
    	{
    		memset(dp,0,sizeof dp);
    		n=rd();
    		m=rd();
    		for(Re int i=1;i<=n;i++)
    		{
    			for(Re int j=1;j<=m;j++)
    			{
    				mp[i][j]=rd();
    			}
    		}
    		int lst=0,now=1;
    		dp[0][0]=1;
    		for(Re int i=1;i<=n;i++)
    		{
    			for(Re int j=1;j<=m;j++)
    			{
    				for(Re int k=0;k<(1<<(m+1));k++)
    				{
    					int cur=k;
    					int L=k&(1<<(j-1));
    					int U=k&(1<<j);
    					if(!mp[i][j])
    					{
    						if(!L&&!U)
    						{
    							dp[now][k]+=dp[lst][k];
    						}
    					}
    					else 
    					{
    						if(!L&&!U)
    						{
    							change(cur,j,1);
    							change(cur,j+1,1);
    							dp[now][cur]+=dp[lst][k];
    						}
    						if(L&&U)
    						{
    							change(cur,j,0);
    							change(cur,j+1,0);
    							dp[now][cur]+=dp[lst][k];
    						}
    						if((L&&!U)||(!L&&U))
    						{
    							change(cur,j,1);
    							change(cur,j+1,0);
    							dp[now][cur]+=dp[lst][k];
    							change(cur,j,0);
    							change(cur,j+1,1);
    							dp[now][cur]+=dp[lst][k];
    						}
    					}
    				}
    				memset(dp[lst],0,sizeof dp[lst]);
    				lst^=1,now^=1;
    			}
    			for(Re int j=0;j<(1<<m);j++)
    			{
    				dp[now][j<<1]=dp[lst][j];
    			}
    			memset(dp[lst],0,sizeof dp[lst]);
    			lst^=1,now^=1;
    		}
    		printf("%lld
    ",dp[lst][0]);
    	}
    	return 0;
    }
    

    练习题

    1. [JLOI2009]神秘的生物

    2. [HNOI2007]神奇游乐园

    3. [ZJOI2009]多米诺骨牌

    4. [SCOI2011]地板

  • 相关阅读:
    PipeCAD设备管口方位图
    Status code: 404 for http://mirrors.cloud.aliyuncs.com/centos/8/AppStream/x86_64/os/repodata/repom
    LNMP下Redis介绍以及安装(Linux)
    CentOS Linux 8 AppStream 错误:为仓库 ‘appstream‘ 下载元数据失败 : Cannot prepare internal mirrorlist: No URLs
    EndNote 使用经验(GT/T 77142015格式引文)
    EndNote 使用经验(标题出现 %J .....)
    EndNote 使用经验(移除重复文献)
    EndNote 使用经验(初次使用)
    20192409潘则宇 实验七 网络欺诈与防范
    20192409潘则宇 实验六 Metasploit攻击渗透实践
  • 原文地址:https://www.cnblogs.com/kebingyi/p/14351391.html
Copyright © 2020-2023  润新知