[BZOJ1478]Sgu282 Isomorphism
Description
给定一个N 个结点的无向完全图( 任意两个结点之间有一条边), 现在你可以用 M 种颜色对这个图的每条边进行染色,每条边必须染一种颜色。 若两个已染色的图,其中一个图可以通过结点重新编号而与另一个图完全相同, 就称这两个染色方案相同。 现在问你有多少种本质不同的染色方法,输出结果 mod P。P 是一个大于N 的质数
Input
仅一行包含三个数,N、M、P。
1≤N≤53,1≤M≤1000
Output
仅一行,为染色方法数 mod P 的结果。
Sample Input
3 4 97
Sample Output
20
polya神题,神题,神题,重要的事情说三遍……
这道题真TM毒瘤,我写了好久,还是不会……只有看题解,不得不说那些大佬真的牛逼。
好,废话不多说,我们进入正题。
这题显然是需要用polya的,但是总置换数到了\(N!\)(我的死因),显然简单的枚举是不可行的,因此我们得换种思想
我们考虑一对点的置换 \(f=i\rightarrow p[i]\)
那么,它对应的对边的置换就是 \(f'=(i,j)\rightarrow (p[i],p[j])\)
我们考虑这两个不同类型的置换各自的循环节个数\(c(f)\)与\(c(f')\)的关系,便可以得到如下结论:
- 点\(i\)和点\(j\)同属于一个长度为\(L\)的循环节中,那么这样的边\((i,j)\)组成的置换中循环节个数显然为\(\lfloor\frac{L}{2}\rfloor\)(考虑隔一个连,隔两个连……即可)
- 点\(i\)和点\(j\)各属于长\(L_{i}\)和\(L_{j}\)的两个不同循环中,那么这样的边\((i,j)\)组成的循环节的个数为\(lcm(L_{i},L_{j})\)
我们设\(L_{1}\geqslant L_{2}\geqslant ...\geqslant L_{m}>0\),且有\(L_{1}+L_{2}+...+L_{m}=N\),因此,\(L_{i}\)即为\(N\)的一个拆分,而且循环数均为关于\(L_{i}\)的一个函数
由于\(N\leqslant 53\),所以对\(N\)的划分我们可以暴力回溯,然后对于每一种划分我们只要求出对应的置换个数和每个置换的循环节个数
循环节个数我们已知了,置换个数呢?
假定我们已经确定了\(L_{1}\geqslant L_{2}\geqslant ...\geqslant L_{m}>0\),接下来我们只要将\(1...N\)这\(N\)个点分别放入这\(m\)个循环节中,满足第\(i\)个循环中有\(L_{i}\)个点,这便是个简单的组合问题,共\(\dfrac{N!}{L_{1}!L_{2}!...L_{m}!}\)种不同的方式
对于每个循环,确定了第一个点,那么剩下的各点的排列所对应的置换也是不同的,所以还要乘上个\((L_{1}-1)!(L_{2}-1)!...(L_{m}-1)!\)
如果有\(L_{i}=L_{i+1}=...=L_{j}\)的话,那么有\((j-i+1)!\)种方案又是重复的,便要除上一个\((j-i+1)!\)
所以置换的个数就是
其中,\(t\)表示有\(t\)种不同的\(L\)值,每种值有\(k_{i}\)个
至此,我们就可以完美地解决这道题了
/*program from Wolfycz*/
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define inf 0x7f7f7f7f
using namespace std;
typedef long long ll;
typedef unsigned int ui;
typedef unsigned long long ull;
inline int read(){
int x=0,f=1;char ch=getchar();
for (;ch<'0'||ch>'9';ch=getchar()) if (ch=='-') f=-1;
for (;ch>='0'&&ch<='9';ch=getchar()) x=(x<<1)+(x<<3)+ch-'0';
return x*f;
}
inline void print(int x){
if (x>=10) print(x/10);
putchar(x%10+'0');
}
const int N=1e3;
int fac[N+10],num[N+10],cnt[N+10];
int n,m,p,tot,Ans;
ll gcd(ll a,ll b){return !b?a:gcd(b,a%b);}
int mlt(ll a,ll b){
int res=1;a%=p;
for (;b;b>>=1,a=1ll*a*a%p) if (b&1) res=1ll*res*a%p;
return res;
}
void dfs(int now,int left){
if (!left){
ll a=1,b=0;
for (int i=1;i<=tot;i++){
a=a*mlt(num[i],cnt[i])%p*fac[cnt[i]]%p; //快速计算L和k!
b+=cnt[i]*(cnt[i]-1)/2*num[i]+num[i]/2*cnt[i];
//这些数gcd都相同,共有k*(k-1)种组成方法,然后计算在同一个循环节的情况
for (int j=i+1;j<=tot;j++) b+=cnt[i]*cnt[j]*gcd(num[i],num[j]); //计算与之后的点不在同一个循环节的情况
}
a=1ll*mlt(a,p-2)*fac[n]%p;
Ans=(Ans+1ll*a*mlt(m,b)%p)%p;
}
if (now>left) return;
dfs(now+1,left); //拆分,不选数
for (int i=1;i*now<=left;i++){//拆分,选数
num[++tot]=now,cnt[tot]=i;
dfs(now+1,left-i*now);
tot--;
}
}
int main(){
n=read(),m=read(),p=read(),fac[0]=1;
for (int i=1;i<N;i++) fac[i]=1ll*fac[i-1]*i%p;
//枚举阶乘,方便计算k!
dfs(1,n);
Ans=1ll*Ans*mlt(fac[n],p-2)%p; //逆元
printf("%d\n",Ans);
return 0;
}