zoukankan      html  css  js  c++  java
  • 插头DP讲解+[BZOJ1814]:Ural 1519 Formula 1(插头DP)

    1.什么是插头$DP$?

    插头$DP$是$CDQ$大佬在$2008$年的论文中提出的,是基于状压$D$P的一种更高级的$DP$多用于处理联通问题路径问题,简单回路问题,多回路问题,广义回路问题,生成树问题)。

    插头$DP$每道题都不一样,且需要进行较为繁琐的分类讨论,所以插头$DP$十分锻炼思维的严谨性和全面性。


    2.插头$DP$思路

     $mathcal{A.}$状态确立

      $alpha.$插头

       插头表示一种联通状态

       在棋盘模型中,一个格子有向某方向的插头,表示这个格子在这个方向与插头那边的格子相连。

       注意:插头并不是说将要去某处的虚拟状态,而是已经到达某处的现实状态

       我们需要考虑的是接下来往哪里走,因为如果有一个插头指向当前格子,说明当前格子已经与插头的来源有联通了。

       有了插头,就方便多啦,我们一般需要的是进行逐格递推,通俗的讲,就是跑一遍。

      $eta.$轮廓线

       轮廓线就是一条分界线,至于它为什么叫轮廓线,我也不知道,就像$CDQ$为什么叫$CDQ$一样,她也不知道,但是她爸妈知道。

       你可以感性的将它理解为是“已经决策了的格子”与“还没有决策的格子”的分界线。

       但是,轮廓线的用途不止如此。

       轮廓线还兼容了存储这条分界线上插头状态的作用。

       需要注意的是,假设一行内有$m$个格子,那么会有$m+1$个插头,为什么呢?

       看一下下面的这张图:

       

       显然一行内有$4$个格子,但是有$5$个插头,多出来的那一个插头在当前正在决策的那个格子的右侧。

       个人习惯将插头编号为$1~m$。

       一般数据范围比较小,我们可以用状压来存储,即定义$dp[i][j][state]$表示当前决策到$(i,j)$这个点,状态为$state$的方案数(或代价,$etc$)。

      $gamma.$插头与轮廓线的结合

       插头与轮廓线结合在一起,就会碰撞出一些美丽的火花。

       我们递归的时候就是依据插头的存在性,来求出所有能转移到的合法状态。

       在回路问题中,对于一个状态一个格子恰好有两个插头,一个“进来”,一个“出去”。

     $mathcal{B}.$记录状态

      下面来介绍三种记录状态的方式:

      $alpha.$最小表示法

       为了方便,所有有障碍的格子记为$0$,第一个非障碍的格子以及所有与它相连的格子标记为$1$,第一个未标记的格子以及与它相邻的格子标记为$2$……

       重复以上的过程,直到所有的格子都标记完毕。

       举个栗子:

        $1,2,5$联通,$3,6$联通,$4$自己和自己卡在一起,那么其最小表示法即为${1,1,2,3,1,2}$。

      $eta.$括号表示法

       先来看一个性质:假设一条轮廓线上有$4$个插头$a,b,c,d$,$a$与$c$联通且不与$b$联通,那么$b$与$d$肯定也不联通。

       这个性质很重要,因为它适用于所有的棋盘问题

       

       就像这样,显然如果$b$与$d$也相交的话就不可能满足情况了。

       再具体一点:

       

       相信细心的你一定会发现,轮廓先上方的路径都是由若干条互不相交的路径构成的,原因就是再上面那张图。

       每条路径上的两个端点也会恰好对应轮廓线上的两个插头

       再来明确一点,对于不同的插头,我们称它们为左插头或右插头,并不是指它们的方向,而是指在途中相对的左右,如下图:

       

       有点跑题了,这种“不交叉”很容易让我们联想到括号匹配,左括号为进栈,右括号为出栈,左括号一定与右括号一一对应。

       当然有可能会让你想到卡特兰数,这点很重要,能帮你卡常。

      $gamma.$状态的编码

       利用状压的思想,我们将每行$m+1$个状态用一个$m+1$位的$k$进制数表示,在做题的时候建议将$k$改为$2$的$n$次幂,方便进行为运算,运行速度会有很大的提升(前提空间允许)。

       小技巧:如需大范围修改联通状态,可以选择$Theta(m)$将其解码到一个数组里,修改后在$Theta(n)$将其计算为$k$进制数,而对于只需要进行局部修改的情况可以直接用$(x div p^{i-1})mod p$来获取第i位的状态,使用$+x imes p^{i-1}$来对第i位的状态进行修改。

     $mathcal{C}.$状态转移

      $alpha.$行间转移

       显然,第$i$行第$j$列的下插头决定了第$i+1$行第$j$列有没有上插头,因此需要将这个信息传递给下一行。

       当枚举到一行的最后一个格子的时候,第$m$个插头一定为$0$(即为没有),如下图。

       

       细心的你还能发现,在行间转移之前的第$0~m-1$个插头会变成行间转移之后的第$1~m$个插头,如下图。

       

       所以,我们每次只需要将上一行的状态左移一位即可。

       代码$downarrow$:

    for(int j=1;j<=cnts[cnt];j++)state[cnt][j]<<=1;
    

       打法奇特,请多谅解。

      $eta.$逐格转移

       因题而异,比行间转移复杂,一般分为多种情况,需要分类讨论。

       在很多的状态转移中都出现了以下三种情况:

        $mathfrak{a}.$新建一个联通分量:

         这个情况只出现在转移位置的轮廓线上没有下插头和右插头:

         

         在上图中,这个格子没有上插头,也没有左插头,那么由于我们要遍历整张图,所以我们要新建插头,把这个格子与其他格子连起来,相应的,我们要把原来轮廓线对应位置的插头改为$1$。

        $mathfrak{b}.$合并两个联通分量:

         不连通的话,当然这样做会把他们变联通:

         

         这个位置有上插头,也有左插头。由于我们不要求只有一条回路,因此回路可以在这里结束。我们直接更新答案即可。

        $mathfrak{c}.$保持原有的联通分量:

         

         只有一个插头。那么这个插头可以向其他方向走:向下和向右均可以。所以我们修改一下轮廓线并更新对应状态的答案即可。

     $mathcal{D}.$优化

      可以使用$hash$表进行优化,下面附一个此题中我用的$hash$表的代码$downarrow$:

    int pre_hash[600000],at;
    int sta_hash[2][600000],cnts[2];
    void hash_map(int sta,long long val)
    {
    	int key=sta%590027;
    	for(int i=pre_hash[key];i;i=hash[i].nxt)
    		if(sta_hash[cnt][hash[i].to]==sta)//如果这个状态已经存在,就加上
    		{dp[cnt][hash[i].to]+=val;return;}
    	sta_hash[cnt][++cnts[cnt]]=sta;//不存在这个状态就新建
    	dp[cnt][cnts[cnt]]=val;
    	hash[++at].nxt=pre_hash[key];
    	hash[at].to=cnts[cnt];
    	pre_hash[key]=at;
    }
    

    例题$downarrow$


    题目描述

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


    输入格式

    第$1$行,$n,m$。
    从第$2$行到第$n+1$行,每行一段字符串($m$个字符),"*"表不能铺线,"."表必须铺。


    输出格式

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


    样例

    样例输入$1$:

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

    样例输出$1$:

    2

    样例输入$2$:

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

    样例输出$2$:

    6


    数据范围与提示

    $2leqslant n,mleqslant 12$。


    题解

    典型的插头$DP$板子题,洛谷更是将它作为了【模板】插头$dp$。

    这道题中,我们需要用$3$进制来表示状态,$0$表示无插头,$1$表示左插头,$2$表示右插头,我使用的是$4$进制,弃掉一位,但是方便位运算的操作。

    下面来列举一下这道题的$7$种情况:

     $alpha.$有障碍:

    if(!Map[i][j]){if(!down&&!right)hash_map(sta,ans);}
    

     $eta.$

    if(!down&&!right){if(Map[i+1][j]&&Map[i][j+1])hash_map(sta+2*(1<<bit[j])+(1<<bit[j-1]),ans);}
    

     $gamma.$

    if(down&&!rght)
    {
    	if(Map[i][j+1])hash_map(sta,ans);
    	if(Map[i+1][j])hash_map(sta+down*((1<<bit[j-1])-(1<<bit[j])),ans);
    }
    

     $delta.$

    if(!down&&right)
    {
    	if(Map[i+1][j])hash_map(sta,ans);
    	if(Map[i][j+1])hash_map(sta+right*((1<<bit[j])-(1<<bit[j-1])),ans);
    }
    

     $epsilon.$

    if(down==1&&rght==1)
    {
    	int count=1;
    	for(int l=j+1;l<=m;l++)
    	{
    		if((sta>>bit[l])%4==1)count++;
    		if((sta>>bit[l])%4==2)count--;
    		if(!count)
    		{
    			hash_map(sta-(1<<bit[j])-(1<<bit[j-1])-(1<<bit[l]),ans);
    			break;
    		}
    	}
    }
    

     注意此时我们不仅需要将两个左插头减去,而且还要将左边的右插头变成左插头。

     $zeta.$

    if(down==2&&rght==2)
    {
    	int count=1;
    	for(int l=j-2;l>=0;l--)
    	{
    		if((sta>>bit[l])%4==1)count--;
    		if((sta>>bit[l])%4==2)count++;
    		if(!count)
    		{
    			hash_map(sta-2*(1<<bit[j])-2*(1<<bit[j-1])+(1<<bit[l]),ans);
    			break;
    		}
    	}
    }
    

     注意上面依然需要减去两个右插头,再把右边的左插头变成右插头。

     $eta.$

    if(down==1&&rght==2){hash_map(sta-(1<<bit[j])-2*(1<<bit[j-1]),ans);}
    

     此时我们合并了一个右插头和一个左插头,减去就好了。

     $ heta.$形成了一个回路,只有在最后一个格子才有可能形成回路。

    if(down==2&&rght==1&&i==endx&&j==endy)ansss+=ans;
    

    $7$种情况已经列举完了,代码实现细节不少,要注意。


    代码时刻

    #include<bits/stdc++.h>
    using namespace std;
    struct hash_table
    {
    	int nxt;
    	int to;
    }hash[600000];
    char ch[20];
    int pre_hash[600000],at;
    int sta_hash[2][600000],cnts[2];
    bool Map[15][15];
    int bit[15];
    bool cnt;
    int endx,endy;
    long long ans,ansss,dp[2][600000];
    void hash_map(int sta,long long val)//hash表
    {
    	int key=sta%590027;
    	for(int i=pre_hash[key];i;i=hash[i].nxt)
    		if(sta_hash[cnt][hash[i].to]==sta)
    		{dp[cnt][hash[i].to]+=val;return;}
    	sta_hash[cnt][++cnts[cnt]]=sta;
    	dp[cnt][cnts[cnt]]=val;
    	hash[++at].nxt=pre_hash[key];
    	hash[at].to=cnts[cnt];
    	pre_hash[key]=at;
    }
    int main()
    {
    	int n,m;
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++)
    	{
    		scanf("%s",ch+1);
    		for(int j=1;j<=m;j++)
    			if(ch[j]=='.')
    			{
    				Map[i][j]=1;
    				endx=i;
    				endy=j;
    			}
    	}
    	for(int i=1;i<=m;i++)//预处理一下,方便下面处理
    		bit[i]=(i<<1);
    	cnts[0]=1;
    	dp[0][1]=1;
    	for(int i=1;i<=n;i++)
    	{
    		for(int j=1;j<=cnts[cnt];j++)sta_hash[cnt][j]<<=2;//将状态左移一位
    		for(int j=1;j<=m;j++)
    		{
    			at=0;
    			memset(pre_hash,0,sizeof(pre_hash));
    			cnt^=1;
    			cnts[cnt]=0;
    			for(int k=1;k<=cnts[cnt^1];k++)
    			{
    				int sta=sta_hash[cnt^1][k];//提取状态
    				long long ans=dp[cnt^1][k];
    				int down=(sta>>bit[j])%4;//提取插头信息,下同
    				int rght=(sta>>bit[j-1])%4;
    				if(!Map[i][j])//α
    					{if(!down&&!rght)hash_map(sta,ans);}
    				else if(!down&&!rght)//β
    					{if(Map[i+1][j]&&Map[i][j+1])hash_map(sta+2*(1<<bit[j])+(1<<bit[j-1]),ans);}
    				else if(down&&!rght)//γ
    				{
    					if(Map[i][j+1])hash_map(sta,ans);
    					if(Map[i+1][j])hash_map(sta+down*((1<<bit[j-1])-(1<<bit[j])),ans);
    				}
    				else if(!down&&rght)//δ
    				{
    					if(Map[i+1][j])hash_map(sta,ans);
    					if(Map[i][j+1])hash_map(sta+rght*((1<<bit[j])-(1<<bit[j-1])),ans);
    				}
    				else if(down==1&&rght==1)//ε
    				{
    					int count=1;
    					for(int l=j+1;l<=m;l++)
    					{
    						if((sta>>bit[l])%4==1)count++;
    						if((sta>>bit[l])%4==2)count--;
    						if(!count)
    						{
    							hash_map(sta-(1<<bit[j])-(1<<bit[j-1])-(1<<bit[l]),ans);
    							break;
    						}
    					}
    				}
    				else if(down==2&&rght==2)//ζ
    				{
    					int count=1;
    					for(int l=j-2;l>=0;l--)
    					{
    						if((sta>>bit[l])%4==1)count--;
    						if((sta>>bit[l])%4==2)count++;
    						if(!count)
    						{
    							hash_map(sta-2*(1<<bit[j])-2*(1<<bit[j-1])+(1<<bit[l]),ans);
    							break;
    						}
    					}
    				}
    				else if(down==1&&rght==2)//η
    					{hash_map(sta-(1<<bit[j])-2*(1<<bit[j-1]),ans);}
    				else if(down==2&&rght==1&&i==endx&&j==endy)ansss+=ans;//θ
    			}
    		}
    	}
    	printf("%lld",ansss);
    	return 0;
    }
    

    rp++

  • 相关阅读:
    当el-input输入多行内容,反显时,字符串需要换行显示
    【前端】记录自己在leetcode上的刷题之路
    本地提代码到github上,不显示贡献值的问题
    关于对象的深拷贝和浅拷贝
    js判断两个数值区间是否存在重叠或覆盖
    解决vue-print-nb打印el-table,不同分辨率下,打印显示不全的问题
    vue实现置顶功能
    el-table设置自定义表头,当表头内容过长时,鼠标悬浮显示完整内容
    省市用element级联选择器,构造树形结构的数据(两层)
    STL隐蔽知识点
  • 原文地址:https://www.cnblogs.com/wzc521/p/11260885.html
Copyright © 2011-2022 走看看