AGC061 F Perfect String

毒瘤出题人,史诗加强 AGC 的 F……

然而我连原题都不会,所以只学了原题做法。

翻译一下题意就是给定一张循环网格图,求经过 \((0,0)\) 的闭合回路条数。

由于网格图中每一个位置都等价,所以转化成求所有闭合回路长度之和。

考虑回路的形态,由于必须要走到右边界/下边界才可以回到起点,所以相当于是从左边界/上边界的若干个插头走到右边界/下边界的若干个插头且路径不交的方案数。

这是经典的 \(\text{LGV Lemma}\) 的形式。显然插头只会按照顺序匹配,否则一定有交叉。我们对相对的行和相对的列的插头编上同样的编号(这样子做的原因以下会说),那么 \((-1)^{\mathrm{inv}(p)}\) 始终是 \((-1)^{ij}\)

有些插头的放置方案会导致连出的不止一个环,然而由于插头总是按顺序匹配,所以连出一个环当且仅当 \(\gcd(r,c)=1\),其中 \(r,c\) 是放插头的行数和列数。

直接做对于每一个行的子集和列的子集求答案,指数级显然不现实。

然而一组子集的贡献只与 \(r,c\) 有关,所以我们考虑计算一个改造一下原 \(\text{DAG}\)

AT_Picture

对每一个插头新建原汇点,然后加入额外边,如果走新加入的额外边代表不选这个行/列,否则乘上一个占位符 \(x/y\)

加入额外边逆序对数看起来会变,然而由于我们相对的插头编号一样,走额外边相当于加入自环,排列的奇偶性又只跟环的个数及环长有关,所以加入一些自环永远不会改变排列的奇偶性。

接下来总不至于带着占位符求行列式吧?二元多项式相乘还要求逆想想就可怕。

然而我们可以求出这个二元多项式 \((n+1)\times (m+1)\) 个点值进行二元多项式插值。

具体地,我们仿照一元拉格朗日插值构造二元多项式 \(F\)

\[F(x,y)=\sum_{i=0}^n \sum_{j=0}^m z_{i,j} \prod_{i'\neq i} \frac{x-x_{i'}}{x_i-x_{i'}} \prod_{j'\neq j} \frac{y-y_{j'}}{y_j-y_{j'}} \]

其中 \(z_{i,j}=F(x_i,x_j)\)

具体怎么计算这个式子呢?

我们考虑拉格朗日插值类似 \(\text{IDFT}\) 是一个线性变换,相当于乘上了一个范德蒙德矩阵的逆矩阵。

类似二元 \(\text{DFT}\) 或者 \(\text{FWT}\) 处理高维多项式的思想,各维之间独立,那么我们先对一维进行线性变换,然后再对进行线性变换后的结果对另一维执行线性变换。

就比如说这里 \(z_{i,j}\),我们先把 \(z\) 看成 \(n+1\) 个行向量,对这个行向量进行拉格朗日插值,把插值得到的系数塞回原来的位置,然后把 \(z\) 竖着的列看成列向量,再拉格朗日插值一遍塞回原来的位置。此时 \(z\) 就变成了二元多项式的系数表示。

这启发我们 \(\text{FWT}\) 的核心思想就是线性变换关于各维独立,所以逐维进行线性变换就是对的,对一维做完后把结果塞回原来的位置再做另一维。

还有不清楚的可以直接推式子:

\[F(x,y)=\sum_{i=0}^n (\sum_{j=0}^m z_{i,j} \prod_{j'\neq j} \frac{y-y_{j'}}{y_j-y_{j'}}) \prod_{i'\neq i} \frac{x-x_{i'}}{x_i-x_{i'}} \]

发现括号内的东西就是一元拉插的式子,设进行插值后的结果是 \(G_i(y)=\sum_{j=0}^m g_{i,j} y^j\)

\[F(x,y)=\sum_{i=0}^n \sum_{j=0}^m g_{i,j} y^j \prod_{i'\neq i} \frac{x-x_{i'}}{x_i-x_{i'}} =\sum_{j=0}^m y^j (\sum_{i=0}^n g_{i,j} \prod_{i'\neq i} \frac{x-x_{i'}}{x_i-x_{i'}}) \]

括号又是一元拉插的式子,再插一下就做完了。

#include <cstdio>
#include <algorithm>
using namespace std;
int read(){
	char c=getchar();int x=0;
	while(c<48||c>57) c=getchar();
	do x=(x<<1)+(x<<3)+(c^48),c=getchar();
	while(c>=48&&c<=57);
	return x;
}
const int P=998244353;
const int N=103,Lim=1003;
typedef long long ll;
int n,m;
int fac[Lim],fiv[Lim],inv[Lim];
int qp(int a,int b=P-2){
	int res=1;
	while(b){
		if(b&1) res=(ll)res*a%P;
		a=(ll)a*a%P;b>>=1;
	}
	return res;
}
int comb(int a,int b){
	return (ll)fac[a+b]*fiv[a]%P*fiv[b]%P;
}
void inc(int &x,int v){if((x+=v)>=P) x-=P;}
void dec(int &x,int v){if((x-=v)<0) x+=P;}
int a[N][N];
int deter(int len){
	bool sgn=0;
	for(int i=1;i<=len;++i){
		int p=i;
		while(p<=len&&!a[p][i]) ++p;
		if(p!=i){
			for(int j=i;j<=len;++j) swap(a[i][j],a[p][j]);
			sgn=!sgn;
		}
		int iv=qp(a[i][i]);
		for(int j=i+1;j<=len;++j){
			int tmp=(ll)iv*a[j][i]%P;
			for(int k=i;k<=len;++k)
				dec(a[j][k],(ll)tmp*a[i][k]%P);
		}
	}
	int res=1;
	if(sgn) res=P-1;
	for(int i=1;i<=len;++i) res=(ll)res*a[i][i]%P;
	for(int i=1;i<=len;++i)
		for(int j=1;j<=len;++j) a[i][j]=0;
	return res;
}
int solve(int x,int y){
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j)
			a[i][j+n]=(ll)comb(n-i,j-1)*x%P;
		for(int j=i;j<=n;++j)
			a[i][j]=((ll)comb(j-i,m-1)*x+(i==j))%P;
	}
	for(int i=1;i<=m;++i){
		for(int j=1;j<=n;++j)
			a[i+n][j]=(ll)comb(j-1,m-i)*y%P;
		for(int j=i;j<=m;++j)
			a[i+n][j+n]=((ll)comb(n-1,j-i)*y+(i==j))%P;
	}
	return deter(n+m);
}
void init(int len){
	inv[1]=1;
	for(int i=2;i<=len;++i) inv[i]=(ll)inv[P%i]*(P-P/i)%P;
	fac[0]=1;
	for(int i=1;i<=len;++i) fac[i]=(ll)fac[i-1]*i%P;
	fiv[len]=qp(fac[len]);
	for(int i=len;i;--i) fiv[i-1]=(ll)fiv[i]*i%P;
}
int b[N][N];
int c[N],d[N],e[N],sz;
void mul(int v,int s){
	for(int i=++sz;i;--i){
		d[i]=(ll)d[i]*v%P;
		inc(d[i],d[i-1]);
	}
	d[0]=(ll)d[0]*v%P;
	for(int i=0;i<=sz;++i) d[i]=(ll)d[i]*s%P;
}
void lagrange(int len){
	for(int i=0;i<=len;++i) e[i]=0;
	for(int i=0;i<=len;++i){
		d[sz=0]=c[i];
		for(int j=0;j<i;++j) mul(P-j,inv[i-j]);
		for(int j=len;j>i;--j) mul(P-j,P-inv[j-i]);
		for(int j=0;j<=len;++j) inc(e[j],d[j]);
		while(sz) d[sz--]=0;
	}
}
int gcd(int a,int b){
	if(!b) return a;
	return gcd(b,a%b);
}
int main(){
	init(1000);
	n=read();m=read();
	if(n==1||m==1){puts("2");return 0;}
	if(n==2){printf("%d\n",(qp(2,m-1)+qp(2,m-2)+1)%P);return 0;}
	if(m==2){printf("%d\n",(qp(2,n-1)+qp(2,n-2)+1)%P);return 0;}
	for(int i=0;i<=n;++i){
		for(int j=0;j<=m;++j) c[j]=solve(i,j);
		lagrange(m);
		for(int j=0;j<=m;++j) b[i][j]=e[j];
	}
	for(int j=0;j<=m;++j){
		for(int i=0;i<=n;++i) c[i]=b[i][j];
		lagrange(n);
		for(int i=0;i<=n;++i) b[i][j]=e[i];
	}
	int res=0;
	for(int i=0;i<=n;++i)
		for(int j=0;j<=m;++j){
			if(!i&&!j) continue;
			if(gcd(i,j)==1)
				if(i&j&1) dec(res,(ll)(i*m+j*n)*b[i][j]%P);
				else inc(res,(ll)(i*m+j*n)*b[i][j]%P);
		}
	res=(ll)res*qp(n*m)%P;
	printf("%d\n",res);
	return 0;
}

拉插实现的比较丑,只写了 \(O(n^4)\),可以做到更优复杂度但没必要。

posted @ 2023-03-17 07:47  yyyyxh  阅读(42)  评论(0编辑  收藏  举报