AGC061 F Perfect String

2023-07-11,,

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

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

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

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

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

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

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

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

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

对每一个插头新建原汇点,然后加入额外边,如果走新加入的额外边代表不选这个行/列,否则乘上一个占位符 \(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)\),可以做到更优复杂度但没必要。

AGC061 F Perfect String的相关教程结束。

《AGC061 F Perfect String.doc》

下载本文的Word格式文档,以方便收藏与打印。