[入门向选讲] 插头DP:从零概念到入门 (例题:HDU1693 COGS1283 BZOJ2310 BZOJ2331)

转载请注明原文地址:http://www.cnblogs.com/LadyLex/p/7326874.htmlhtml

最近搞了一下插头DP的基础知识……这真的是一种很锻炼人的题型……node

每一道题的状态都不同,而且有很多的分类讨论,让插头DP十分锻炼思惟的全面性和严谨性。数组

下面咱们一块儿来学习插头DP的内容吧!app

插头DP主要用来处理一系列基于连通性状态压缩的动态规划问题,处理的具体问题有不少种,而且通常数据规模较小。ide

因为棋盘有很特殊的结构,使得它能够与“连通性”有很强的联系,所以插头DP最多见的应用要数在棋盘模型上的应用了。函数

下面咱们给出一道很简单的例题,而且由这道简单的例题构建出插头DP的基本解题思路,在状态确立,状态转移以及程序实现几个方面进行一一介绍.post

例题一:HDU1693 Eat the Trees

Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)学习

Problem Description
Most of us know that in the game called DotA(Defense of the Ancient), Pudge is a strong hero in the first period of the game. When the game goes to end however, Pudge is not a strong hero any more.
So Pudge’s teammates give him a new assignment—Eat the Trees!
The trees are in a rectangle N * M cells in size and each of the cells either has exactly one tree or has nothing at all. And what Pudge needs to do is to eat all trees that are in the cells.
There are several rules Pudge must follow:
I. Pudge must eat the trees by choosing a circuit and he then will eat all trees that are in the chosen circuit.
II. The cell that does not contain a tree is unreachable, e.g. each of the cells that is through the circuit which Pudge chooses must contain a tree and when the circuit is chosen, the trees which are in the cells on the circuit will disappear.
III. Pudge may choose one or more circuits to eat the trees.
Now Pudge has a question, how many ways are there to eat the trees?
At the picture below three samples are given for N = 6 and M = 3(gray square means no trees in the cell, and the bold black line means the chosen circuit(s))

Input
The input consists of several test cases. The first line of the input is the number of the cases. There are no more than 10 cases.
For each case, the first line contains the integer numbers N and M, 1<=N, M<=11. Each of the next N lines contains M numbers (either 0 or 1) separated by a space. Number 0 means a cell which has no trees and number 1 means a cell that has exactly one tree.
Output
For each case, you should print the desired number of ways in one line. It is guaranteed, that it does not exceed 2 63 – 1. Use the format in the sample.
题目大意:给出一张n*m有障碍的棋盘,要求用 任意条回路 遍历整个棋盘,不能通过障碍格子,要求统计不一样的行走方案数。
Sample Input
2
6 3
1 1 1
1 0 1
1 1 1
1 1 1
1 0 1
1 1 1
2 4
1 1 1 1
1 1 1 1
Sample Output
Case 1: There are 3 ways to eat the trees.
Case 2: There are 2 ways to eat the trees.

状态确立

  首先,咱们要了解插头DP中最重要的关键词:“插头”优化

 ---插头ui

  在插头DP中,插头表示一种联通的状态,以棋盘为例,一个格子有一个向某方向的插头,就意味着这个格子在这个方向能够与外面相连(与插头那边的格子联通)。

  值得注意的一点是,插头不是表示将要去某处的虚拟状态,而是表示已经到达某处的现实状态。

  也就是说,若是有一个插头指向某个格子,那么这个格子已经和插头来源联通了,咱们接下来要考虑的是从这个插头往哪里走。

  这是很重要的一点理解,下面讨论的状态转移都是在此基础上展开的,请务必注意。

  咱们已经有了插头,天然要利用插头来状态转移。通常来讲,咱们从上往下,从左往右逐行逐格递推。

 ---逐格递推

  咱们考虑第i行的某一个格子:走向它的方案,可能由上一行的下插头转移而来,也多是本行的右插头转移而来。

  所以咱们须要记录这些地方有没有插头,也就是利用状压的思想。咱们记录的这个“有没有插头”的东西,就被咱们称为轮廓线。字面意思,轮廓线就是记录了棋盘这一行与上一行交界的轮廓中插头的状况。轮廓线上方是已经决策完的格子,下方是未决策的。显然,对于本题的轮廓线,与它直接相连的格子有m个,插头有m+1个,我我的的习惯是给插头编号0~m。

  

  ·上图就是轮廓线的一种可能状况。

  因为数据范围比较小,轮廓线的插头状态咱们通常能够利用X进制压位来表示。

  对于本题来讲,题目的限制条件比较少,能够走多个回路,而不是像某些题同样只能走一个回路(走一个回路的时候要维护插头间的连通性,咱们下文再讨论),所以咱们直接记录,只要用二进制来表示某一位置有没有插头便可:设0表示没有插头,1表示有插头。(大概这是最简单的一种插头类型了......)

状态转移

 ---行间转移

  咱们先考虑两行之间的转移:显然,第i行的下插头决定了第i+1行的格子有没有上插头,所以咱们应该把这个信息传递到下一行。

  在转移的时候,当前行插头0到插头m-1可能会给下一行带来贡献,而第m个插头必定为0(结合定义,想一下为何)。

  容易发现,当前行的0~m-1号插头会变成下一行初始的1~m号插头,所以咱们能够直接利用位运算进行转移。

  对于本题,只须要将上一行的某个状态左移一位(<<1,即*2)便可

  行间的转移仍是比较简单的,具体代码实现的话,下面是一种能够参考的方式

  (这是我刚学插头DP时候用的一种比较蠢的打法,使用状态数组f[i][j][k]表示决策到第i行第j列,插头状态为k的方案数,后面使用Hash表的时候咱们还有其余方式)

1 if(i<n)//bin[i]表示2的i次方
2     for(int j=0;j<bin[m];j++)
3         f[i+1][0][j<<1]=f[i][m][j];

  下面咱们考虑具体的逐格转移,这也是插头DP的核心模块所在。

  对于本题来讲,当咱们决策到某个格子(x,y)时,假如它不是障碍格子,可能会出现以下三种状况:

  

  状况1,这个格子没有上插头,也没有左插头,那么因为咱们要遍历整张图,因此咱们要新建插头,把这个格子与其余格子连起来,相应的,咱们要把原来轮廓线对应位置的插头改成1.

  状况2,这个位置有上插头,也有左插头。因为咱们不要求只有一条回路,所以回路能够在这里结束。咱们直接更新答案便可。

  状况3,只有一个插头。那么这个插头能够向其余方向走:向下和向右都可以。因此咱们修改一下轮廓线并更新对应状态的答案便可。

  值得注意的是,若是一个格子是障碍格,那么当且仅当没有插头连向它时,这才是一个合法状态。由于根据咱们刚才插头的定义:

  

  “值得注意的一点是,插头不是表示将要去某处的虚拟状态,而是表示已经到达某处的现实状态。

  也就是说,若是有一个插头指向某个格子,那么这个格子已经和插头来源联通了,咱们接下来要考虑的是从这个插头往哪里走。”

 

  因此,对应障碍格既不能连入插头,也不能连出插头。这一点须要特别注意。

代码实现

  本题的分类讨论仍是相对简单的,在处理完上面的内容后咱们只须要按照上面的思路代码实现便可。

  (代码里状态转移颇有意思……)

 1 #include <cstdio>
 2 #include <cstring>
 3 using namespace std;
 4 typedef long long LL;
 5 int n,m,bin[20],mp[13][13];
 6 LL f[13][13][(1<<12)+10];
 7 inline void Execution(int x,int y)
 8 {
 9     int plug1=bin[y-1],plug2=bin[y];
10     for(int j=0;j<bin[m+1];j++)
11         if(mp[x][y])
12         {
13             f[x][y][j]+=f[x][y-1][j^plug1^plug2];
14             if( (( j>>(y-1) )&1)== ((j>>(y) )&1) )continue;
15             f[x][y][j]+=f[x][y-1][j];
16         }
17         else
18             if(!(j&plug1)&&!(j&plug2))f[x][y][j]=f[x][y-1][j];
19             else f[x][y][j]=0;
20 }
21 int main()
22 {
23     int t;scanf("%d",&t);
24     bin[0]=1;for(int i=1;i<=15;i++)bin[i]=bin[i-1]<<1;
25     for(int u=1;u<=t;u++)
26     {
27         scanf("%d%d",&n,&m);
28         for(int i=1;i<=n;i++)
29             for(int j=1;j<=m;j++)
30                 scanf("%d",&mp[i][j]);
31         memset(f,0,sizeof(f));f[1][0][0]=1;
32         for(int i=1;i<=n;i++)
33         {
34             for(int j=1;j<=m;j++)Execution(i,j);
35             if(i!=n)for(int j=0;j<bin[m];j++)
36                     f[i+1][0][j<<1]=f[i][m][j];
37         }
38         printf("Case %d: There are %lld ways to eat the trees.\n",u,f[n][m][0]);
39     }
40 }

  经过刚才这道题,你应该已经对插头DP是什么,以及插头DP的基本概念与思想有了基本的了解。

  那么下面,咱们经过下一道题来强化分类讨论能力,以及学习对连通性的限制方法。

例题2:COGS1283. [HNOI2004] 邮递员

时间限制:10 s   内存限制:162 MB

【题目描述】

Smith在P市的邮政局工做,他天天的工做是从邮局出发,到本身所管辖的全部邮筒取信件,而后带回邮局。

他所管辖的邮筒很是巧地排成了一个m*n的点阵(点阵中的间距都是相等的)。左上角的邮筒刚好在邮局的门口。

Smith是一个很是标新立异的人,他但愿天天都能走不一样的路线,可是同时,他又不但愿路线的长度增长,他想知道他有多少条不一样的路线可走。

你的程序须要根据给定的输入,给出符合题意的输出:

l 输入包括点阵的m和n的值;

l 你须要根据给出的输入,计算出Smith可选的不一样路线的总条数;

【输入格式】

输入文件postman.in只有一行。包括两个整数m, n(1 <= m <= 10, 1 <= n <= 20),表示了Smith管辖内的邮筒排成的点阵。

【输出格式】

输出文件只有一行,只有一个整数,表示Smith可选的不一样路线的条数。

【样例输入】

2 2 说明:该输入表示,Smith管辖了2*2的一个邮筒点阵。

【样例输出】

2

【提示】

  有了上一题的经验,不难看出,本题依然是一个在棋盘模型上解决的简单回路问题(简单回路是指起点和终点相同的简单路径)。

  而咱们要求的是能一遍遍历整个棋盘的简单回路个数。

  但是,若是直接搬用上一题的作法,你会发现一些问题:

  

  好比对于上图的状况,在上一题中这是一个合法解,但在本题中不是。那么咱们就应该思考上题插头定义的片面性在哪里,并想出新的插头定义

  容易观察到,若是每一个格子都在回路中的话,最后全部的格子应该都经过插头链接成了一个连通块

  所以咱们还须要记录每行格子的连通状况.这时咱们就要引入一种新的方法:最小表示法。这是一种用来标记连通性的方法。

  具体的过程是:第一个非障碍格子以及与它连通的全部格子标记为1,而后再找第一个未标记的非障碍格子以及与它连通的格子标记为2,……重复这个过程,直到全部的格子都标记完毕.好比连通讯息((1,2,5),(3,6),(4)),就能够表示为{1,1,2,3,1,2}

  可是,在实际的代码实现中,这样的最小表示法有些冗余:若是某个格子没有下插头,那么它就不会对下一行的格子产生影响,这个状态就是多余的。

  所以,咱们转换优化的角度,用最小表示法来表示插头的联通性:若是这个插头存在,那么就标记这个插头对应的格子的连通标号,若是这个插头不存在,那么标记为0..

  在这样优化后,不只状态表示更加简单,并且状态总数将会大大减小.

  接下来,咱们用改进过的最小表示法,继续思考上面的问题:如何定义新的插头状态?

  若是每一个格子都在回路中的话,咱们还能够获得,每一个格子应该刚好有且仅有2个插头。

  咱们来看下面几张图片:

  

  相信细心的你可以发现,轮廓线上方的路径是由若干条互不相交的路径构成的(这是确定的,简单反证:若是最终相交就构不成回路了)。

  更有趣的是,每条路径的两个端点刚好对应了轮廓线上的两个插头

  咱们又知道,一条路径应该对应着一个连通块,所以这两个插头同属一个连通块,而且不与其余的连通块联通

  而且,咱们在状态转移的时候也不会改变这种性质:

  上文的状况1对应着新增一条路径,插头为2;

  状况2意味着把2条路径合为一条,联通块变为1个,插头仍是2个;

  状况3只有一个插头压根不会改变插头数量。

  那么如今咱们知道了,简单回路问题必定知足任什么时候候轮廓线上每个连通份量刚好有2个插头。

  互不相交……两个插头……展开你的联想,你能想到什么?

  没错,这正是括号匹配!咱们能够按照与括号匹配类似的方式,将轮廓线上每一条路径上中左边那个插头标记为左括号插头,右边那个插头标记为右括号插头。

  因为插头之间不会交叉,那么左括号插头必定能够与右括号插头一一对应。

  这样咱们就能够解决上面的联通性问题:咱们可使用一种新的定义方式:3进制表示——0表示无插头,1表示左括号插头,2表示右括号插头,记录下全部的轮廓线信息。

  可是,值得注意的是,X进制的解码转码是较慢并且较麻烦的。

  在空间容许的状况下,建议使用2k进制,而且加上Hash表去重。这样不只能够减小状态,因为仍然可使用二进制位运算,运算速度相比之下也增长了很多。

  下面,咱们利用刚才新的插头定义方式来考虑本题的状态转移问题。

  依然设当前转移到格子(x,y),设y-1号插头状态为p1,y号插头状态为p2。

  状况1:p1==0&&p2==0.

    这种状态和上一题的状况1是相似的,咱们只须要新建一个新路径便可:下插头设为左括号插头,右插头设为右括号插头

  状况2:p1==0&&p2!=0.

    这种状态和上一题的状态3相似,咱们依然能够选择“直走”和“转弯”两种策略

  状况3:p1!=0&&p2==0.

    这种状态和状况2相似,再也不赘述。

  状况4:p1==1&&p2==1.

    这种状态把2个左括号插头相连,那么咱们须要将右边那个左括号插头(p2)对应的右括号插头q2修改为左括号插头。

  状况5:p1==1&&p2==2.

    因为路径两两不相交,因此这种状况只能是本身和本身撞在了一块儿,即造成了回路。

    因为只能有一条回路,所以只有在x==n&&y==m时,这种状态才是合法的,咱们能够用它更新答案。

  

  状况6:p1==2&&p2==1.

    这种状态至关于把2条路径相连,并无更改其余的插头

  状况7:p1==2&&p2==2.

    这种状态与状况4类似,这种状态把2个右括号插头相连,那么咱们须要将左边那个右括号插头(p1)对应的左括号插头q1修改为右括号插头。

  接下来咱们只要代码实现上述过程便可。

  但咱们依然有一个很大的优化点:Hash表的使用。Hash表能够经过去重以及排除无用状态极大的加速插头DP的速度。

  Hash表的打法不惟一,下面仅介绍我学习的打法(感谢stdafx学长)

  与Hash表相关的主要内容有:

  1.mod变量,为Hash表的大小和模数

  2.size变量,存储Hash表大小;

  3.hash数组,存储某个余数对应的编号

  4.key数组,存储状态

  5.val数组,存某个状态对应的方案数

  在给出一个新状态时,咱们在已有Hash表内搜索是否存在这一状态,若是有,那就修改这个状态对应的val值;若是没有,那就给他新建一个编号

  具体的代码实现大概长这样:

 

 1 struct node{int state,next;};
 2 struct Hash_map
 3 {
 4     int val[MOD],adj[MOD],e;node s[MOD];
 5     inline void intn()
 6     {
 7         memset(val,0x7f,sizeof(val)),e=0,
 8         memset(s,0,sizeof(s)),memset(adj,0,sizeof(adj));
 9     }
10     inline int &operator [] (const int &State)
11     {
12         int pos=State%MOD,i;
13         for(i=adj[pos];i&&s[i].state!=State;i=s[i].next);
14         if(!i)s[++e].state=State,s[e].next=adj[pos],adj[pos]=i=e;
15         return val[i];
16     }
17 }f[2];

 

有了Hash表,咱们再来考虑状态转移时的几个小细节:

咱们状态转移的主要工做通常有三个:

1.查询某个插头对应的类型(对应下文Find)

2.查找与某个插头匹配的对应插头(对应下文Link)

3.修改状态中某个插头的类型(对应下文Set)

因为这三个操做很经常使用,因此我把他们写成了函数,方便调用。这三个操做的代码见下:

 1 inline int Find(int State,int id){return (State>>((id-1)<<1))&3;}
 2 inline void Set(int &State,int bit,int val){bit=(bit-1)<<1;State|=3<<bit,State^=3<<bit,State|=val<<bit;}
 3 inline int Link(int State,int pos)
 4 {
 5     int cnt=0,Delta=(Find(State,pos)==1)?1:-1;//这个变量决定向左寻找匹配仍是向右
 6     for(int i=pos;i&&i<=m+1;i+=Delta)
 7     {
 8         int plug=Find(State,i);
 9         if(plug==1)cnt++;
10         else if(plug==2)cnt--;
11         if(cnt==0)return i;
12     }
13     return -1;
14 }

有了上面这些操做,本题的所有代码实现已经水到渠成了:咱们只须要把上面7种状况一一对应实现便可。代码见下:

(还有一个注意点,记得写高精度!)

  1 #include <cstdio>
  2 #include <cstring>
  3 #include <algorithm>
  4 #include <cmath>
  5 using namespace std;
  6 typedef long long LL;
  7 const int cube=(int)1e9,mod=2601;
  8 int n,m;
  9 struct Data_Analysis
 10 {
 11     int bit[6];
 12     inline void Clear(){memset(bit,0,sizeof(bit));}
 13     Data_Analysis(){Clear();}
 14     inline void Set(int t){Clear();while(t)bit[++bit[0]]=t%cube,t/=cube;}
 15     inline int &operator [](int x){return bit[x];}
 16     inline void Print()
 17     {
 18         printf("%d",bit[bit[0]]);
 19         for(int i=bit[0]-1;i>0;i--)printf("%09d",bit[i]);
 20         printf("\n");
 21     }
 22     inline Data_Analysis operator + (Data_Analysis b)
 23     {
 24         Data_Analysis c;c.Clear();
 25         c[0]=max(bit[0],b[0])+1;
 26         for(int i=1;i<=c[0];i++)
 27             c[i]+=bit[i]+b[i],c[i+1]+=c[i]/cube,c[i]%=cube;
 28         while(!c[c[0]])c[0]--;
 29         return c;
 30     }
 31     inline void operator += (Data_Analysis b){*this=*this+b;}
 32     inline void operator = (int x){Set(x);}
 33 }Ans;
 34 struct Hash_Sheet
 35 {
 36     Data_Analysis val[mod];
 37     int key[mod],size,hash[mod];
 38     inline void Initialize()
 39     {
 40         memset(val,0,sizeof(val)),memset(key,-1,sizeof(key));
 41         size=0,memset(hash,0,sizeof(hash));
 42     }
 43     inline void Newhash(int id,int v){hash[id]=++size,key[size]=v;}
 44     Data_Analysis &operator [](const int State)
 45     {
 46         for(int i=State%mod;;i=(i+1==mod)?0:i+1)
 47         {
 48             if(!hash[i])Newhash(i,State);
 49             if(key[hash[i]]==State)return val[hash[i]];
 50         }
 51     }
 52 }f[2];
 53 inline int Find(int State,int id){return (State>>((id-1)<<1))&3;}
 54 inline void Set(int &State,int bit,int val){bit=(bit-1)<<1;State|=3<<bit,State^=3<<bit,State|=val<<bit;}
 55 inline int Link(int State,int pos)
 56 {
 57     int cnt=0,Delta=(Find(State,pos)==1)?1:-1;
 58     for(int i=pos;i&&i<=m+1;i+=Delta)
 59     {
 60         int plug=Find(State,i);
 61         if(plug==1)cnt++;
 62         else if(plug==2)cnt--;
 63         if(cnt==0)return i;
 64     }
 65     return -1;
 66 }
 67 inline void Execution(int x,int y)
 68 {
 69     int now=((x-1)*m+y)&1,last=now^1,tot=f[last].size;
 70     f[now].Initialize();
 71     for(int i=1;i<=tot;i++)
 72     {
 73         int State=f[last].key[i];
 74         Data_Analysis Val=f[last].val[i];
 75         int plug1=Find(State,y),plug2=Find(State,y+1);
 76         if(Link(State,y)==-1||Link(State,y+1)==-1)continue;
 77         if(!plug1&&!plug2){if(x!=n&&y!=m)Set(State,y,1),Set(State,y+1,2),f[now][State]+=Val;}
 78         else if(plug1&&!plug2)
 79         {
 80             if(x!=n)f[now][State]+=Val;
 81             if(y!=m)Set(State,y,0),Set(State,y+1,plug1),f[now][State]+=Val;
 82         }
 83         else if(!plug1&&plug2)
 84         {
 85             if(y!=m)f[now][State]+=Val;
 86             if(x!=n)Set(State,y,plug2),Set(State,y+1,0),f[now][State]+=Val;
 87         }
 88         else if(plug1==1&&plug2==1)
 89             Set(State,Link(State,y+1),1),Set(State,y,0),Set(State,y+1,0),f[now][State]+=Val;
 90         else if(plug1==1&&plug2==2){if(x==n&&y==m)Ans+=Val;}
 91         else if(plug1==2&&plug2==1)Set(State,y,0),Set(State,y+1,0),f[now][State]+=Val;
 92         else if(plug1==2&&plug2==2)
 93             Set(State,Link(State,y),2),Set(State,y,0),Set(State,y+1,0),f[now][State]+=Val;
 94     }
 95 }
 96 int main()
 97 {
 98     scanf("%d%d",&n,&m);
 99     if(n==1||m==1){printf("1\n");return 0;}
100     if(m>n)swap(n,m);
101     f[0].Initialize();f[0][0]=1;
102     for(int i=1;i<=n;i++)
103     {
104         for(int j=1;j<=m;j++)Execution(i,j);
105         if(i!=n)
106         {
107             int now=(i*m)&1,tot=f[now].size;
108             for(int j=1;j<=tot;j++)
109                 f[now].key[j]<<=2;
110         }
111     }
112     Ans+=Ans;Ans.Print();
113 }

经过这道题的历练,相信你对插头DP的插头定义,最小表示法,以及状态优化的方法有了必定的了解。

尤为须要培养的是插头定义的“手感”,插头定义绝对是你解题的关键。

接下来,咱们把目光转移到“简单路径”上来。经过下面这道例题,相信会对这类简单路径&回路问题有更深的理解。

BZOJ 2310: ParkII

Time Limit: 20 Sec  Memory Limit: 128 MB

Description

Hnoi2007-Day1有一道题目 Park:给你一个 m * n 的矩阵,每一个矩阵内有个
权值V(i,j) (可能为负数),要求找一条回路,使得每一个点最多通过一次,而且通过
的点权值之和最大,想必你们印象深入吧. 
无聊的小 C 同窗把这个问题稍微改了一下:要求找一条路径,使得每一个点
最多通过一次,而且点权值之和最大,若是你跟小 C 同样无聊,就麻烦作一下
这个题目吧.

Input

第一行 m, n,接下来 m行每行 n 个数即V( i,j)

Output

一个整数表示路径的最大权值之和.

Sample Input

2 3
1 -2 1
1 1 1

Sample Output

5
【数据范围】
30%的数据,n≤6;100%的数据,m<=100,n ≤ ≤8.
注意:路径上有可能只有一个点.
 
咱们再来分析一下,这道题与上一道题又有什么区别?
首先,从统计方案问题变成了最优解问题;
其次,问题由回路问题变成了路径问题;
还有,从遍历整张图变成了无需遍历整张图。
咱们来一项一项解决新出现的问题。
对于第一个问题:
  最优解问题能够经过更改一下统计答案的方式来解决:原来咱们是加和求方案数,如今咱们是取max来求最优解。
对于第二个问题:
  咱们须要考虑一下路径与回路之间的差异在哪:
    ---对于回路问题,轮廓线上的每个插头,都有惟一肯定的另一个插头与其对应;
    ---而对于路径来讲,轮廓线上的某些插头可能没有匹配插头与之对应:它的另外一端多是路径的一段
  所以,咱们考虑新加一种插头类型来表示这种插头:咱们使用4进制状压,用3表示这种没有匹配插头的插头——我称它为“独立插头”
  在想好了新插头的定义以后,咱们来考虑它带来了哪些转移:
    ---首先,以前的几种转移依然适用。
    ---同时,咱们新增:了下面几种新状况:
      状况8:p1==0&&p2==0
        这时咱们有一种新策略:即除了以前添加一对括号插头以外,咱们还能够选择在某一个方向添加独立插头,
         即p1←3或p2←3
      状况9:p1==3&&p2==3
        这时,咱们把两个独立插头连在一块儿就造成了一条完整的路径,若是此时除了p1,p2没有其余插头,咱们就能够在这时更新答案了。
      状况10:p1==3&&p2!=3&&p2!=0
        这时,若是咱们把p1,p2链接,那么与p2对应的插头q2就变成了独立插头。
      状况11:p1!=3&&p1!=0&&p2==3
        这种状况与状况10相似,再也不赘述。
      状况12:p1==3&&p2==0
        在这种状况下,咱们有2种选择:
          ①路径在此中止。若是没有其余的插头,咱们此时就能够统计答案了。
          ②路径延续。咱们用和以前类似的方法把独立插头传递下去便可。
      状况13:p1==0&&p2==3
        这种状况与状况12相似,再也不赘述。
      状况14:p1==0&&p2!=3&&p2!=0
        这时咱们有一种新策略:
          把p2做为路径的一端点不在扩展,后继插头置为0;同时把与p2匹配的插头q2修改成独立插头3.
      状况15:p1!=0&&p1!=3&&p2==0
        这种状况与状况14相似,再也不赘述。
      状况16:p1==1&&p2==2
        原来在回路问题中这种状况是合法的,可是如今咱们正在考虑路径问题,这种状况(本身的左括号插头接本身的右括号插头)造成了回路,所以应该舍去。
对于第三个问题:
  这个问题其实也是对应着一个新的状态:
  若是当前状态为p1==0&&p2==0,那么咱们能够不选这个格子,直接跳过,这样咱们就解除了对遍历整张棋盘的限制。
至此,新出现的问题所有获得解决。咱们只须要将上面的新状况转化成代码实现便可。
  可是,在实际操做中,咱们仍然有一个能够优化的地方:因为本题限制只用一条路径,所以表示路径的独立插头在同一个状态内最多只能出现2个。所以,咱们能够在枚举到每一个状态时用O(m)时间进行一下判断。这样的效果是十分显著的。
  
  如图,上面一份代码是不加检验函数的程序运行时间,下面一份是加上以后的运行时间。很明显,效率获得了极大的提高
  检验函数大概长这样:
 1 inline bool check(int State)
 2 {
 3     int cnt=0,cnt1=0;
 4     for(int i=1;i<=m+1;i++)
 5     {
 6         int plug=Find(State,i);
 7         if(plug==3)cnt++;
 8         else if(plug==1)cnt1++;
 9         else if(plug==2)cnt1--;
10     }
11     return cnt>2||cnt1!=0;
12 }
 至此,这道题被咱们彻底解决。具体的AC代码见下:
  1 #include <cstdio>
  2 #include <cstring>
  3 #include <algorithm>
  4 using namespace std;
  5 const int N=110,M=10,mod=16631;
  6 int n,m,ans,v[N][M];
  7 struct Hash_System
  8 {
  9     int key[mod],size,hash[mod],val[mod];
 10     inline void Initialize()
 11     {
 12         memset(key,-1,sizeof(key));memset(val,0xaf,sizeof(val));
 13         size=0;memset(hash,0,sizeof(hash));
 14     }
 15     inline void Newhash(int id,int State){hash[id]=++size;key[size]=State;}
 16     inline int &operator [] (const int State)
 17     {
 18         for(int i=State%mod;;i=(i+1==mod)?0:i+1)
 19         {
 20             if(!hash[i])Newhash(i,State);
 21             if(key[hash[i]]==State)return val[hash[i]];
 22         }
 23     }
 24 }f[2];
 25 inline int max(int a,int b){return a>b?a:b;}
 26 inline int Find(int State,int pos){return (State>>((pos-1)<<1))&3;}
 27 inline void Set(int &State,int pos,int val){pos=(pos-1)<<1,State|=(3<<pos),State^=(3<<pos),State^=(val<<pos);}
 28 inline int Link(int State,int pos)
 29 {
 30     int cnt=0,Delta=(Find(State,pos)==1)?1:-1;
 31     for(int i=pos;i&&i<=m+1;i+=Delta)
 32     {
 33         int plug=Find(State,i);
 34         if(plug==1)cnt++;
 35         else if(plug==2)cnt--;
 36         if(cnt==0)return i;
 37     }
 38     return -1;
 39 }
 40 inline bool check(int State)
 41 {
 42     int cnt=0,cnt1=0;
 43     for(int i=1;i<=m+1;i++)
 44     {
 45         int plug=Find(State,i);
 46         if(plug==3)cnt++;
 47         else if(plug==1)cnt1++;
 48         else if(plug==2)cnt1--;
 49     }
 50     return cnt>2||cnt1!=0;
 51 }
 52 inline void Execution(int x,int y)
 53 {
 54     int now=((x-1)*m+y)&1,last=now^1,tot=f[last].size;
 55     f[now].Initialize();
 56     for(int i=1;i<=tot;i++)
 57     {
 58         int State=f[last].key[i],Val=f[last].val[i];
 59         if (check(State)||State>=(1<<((m+1)<<1)))continue;
 60         int plug1=Find(State,y),plug2=Find(State,y+1);
 61         int ideal=State;Set(ideal,y,0),Set(ideal,y+1,0);//ideal表明去掉y-1,y两个插头以后的轮廓线状态。
 62         int empty1=ideal,empty2=ideal;
 63         if(!plug1&&!plug2)
 64         {
 65             f[now][ideal]=max(f[now][ideal],Val);
 66             if(x<n&&y<m)Set(State,y,1),Set(State,y+1,2),f[now][State]=max(f[now][State],Val+v[x][y]);
 67             if(x<n)Set(empty1,y,3),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
 68             if(y<m)Set(empty2,y+1,3),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
 69         }
 70         else if(plug1&&!plug2)
 71         {
 72             if(x<n)f[now][State]=max(f[now][State],Val+v[x][y]);
 73             if(y<m)Set(empty1,y+1,plug1),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
 74             if(plug1==3){if(!ideal)ans=max(ans,Val+v[x][y]);}
 75             else Set(empty2,Link(State,y),3),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
 76         }
 77         else if(!plug1&&plug2)
 78         {
 79             if(y<m)f[now][State]=max(f[now][State],Val+v[x][y]);
 80             if(x<n)Set(empty2,y,plug2),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
 81             if(plug2==3){if(!ideal)ans=max(ans,Val+v[x][y]);}
 82             else Set(empty1,Link(State,y+1),3),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
 83         }
 84         else if(plug1==1&&plug2==1)Set(empty1,Link(State,y+1),1),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
 85         else if(plug1==1&&plug2==2)continue;
 86         else if(plug1==2&&plug2==1)f[now][ideal]=max(f[now][ideal],Val+v[x][y]);
 87         else if(plug1==2&&plug2==2)Set(empty2,Link(State,y),2),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
 88         else if(plug1==3&&plug2==3){if(!ideal)ans=max(ans,Val+v[x][y]);}
 89         else if(plug2==3)Set(empty1,Link(State,y),3),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
 90         else if(plug1==3)Set(empty2,Link(State,y+1),3),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
 91     }
 92 }
 93 int main()
 94 {
 95     scanf("%d%d",&n,&m);
 96     for(register int i=1;i<=n;i++)
 97         for(register int j=1;j<=m;j++)
 98             scanf("%d",&v[i][j]),ans=max(ans,v[i][j]);
 99     f[0].Initialize();f[0][0]=0;
100     for(register int i=1;i<=n;i++)
101     {
102         for(register int j=1;j<=m;j++)Execution(i,j);
103         if(i!=n)    
104             for(int j=1,last=(i*m)&1,tot=f[last].size;j<=tot;j++)
105                 f[last].key[j]<<=2;
106     }
107     printf("%d\n",ans);
108 }
 
 通过了这三道题的“洗礼”,相信你已经对插头DP中棋盘简单路径&回路问题略知一二了。
可是,插头DP不只仅只有这一种类型题;有的时候,某些题的插头定义只适用于那一道题。
这时候,就须要咱们培养一种灵活的思惟, 从多个角度去寻找新的插头定义方式
下面咱们再来看一道题目。这道题的插头定义……和上面几道题都有不一样。

BZOJ 2331: [SCOI2011]地板

Time Limit: 5 Sec  Memory Limit: 128 MB

Description

lxhgww的小名叫L”,这是由于他老是很喜欢L型的东西。小L家的客厅是一个矩形,如今他想用L型的地板来铺满整个客厅,客厅里有些位置有柱子,不能铺地板。如今小L想知道,用L型的地板铺满整个客厅有多少种不一样的方案?

须要注意的是,以下图所示,L型地板的两端长度能够任意变化,但不能长度为0。铺设完成后,客厅里面全部没有柱子的地方都必须铺上地板,但同一个地方不能被铺屡次。

Input

输入的第一行包含两个整数,RC,表示客厅的大小。

接着是R行,每行C个字符。’_’表示对应的位置是空的,必须铺地板;’*’表示对应的位置有柱子,不能铺地板。

Output

输出一行,包含一个整数,表示铺满整个客厅的方案数。因为这个数可能很大,只需输出它除以20110520的余数。

Sample Input

2 2

*_

__

Sample Output

1

HINT

R*C<=100

看到这道题,咱们很容易发现上面几道题的全部插头定义方式都不在适用了。

所以,咱们须要自行寻找到新的插头定义方式。

容易注意到,和上题的“路径”相比,本题的合法路径“L型地板”有一些特殊的地方:拐弯且仅拐弯一次。

这因为一条路径只有两种状态:拐弯过和没拐弯过,所以咱们能够尝试着这样定义新的插头:

咱们使用三进制,0表明没有插头,1表明没拐弯过的路径,2表明已经拐弯过的路径。

依然设当前转移到格子(x,y),设y-1号插头状态为p1,y号插头状态为p2。

那么会有下面的几种状况:

  状况1:p1==0&&p2==0

    这时咱们有三种可选的策略:

      ①以当前位置为起点,从p1方向引出一条新的路径(把p1修改成1号插头)

      ②以当前位置为起点,从p2方向引出一条新的路径(把p2修改成1号插头)

      ③以当前位置为“L”型路径的转折点,向p1,p2两个方向均引出一个2号插头.

  状况2:p1==0&&p2==1

    因为p2节点尚未拐过弯,所以咱们有2种可选的策略:

      ①继续直走,不拐弯,即上->下(把p1修改成1号插头,p2置0)

      ②选择拐弯,即上->右(把p2改成2号插头)

  状况3:p1==1&&p2==0

    这种状况和状况2相似,再也不赘述。

  状况4:p1==0&&p2==2

    因为p2节点已经拐过弯,因此咱们有以下的两种策略:

    ①路径在此中止。那么咱们以本格做为L型路径的一个端点。若是当前处于最后一个非障碍格子,若是没有其余的插头,咱们此时就能够统计答案了。
    ②路径延续。因为p2已经转弯过,所以咱们只能选择继续直走,即上->下(把p1修改成2号插头,p2置0)和以前类似的方法把独立插头传递下去便可。

  状况5:p1==2&&p2==0

    这种状况与状况4相似,再也不赘述。

  状况6:p1==1&&p2==1

    这种状况下,两块地板均没有拐过弯,所以咱们能够在本格将这两块地板合并,造成一个合法的“L”型路径,并将本格看作他们的转折点。(把p1和p2都置为0)

至此,新插头定义的状态转移已经讨论完成。

不难发现,这种新的插头定义能够处理可能发生的全部可行状况。

咱们只须要把他们转化为代码实现便可,具体见下:

  1 #include <cstdio>
  2 #include <cstring>
  3 #include <algorithm>
  4 using namespace std;
  5 const int N=105,HASH=200097,mod=20110520;
  6 int ans,n,m,last_x,last_y;char c[N][N];bool room[N][N];
  7 struct Hash_System
  8 {
  9     int val[HASH],key[HASH],hash[HASH],size;
 10     inline void Initialize()
 11     {
 12         memset(val,0,sizeof(val)),memset(hash,0,sizeof(hash));
 13         memset(key,-1,sizeof(key)),size=0;
 14     }
 15     inline void Newhash(int id,int State){hash[id]=++size,key[size]=State;}
 16     inline int &operator [] (const int State)
 17     {
 18         for(register int i=State%HASH;;i=(i+1==HASH)?0:i+1)
 19         {                
 20             if(!hash[i])Newhash(i,State);
 21             if(key[hash[i]]==State)return val[hash[i]];
 22         }
 23     }
 24 }f[2];
 25 inline int Find(int State,int pos){return (State>>((pos-1)<<1))&3;}
 26 inline void Set(int &State,int pos,int val)
 27     {pos=(pos-1)<<1,State|=(3<<pos),State^=(3<<pos),State^=(val<<pos);}
 28 inline void Print(int State){for(int i=0;i<=m;i++)printf("%d",(State>>(i<<1))&3);}
 29 inline void Execution(int x,int y)
 30 {
 31     register int now=((x-1)*m+y)&1,last=now^1,tot=f[last].size;
 32     f[now].Initialize();
 33     for(register int i=1;i<=tot;i++)
 34     {
 35         int State=f[last].key[i],Val=f[last].val[i];
 36         int plug1=Find(State,y),plug2=Find(State,y+1);
 37         if(room[x][y])
 38         {
 39             if(!plug1&&!plug2)
 40             {
 41                 if(room[x+1][y])Set(State,y,1),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 42                 if(room[x][y+1])Set(State,y,0),Set(State,y+1,1),f[now][State]=(f[now][State]+Val)%mod;
 43                 if(room[x][y+1]&&room[x+1][y])Set(State,y,2),Set(State,y+1,2),f[now][State]=(f[now][State]+Val)%mod;
 44             }
 45             else if(!plug1&&plug2)
 46             {
 47                 if(plug2==1)
 48                 {
 49                     if(room[x+1][y])Set(State,y,1),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 50                     if(room[x][y+1])Set(State,y,0),Set(State,y+1,2),f[now][State]=(f[now][State]+Val)%mod;
 51                 }
 52                 else 
 53                 {
 54                     Set(State,y,0),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 55                     if(x==last_x&&y==last_y&&!State)ans=(ans+Val)%mod;
 56                     if(room[x+1][y])Set(State,y,2),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 57                 }
 58             }
 59             else if(plug1&&!plug2)
 60             {
 61                 if(plug1==1)
 62                 {
 63                     if(room[x][y+1])Set(State,y,0),Set(State,y+1,1),f[now][State]=(f[now][State]+Val)%mod;
 64                     if(room[x+1][y])Set(State,y,2),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 65                 }
 66                 else 
 67                 {
 68                     Set(State,y,0),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 69                     if(x==last_x&&y==last_y&&!State)ans=(ans+Val)%mod;
 70                     if(room[x][y+1])Set(State,y,0),Set(State,y+1,2),f[now][State]=(f[now][State]+Val)%mod;
 71                 }
 72             }
 73             else if(plug1==1&&plug2==1)
 74             {
 75                 Set(State,y,0),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 76                 if(x==last_x&&y==last_y&&!State)ans=(ans+Val)%mod;
 77             }
 78         }
 79         else if(!plug1&&!plug2)f[now][State]=(f[now][State]+Val)%mod;
 80     }
 81 }
 82 int main()
 83 {
 84     scanf("%d%d",&n,&m);
 85     for(register int i=1;i<=n;i++)scanf("%s",c[i]+1);
 86     for(register int i=1;i<=n;i++)
 87         for(register int j=1;j<=m;j++)room[i][j]=(c[i][j]=='*')?0:1;
 88     if(m>n)
 89     {
 90         for(register int i=1;i<=n;i++)
 91             for(register int j=i+1;j<=m;j++)
 92                 swap(room[i][j],room[j][i]);
 93         swap(n,m);
 94     }
 95     for(register int i=1;i<=n;i++)
 96         for(register int j=1;j<=m;j++)
 97             if(room[i][j])last_x=i,last_y=j;
 98     f[0].Initialize();f[0][0]=1;
 99     for(register int i=1;i<=n;i++)
100     {
101         for(register int j=1;j<=m;j++)Execution(i,j);
102         if(i!=n)
103             for(register int j=1,last=(i*m)&1,tot=f[last].size;j<=tot;j++)
104                 f[last].key[j]<<=2;
105     }
106     printf("%d\n",ans);
107 }

 面对一道以前没有见过的问题,咱们经过寻找插头的新定义成功解决了该题。

相信你已经对插头定义有了初步的了解,接下来,必定要在作题时继续培养这种能力,这样才能百战不殆。

下面,再给出几道不是那么简单的题目,有兴趣的读者能够试着作一下:

BZOJ1187.[HNOI2007]神奇游乐园
BZOJ2595[Wc2008]游览计划
POJ3133Manhattan Wiring
POJ1739 Tony's Tour

上面讲解的4道题都是插头DP中比较基础的问题,但各自都体现出了不一样的侧重点。

插头DP是一类很美妙的问题,当你看到本身辛苦想出、码出的分类讨论AC的时候,心中必定会有很大的成就感吧!

但愿读完这篇博文的你能有所收获,对插头DP有更深入的了解!

相关文章
相关标签/搜索