*插头,真的插,插了又插*
像这样在一个n*m的棋盘上(n与m很小),求有多少种不同的回路数,或是用1条回路经过所有点或部分点的方案数,或是求一条路径上的权值和最大的问题。
通常称为插头dp。
这类问题通常很明显,但代码量大又容易出错,有时TLE有时MLE。
插头:对于一个4连通的问题来说,它通常有上下左右4个插头,一个方向的插头存在表示这个格子在这个方向可以与外面相连。
对于一个回路上的格子,必然是从一个方向进入另一个方向出去,共有图示6中可能。
解插头dp的一般方法是逐格递推,按照从上到下,从左到右的顺序依次考虑每一格
我们称图中的蓝线为轮廓线,任何时候只有轮廓线上方的格子才会对轮廓线以下的格子产生直接的影响。
以图中第三行第三列的格子D为例,假设逐格推到当前格子:
当左边有向右的插头,上边有向下的插头,那么D只能考虑左上插头。
当左边有向右的插头、上边向下的插头中有且只有一个,那么D可以接受到该方向的插头并有向右或向下两个插头的其中一个。
当左边上边都没有插头时,D可以有右下插头。
这三种情况就是在递推时要考虑的基本状态。
易知,对于m列的格子,轮廓线上有m+1个插头信息,m个格子的上方的插头信息,以及当前推到的格子左侧的插头信息。
那么对于一条路的问题,该如何保证最后在图中只有一个连通分量呢。
我们用最小表示法表示格子的连通性。
所有的障碍格子标记为0,第一个非障碍格子以及与它连通的所有格子标记为1,然后再找第一个未标记的非障碍格子以及与它连通的格子标记为2,……,重复这个过程,直到所有的格子都标记完毕。
当两个属于不同连通分量的格子合并到一起时,我们将所有属于这两个连通分量的格子的连通性更新,使其具有相同的值。
对于逐格递推,在每一行开始时,我们应该有一个数组,数组中存放着m个元素,即m列,上一行的每一列是否有向下的插头,即当前行是否有向上的插头。
在每个格子开始时,我们还应该知道这个格子左侧的是否有插头。
在每个格子结束时,我们要设置下一行同一列的格子的插头也要设置这个格子右边的插头。
为了处理方便,我们要把递推到每个格子时的轮廓线用一个整数来表示,这个过程成为 encode。
m列的格子,用一个m+1的数组code来表示轮廓线上的信息(包括连通性)。
对于当前的格子(i,j),code[j-1]中是它左侧格子的插头信息,code[j]中时它上方格子的插头信息。
我们根据这两个值枚举(i,j)的所有可能的状态,然后将 code[j-1]设为(i,j)下方格子的插头信息,code[j]设为(i,j)右侧插头信息。
将code数组编码为整数,作为(i,j+1)格子的起始状态。
当j已经是最后一列时,我们将code数组的所有元素向右平移,将第一个元素code[0]设为0。
这样对于一个新的一列,code数组就能表示它上方所有格子的信息。
对于涉及到连通性的问题,code数组储存的是插头的连通性。
对于不涉及连通性的问题,code数组储存插头的有无。
【例】Formula 1 [Ural1519]
给你一个m * n的棋盘,有的格子是障碍,问共有多少条回路使得经过每个非障碍格子恰好一次.m, n ≤ 12.
解题步骤
首先将棋盘读入,有障碍的格子设为0,没有障碍设为1,注意要将棋盘之外的格子都设为有障碍。
题目要求要经过所有的格子,这说明在某个格子形成闭合回路时,在它之后不会再有别的空格子了,因此形成闭合回路的格子必定是最右下的格子,用(ex,ey)表示。
memset(maze,0,sizeof(maze));
ex=ey=0;
for (int i=1;i<=n;i++){
scanf("%s",s+1);
for (int j=1;j<=m;j++){
if (s[j]=='.'){
maze[i][j]=1;
ex=i;
ey=j;
}
}
}
我们用两个hash表来储存当前格子轮廓线上所有可能的状态与下一个格子轮廓线所有可能的状态。
这种状态可能出现的次数记在 f 中。
struct HASHMAP{
int head[seed],next[maxn],size;
LL state[maxn];
LL f[maxn];
void clear(){
size=0;
memset(head,-1,sizeof(head));
}
void insert(LL st,LL ans){
int h=st%seed;
for (int i=head[h];i!=-1;i=next[i]){
if (state[i]==st){
f[i]+=ans;
return;
}
}
state[size]=st;
f[size]=ans;
next[size]=head[h];
head[h]=size++;
}
}hm[2];
递推完最后一个格子后,将所有可能的状态中的方案数相加即为答案,实际上对于本题来说,最终只会有一个状态,就是code数组中的元素全为0,因为最后行上不可能有向下的插头。
int cur=0;
LL ans=0;
hm[cur].clear();
hm[cur].insert(0,1);
for (int i=1;i<=n;i++){
for (int j=1;j<=m;j++){
hm[cur^1].clear();
if (maze[i][j]) dpblank(i,j,cur);
else dpblock(i,j,cur);
cur^=1;
}
}
for (int i=0;i
由于题目中m<=12,所以显然最多只有6个不同的连通分量。因此code数组中元素的值不应超过6,用3个二进制位来表示0~7的整数。
注意在压位的过程中,由于在合并不同连通性的插头时会消去一个连通分量,因此要对连通性的编号重新做处理,重新按1~cnt编码。
LL encode(int code[],int m){
LL st=0;
int cnt=0;
memset(ch,-1,sizeof(ch));
ch[0]=0;
for (int i=0;i<=m;i++){
if (ch[code[i]]==-1) ch[code[i]]=++cnt;
code[i]=ch[code[i]];
st<<=3;
st|=code[i];
}
return st;
}
void decode(int code[],int m,LL st){
for (int i=m;i>=0;i--){
code[i]=st&7;
st>>=3;
}
shift 函数将code中的所有元素向右移动一位。当j==m时需要这样做。
void shift(int code[],int m){
for (int i=m;i>0;i--) code[i]=code[i-1];
code[0]=0;
}
对于每个状态,先用 decode 解码出 code 数组。
那么它左侧的信息left=code[j-1],上方的信息up=code[j]。
按照解法中所说的情况进行讨论。
当左上有插头时,当两个插头属于相同的连通分量时,如果这个格子恰好是最后一个格子才能合并回路。两个插头不属于相同连通分量时,合并它们。
对于左边有一个插头或上方有一个插头的情况,判断右边或下边是否是障碍,如果不是的话就连接一个插头,这个插头跟接入这个格子的插头属于相同的连通分量。
对于没有插头的格子,那么他只能是一个新的连通分量,向右下设置插头。将新的连通分量设为一个不可能出现的最大值,当编码时会对它重新设置,不用担心溢出。
对于每一种讨论出的状态,将其加入下一个哈希表中。
当j==m时,在编码之前要进行shift,但是某些情况下j不可能等于m,因此不做shift操作也可以。
void dpblank(int i,int j,int cur){
int left,up;
for (int k=0;k
void dpblock(int i,int j,int cur){
for (int k=0;k
多回路经过所有格子的方案数。
不需要记录连通性,最简单的题,对所有空格子都考虑适当的插头即可。单回路经过所有格子的方案数。
记录连通性的入门题,要保证只在最后一个格子形成回路。
单回路数,格子有了三种:障碍格子、必选格子和可选格子。
在编码时添加一位标志,表示是否形成了回路,如果形成了回路后还遇到了必选格子,就废弃掉这个状态。
求花费的题,在哈希时改为记录当前的最佳值。在最后一个格子判断才能形成回路。
从左上角走到右下角,可选格子,每个格子有个分数,求最大分数。
对左上和右下的格子单独进行处理,左上的格子只能有向下或向右的插头,而右下的格子只能有向上和向左的插头,不能形成回路。
在最后添加两行,倒数第二行中间部分全设置为障碍。然后求一条回路的方案数即可。
对2和3的点单独进行考虑,格子上不能形成回路,而且只有出入两种可能,加上方向只有四种可能。
连通性只有两个选择2或3。对于不确定2还是3的普通格子就两个都尝试一下。
将格子行列颠倒一下,发现一个格子有6个方向的插头,左右,加上上边两个下边两个。
将code数组扩展成两倍,一个格子占用code数组中的三个元素,code[2*j-2]为左边的插头,code[2*j-1]为左上的插头,code[2*j]为右上的插头。
按照不同情况进行推导之后,将code[2*j-2]为左下的插头,code[2*j-1]为右下的插头,code[2*j]为右边的插头。这样下一个格子仍然可以从code中取到合适的信息。
而奇数行与偶数行的左下与右下坐标的计算方法是不同的,这个要注意处理,而且由于六边形的特殊性,只有偶数行才需要shift操作。
将当前的闭合回路数压入状态中。
对于环套环,如果当前格子左边有奇数个不同连通分量的插头,那么如果在左上形成闭合的回路,那么就会出现环套环的情况,只要对这种情况跳过即可。