Luogu P4727 [HNOI2009]图的同构记数
题意与数据范围
求 \(n\) 个点不同构的简单无向图的数目,答案对 \(997\) 取模
\(A\) 图与 \(B\) 图被认为是同构的是指:\(A\) 图的顶点经过一定的重新标号以后,\(A\) 图的顶点集和边集要完全与 \(B\) 图一一对应
\(0\le n\le 60\)
Solution
我们把无向图点的每一种重新排布的方式看作一种置换,则该置换群 \(G\) 的大小显然为 \(n!\)
对于置换群 \(G\) 中的每一个置换 \(g\) ,在 \(g\) 的作用下的不动点即为这样的一种连边方案:
其中 \((a,b)\) 表示一条从 \(a\) 到 \(b\) 的无向边,\((V,E)\) 表示一张 \(V\) 个节点,\(E\) 条边的无向图,\(P_x\) 表示在置换 \(g\) 中编号为 \(x\) 的点所对应的置换
定义 \(X\) 为所有连边方案的集合,我们把在置换 \(g\) 下拥有上述性质的方案集合称为 \(X^g\) ,那么根据 \(\text{Burnside}\) 引理,答案即为 \(\frac{1}{|G|}\sum\limits_{g\in G}|X^g|\)
考虑如何计算 \(|X^g|\)
首先我们来考虑一下对于一个置换 \(g\) ,我们将它分解成若干个循环的乘积后,每一个循环内部的不动点如何计算
假设现在我们有一个循环 \(A\),其中第 \(i\) 个元素为 \(A_i\),假如 \(A_i\) 与 \(A_j\) 之间有一条边,那么所有下标“相距” \(|i-j|\) 的元素之间都必须有一条边,所以共有 \(\lfloor \frac{x}{2} \rfloor\) 种不同类型的边,对于同一种类型的边,我们要不都不连,要么都连,所以对于一个大小为 \(x\) 的循环,内部有 \(2^{\lfloor \frac{x}{2} \rfloor}\) 种连边方案
现在我们再考虑两个大小分别为 \(x\) 、\(y\) 的循环之间的影响
显然,若我们在 \(x\) 中的第 \(i\) 个元素与在 \(y\) 中的第 \(j\) 个元素之间连了一条边,那么对于 \((i+1,j+1),(i+2,j+2)...\) 直至 \(i\) 与 \(j\) 再次连边。那么一共要连 \(lcm(x,y)\) 条边,而我们一共有 \(xy\) 种对应方案,所以一共有 \(\frac{xy}{lcm(x,y)}\) 种边,即 \(\gcd(x,y)\) 种边,所以这些循环之间产生的贡献就是 \(2^{\gcd(x,y)}\)
那么我们现在可以考虑枚举置换 \(g\) ,设其分解成的第 \(i\) 个循环的元素集合为 \(g_i\) ,那么答案就是
不幸的是,由于要枚举整个置换群,这样的复杂度是 \(O(n!)\) 的
我们换个角度考虑
可以发现,我们只关心置换在分解成若干个循环的乘积后每个循环的大小,而并不在意这些循环究竟包含了哪些元素,所以我们可以考虑枚举每个置换可能是由哪些循环乘起来的
这个可以通过搜索求出,复杂度是自然数划分的方案数,可以接受
对于一种大小为 \(k\) 的划分方案,设 \(L_i\) 表示其中第 \(i\) 个循环的长度
首先考虑为这 \(k\) 个循环安排它们的位置,这步的方案数是带重复元素的排列数,即 \(\dbinom{n}{L_1\ L_2\ L_3 \ ...\ L_k}\)
然后我们再来考虑这 \(k\) 个循环内部的安排方式
直接 \(L_i!\) 肯定是不行的,因为这样无法保证它不能再被分解成更小的循环
但其实这也很简单,我们只要每次选出一个元素并在除了它以外且没有选过的元素中选择一个就可以了,所以这一步的方案数实际上是 \((L_i-1)!\)
两式相乘,得 \(\frac{n!}{\prod\limits_{i=1}^{k}L_i}\)
但这还不止,如果我们枚举到两个大小相同的循环,那么我们会把它们交换后的方案也算上,举例而言,就是:
如果有两个循环 \((2\ 1)(4\ 3)\) ,我们把它们交换一下,有 \((4\ 3)(2\ 1)\) ,然而这两个玩意儿是本质相同的,所以如果用 \(C_i\) 来表示一个置换中长度为 \(i\) 的循环的个数,那么方案数最后还得除上 \(\prod\limits_{i=1}^{n}C_i!\)
所以我们最后得到将 \(k\) 个长度分别为 \(L_1,L_2,...,L_k\) 的循环安放进去的方案数为
而安放完这些循环后,我们还得再乘上之前分析过的循环内部及循环之间产生的贡献数,得到最后的答案为
其中 \(|G|\) 与 \(n!\) 抵消了,然后直接计算即可
复杂度为 \(O(B_n\times n^2)\) ,\(B_n\) 为 \(n\) 的自然数划分的方案数
代码如下:
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int N=1e2+10;
const int mod=997;
int n,L[N],bin[N],fac[N],gcd[N][N],ans,C[N];
inline void Add(int &x,int y){x+=y;x-=x>=mod? mod:0;}
inline int MOD(int x){x-=x>=mod? mod:0;return x;}
inline int Minus(int x){x+=x<0? mod:0;return x;}
inline int exgcd(int x,int y){int r;if(y)swap(x,y);while(x&&y)r=x%y,x=y,y=r;return x;}
inline int fas(int x,int p){int res=1;while(p){if(p&1)res=1ll*res*x%mod;p>>=1;x=1ll*x*x%mod;}return res;}
inline void Calc(int m){
int sum=1;
for(register int i=1;i<=m;i++)sum=1ll*sum*bin[L[i]>>1]%mod;
for(register int i=1;i<=m;i++)
for(register int j=i+1;j<=m;j++)
if(i!=j)sum=1ll*sum*bin[gcd[L[i]][L[j]]]%mod;
int fm=1;
for(register int i=1;i<=m;i++)fm=1ll*fm*L[i]%mod;
memset(C,0,sizeof(C));
for(register int i=1;i<=m;i++)C[L[i]]++;
for(register int i=1;i<=n;i++)fm=1ll*fm*fac[C[i]]%mod;
Add(ans,1ll*sum*fas(fm,mod-2)%mod);
}
inline void DFS(int rest,int las,int num){
if(!rest){Calc(num-1);return;}
for(register int i=1;i<=min(rest,las);i++)
L[num]=i,DFS(rest-i,i,num+1);
}
inline void Preprocess(){
for(register int i=1;i<=n;i++)
for(register int j=1;j<=n;j++)
gcd[i][j]=exgcd(i,j);
fac[0]=1;for(register int i=1;i<=n;i++)fac[i]=1ll*fac[i-1]*i%mod;
bin[0]=1;for(register int i=1;i<=n;i++)bin[i]=2ll*bin[i-1]%mod;
}
int main(){
scanf("%d",&n);if(!n){puts("1");return 0;}
Preprocess();DFS(n,n,1);printf("%d\n",ans);
return 0;
}