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

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

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

每一道题的状态都不一样,并且有不少的分类讨论,让插头DP十分锻炼思维的全面性和严谨性。

下面我们一起来学习插头DP的内容吧!

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

由于棋盘有很特殊的结构,使得它可以与“连通性”有很强的联系,因此插头DP最常见的应用要数在棋盘模型上的应用了。

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

例题一: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))

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

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 263 – 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中最重要的关键词:“插头”

 ---插头

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

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

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

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

  我们已经有了插头,自然要利用插头来状态转移。一般来说,我们从上往下,从左往右逐行逐格递推。

 ---逐格递推

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

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

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

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

  由于数据范围比较小,轮廓线的插头状态我们一般可以利用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表的时候我们还有其他方式)

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

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

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

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

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

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

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

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

  

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

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

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

代码实现

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

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

 #include <cstdio>
#include <cstring>
using namespace std;
typedef long long LL;
int n,m,bin[],mp[][];
LL f[][][(<<)+];
inline void Execution(int x,int y)
{
int plug1=bin[y-],plug2=bin[y];
for(int j=;j<bin[m+];j++)
if(mp[x][y])
{
f[x][y][j]+=f[x][y-][j^plug1^plug2];
if( (( j>>(y-) )&)== ((j>>(y) )&) )continue;
f[x][y][j]+=f[x][y-][j];
}
else
if(!(j&plug1)&&!(j&plug2))f[x][y][j]=f[x][y-][j];
else f[x][y][j]=;
}
int main()
{
int t;scanf("%d",&t);
bin[]=;for(int i=;i<=;i++)bin[i]=bin[i-]<<;
for(int u=;u<=t;u++)
{
scanf("%d%d",&n,&m);
for(int i=;i<=n;i++)
for(int j=;j<=m;j++)
scanf("%d",&mp[i][j]);
memset(f,,sizeof(f));f[][][]=;
for(int i=;i<=n;i++)
{
for(int j=;j<=m;j++)Execution(i,j);
if(i!=n)for(int j=;j<bin[m];j++)
f[i+][][j<<]=f[i][m][j];
}
printf("Case %d: There are %lld ways to eat the trees.\n",u,f[n][m][]);
}
}

  通过刚才这道题,你应该已经对插头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

【提示】

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

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

  可是,如果直接搬用上一题的做法,你会发现一些问题:

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

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

  容易观察到,如果每个格子都在回路中的话,最后所有的格子应该都通过插头连接成了一个连通块

  因此我们还需要记录每行格子的连通情况.这时我们就要引入一种新的方法:最小表示法。这是一种用来标记连通性的方法。

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

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

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

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

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

  如果每个格子都在回路中的话,我们还可以得到,每个格子应该恰好有且仅有2个插头。

  我们来看下面几张图片:

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

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

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

  我们又知道,一条路径应该对应着一个连通块,因此这两个插头同属一个连通块,并且不与其他的连通块联通

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

  上文的情况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时,这种状态才是合法的,我们可以用它更新答案。

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

  情况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值;如果没有,那就给他新建一个编号

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

 struct node{int state,next;};
struct Hash_map
{
int val[MOD],adj[MOD],e;node s[MOD];
inline void intn()
{
memset(val,0x7f,sizeof(val)),e=,
memset(s,,sizeof(s)),memset(adj,,sizeof(adj));
}
inline int &operator [] (const int &State)
{
int pos=State%MOD,i;
for(i=adj[pos];i&&s[i].state!=State;i=s[i].next);
if(!i)s[++e].state=State,s[e].next=adj[pos],adj[pos]=i=e;
return val[i];
}
}f[];

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

我们状态转移的主要工作一般有三个:

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

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

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

由于这三个操作很常用,所以我把他们写成了函数,方便调用。这三个操作的代码见下:

 inline int Find(int State,int id){return (State>>((id-)<<))&;}
inline void Set(int &State,int bit,int val){bit=(bit-)<<;State|=<<bit,State^=<<bit,State|=val<<bit;}
inline int Link(int State,int pos)
{
int cnt=,Delta=(Find(State,pos)==)?:-;//这个变量决定向左寻找匹配还是向右
for(int i=pos;i&&i<=m+;i+=Delta)
{
int plug=Find(State,i);
if(plug==)cnt++;
else if(plug==)cnt--;
if(cnt==)return i;
}
return -;
}

有了上面这些操作,本题的全部代码实现已经水到渠成了:我们只需要把上面7种情况一一对应实现即可。代码见下:

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

 #include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
typedef long long LL;
const int cube=(int)1e9,mod=;
int n,m;
struct Data_Analysis
{
int bit[];
inline void Clear(){memset(bit,,sizeof(bit));}
Data_Analysis(){Clear();}
inline void Set(int t){Clear();while(t)bit[++bit[]]=t%cube,t/=cube;}
inline int &operator [](int x){return bit[x];}
inline void Print()
{
printf("%d",bit[bit[]]);
for(int i=bit[]-;i>;i--)printf("%09d",bit[i]);
printf("\n");
}
inline Data_Analysis operator + (Data_Analysis b)
{
Data_Analysis c;c.Clear();
c[]=max(bit[],b[])+;
for(int i=;i<=c[];i++)
c[i]+=bit[i]+b[i],c[i+]+=c[i]/cube,c[i]%=cube;
while(!c[c[]])c[]--;
return c;
}
inline void operator += (Data_Analysis b){*this=*this+b;}
inline void operator = (int x){Set(x);}
}Ans;
struct Hash_Sheet
{
Data_Analysis val[mod];
int key[mod],size,hash[mod];
inline void Initialize()
{
memset(val,,sizeof(val)),memset(key,-,sizeof(key));
size=,memset(hash,,sizeof(hash));
}
inline void Newhash(int id,int v){hash[id]=++size,key[size]=v;}
Data_Analysis &operator [](const int State)
{
for(int i=State%mod;;i=(i+==mod)?:i+)
{
if(!hash[i])Newhash(i,State);
if(key[hash[i]]==State)return val[hash[i]];
}
}
}f[];
inline int Find(int State,int id){return (State>>((id-)<<))&;}
inline void Set(int &State,int bit,int val){bit=(bit-)<<;State|=<<bit,State^=<<bit,State|=val<<bit;}
inline int Link(int State,int pos)
{
int cnt=,Delta=(Find(State,pos)==)?:-;
for(int i=pos;i&&i<=m+;i+=Delta)
{
int plug=Find(State,i);
if(plug==)cnt++;
else if(plug==)cnt--;
if(cnt==)return i;
}
return -;
}
inline void Execution(int x,int y)
{
int now=((x-)*m+y)&,last=now^,tot=f[last].size;
f[now].Initialize();
for(int i=;i<=tot;i++)
{
int State=f[last].key[i];
Data_Analysis Val=f[last].val[i];
int plug1=Find(State,y),plug2=Find(State,y+);
if(Link(State,y)==-||Link(State,y+)==-)continue;
if(!plug1&&!plug2){if(x!=n&&y!=m)Set(State,y,),Set(State,y+,),f[now][State]+=Val;}
else if(plug1&&!plug2)
{
if(x!=n)f[now][State]+=Val;
if(y!=m)Set(State,y,),Set(State,y+,plug1),f[now][State]+=Val;
}
else if(!plug1&&plug2)
{
if(y!=m)f[now][State]+=Val;
if(x!=n)Set(State,y,plug2),Set(State,y+,),f[now][State]+=Val;
}
else if(plug1==&&plug2==)
Set(State,Link(State,y+),),Set(State,y,),Set(State,y+,),f[now][State]+=Val;
else if(plug1==&&plug2==){if(x==n&&y==m)Ans+=Val;}
else if(plug1==&&plug2==)Set(State,y,),Set(State,y+,),f[now][State]+=Val;
else if(plug1==&&plug2==)
Set(State,Link(State,y),),Set(State,y,),Set(State,y+,),f[now][State]+=Val;
}
}
int main()
{
scanf("%d%d",&n,&m);
if(n==||m==){printf("1\n");return ;}
if(m>n)swap(n,m);
f[].Initialize();f[][]=;
for(int i=;i<=n;i++)
{
for(int j=;j<=m;j++)Execution(i,j);
if(i!=n)
{
int now=(i*m)&,tot=f[now].size;
for(int j=;j<=tot;j++)
f[now].key[j]<<=;
}
}
Ans+=Ans;Ans.Print();
}

通过这道题的历练,相信你对插头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)时间进行一下判断。这样的效果是十分显著的。
  [入门向选讲] 插头DP:从零概念到入门 (例题:HDU1693 COGS1283 BZOJ2310 BZOJ2331)
  如图,上面一份代码是不加检验函数的程序运行时间,下面一份是加上之后的运行时间。很明显,效率得到了极大的提升
  检验函数大概长这样:
 inline bool check(int State)
{
int cnt=,cnt1=;
for(int i=;i<=m+;i++)
{
int plug=Find(State,i);
if(plug==)cnt++;
else if(plug==)cnt1++;
else if(plug==)cnt1--;
}
return cnt>||cnt1!=;
}
 至此,这道题被我们完全解决。具体的AC代码见下:
 #include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=,M=,mod=;
int n,m,ans,v[N][M];
struct Hash_System
{
int key[mod],size,hash[mod],val[mod];
inline void Initialize()
{
memset(key,-,sizeof(key));memset(val,0xaf,sizeof(val));
size=;memset(hash,,sizeof(hash));
}
inline void Newhash(int id,int State){hash[id]=++size;key[size]=State;}
inline int &operator [] (const int State)
{
for(int i=State%mod;;i=(i+==mod)?:i+)
{
if(!hash[i])Newhash(i,State);
if(key[hash[i]]==State)return val[hash[i]];
}
}
}f[];
inline int max(int a,int b){return a>b?a:b;}
inline int Find(int State,int pos){return (State>>((pos-)<<))&;}
inline void Set(int &State,int pos,int val){pos=(pos-)<<,State|=(<<pos),State^=(<<pos),State^=(val<<pos);}
inline int Link(int State,int pos)
{
int cnt=,Delta=(Find(State,pos)==)?:-;
for(int i=pos;i&&i<=m+;i+=Delta)
{
int plug=Find(State,i);
if(plug==)cnt++;
else if(plug==)cnt--;
if(cnt==)return i;
}
return -;
}
inline bool check(int State)
{
int cnt=,cnt1=;
for(int i=;i<=m+;i++)
{
int plug=Find(State,i);
if(plug==)cnt++;
else if(plug==)cnt1++;
else if(plug==)cnt1--;
}
return cnt>||cnt1!=;
}
inline void Execution(int x,int y)
{
int now=((x-)*m+y)&,last=now^,tot=f[last].size;
f[now].Initialize();
for(int i=;i<=tot;i++)
{
int State=f[last].key[i],Val=f[last].val[i];
if (check(State)||State>=(<<((m+)<<)))continue;
int plug1=Find(State,y),plug2=Find(State,y+);
int ideal=State;Set(ideal,y,),Set(ideal,y+,);//ideal代表去掉y-1,y两个插头之后的轮廓线状态。
int empty1=ideal,empty2=ideal;
if(!plug1&&!plug2)
{
f[now][ideal]=max(f[now][ideal],Val);
if(x<n&&y<m)Set(State,y,),Set(State,y+,),f[now][State]=max(f[now][State],Val+v[x][y]);
if(x<n)Set(empty1,y,),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
if(y<m)Set(empty2,y+,),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
}
else if(plug1&&!plug2)
{
if(x<n)f[now][State]=max(f[now][State],Val+v[x][y]);
if(y<m)Set(empty1,y+,plug1),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
if(plug1==){if(!ideal)ans=max(ans,Val+v[x][y]);}
else Set(empty2,Link(State,y),),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
}
else if(!plug1&&plug2)
{
if(y<m)f[now][State]=max(f[now][State],Val+v[x][y]);
if(x<n)Set(empty2,y,plug2),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
if(plug2==){if(!ideal)ans=max(ans,Val+v[x][y]);}
else Set(empty1,Link(State,y+),),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
}
else if(plug1==&&plug2==)Set(empty1,Link(State,y+),),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
else if(plug1==&&plug2==)continue;
else if(plug1==&&plug2==)f[now][ideal]=max(f[now][ideal],Val+v[x][y]);
else if(plug1==&&plug2==)Set(empty2,Link(State,y),),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
else if(plug1==&&plug2==){if(!ideal)ans=max(ans,Val+v[x][y]);}
else if(plug2==)Set(empty1,Link(State,y),),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
else if(plug1==)Set(empty2,Link(State,y+),),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(register int i=;i<=n;i++)
for(register int j=;j<=m;j++)
scanf("%d",&v[i][j]),ans=max(ans,v[i][j]);
f[].Initialize();f[][]=;
for(register int i=;i<=n;i++)
{
for(register int j=;j<=m;j++)Execution(i,j);
if(i!=n)
for(int j=,last=(i*m)&,tot=f[last].size;j<=tot;j++)
f[last].key[j]<<=;
}
printf("%d\n",ans);
}
 
 经过了这三道题的“洗礼”,相信你已经对插头DP中棋盘简单路径&回路问题略知一二了。
但是,插头DP不仅仅只有这一种类型题;有的时候,某些题的插头定义只适用于那一道题。
这时候,就需要我们培养一种灵活的思维,从多个角度去寻找新的插头定义方式
下面我们再来看一道题目。这道题的插头定义……和上面几道题都有不同。

BZOJ 2331: [SCOI2011]地板

Time Limit: 5 Sec  Memory Limit: 128 MB

Description

lxhgww的小名叫“小L”,这是因为他总是很喜欢L型的东西。小L家的客厅是一个矩形,现在他想用L型的地板来铺满整个客厅,客厅里有些位置有柱子,不能铺地板。现在小L想知道,用L型的地板铺满整个客厅有多少种不同的方案?

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

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

Input

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

接着是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)

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

不难发现,这种新的插头定义可以处理可能发生的所有可行情况。

我们只需要把他们转化为代码实现即可,具体见下:

 #include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=,HASH=,mod=;
int ans,n,m,last_x,last_y;char c[N][N];bool room[N][N];
struct Hash_System
{
int val[HASH],key[HASH],hash[HASH],size;
inline void Initialize()
{
memset(val,,sizeof(val)),memset(hash,,sizeof(hash));
memset(key,-,sizeof(key)),size=;
}
inline void Newhash(int id,int State){hash[id]=++size,key[size]=State;}
inline int &operator [] (const int State)
{
for(register int i=State%HASH;;i=(i+==HASH)?:i+)
{
if(!hash[i])Newhash(i,State);
if(key[hash[i]]==State)return val[hash[i]];
}
}
}f[];
inline int Find(int State,int pos){return (State>>((pos-)<<))&;}
inline void Set(int &State,int pos,int val)
{pos=(pos-)<<,State|=(<<pos),State^=(<<pos),State^=(val<<pos);}
inline void Print(int State){for(int i=;i<=m;i++)printf("%d",(State>>(i<<))&);}
inline void Execution(int x,int y)
{
register int now=((x-)*m+y)&,last=now^,tot=f[last].size;
f[now].Initialize();
for(register int i=;i<=tot;i++)
{
int State=f[last].key[i],Val=f[last].val[i];
int plug1=Find(State,y),plug2=Find(State,y+);
if(room[x][y])
{
if(!plug1&&!plug2)
{
if(room[x+][y])Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
if(room[x][y+])Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
if(room[x][y+]&&room[x+][y])Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
}
else if(!plug1&&plug2)
{
if(plug2==)
{
if(room[x+][y])Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
if(room[x][y+])Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
}
else
{
Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
if(x==last_x&&y==last_y&&!State)ans=(ans+Val)%mod;
if(room[x+][y])Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
}
}
else if(plug1&&!plug2)
{
if(plug1==)
{
if(room[x][y+])Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
if(room[x+][y])Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
}
else
{
Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
if(x==last_x&&y==last_y&&!State)ans=(ans+Val)%mod;
if(room[x][y+])Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
}
}
else if(plug1==&&plug2==)
{
Set(State,y,),Set(State,y+,),f[now][State]=(f[now][State]+Val)%mod;
if(x==last_x&&y==last_y&&!State)ans=(ans+Val)%mod;
}
}
else if(!plug1&&!plug2)f[now][State]=(f[now][State]+Val)%mod;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(register int i=;i<=n;i++)scanf("%s",c[i]+);
for(register int i=;i<=n;i++)
for(register int j=;j<=m;j++)room[i][j]=(c[i][j]=='*')?:;
if(m>n)
{
for(register int i=;i<=n;i++)
for(register int j=i+;j<=m;j++)
swap(room[i][j],room[j][i]);
swap(n,m);
}
for(register int i=;i<=n;i++)
for(register int j=;j<=m;j++)
if(room[i][j])last_x=i,last_y=j;
f[].Initialize();f[][]=;
for(register int i=;i<=n;i++)
{
for(register int j=;j<=m;j++)Execution(i,j);
if(i!=n)
for(register int j=,last=(i*m)&,tot=f[last].size;j<=tot;j++)
f[last].key[j]<<=;
}
printf("%d\n",ans);
}

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

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

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

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

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

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

希望读完这篇博文的你能有所收获,对插头DP有更深刻的了解!

上一篇:ASP------ActioinResult之多种返回值


下一篇:在DevExpress程序中使用GridView直接录入数据的时候,增加列表选择的功能