题意

题目背景

ural 1519

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

题目描述

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

输入输出格式

输入格式:

第1行,n,m(2<=n,m<=12)

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

输出格式:

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

输入输出样例

输入样例#1:
复制

4 4
**..
....
....
....
输出样例#1:
复制

2
输入样例#2:
复制

4 4
....
....
....
....
输出样例#2:
复制

6

GNAQ的题解

1. 什么是插头DP

插头DP (PlugDP),也就是一类基于连通性的状态压缩动态规划,是用状压DP处理联通问题的强劲武器。

常见的类型有棋盘插头DP和CDQ论文里的两个非棋盘上的模型。

常见的联通问题:多回路问题、路径问题、简单回路问题、广义路径问题、生成树问题

2. 插头DP的大致思路

2.1 划分阶段

大家都知道了这是基于联通性的状压 DP ,所以本质上还是状压 DP 。

一般设 \(\text{dp}[i][j][\mathrm{state}]\) 为 \((i,j)\) 位置,状态为 $\mathrm{state} $ 的方案数(或者代价,等等让你求的东西……)

所以我们状压什么呢?轮廓线。

DP求解棋盘问题是逐格转移的。所以已经转移过的格子和没转移过的格子被一个折线分成了两半儿。这个折线就是轮廓线。

(@远航之曲 的简洁解释:已决策格子和未决策格子的分界线)



就这个蓝线(现在转移的格子是第3行第3个)。

插头又是什么呢?一个格子有四个插头,一个存在的插头表示在它代表的方向上能与相邻的格子连接(联通)。不存在就不能。

为什么要引入插头?要求一个回路,也就意味着最后所有的非障碍格子通过插头连接成了一个连通块,因此转移时需要记录格子的连通情况。

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

显然回路问题中一个格子恰好有两个插头,一个是 “进来” 的一个是 “出去” 的。

2.2 记录状态

最小表示法:

所有的障碍格子标记为 \(0\) ,第一个非障碍格子以及与它连通的所有格子标记为 \(1\) ,然后再找第一个未标记的非障碍格子以及与它连通的格子标记为 \(2\)

……重复这个过程,直到所有的格子都标记完毕。

比如连通信息 \(((1,2,5),(3,6),(4))\) 表示为 \(\{ 1,1,2,3,1,2 \}\) 。

(还有一种极不常用的最小表示法,即一个连通块内所有的格子都标记成该连通块最左边格子的列编号。)

括号表示法:

【性质】轮廓线上从左到右 \(4\) 个插头 \(a, b, c, d\) ,如果 \(a, c\) 连通,并且与 \(b\) 不连通,那么 \(b, d\) 一定不连通。这个性质对所有的棋盘模型的问题都适用。 (证明详见CDQ论文。)



“两两匹配”,“不会交叉”这样的性质,我们很容易联想到括号匹配。



将轮廓线上每一个连通分量中左边那个插头标记为左括号,右边那个插头标记为右括号,由于插头之间不会交叉,那么左括号一定可以与右括号一一对应,这样我们就可以使用 \(3\) 进制, \(0\) 表示无插头, \(1\) 表示左括号插头, \(2\) 表示右括号插头,记录下所有的轮廓线信息。不妨用 \(\#\) 表示无插头,那么上面的三幅图分别对应的是 \((())\#(),(()\#)(),(()\#\#\#)\) ,即 \((1122012),(1120212),(1120002)\) ,这种表示方法称为括号表示法。

状态的编码:

对于最小表示法,编码最简单的方法就是表示成一个 \(n+1\) 位的 \(p\) 进制数, \(p\) 可以取能够达到的最大的连通块标号 \(+1\) 。在不会超过数据类型的范围的前提下,建议将 \(p\) 改成 \(2\) 的幂,因为位运算比普通的运算要快很多。

如需大范围修改连通块标号,最好将状态 \(O(n)\) 解码到一个数组中,修改后再 \(O(n)\) 计算出新的 \(p\) 进制数,而对于只需要局部修改几个标号的情况下,可以直接用 \((x \;\mathrm{div}\; p^{i-1} ) \;\mathrm{mod}\; p\) 来获取第 \(i\) 位的状态,然后 \(+k \times p^{i-1}\) 直接对第 \(i\) 位进行修改。

2.3 转移状态

首先,因为逐行递推要讨论的情况还是比较难处理的,除非要求解的问题特别适合这样做,要不然我们一般使用逐格递推,这样可以方便讨论情况。

一般来说轮廓线上有不少状态都是无用的,而且一般来说头插插头 DP 的状态数很多,所以我们使用一个技巧来加速,那就是,我们用一个线性数据结构(我愿意开一个/模拟一个 vector )来存储状态,每次读取上一格的所有合法状态扩展,而不是xjb枚举状态来扩展。

然后,我们来研究一下怎么转移。我们看一种题型吧。

(其实就是这道题)

给你一个 \(m \times n\) 的棋盘,有的格子是障碍,问共有多少条回路使得经过每个非障碍格子恰好一次。

\(m, n \leqslant 12\) 。

1. 新建一个连通分量

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



如图。然后我们只有一种转移方式就是当前格做右插头和下插头。

括号表示法就是新建一对紧挨着的左右括号。最小表示法就直接解码重编一下。

2. 合并两个连通分量

如果当前轮廓线上既有右插又有下插,那你只能当前格做左插和上插,要不然就不是回路了。

然后如果右插和下插联通,那这种情况只能是在最后一个非障碍格是合法的。

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



括号表示法里就直接删一对括号,最小表示法还是解码重编。

3. 保持原来的连通分量

当轮廓线上只有下插或者右插,就只能当前格做一个左插/上插来满足回路性质,剩下一个插头是随便安排的。

图:



括号表示法的话就把下插/右插对应的括号移到你加的插头上就行了。

最小表示法也类似,把下插/右插位置的记号移到你加的插头对应位置就行(因为是延续了一个连通分量)。

注意当从一行的最后一个格子转移到下一行的第一个格子的时候,轮廓线需要特殊处理。这个看代码最好解释了。

(还要多啰嗦一句,一般状态编码的左右和轮廓线的左右是反着对应的……也就是编码最右面一位是对应轮廓线最左面格子)( 这样大概比较好写? )

一个小优化

有时候你转移的时候有可能从两个不同状态转移出同一个状态,这个时候我们用 hash 优化它就好了。 hash 表的实现用挂表法就行。



还有提取每个插头状态的时候我们可以预处理一个对应 bit 的数组,然后用上文提到的解码方式去解码。

然后我们就讨论完了插头DP的第一道入门题该怎么做。上代码。这里由于洛谷排版的原因,解释我压在代码下面了,一定要好好看。后面一堆图,我就不信你看不明白是怎么转移的(不过最好的方法是去博客看原文)

  • state 是表示状态,dp 表示方案数。这里用了滚动数组来优化空间( pre , cnt 来滚动)

  • bits 是一个方便提取插头的数组。比如 bits[3]=6,那提取第三个格子上面和左面的插头(分别叫做 is_d 和 is_r )就是 state>>bits[3-1] 和 state>>bits[3]

  • 我们存储状态是四进制,括号表示法。 \(1\) 表示 \((\) , \(2\) 表示 \()\) 。

  • hash 表的内部实现你可以看到就是一个链表。( struct hash_table )

  • 因为可能最后一格也有障碍,所以要记录一下最后一个无障碍格子( endx , endy )

代码

要考虑的8种情况:

0. 有障碍。

  1. (要改插头)

  2. 形成一个回路。只有最后一个格子才能形成一个回路。

#include<bits/stdc++.h>
#define rg register
#define il inline
#define co const
template<class T>il T read(){
rg T data=0,w=1;rg char ch=getchar();
while(!isdigit(ch)) {if(ch=='-') w=-1;ch=getchar();}
while(isdigit(ch)) data=data*10+ch-'0',ch=getchar();
return data*w;
}
template<class T>il T read(rg T&x) {return x=read<T>();}
typedef long long ll; co int N=6e5;
int n,m,mapx[14][14],endx,endy,bits[13]; // edit 1: mapx 14*14=AC 13*13=WA
// bits是一个方便提取插头的数组。比如bits[3]=6,那提取第三个格子上面和左面的插头
//(分别叫做is_d和is_r)就是state>>bits[3-1]和state>>bits[3]
int state[2][N],tots[2];
ll all_ans=0,dp[2][N];
int pre=1,cnt=0;
// state是表示状态,dp表示方案数。这里用了滚动数组来优化空间(pre,cnt来滚动)
// 我们存储状态是四进制,括号表示法。1表示(,2表示) 。
co int mo=590027;
struct hash_table{
int pre,to;
}idx[N];
int ptr[N],at=0;
// hash表的内部实现你可以看到就是一个链表。(struct hash_table)
void hah(int sta,ll val){ // 添加状态压进hah()函数里了。
int key=sta%mo;
for(int prex=ptr[key];prex;prex=idx[prex].pre)
if(state[cnt][idx[prex].to]==sta)
return dp[cnt][idx[prex].to]+=val,void();
++tots[cnt],state[cnt][tots[cnt]]=sta,dp[cnt][tots[cnt]]=val;
idx[++at].pre=ptr[key],idx[at].to=tots[cnt],ptr[key]=at;
} int main(){
// init
read(n),read(m);
char cht=0;
for(int i=1;i<=n;++i)for(int j=1;j<=m;++j,cht=0){
while(cht!='.'&&cht!='*') cht=getchar();
if(cht=='.') mapx[i][j]=1,endx=i,endy=j;
}
// 因为可能最后一格也有障碍,所以要记录一下最后一个无障碍格子(endx,endy)
for(int i=1;i<=12;++i) bits[i]=i<<1;
// DP
tots[cnt]=1,dp[cnt][1]=1,state[cnt][1]=0;
// 一开始要初始化dp(0,0)=1
for(int i=1;i<=n;++i){
for(int j=1;j<=tots[cnt];++j) state[cnt][j]<<=2;
// 这是为了转移到下一行的时候处理第一个格子的is_r插头(建议在纸上模拟一下)
for(int j=1;j<=m;++j){
at=0,memset(ptr,0,sizeof ptr); //clear hash_table
std::swap(pre,cnt),tots[cnt]=0; //rolling, clear state counter
int nowsta,is_d,is_r;
ll nowans;
for(int k=1;k<=tots[pre];++k){
nowsta=state[pre][k],nowans=dp[pre][k]; //get previous state&answer
is_r=(nowsta>>bits[j-1])%4,is_d=(nowsta>>bits[j])%4; //get current plugs
if(!mapx[i][j]){ //case 0 有障碍
if(!is_r&&!is_d) hah(nowsta,nowans);
}
else if(!is_r&&!is_d){ //case 1
if(mapx[i+1][j]&&mapx[i][j+1])
hah(nowsta+(1<<bits[j-1])+(2<<bits[j]),nowans);
}
else if(is_r&&!is_d){ //case 2
if(mapx[i+1][j]) hah(nowsta,nowans); //go down
if(mapx[i][j+1]) //go right
hah(nowsta-(is_r<<bits[j-1])+(is_r<<bits[j]),nowans);
}
else if(!is_r&&is_d){ //case 3
if(mapx[i][j+1]) hah(nowsta,nowans); // go right
if(mapx[i+1][j]) // go down
hah(nowsta-(is_d<<bits[j])+(is_d<<bits[j-1]),nowans);
}
else if(is_r==1&&is_d==1){ // case 4
int count=1;
for(int l=j+1;l<=m;++l){
if((nowsta>>bits[l])%4==1) ++count;
else if((nowsta>>bits[l])%4==2) --count;
if(!count){
hah(nowsta-(1<<bits[j-1])-(1<<bits[j])-(1<<bits[l]),nowans);
break;
}
}
}
else if(is_r==2&&is_d==2){ // case 5
int count=1;
for(int l=j-2;l>=0;--l){
if((nowsta>>bits[l])%4==1) --count;
else if((nowsta>>bits[l])%4==2) ++count;
if(!count){
hah(nowsta-(2<<bits[j-1])-(2<<bits[j])+(1<<bits[l]),nowans);
break;
}
}
}
else if(is_r==2&&is_d==1) // case 6
hah(nowsta-(2<<bits[j-1])-(1<<bits[j]),nowans);
else if(is_r==1&&is_d==2) // case 7
if(i==endx&&j==endy) all_ans+=nowans;
}
}
}
printf("%lld\n",all_ans);
return 0;
}

LG5056 【模板】插头dp的更多相关文章

  1. 模板—插头dp(Ural 1519 Formula 1)

    括号表示法: 据说比下一个要快而且灵活. #include<iostream> #include<cstring> #include<cstdio> #define ...

  2. 插头DP模板

    /* 插头dp模板 抄的GNAQ 的 括号表示法 */ #include<cstdio> #include<algorithm> #include<cstring> ...

  3. 模板:插头dp

    前言: 严格来讲有关dp的都不应该叫做模板,因为dp太活了,但是一是为了整理插头dp的知识,二是插头dp有良好的套路性,所以姑且还叫做模板吧. 这里先推荐一波CDQ的论文和这篇博客http://www ...

  4. bzoj1814 Ural 1519 Formula 1(插头dp模板题)

    1814: Ural 1519 Formula 1 Time Limit: 1 Sec  Memory Limit: 64 MBSubmit: 924  Solved: 351[Submit][Sta ...

  5. P5056 【模板】插头dp

    \(\color{#0066ff}{ 题目描述 }\) 给出n*m的方格,有些格子不能铺线,其它格子必须铺,形成一个闭合回路.问有多少种铺法? \(\color{#0066ff}{输入格式}\) 第1 ...

  6. 【模板】插头dp

    题目描述 题解: 插头$dp$中经典的回路问题. 首先了解一下插头. 一个格子,上下左右四条边对应四个插头.就像这样: 四个插头. 一个完整的哈密顿回路,经过的格子一定用且仅用了两个插头. 所以所有被 ...

  7. 插头dp

    插头dp 感受: 我觉得重点是理解,算法并不是直接想出怎样由一种方案变成另一种方案.而是方案本来就在那里,我们只是枚举状态统计了答案. 看看cdq的讲义什么的,一开始可能觉得状态很多,但其实灰常简单 ...

  8. hdu1964之插头DP求最优值

    Pipes Time Limit: 5000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Subm ...

  9. 插头dp初探

    问题描述 插头dp用于解决一类可基于图连通性递推的问题.用插头来表示轮廓线上的连通性,然后根据连通性与下一位结合讨论进行转移. 表示连通性的方法 与字符串循环最小表示不同,这种方法用于给轮廓线上的联通 ...

随机推荐

  1. 总结5条对学习Linux系统有帮助的经验心得

    作为国产手机中的代表厂商,OPPO一直走在国内的前沿.不仅手机出货量在国内遥遥领先,而且在国外也抢占不少的市场份额.前段时间,OPPO在台湾地区签下田馥甄和林宥嘉担任OPPO R9s的代言人外,在东南 ...

  2. Jdbc连接数据库基本步骤

    Jdbc连接数据库的基本步骤: package demo.jdbc; import java.sql.Connection; import java.sql.DriverManager; import ...

  3. Centos防火墙设置与端口开放的方法

    Centos升级到7之后,内置的防火墙已经从iptables变成了firewalld.所以,端口的开启还是要从两种情况来说明的,即iptables和firewalld.更多关于CentOs防火墙的最新 ...

  4. Cracking The Coding Interview 2.0 单链表

    #include <iostream> #include <string> using namespace std; class linklist { private: cla ...

  5. 深入理解java虚拟机---lanmbda表达式简介(三)

    1.lanmbda表达式使用  lanbmda表达式的作用: A: 取代内部类 B;增加对集合的操作,从而增强其性能

  6. 虚拟机中扩展linux系统存储空间

    reference: https://blog.csdn.net/greenapple_shan/article/details/52799631 https://blog.csdn.net/lyd1 ...

  7. HTML5中input[type='date']自定义样式

    HTML5提供了日历控件功能,缩减了开发时间,但有时它的样式确实不如人意,我们可以根据下面的代码自行修改. 建议:复制下面的代码段,单独建立一个css文件,方便我们修改. /* 修改日历控件类型 */ ...

  8. C点滴成海----函数声明、函数定义、函数原型

    一.函数声明 1.格式 函数体去掉函数定义中的内容再加上分号,如下所示: 返回值类型 函数名( 类型 形参, 类型 形参… ); 返回值类型 函数名( 类型, 类型…); 2.特点 函数声明只是对编译 ...

  9. nginx的日志切割

    nginx日志默认情况下统统写入到一个文件中,文件会变的越来越大,非常不方便查看分析.以日期来作为日志的切割是比较好的,通常我们是以每日来做统计的.下面来说说nginx日志切割. 如果我们使用的是yu ...

  10. shell脚本实例-nginx日志分析

    统计2018/8/6 PV量 grep "06/Aug/2018" access.log|wc -l 统计当天8:00 到9:00的PV awk '$4>="[06 ...