ECNU 2018 - 棋盘染色

发布时间 2023-05-05 21:12:42作者: jucason_xu

首先看到这个数据范围限制,我们不难猜到是状压 \(dp\),首先就猜测复杂度应该是 \(O(nmS)\)\(S\) 是状压大小,和 \(m\) 有关的某个非多项式的级数。

错误的思考过程

然后我们就很快能找到一个状压 \(dp\) 的方法。我们可以记录当前一列的黑白染色情况和连通块的集合划分。粗略估计是 \(\sum_{i=0}^{m}\binom{m}{i}Bell_iBell_{m-i}\) 的,很寄就是了。

接下来我们考虑如何节省无用的状态。首先,我们发现,在当前列相邻的两个同色的格子是一定是相同连通块的。那么我们可以把状态中相邻的相同颜色合并。因此,每个状态中,最多只有四个相同颜色的部分需要划分。而 \(Bell_4\) 是很小的,只有 \(15\)。而且取到 \(4\) 的条件是非常苛刻的。如果我们仔细计算一下,我们会发现合法的状态一共就只有 \(3060\) 种。

由此我们就欣喜若狂了,我们的复杂度似乎可以做到 \(3.06\cdot 10^7\) 次运算,这是非常优秀的。但是一旦我们冷静下来就会发现,这个东西的转移非常困难。

首先,这些序列和状态之间没有好的哈希关系,这就需要我们在程序的一开始暴力枚举从而对状态进行编码。其次,我们为了 \(Bell_4\) 分开了白和黑,这就导致将其重构成序列再压缩回集合划分的步骤繁琐。

其次,我们每次都需要重新构造序列,然后再把集合划分序列转成哈希值存进状态里。这个过程是 \(O(m)\) 的。并且转移很多步骤都非常苛刻,难以统一,需要分类讨论很多。

总而言之,一开始不顾转移对状态的过度精简,导致这个程序怀着美好的梦想但几乎无法实现。下面贴一下按照这个思路实现出来的代码,\(Bug\) 连篇,可能什么时候码力上升了会试着回来调一下。但现在还是算了。

ll n,m,P;
int bel[5]={1,1,2,5,15};

int stac;
int divc;
int divt[4][4][4][4];
int stat[256][15][15];
array<int,4> divh[15];
array<int,3> stah[3060];

int cb[2][256];
namespace init{
	inline void init_div(){
		array<int,4>ori;
		vt<array<int,4> >res;
		function<void(int,int,array<int,4>)>dfs=[&](int x,int y,array<int,4>cur) {
			if(x==4)res.pb(cur);
			else rep(j,0,y+1)cur[x]=j,dfs(x+1,max(j,y),cur);
	    };
		dfs(0,-1,ori);
		sort(res.begin(),res.end(),[](array<int,4>a,array<int,4>b){
			if(a[3]!=b[3])return a[3]<b[3];
			if(a[2]!=b[2])return a[2]<b[2];
			if(a[1]!=b[1])return a[1]<b[1];
			return a[0]<b[0];
		});
		for(auto a:res){
			divt[a[0]][a[1]][a[2]][a[3]]=divc;
			divh[divc++]=a;
		}
	}
	inline void init_sta(){
		for(int msk=0;msk<(1<<m);msk++){
			cb[msk&1][msk]++;
			rep(j,1,m-1)cb[msk>>j&1][msk]+=((msk>>j&1)!=(msk>>(j-1)&1));
			for(int i=0;i<bel[cb[0][msk]];i++){
				for(int j=0;j<bel[cb[1][msk]];j++){
					stat[msk][i][j]=stac;
					stah[stac++]={msk,i,j};
				}
			}
		}
	}
};
using namespace init;

int dp[3060][4],f[3060][4],color[8],cur[8],num[8];
int cnt[2][5];
inline void restruct(int &a,int &b){
	array<int,4>A={0,0,0,0},B={0,0,0,0};
	int ca=0,cb=0,cA=0,cB=0;
	rd(i,2)rd(j,5)cnt[i][j]=0;
	rep(i,0,7)if(i==0||color[i]!=color[i-1]){
		if(color[i]){
			if(!cnt[1][cur[i]])cnt[1][cur[i]]=ca++;
			A[cA++]=cnt[1][cur[i]];
		}else{
			if(!cnt[0][cur[i]])cnt[0][cur[i]]=cb++;
			B[cB++]=cnt[0][cur[i]];
		}
	}
	a=divt[A[0]][A[1]][A[2]][A[3]];
	b=divt[B[0]][B[1]][B[2]][B[3]];
}
inline void add(int &x,int y){
	x=(x+y)%P;
}
inline void solve(int y){
	//现在要填第y列
	rd(msk,(1<<m))rd(bl,bel[cb[0][msk]]-1)rd(wh,bel[cb[1][msk]]-1)rd(sa,4){
		if(!f[stat[msk][bl][wh]][sa])continue;
		int res=f[stat[msk][bl][wh]][sa];
		//现在安排的是这样的。
		//分类现在是0还是1 
		int n[2]={0,0},cnt=0;
		rd(i,m){
			if(msk>>i&1){
				color[i]=1,num[i]=n[1];
				cur[i]=divh[bl][n[1]];
			}else{
				color[i]=0,num[i]=n[0];
				cur[i]=divh[wh][n[0]];
			}
			if(i!=7&&(msk>>i&1)!=(msk>>(i+1)&1))n[msk>>i&1]++;
		}
		rd(i,m)if(color[i]==color[y]&&cur[i]==cur[y])cnt++;
		if(y!=0&&sa!=cur[y-1])cnt++;
		//左边是什么 
		if(color[y]){
			if(y==0){
				add(dp[stat[msk][bl][wh]][sa],res);
				if(y!=8&&color[y+1]){
					add(dp[stat[msk][bl][wh]][sa],res);
				}else if(cnt!=1){
					int nb,nw;
					color[y]=0,cur[y]=4;
					restruct(nb,nw);
					add(dp[stat[msk^(1<<y)][nb][nw]][(y==8?cur[0]:color[y+1])],res);
				}
			}else if(y==0||!color[y-1]){
				add(dp[stat[msk][bl][wh]][sa],res);
				if(y!=8&&color[y+1]){
					add(dp[stat[msk][bl][wh]][sa],res);
				}else if(cnt!=1){
					int nb,nw;
					color[y]=0,cur[y]=cur[y-1];
					restruct(nb,nw);
					add(dp[stat[msk^(1<<y)][nb][nw]][(y==8?cur[0]:cur[y+1])],res);
				}
			}else{//上面是1 
				if(sa==cur[y-1]){//上面连通块一样 
					//填1
					add(dp[stat[msk][bl][wh]][sa],res);
					//填0 
					int nb,nw;
					color[y]=0,cur[y]=4;
					restruct(nb,nw);
					add(dp[stat[msk^(1<<y)][nb][nw]][(y==8?cur[0]:cur[y+1])],res);
				}else{//上面连通块不一样 
					//填0 
					if(cnt!=1){
						int nb,nw;
						color[y]=0,cur[y]=4;
						restruct(nb,nw);
						add(dp[stat[msk^(1<<y)][nb][nw]][(y==8?cur[0]:cur[y+1])],res);
					}
					//填1
					int nb,nw,dc=cur[y-1],df=cur[y];
					rd(j,8)if(color[j]&&cur[j]==df)cur[j]=dc;
					restruct(nb,nw);
					add(dp[stat[msk][nb][nw]][(y==8?cur[0]:cur[y+1])],res);
				}
			}
		}else{//左边是0
			if(y==0){
				add(dp[stat[msk][bl][wh]][sa],res);
				if(y!=8&&!color[y+1]){
					add(dp[stat[msk][bl][wh]][sa],res);
				}else if(cnt!=1){
					int nb,nw;
					color[y]=1,cur[y]=4;
					restruct(nb,nw);
					add(dp[stat[msk^(1<<y)][nb][nw]][(y==8?cur[0]:cur[y+1])],res);
				}
			}else if(color[y-1]){
				add(dp[stat[msk][bl][wh]][sa],res);
				if(y!=8&&!color[y+1]){
					add(dp[stat[msk][bl][wh]][sa],res);
				}else if(cnt!=1){
					int nb,nw;
					color[y]=1,cur[y]=cur[y-1];
					restruct(nb,nw);
					add(dp[stat[msk^(1<<y)][nb][nw]][(y==8?cur[0]:cur[y+1])],res);
				}
			}else{//上面是0
				if(sa==cur[y-1]){//上面连通块一样 
					//填0
					add(dp[stat[msk][bl][wh]][sa],res);
					//填1
					int nb,nw;
					color[y]=1,cur[y]=4;
					restruct(nb,nw);
					add(dp[stat[msk^(1<<y)][nb][nw]][(y==8?cur[0]:cur[y+1])],res);
				}else{//上面连通块不一样 
					//填01
					if(cnt!=1){
						int nb,nw;
						color[y]=1,cur[y]=4;
						restruct(nb,nw);
						add(dp[stat[msk^(1<<y)][nb][nw]][(y==8?cur[0]:cur[y+1])],res);
					}
					//填1
					int nb,nw,dc=cur[y-1],df=cur[y];
					rd(j,8)if(!color[j]&&cur[j]==df)cur[j]=dc;
					restruct(nb,nw);
					add(dp[stat[msk][nb][nw]][(y==8?cur[0]:cur[y+1])],res);
				}
			}
		}
	} 
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n>>m>>P;
	init_div();
	init_sta();
	rd(i,(1<<m))f[stat[i][bel[cb[0][i]]-1][bel[cb[1][i]]-1]][0]=1;
	rep(x,1,n-1)rep(y,0,m-2){
		solve(y);
		rd(i,stac)rd(j,4)f[i][j]=dp[i][j],dp[i][j]=0;
	}
	int ans=0;
	rd(i,(1<<m))rd(j,4)add(ans,f[stat[i][0][0]][j]);
	cout<<ans<<endl;
	return 0;
}
//Crayan_r

便于实现的思路

既然一开始,我们就把状态设计的过于精简了,我们不如用别的方式。比如,我们可以用 \(Bell_8\)\(Bell_8\) 其实也不大,只有 \(4000\) 左右,但它胜在从集合划分可以唯一映射到黑白染色方案。轮廓线 \(dp\) 的话,虽然会导致轮廓线处出现相同颜色不同集合,但是我们可以直接用一个 bool 变量记录在状态里。

然后,\(8\) 的集合划分也同样无序啊?但是我们可以把它编码了。首先,我们默认的集合划分是一个序列,表示每一位属于哪个集合。因为集合之间是不区分的,所以我们可以钦定第一个出现的是 \(0\),第二个是 \(1\)。。。这样。我们发现,集合划分映射到一个长度 \(8\) 的串,第 \(i\) 位的值域是 \([0,i)\)

这样,我们就可以哈希编码了。编码的总值域是 \(8!=40320\),是上一个做法的 \(10\) 倍还要多。但是多出的这部分无关紧要,因为在常数部分就能补回来,而且稍微调大的状态数带来的是好写的转移。

然后,转移就比较方便。我们可以把转移拆成解码、转移和编码,解码部分就是把当前的状态解出黑白染色方案和集合划分方案,在同一列中,我们钦定第一个为黑色。转移的时候,通过枚举当前选择的颜色 \(t\),存在四种情况:

  • 和上面相同,和左面相同,则合并上面和左面的集合

  • 和上面相同,和左面不同,首先要左面的集合在轮廓上还有其他出现,然后覆盖掉当前位置

  • 和上面不同,和左面相同,等于什么都没有改变

  • 和上面不同,和左面不同,还是要左边的集合在轮廓上有其他出现,然后建立新的划分集合。

这些转移都非常方便,可以两行左右写完。

然后着重是统计答案的问题。为了不算重,我们钦定凡是一列全是同一种颜色的都不再往下转移或者统计答案。也就是我们只统计整个图形黑白都有的列。

我们在每次 \(dp\) 完一列之后,先统计所有的黑白都有的答案,看看能否右边剩下的全填白色或者黑色。然后加入新的状态——左边全都是黑色,进行下一轮转移。尤其注意的是,在最后一列统计答案的时候,因为右边不会有东西了,所以我们强制要求有且仅有两个集合。其他的列上统计答案,只要某个颜色只有一个集合就可以计算一次贡献。

然后因为我们一开始钦定了左下角的颜色是黑色,所以方案乘以二。还要加上全黑或全白,得到最终答案。

ll n,m,P;
inline void add(ll &x,ll y){
	x+=y;
	if(x>=P)x-=P;
}
int a[9],x[8],cnt[16];
inline int incode(){
	for(int i=0;i<16;i++)cnt[i]=0;
	int tp=0;
	rd(i,m){
		if(!cnt[x[i]])cnt[x[i]]=++tp;
		x[i]=cnt[x[i]]-1;
	}
	int res=0;
	rep(i,0,m-1)res=res*(i+1)+x[i];
	return res;
}
inline void decode(int c,int y){
	per(i,0,m-1){
		x[i]=c%(i+1);
		c/=(i+1);
	}
	a[0]=0;
	rep(i,1,m-1)if(x[i]!=x[i-1]){
		a[i]=a[i-1]^(i!=y);
	}else a[i]=a[i-1];
}
ll dp[50005][2],f[50005][2],N=50000;
inline void solve(int i){
	rd(msk,N)rd(j,2)if(f[msk][j])rd(t,2){
		decode(msk,i*j);
		int cnt=0;
		rd(p,m)if(x[p]==x[i])cnt++;
		if(i!=0&&a[i-1]==t&&a[i]==t){
			int fg=x[i-1],ft=x[i];
			rd(p,m)if(x[p]==ft)x[p]=fg;
			add(dp[incode()][a[i+1]==t],f[msk][j]);
		}else if(i!=0&&a[i-1]==t&&a[i]!=t){
			if(cnt!=1){
				x[i]=x[i-1];
				add(dp[incode()][a[i+1]==t],f[msk][j]);
			}
		}else if(a[i]==t){
			add(dp[msk][a[i+1]==t],f[msk][j]);
		}else if(cnt!=1){
			x[i]=8;
			add(dp[incode()][a[i+1]==t],f[msk][j]);
		}
	}
}
inline ll calc(){
	ll res=0;
	for(int cur=1;cur<N;cur++){
		decode(cur,0);
		int la=-1,lb=-1,fa=1,fb=1;
		for(int i=0;i<m;i++){
			if(a[i]){
				if(la==-1)la=x[i];
				else if(la!=x[i])fa=0;
			}else{
				if(lb==-1)lb=x[i];
				else if(lb!=x[i])fb=0;
			}
		}
		if(fa)add(res,f[cur][0]);
		if(fb)add(res,f[cur][0]);
	}
//	cout<<res<<endl;
	return res;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin>>n>>m>>P;
	if(n==1){
		cout<<2*m%P<<endl;
		return 0;
	}
	if(m==1){
		cout<<2*n%P<<endl;
		return 0;
	}
	for(int msk=0;msk<(1<<(m-1));msk++){
		x[0]=0;
		rd(i,m)a[i]=(msk>>i&1);
		rep(i,1,m-1)x[i]=x[i-1]+(a[i]!=a[i-1]);
		f[incode()][0]=1;
	}a[m]=114514;
	ll ans=calc();
	rep(_,1,n-1){
		rep(i,0,m-1){
			solve(i);
			x[0]=0,x[1]=1;
			rep(i,0,N-1)rd(j,2)f[i][j]=dp[i][j],dp[i][j]=0;
		}
		if(_!=n-1)add(ans,calc());
		f[0][0]=1;
	}
	for(int cur=1;cur<N;cur++){
		decode(cur,0);
		int la=-1,lb=-1,fa=1,fb=1;
		for(int i=0;i<m;i++){
			if(a[i]){
				if(la==-1)la=x[i];
				else if(la!=x[i])fa=0;
			}else{
				if(lb==-1)lb=x[i];
				else if(lb!=x[i])fb=0;
			}
		}
		if(fa&&fb&&f[cur][0]){
			add(ans,f[cur][0]);
		}
	}
	ans=(ans+n)%P;
	cout<<(ans*2)%P<<endl;
	return 0;
}
//Crayan_r