这是一个很菜的 Oier 的博客|

Hanx16Msgr

园龄:2年8个月粉丝:12关注:3

2022-08-10 16:39阅读: 148评论: 0推荐: 0

插头DP学习笔记

插头 DP

插头 DP 是在状态压缩 DP 的基础上用于解决一些需要处理连通性问题(比如求棋盘上的哈密顿回路)。

要了解插头 DP ,首先先回到普通的状压 DP。状压 DP 的一类经典解法就是设 DP 数组 dp[i][j],用 i 表示枚举到了第 i 行,并且第 i 行的状态为 j,然后用相邻两行互相影响的状态来计算方案数,最典型的题目有在棋盘上放国王这类题。

有些时候,这种逐行枚举的办法可能会发现状态并不好转移,而插头 DP 最为典型的做法是用逐格枚举代替普通状压 DP 的逐行枚举。

为了能够清楚的说明插头 DP,需要引入两个概念:轮廓线插头

轮廓线是当前已经进行转移了的格子与未进行转移格子的分界线,是一条将原来的方格图划分成为两部分的折线。

插头是一个格子所具有的状态,可以具有上下左右四个插头,表示与上下左右四个格子有连接。

接下来,结合模板题,将会讲解这一神奇的 DP。

【模板】插头dp

Luogu P5056

题目背景

ural 1519

陈丹琦《基于连通性状态压缩的动态规划问题》中的例题。

题目描述

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

输入格式

第一行,两个整数,分别代表 n,m

从第二行到第 (n+1) 行,每行有一个长度为 m 的只含 *. 的字符串,* 表不能铺线,. 表必须铺。

输出格式

输出一行一个整数,表示总方案数。

样例 #1

样例输入 #1

4 4
**..
....
....
....

样例输出 #1

2

样例 #2

样例输入 #2

4 4
....
....
....
....

样例输出 #2

6

提示

数据规模与约定
  • 对于 100% 的数据,保证 2n,m12

Solution

这道题的要求是求出这个棋盘的哈密顿回路,如果采用普通的状压 DP 来转移状态,会发现很难转移,所以用插头 DP 来解决。

首先需要知道的是,因为需要构成回路,那么每一个格子都会具有两个插头,一个进、一个出。这一性质十分重要。

插头 DP 是在轮廓线上进行 DP 。设 DP 数组 dp[i][j][s] 表示当前枚举到 (i,j) 且状态为 s 的方案总数,这里的 s 就表示了轮廓线的状态,具体如何编码接下来再进行讨论。

对于轮廓线上的状态,轮廓线会将可能的回路切割成为两半,并且在轮廓线上会有这些回路留下的插头,那么因为要形成回路,留下的插头一定是偶数个并且成对出现(可以把理解为一个插头用于进入当前路径,另一个插头用于离开当前路径)。并且更重要的是这些插头的匹配是满足括号匹配的(我不会证明,但是可以感性认识下,如果 ac 是匹配的两个插头,bd 也是匹配的两个插头,如果 b 被夹在 a , c 间,那么 bd 这条路径一定会与 ac 有重合,所以 b 一定不会被 ac 夹在中间)。既然满足括号匹配,那么就有一种比较巧妙的编码方式,就是将左括号编码为 1 ,右括号编码为 2 ,没有括号(插头)的地方编码为 0 (这样的编码规则下可以将 1 理解为进入路径的插头,2 理解为离开路径的插头,0 表示没有插头的位置)。

既然已经知道了轮廓线的编码方式,那么就可以来讨论转移的问题了。插头 DP 的转移的情况较多,接下来将逐个分析,为了方便表述,将当前格子的左侧的插头记作 isR(理解作向右的插头),上侧的插头记作 isD(理解作向下的插头),这两个值可以从轮廓线的状态上很方便地获取到。

  1. 当前格子为障碍物

    如果当前格子为障碍物,那么能转移到当前格子的状态只能是当 isRisD 两个都不存在(即 isR=isD=0),此时的轮廓线的状态将不发生改变,直接转移至当前格子。

  2. 左侧插头与上侧插头都不存在(isR=isD=0

    既然当前格子上侧与左侧插头都不存在,并且为了为了满足一个格子有两个插头这一性质,当前格子将会具有右插头和下插头,并需要用这两个插头来更新轮廓线状态。

  3. 仅存在左侧插头(isR=1/2,isD=0

    仅存在左插头并且不存在上插头,那么当前格子就可以具有下插头或者是右插头,分开讨论并转移这两种状态即可。

  4. 仅存在上侧插头(isR=0,isD=1/2

    与仅存在左插头的情况相同,当前格子可以就有下插头或者是右插头,分开讨论并转移。

  5. 当前格子的上侧插头与左侧插头都为 1isR=isD=1

    理解作有两个需要进入当前格子的插头,因为一个格子只能具有两个插头,所以这个格子就只会具有左和上插头。因为要构成回路,所以就需要将上插头对应的那个插头的值改为 1 ,可以理解成为将这组插头表示的路径颠倒方向。因为轮廓线在这个格子更新后已经不再与这两个插头有关(这两个插头已经被接上,所以不存在在轮廓线上了),所以轮廓线的状态需要将这两个插头删除,并且将上插头对应的那个插头的值改为 1

  6. 当前格子的上侧插头与左侧插头都为 2isR=isD=2

    这种情况与都为 1 的情况也类似,因为需要接上这两个插头,所以需要将左插头代表的路径反向,即需要向左找到与左插头对应的插头并将值改为 1

  7. 当前格子的左侧插头值为 2,上侧插头值为 1isR=2,isD=1

    即从上侧进入当前格子并从左侧离开,不需要对任何插头进行操作,直接删除这两个插头就可以了(同样是因为这两个插头不存在于轮廓线上了)。

  8. 当前格子的左侧插头值为 1,上侧插头值为 2isR=1,isD=2

    从左侧进入当前格子并从上侧离开,因为向上走会回到原来的位置,所以这一种情况是用于最后一个格子来构成回路用的,当到达了最后一个格子轮廓线状态满足这种情况,那么这种状态就是一种合法的回路方案,计算进最终答案就行了。

在代码实现的时候,因为状态数量是比较零散的,直接开数组存储可能存在浪费空间的情况,所以可以用哈希表来优化空间。并且如果每到达一个新格子就要枚举所有可能情况的话,时间开销又会比较大,所以可以记录下可能的状态,在需要枚举的时候直接继承上一个格子的结果,这样就可以省下一些时间。

其他有一些小技巧,此题存储的轮廓线状态由 0,1,2 构成,所以可以看成三进制串来压缩进行存储,但是在代码实现的时候可以用四进制来进行存储,因为 42 的二次幂,可以用位运算,运算速度可以大大加快。

开始前先预处理了一个 bits 数组,作用是访问轮廓线状态 s 的四进制第 i 位时,可以用 (i>>bits[i])mod4 实现快速计算。

Code

#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof a)
#define int long long
using namespace std;
template<typename T> void read(T &k)
{
	k=0;T flag=1;char b=getchar();
	while (!isdigit(b)) {flag=(b=='-')?-1:1;b=getchar();}
	while (isdigit(b)) {k=k*10+b-48;b=getchar();}
	k*=flag;
}
int n,m;
bool mp[15][15];
int ex,ey;//记录下最后一个非障碍物的格子的坐标,并在这个格子统计答案
int bits[30];
int pre=1,cur=0;//pre表示上一行存储的位置,cur表示当前行存储的位置,当进入新的一行的时候就交换这两个值来达到滚动数组的目的
const int _SIZE=6e5,MOD=590027;
struct HASH_TABLE{//链表实现哈希表
	int pre,to;
}H[_SIZE+5];
int tot[2],ptr[_SIZE+5],at=0;
int state[2][_SIZE+5],dp[2][_SIZE+5];//存储状态和dp数组
int Fans=0;//统计最终答案
void modify(int sta,int val)//查看hash表内是否存在键值sta
{
	int key=sta%MOD;//计算表头
	for (int i=ptr[key];i;i=H[i].pre)//遍历当前表头的所有sta
		if (state[cur][H[i].to]==sta)//存在
		{
			dp[cur][H[i].to]+=val;//说明在dp数组中出现过,直接将val加入
			return;
		}
	tot[cur]++;//如果没有的话就添加进当前表头
	state[cur][tot[cur]]=sta;//存状态(类似映射表)
	dp[cur][tot[cur]]=val;
	H[++at].pre=ptr[key];//链表的处理
	H[at].to=tot[cur];
	ptr[key]=at;
}
void init() {for (int i=1;i<=25;i++) bits[i]=(i<<1);}//初始化bits数组
void PlugDP()//插头DP
{
	tot[cur]=1,dp[cur][1]=1;//初始化边界条件
	state[cur][1]=0;
	for (int i=1;i<=n;i++)
	{
		for (int j=1;j<=tot[cur];j++) state[cur][j]<<=2;//先将轮廓线下移,即将所有插头向后走一位
		for (int j=1;j<=m;j++)
		{
			at=0;mem(ptr,0);//新的格子,清空hash表
			swap(pre,cur);//滚动
			tot[cur]=0;
			int nowsta,isD,isR,nowans;
			for (int k=1;k<=tot[pre];k++)
			{
				nowsta=state[pre][k],nowans=dp[pre][k];//先存储当前状态以及答案
				isD=(nowsta>>bits[j])%4,isR=(nowsta>>bits[j-1])%4;//获取左插头和上插头
				if (!mp[i][j])//障碍物
				{
					if ((!isD) && (!isR)) modify(nowsta,nowans);//判断插头情况,只有两个插头都不存在才是合法的
				}
				else if ((!isD) && (!isR))//两个插头都不存在
				{
					if (mp[i+1][j] && mp[i][j+1]) modify(nowsta+(1<<bits[j-1])+2*(1<<bits[j]),nowans);//一个插头设置为1,一个设置为2
				}
				else if (isR && (!isD))//仅存在左插头
				{
					if (mp[i+1][j]) modify(nowsta,nowans);//插头的位置不会发生改变
					if (mp[i][j+1]) modify(nowsta-isR*(1<<bits[j-1])+isR*(1<<bits[j]),nowans);//插头位置右移一位,即删除原来位置并加入原来右侧位置
				}
				else if (isD && (!isR))//仅存在上插头
				{
					if (mp[i][j+1]) modify(nowsta,nowans);//同样不发生改变
					if (mp[i+1][j]) modify(nowsta-isD*(1<<bits[j])+isD*(1<<bits[j-1]),nowans);//插头位置左移一位
				}
				else if (isD==1 && isR==1)//两个插头都为1
				{
					int cnt=1;
					for (int l=j+1;l<=m;l++)//向右找对应的2
					{
						if ((nowsta>>bits[l])%4==1) cnt++;
						else if ((nowsta>>bits[l])%4==2) cnt--;
						if (!cnt)//找到匹配的了
						{
							modify(nowsta-(1<<bits[j-1])-(1<<bits[j])-(1<<bits[l]),nowans);//将l位置设为1,也就是将2减去1
							break;
						}
					}
				}
				else if (isD==2 && isR==2)//两个插头都为2
				{
					int cnt=1;
					for (int l=j-2;l;l--)//向左找对应的1
					{
						if ((nowsta>>bits[l])%4==1) cnt--;
						else if ((nowsta>>bits[l])%4==2) cnt++;
						if (!cnt)
						{
							modify(nowsta-2*(1<<bits[j-1])-2*(1<<bits[j])+(1<<bits[l]),nowans);//将插头去除,插头l加1变成2
							break;
						}
					}
				}
				else if (isD==1 && isR==2)//上1左2
					modify(nowsta-2*(1<<bits[j-1])-(1<<bits[j]),nowans);//直接删除这两个插头
				else if (isR==1 && isD==2 && i==ex && j==ey)//上2左1,必须到达最后一个点才是合法的
					Fans+=nowans;//统计入最终答案
			}
		}
	}
}
signed main()
{
	read(n),read(m);
	for (int i=1;i<=n;i++)
	{
		char st[15];scanf("%s",st+1);
		for (int j=1;j<=m;j++)
			if (st[j]=='.') ex=i,ey=j,mp[i][j]=1;
	}
	init();
	PlugDP();
	printf("%lld\n",Fans);
	return 0;
}

推荐题目: Luogu P5074 多回路问题

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