简介

  

  插头DP(轮廓线DP)是用来解决网格图回路问题的一种算法。

  

  插头DP解决的经典问题就是统计经过所有格子的哈密顿回路条数,某些格子有障碍。

  

​  如果问题稍微进阶一点的话,不一定要求路径是回路、路径带权等等情况都可能出现。

  

​  它的时间复杂度比较高,但是已经属于比较高效的算法了。

  

  

  

基本概念

  

​  首先看经典问题:统计一个带障碍的\(n*m\)网格中\((n,m\le 12)\),经过所有格子的哈密顿回路的条数。

  

  插头DP的状态围绕轮廓线进行转移。

  

  我们的状态是\(f(i,j,state)\),表示\((i,j)\)格子转移完成后、轮廓线状态为\(state\)时的情况数。

  

  轮廓线的形状相对于每一个\((i,j)\)是确定的。\((i,j)\)转移之前到转移之后,轮廓线的形状、变化如图所示:

  



  

  即转移的格子\((i,j)\)原来是轮廓线上的一个凸出点,转移后把轮廓线“从左上往右下拉”,使得\((i,j)\)变成一个凹格。

  

​  轮廓线的长度为\(m+1\),我们需要记录轮廓线的每一条边的上方(对于轮廓线中打竖的那条边,则是左方),是否有边作为接口,即是否有插头。这个信息,我们存在\(state\)中。

  

  轮廓线所体现的,一是轮廓线上的插头状态,告诉你每一个地方是否应该用一条路径“接上”;二是所有路径的连通性:显然,路径是两两相连,两两成对的,我们分组标号来记录。下面用几幅图来表达一下轮廓线的记录方式:

  



  

  括号的每个数代表对应轮廓线边的状态,"0"表示没有路径连接,其他数表示一对路径,每对路径的标号一样。

  

​  考虑到标号的大小没有保证,会导致状态的储存十分困难。

  

  我们另寻它法:注意到每对路径在轮廓线上的接口不会相交,即不会出现\((...,x,...,y,...x,...y,...)\)的这种诡异情况。所以我们可以用括号序列来表示路径信息。还是上述三个例子:

  

  

  

  这样一来,状态\(state\)可以看做一个\(m+1\)位的3进制数:0表示无接口,1表示一对路径的左端,2表示一对路径的右端。

  

    

  

转移

  

​  \(f(i,j,state)\)的转移来源,是\(f(i,j-1,state')\),但这样不好考虑和枚举。我们用\(f(i,j,state)\),转移到\(f(i,j+1,state')\)。

  

​  我们将\(f(i,j,\sum state)\)转移到\(f(i,j+1,\sum state')\)称作大转移。

  

​  显然,我们只需要关注轮廓线上唯一变动的两条边:我们把轮廓线从左上拉到右下,这两条边的状态会更改,更改后是什么呢?取决于新的一格内路径怎么走:

  

​  

  

​  我们现在关注转移的位点:

  

  

  

  大转移的整体步骤是:枚举每一个状态\(state\),得到\(p_a\)和\(p_b\),根据\(p_a\)和\(p_b\)的取值,枚举\((i,j)\)的路径走法,对于选择的\((i,j)\)的走法,由\(state\)修改\(p_a\)和\(p_b\)分别变成\(p_a'\)和\(p_b'\)而得到新状态\(state'\),执行\(f(i,j+1,state')+=f(i,j,state)\)。

  

​  根据\(p_a,p_b\)枚举走法分三大类情况:

  

  (1)\(p_a=0\;,p_b=0\):

  

    这表明\((i,j)\)的左方和上方没有路径连接,则当前格只能选取1号走法。

    

    令\(p_a'=1,p_b'=2\)得到新状态。(转移前提:\(且i<n且j<m\),否则将出现连向网格边界的路径)

  

    

  

  (2)\(p_a\)和\(p_b\)恰好有一个是0:

  

    这表明\((i,j)\)的左方或者上方有一条路径连接。

    

    由于这种走法只是将连进来的路径延长,所以这条路径在原轮廓线和新轮廓线上的性质是一样的,括号表示相同,其值也相同。

    

    令\(p_a'=p_a+p_b,\;p_b'=0\)代表3号(\(p_a\neq0\))或5号(\(p_b\neq0\))走法。(转移前提:\(i<n\))

  

    令\(p_a'=0,\;p_b'=p_a+p_b\)代表2号(\(p_a\neq0\))或4号(\(p_b\neq 0\))走法。(转移前提:\(j<m\))

  

   

  

  (3)\(p_a\)和\(p_b\)皆不为0:

  

​    这表明\((i,j)\)的左方和上方都有路径连接,所以\((i,j)\)能填的只有6号走法,令\(p_a'=0,\;p_b'=0\)。可是我们将两条路径连接起来后,其他位置的状态也需要改变,因为括号序列发生了变动。

    

    \(p_a=1,\;p_b=1\):两条左端路径此刻相连,对于\(b\)对应的右端路径位置\(c\),应该令\(p_c'=1\),此时\(c\)和\(a\)对应的右端路径\(d\)是一对路径。

    

    \(p_a=2,\;p_b=2\):两条右端路径此刻相连,同上,对于\(a\)的左端路径位置\(c\),令\(p_c'=2\)。

    

    以上寻找对应括号路径,用括号序列配对的方式实现,复杂度\(\mathcal O(m)\)

    

    \(p_a=1\;,p_b=2\):一条回路此时形成。注意!这个转移只能在最后一个非障碍的位置发生,在其他任意位置连成回路都是不合法的方案。而且,答案就是被这种方式转移的量之和。这种情况我们不需要转移了,你可以姑且理解为这已经是最后一步大转移;但是对于进阶问题,如回路不一定要经过所有点,则是因为这种转移不应该被后续过程所利用(因为路径已经形成),并且答案统计可以在任意位置进行。

    

    \(p_a=2,\;p_b=1\):相当于从中间拼接两条路径,这种情况最舒服,因为其他位置都不会有任何变化。

        

    

  转移种类较多,但是写起来其实是很好实现的。

    

    

    

实现

    

  你可以将\(f(i,j,state)\)设成一个数组,但是这样非常不便,且大转移时要枚举所有状态,非常的缓慢,而在实际中,许多状态是不合法的。

    

  我们用两个哈希表\(s_0,s_1\)来分别模拟\(f(i,j)\)和\(f(i,j+1)\):所有有效的\(f(i,j,state)\)存在\(s_0\)中,大转移开始前,我们可以从\(s_0\)里提取所有有效状态,逐一转移至\(s_1\)中,也就是有效的\(f(i,j+1,state)\)。下一步大转移开始前,交换两个哈希表,并清空\(s_1\)即可。

  

  对于一个轮廓线状态\(x\),为了操作方便,需要实现两个函数:提取某一位的值、改变某一位的值。上述括号配对表达方式中状态仅有012三种,但我们可以用四进制来表示,因为位运算的效率相对来说会比较高。

  

  

  

  

  

总结

  

  插头DP看起来十分难写,但只要自己动手实现一遍,就能理清楚插头DP的基本架构,就会发现它的实现方式其实挺简明的。建议读者还是自我摸索比较好。不过这里还是贴上模板题BZOJ1814的代码:

  

  1. #include <cstdio>
  2. #define Push(x,y) s[hv].insert((x),(y));
  3. using namespace std;
  4. typedef long long ll;
  5. const int N=13,BAS[13]={1,4,16,64,256,1024,4096,16384,65536,262144,1048576,4194304,16777216};
  6. const int HASH_MOD=4001,S=50000;
  7. int n,m,mp[N][N],ln,lm;
  8. int qcnt,q1[S],hu,hv;
  9. ll q2[S];
  10. ll ans;
  11. char str[N];
  12. inline void swap(int &x,int &y){x^=y^=x^=y;}
  13. inline int get(int st,int x){
  14. st>>=(x-1)<<1;
  15. return st-((st>>2)<<2);
  16. }
  17. inline void mdf(int &st,int x,int y){
  18. st+=(y-get(st,x))*BAS[x-1];
  19. }
  20. int match(int st,int x,int d){
  21. int top=1,stdcol=get(st,x);
  22. for(x+=d;top;x+=d){
  23. int v=get(st,x);
  24. if(v==0) continue;
  25. (v==stdcol)?top++:top--;
  26. }
  27. return x-d;
  28. }
  29. struct Hash{/*{{{*/
  30. int head[S],tot,id[S],nex[S];
  31. ll val[S];
  32. void reset(){
  33. tot=0;
  34. for(int i=0;i<HASH_MOD;i++) head[i]=0;
  35. }
  36. inline int get_hash(int x){return x%HASH_MOD;}
  37. void insert(int x,ll value){
  38. int hs=get_hash(x),u;
  39. for(u=head[hs];u&&id[u]!=x;u=nex[u]);
  40. if(!u){
  41. id[++tot]=x; val[tot]=value;
  42. nex[tot]=head[hs]; head[hs]=tot;
  43. }
  44. else val[u]+=value;
  45. }
  46. void layout(int &n,int *lis1,ll *lis2){
  47. n=0;
  48. for(int i=0;i<HASH_MOD;i++)
  49. for(int u=head[i];u;u=nex[u])
  50. n++,lis1[n]=id[u],lis2[n]=val[u];
  51. }
  52. }s[2];/*}}}*/
  53. void draw(int i,int j){
  54. int a=j,b=j+1,pa,pb;
  55. s[hu].layout(qcnt,q1,q2);
  56. int x; ll y;
  57. while(qcnt){
  58. x=q1[qcnt]; y=q2[qcnt--];
  59. if(!y) continue;
  60. if(j==1) x=(x-get(x,m+1)*BAS[m])<<2;
  61. pa=get(x,a); pb=get(x,b);
  62. if(mp[i][j]==1){
  63. if(!pa&&!pb)
  64. Push(x,y);
  65. continue;
  66. }
  67. if(!pa&&!pb){
  68. if(i<n&&j<m){
  69. mdf(x,a,1); mdf(x,b,2);
  70. Push(x,y);
  71. }
  72. }
  73. else if(pa&&pb){
  74. if(pa==1&&pb==1){
  75. int pos=match(x,b,1);
  76. mdf(x,pos,1);
  77. mdf(x,a,0); mdf(x,b,0);
  78. Push(x,y);
  79. }
  80. else if(pa==2&&pb==2){
  81. int pos=match(x,a,-1);
  82. mdf(x,pos,2);
  83. mdf(x,a,0); mdf(x,b,0);
  84. Push(x,y);
  85. }
  86. else if(pa==1&&pb==2){
  87. if(i==ln&&j==lm){
  88. bool flag=true;
  89. for(int k=1;k<=m+1&&flag;k++) if(k!=a&&k!=b&&get(x,k)) flag=false;
  90. if(flag)
  91. ans+=y;
  92. }
  93. }
  94. else{//pa==2&&pb==1
  95. mdf(x,a,0); mdf(x,b,0);
  96. Push(x,y);
  97. }
  98. }
  99. else{
  100. int u=pa,v=pb;
  101. if(i<n){
  102. mdf(x,a,u+v); mdf(x,b,0);
  103. Push(x,y);
  104. }
  105. if(j<m){
  106. mdf(x,a,0); mdf(x,b,u+v);
  107. Push(x,y);
  108. }
  109. }
  110. }
  111. }
  112. int main(){
  113. scanf("%d%d",&n,&m);
  114. for(int i=1;i<=n;i++){
  115. scanf("%s",str+1);
  116. for(int j=1;j<=m;j++){
  117. if(str[j]=='.') mp[i][j]=0;
  118. else mp[i][j]=1;
  119. if(mp[i][j]!=1) ln=i,lm=j;
  120. }
  121. }
  122. hu=0; hv=1;
  123. s[hu].insert(0,1);
  124. for(int i=1;i<=n;i++)
  125. for(int j=1;j<=m;j++){
  126. draw(i,j);
  127. swap(hu,hv);
  128. s[hv].reset();
  129. }
  130. printf("%lld\n",ans);
  131. return 0;
  132. }

【Learning】插头DP的更多相关文章

  1. 插头dp

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

  2. HDU 4113 Construct the Great Wall(插头dp)

    好久没做插头dp的样子,一开始以为这题是插头,状压,插头,状压,插头,状压,插头,状压,无限对又错. 昨天看到的这题. 百度之后发现没有人发题解,hust也没,hdu也没discuss...在acm- ...

  3. HDU 4949 Light(插头dp、位运算)

    比赛的时候没看题,赛后看题觉得比赛看到应该可以敲的,敲了之后发现还真就会卡题.. 因为写完之后,无限TLE... 直到后来用位运算代替了我插头dp常用的decode.encode.shift三个函数以 ...

  4. 插头DP专题

    建议入门的人先看cd琦的<基于连通性状态压缩的动态规划问题>.事半功倍. 插头DP其实是比较久以前听说的一个东西,当初是水了几道水题,最近打算温习一下,顺便看下能否入门之类. 插头DP建议 ...

  5. HDU 1693 Eat the Trees(插头DP、棋盘哈密顿回路数)+ URAL 1519 Formula 1(插头DP、棋盘哈密顿单回路数)

    插头DP基础题的样子...输入N,M<=11,以及N*M的01矩阵,0(1)表示有(无)障碍物.输出哈密顿回路(可以多回路)方案数... 看了个ppt,画了下图...感觉还是挺有效的... 参考 ...

  6. HDU 1693 Eat the Trees(插头DP)

    题目链接 USACO 第6章,第一题是一个插头DP,无奈啊.从头看起,看了好久的陈丹琦的论文,表示木看懂... 大体知道思路之后,还是无法实现代码.. 此题是插头DP最最简单的一个,在一个n*m的棋盘 ...

  7. HDU 4064 Carcassonne(插头DP)(The 36th ACM/ICPC Asia Regional Fuzhou Site —— Online Contest)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4064 Problem Description Carcassonne is a tile-based ...

  8. URAL 1519 基础插头DP

    题目大意: 给定一个图,一部分点'*'作为障碍物,求经过所有非障碍点的汉密尔顿回路有多少条 基础的插头DP题目,对于陈丹琦的论文来说我觉得http://blog.sina.com.cn/s/blog_ ...

  9. uva 11270 - Tiling Dominoes(插头dp)

    题目链接:uva 11270 - Tiling Dominoes 题目大意:用1∗2木块将给出的n∗m大小的矩阵填满的方法总数. 解题思路:插头dp的裸题,dp[i][s]表示第i块位置.而且该位置相 ...

随机推荐

  1. 一句话打印'*'图案(列表推导式, 人生苦短, 我用Python)

    ```python # coding=utf-8 print ('\n'.join(['*'*6 for i in range(4)])) # ****** # ****** # ****** # * ...

  2. 如何在unix系统中用别的用户运行一个程序?

    1.问题的缘由 实际开发系统的时候,经常需要用别的用户运行一个程序.比如,有些系统为保证系统安全,不允许使用root来运行.这里,我们总结了unix系统下如何解决这个问题的一些方法.同时,我们还讨论如 ...

  3. Deep learning for visual understanding: A review 视觉理解中的深度学习:回顾 之一

    Deep learning for visual understanding: A review 视觉理解中的深度学习:回顾 ABSTRACT: Deep learning algorithms ar ...

  4. 【RL系列】Multi-Armed Bandit问题笔记

    这是我学习Reinforcement Learning的一篇记录总结,参考了这本介绍RL比较经典的Reinforcement Learning: An Introduction (Drfit) .这本 ...

  5. Xcode中的文件类型

    文件类型 Xcode中的文件类型,总共4种类型: 1 普通文件(File) 2 Group(在Xcode中就是黄色的文件夹) 3 Folder(在Xcode中就是蓝色的文件夹) 4 Framework ...

  6. Scrum Meeting 10.26

    1.会议内容 姓名 今日任务 明日任务 预估时间(h) 徐越 学习服务器配置 配置SQLserver 4 卞忠昊 阅读代码 找上届代码的bug 3 武鑫 查阅资料 查阅资料,各种app的界面设计 3 ...

  7. 信息安全系统设计基础_exp1

    北京电子科技学院(BESTI) 实     验    报     告 课程:信息安全系统设计基础 班级:1353 姓名:吴子怡.郑伟 学号:20135313.20135322 指导教师: 娄嘉鹏 实验 ...

  8. C/C++:static用法总结

    前言:static是C/C++中一个很重要的关键字,最近阅读了很多博客和资料,遂在此对自己的学习笔记进行简单的总结并发表在这里 一.C语言中的static • 静态全局变量:在全局变量之前加上关键字s ...

  9. Android笔记-5-EditText密码和Checkbox二选一

    EditText密码:明文和密文 密文: public class MainActivity extends Activity { private EditText password = null; ...

  10. asp.net如何隐藏表格(table)的一行

    直接用jquery $("#id1").click(function(){ $("#trId").css("display""no ...