插头DP,哈希判重,高精度
BZOJ 1210: [HNOI2004]邮递员
输出走完N*M个方格的回路数(注意回路有方向要乘2,特判行列是1的情况)
解释:当前转移到第I行第J 格,读到第J状态是左边插头,第J+1个状态是上边插头,如果这个S不变,则左边插头就会变为下边插头,上边插头就会变成右边插头,对右边及下面的格产生影响。一句话,当前格读入的J+1状态是上边插头,但更新之后的J+1状态是右边格的左边插头。J状态同理。
转移方程解释:设当前转移到格子(x,y),设y-1号插头状态为p1,y号插头状态为p2。
情况1:p1 == 0 && p2 == 0.新建一个新路径:下插头设为左括号插头,右插头设为右括号插头
情况2:p1 == 0 && p2 != 0.可以选择“直走”和“转弯”两种策略
情况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修改成右括号插头。
需要用bitint的解释:因为方案数没有取模
需要用HASH的解释:因为空间复杂度!首先若用的是4进制表示,如果用f[i][j][s]来表示状态的话,复杂度为O(nm4^(m+1)),
然而空间限制64M,挂定。使用滚动数组:复杂度:O(2*4^ (m+1))==O(2^(2m+3)),也挂了。所以要用Hash,不是每一种状态都存,而是出现的状态才进行存储,毕竟有很多状态是不合法的,例如某一状态的两个位就不会同时为1,因为没有定义3状态。
#include<bits/stdc++.h>
using namespace std;
const int base=1e9;
const int maxhash=2601;//最大哈希
int get(int s,int x){return (s>>((x-1)<<1))&3;}//X-1是第X列低位状态,(x-1)<<1是第X列高位状态,&3是两位状态
void change(int &s,int p,int b){//改S的第P个状态为B
int w=p-1<<1;//如果是单位状态存储则p-1就是对应列,现在是双位所以要位移p-1<<1位,减法优先
s|=3<<w;//先把对应的那个状态的两个位都置1
s^=3<<w;//再把对应的那个状态的两个位都清0
s|=b<<w;//最后赋值为B
}
int find(int s,int p){//找到S的第P个插头状态的另一端,i是起始状态,d是当前括号匹配情况,step决定正走还是负走
for(int i=p,d=0,step=get(s,p)==1?1:-1;;i+=step){//get(s,p)读出来是左插头的话,STEP=1往右走,否则为-1往左走
d+=get(s,i)==1?1:-1;//每次读出走到的第I状态是不是左插头,是的话继续+1,如果是右插头或空插头就-1
d+=get(s,i)==0?1:0;//每次读出走到的第I状态是不是空插头,是就+1(跟上一行抵消),否则不动(就是右插头了)
if(!d)return i;//最后D为0即括号刚好匹配完了,那就走到的i位就是结果
}
return -1;//这句返回是不可能的
}
int n,m,now,last;
struct bigint{//高精度,用来存ans
int a[5];//int=2^32=4e9+,,所以用1E9来模
void clear(){memset(a,0,sizeof(a));}
bigint(){clear();}//构造函数
void set(int x){clear();while(x)a[++a[0]]=x%base,x/=base;};//当X未赋值完,就新增一位值记为x取模1e9,X自减
bigint operator + (bigint b){//重置+运算符,输入参数是B
static bigint c;c.clear();c.a[0]=max(a[0],b.a[0])+1;//读出A与B的最高位再+1,先假定会进位
for(int i=1;i<=c.a[0];i++){//逐位更新
c.a[i]+=a[i]+b.a[i];//当前位先等于两个位相加(不会超2E9,放心)
c.a[i+1]+=c.a[i]/base;//然后进位
c.a[i]%=base;//当前位取余数存下来
}
while(!c.a[c.a[0]])c.a[0]--;//最后再把多余前导0减去
return c;//返回C就是结果
}
void operator += (bigint b){*this=*this+b;}//重置+=运算符
void operator = (int x){set(x);}//重置=运算符,这里传入的x是int,若是bigint类赋给bigint类直接复制a数组就行
void print(){printf("%d",a[a[0]]);for(int i=a[0]-1;i>0;i--)printf("%09d",a[i]);}//定义输出函数
//a[0]存的是一共有多少位,所以先输出a[a[0]]最高位,然后低位再依次输出
}ans;
struct hashtable{//哈希表,用来存f[2]两种状态,其实关键就是重置了[]二重运算符
int key[maxhash];
int cnt,hash[maxhash];
bigint val[maxhash];
void clear(){//三个数组全部清空
memset(key,-1,sizeof(key));//键,即状压得到的值
memset(hash,0,sizeof(hash));//即每个状压值存在表中的第几个位置
memset(val,0,sizeof(val));//键值,方案数
cnt=0;//哈希表当前的映射数目
}
void newnode(int i,int _key){hash[i]=++cnt;key[cnt]=_key;}//hash第I位就是第CNT个映射,KEY存第CNT个状压值,VAL存第CNT个状压值的方案数
bigint& operator[](const int _hash){//对[]运算符进行重置,传入的是int类型_hash键值即状压情况,返回bigint方案数
int k;
for(int i=_hash%maxhash;;i++){//状压值模上最大的状压数量得到其开始位置
if(i==maxhash)i=0;//如果达到了最大键的数量,就变为0,因为会有不同状态的状压值模后值相同,要处理冲突
//I++是为了防止冲突而往后移的操作,所以会有可能I=MAXHASH
if(!hash[i])newnode(i,_hash);//hash表第I位为空表示当前压状值未被打入,所以HASH表第I位更新有新映射CNT,KEY表存_hash
if(key[hash[i]]==_hash){k=hash[i];break;}//第I位的键的键值就是_hash,表明已经存在经状态,读出k是第i个映射
}//如果hash表第I位已被打入但是其状态值不是_hash就表示有冲突,被往后移了
return val[k];//返回第K个映射的方案数
}
}f[2];
void dp(int i,int j){//DP
now^=1;last^=1;//每到一格就互换
int tot=f[last].cnt;//上一次的哈希表一共有多少种状态
f[now].clear();//清空本次哈希表
for(int k=1,x,y;k<=tot;k++){//逐个映射扫一次
int s=f[last].key[k];//S是第K个映射的键,即状压值
bigint val=f[last].val[k];//val是第K个映射的键值,即方案数
x=get(s,j);y=get(s,j+1);//得到当前转移点的左边的插头与上边的插头
if(!x&&!y){//两个插头都是空插头且非最后一格则S状态的第J状态改为1,第J+1列状态改为2(多了一对括号)
if(i!=n&&j!=m){change(s,j,1),change(s,j+1,2),f[now][s]+=val;}//当前状态方案数累加
}else if(x==1&&y==1){//两个插头都是左插头(括号匹配的左括号)
change(s,find(s,j+1),1),change(s,j,0),change(s,j+1,0);//则第J+1状态连出去的那路径的另一端要转换为右插头
f[now][s]+=val;//J状态与J+1状态都不可能有插头射出
}else if(x==2&&y==2){//两个插头都是右插头(括号匹配的右括号)
change(s,find(s,j),2),change(s,j,0),change(s,j+1,0);//则J状态连出去的那路径的另一端要转换为右插头
f[now][s]+=val;//J状态与J+1状态都不可能有插头射出
}else if(x==1&&y==2){//左边的插头都是左插头,上边的插头是右插头
if(i==n&&j==m)ans+=val;//到终点累加答案,否则本状态方案数不更改
}else if(x==2&&y==1){//左边的插头都是右插头,上边的插头是左插头
change(s,j,0),change(s,j+1,0);//S状态的第J状态改为0,第J+1列状态改为0(消去括号)
f[now][s]+=val;
}else if(x){//只有左边的插头,上边的插头是空插头
if(i!=n)f[now][s]+=val;//不是最后一行,则当前左边的插头可变为向下指,对下一方格而言还是加S方案数
if(j!=m)change(s,j,0),change(s,j+1,x),f[now][s]+=val;//不是最后一列,则可以改下边插头为空,右边插头为X
}else if(y){//只有上边的插头,左边的插头是空插头
if(j!=m)f[now][s]+=val;//不是最后一列,则当前上边插头可以变为向右指,对下在方格而言还是加S方案数
if(i!=n)change(s,j+1,0),change(s,j,y),f[now][s]+=val;//不是最后一行,则可以改右边插头为空,下边插头为y
}//以上两种情况见文末图解!
}//f[now][s]+=val;}//当前状态方案数累加这一句其实每一种情况都有的就不重复注释了
}
int main(){
scanf("%d%d",&n,&m);//输入行列数
if(n==1||m==1){puts("1");return 0;}//特判行列是1的情况
if(n<m)swap(n,m);//保证N大于等于M,行比列多
now=0;last=1;//两个点互换
f[now][0]=1;//初始点没插头就有一个方案
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++)
dp(i,j);//在I,J格DP枚举所有状态,同时更新答案就在里面了
if(i!=n)//除了最后一行,每行行末都要执行以下操作
for(int j=1,k=f[now].cnt;j<=k;j++)//f[now].cnt当前哈希表的映射全部扫一次
f[now].key[j]<<=2;//里面的每个状态都往左移两位(因为这里是两位表示一个状态)
}
ans+=ans;//由于回路有方向所以答案直接翻倍就好了
ans.print();//类方法输出
return 0;
}
/*
Sample Input
2 2
说明:该输入表示,Smith管辖了2*2的一个邮筒点阵。
Sample Output
2
*/