# 例题一：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 263 – 1. Use the format in the sample.

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。

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

由于数据范围比较小，轮廓线的插头状态我们一般可以利用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];//如果2个都有插头，异或后变成均没有，2个都没有就变成都有
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[0][m][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)
36                 for(int j=0;j<bin[m];j++)
37                     f[i+1][0][j<<1]=f[i][m][j];
38         }
39         printf("Case %d: There are %lld ways to eat the trees.\n",u,f[n][m][0]);
40     }
41 }```

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

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

# 例题2：COGS1283. [HNOI2004] 邮递员

### 【题目描述】

Smith在P市的邮政局工作，他每天的工作是从邮局出发，到自己所管辖的所有邮筒取信件，然后带回邮局。

Smith是一个非常标新立异的人，他希望每天都能走不同的路线，但是同时，他又不希望路线的长度增加，他想知道他有多少条不同的路线可走。

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

l 你需要根据给出的输入，计算出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 Hash_Sheet
2 {
3     Data_Analysis val[mod];//本题（cogs1283）还要打高精度，所以比较毒瘤233
4     int key[mod],size,hash[mod];
5     inline void Initialize()
6     {
7         memset(val,0,sizeof(val)),memset(key,-1,sizeof(key));
8         size=0,memset(hash,0,sizeof(hash));
9     }
10     inline void Newhash(int id,int v){hash[id]=++size,key[size]=v;}
11     Data_Analysis &operator [](const int State)
12     {
13         for(int i=State%mod;;i=(i+1==mod)?0:i+1)
14         {
15             if(!hash[i])Newhash(i,State);
16             if(key[hash[i]]==State)return val[hash[i]];
17         }
18     }
19 }f[2];//每次清空Hash表，实现滚动```

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

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 }```

（还有一个注意点，记得写高精度！）

```  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);
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)
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)
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 }```

## BZOJ 2310: ParkII

Time Limit: 20 Sec  Memory Limit: 128 MB

## Description

Hnoi2007-Day1有一道题目 Park：给你一个 m * n 的矩阵，每个矩阵内有个

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```