Luogu P4727 [HNOI2009]图的同构记数

本文探讨了计算n个点的不同构简单无向图数量的问题,利用Burnside引理和置换群理论,提出了一种高效的算法,通过枚举置换群中的循环结构,计算在每个置换下的不动点数,最终得出在模997下的答案。
部署运行你感兴趣的模型镜像

题意与数据范围

\(n\) 个点不同构的简单无向图的数目,答案对 \(997\) 取模

\(A\) 图与 \(B\) 图被认为是同构的是指:\(A\) 图的顶点经过一定的重新标号以后,\(A\) 图的顶点集和边集要完全与 \(B\) 图一一对应

\(0\le n\le 60\)

Solution

我们把无向图点的每一种重新排布的方式看作一种置换,则该置换群 \(G\) 的大小显然为 \(n!\)

对于置换群 \(G\) 中的每一个置换 \(g\) ,在 \(g\) 的作用下的不动点即为这样的一种连边方案:
\[ \forall (a,b)\in (V,E),\exist (P_a,P_b)\in (V,E) \]
其中 \((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\) ,那么答案就是
\[ \frac{1}{|G|}\sum\limits_{g\in G}(\prod\limits_{i=1}^{|g|}2^{\lfloor \frac{|g_i|}{2} \rfloor}\prod\limits_{i<j\le |g|}2^{\gcd(|g_i|,|g_j|)}) \]
不幸的是,由于要枚举整个置换群,这样的复杂度是 \(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\) 的循环安放进去的方案数为
\[ \frac{n!}{\prod\limits_{i=1}^{k}L_i\prod\limits_{i=1}^{n}C_i!} \]
而安放完这些循环后,我们还得再乘上之前分析过的循环内部及循环之间产生的贡献数,得到最后的答案为
\[ \sum\limits_{\ \ \sum\limits_{i=1}^{k}L_i=n,\\L_1\ge L_2\ge...\ge L_k}\frac{\prod\limits_{i=1}^{k}2^{\lfloor \frac{L_i}{2} \rfloor}\prod\limits_{i<j\le k}2^{\gcd(L_i,L_j)}}{\prod\limits_{i=1}^{k}L_i\prod\limits_{i=1}^{n}C_i!} \]
其中 \(|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;
}

转载于:https://www.cnblogs.com/ForwardFuture/p/11478566.html

您可能感兴趣的与本文相关的镜像

HunyuanVideo-Foley

HunyuanVideo-Foley

语音合成

HunyuanVideo-Foley是由腾讯混元2025年8月28日宣布开源端到端视频音效生成模型,用户只需输入视频和文字,就能为视频匹配电影级音效

洛谷P1177是【模板】排序题,可使用归并排序来解决。归并排序的核心思想是分治法,即将一个大问题分解为多个小问题,分别解决后再合并结果。 归并排序主要步骤如下: 1. **分解**:将待排序数组从中间分成两个子数组,递归地对这两个子数组进行排序。 2. **合并**:将两个已排序的子数组合并成一个有序数组。 以下是使用归并排序解决洛谷P1177题目的代码实现: ```cpp #include<bits/stdc++.h> #include<iomanip> using namespace std; #define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) const int MAXN = 1e5 + 5; int a[MAXN], b[MAXN]; int n; // 数组长度 // 合并两个已排序的子数组 void mergesort(int l1, int r1, int l2, int r2) { int i = l1, j = l2, k = l1; while (i <= r1 && j <= r2) { if (a[i] <= a[j]) { b[k++] = a[i++]; } else { b[k++] = a[j++]; } } while (i <= r1) b[k++] = a[i++]; while (j <= r2) b[k++] = a[j++]; for (i = l1; i <= r2; i++) { a[i] = b[i]; } } // 递归进行归并排序 void merge(int l, int r) { if (l >= r) { return; } int mid = (l + r) / 2; merge(l, mid); merge(mid + 1, r); mergesort(l, mid, mid + 1, r); } int main() { IOS; cin >> n; for (int i = 0; i < n; i++) { cin >> a[i]; } merge(0, n - 1); for (int i = 0; i < n; i++) { cout << a[i]; if (i < n - 1) cout << " "; } cout << endl; return 0; } ``` 上述代码中,`merge`函数用于递归地将数组分解为子数组,`mergesort`函数用于合并两个已排序的子数组。在`main`函数中,首先读取输入的数组,然后调用`merge`函数进行排序,最后输出排序后的数组。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值