[SHOI2006]有色图

这篇博客介绍了如何运用Burnside引理解决SHOI2006比赛中的有色图问题。通过计算不动点个数和边的置换循环,将问题转化为求解循环个数,并讨论了不同循环内的边贡献和不同点循环间边的贡献。最终提出一种比O(n!)更快的算法,适合比赛时使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、题目

点此看题

二、解法

你多半是被题目名吸引了(手动狗头)

很容易看出来是 polya \text{polya} polya的题,设置换群为 ∣ G ∣ |G| G,它就是 n ! n! n!的全排,利用 burnside \text{burnside} burnside引理,问题转化成了求不动点个数,可以求出循环个数(其实是点的置换影响了边,这里是边的置换循环),那么就可以用 m m m的几次方来算了。

设点的循环是 b 1 , b 2 . . . . . b k b_1,b_2.....b_k b1,b2.....bk,先考虑循环内部的边,定义边的长度为连接点在环上顺次连接的最小距离,那么相同长度的边一定属于同一循环(每次置换相当于把点挨个平移 1 1 1格,画个图就知道了),所以这一部分的贡献是 ∑ i = 1 k b i 2 \sum_{i=1}^k\frac{b_i}{2} i=1k2bi

再考虑连接不同点循环的边,假设它连接 b i , b j b_i,b_j bi,bj,易得置换 l c m ( b i , b j ) lcm(b_i,b_j) lcm(bi,bj)次就会回到原样,但置换 [ 0 , l c m ( b i , b j ) − 1 ] [0,lcm(b_i,b_j)-1] [0,lcm(bi,bj)1]次就要求处于同一个循环(染相同的颜色),所以一个循环有 l c m ( b i , b j ) lcm(b_i,b_j) lcm(bi,bj)条边,共有 b i b j l c m ( b i , b j ) = gcd ⁡ ( b i , b j ) \frac{b_ib_j}{lcm(b_i,b_j)}=\gcd(b_i,b_j) lcm(bi,bj)bibj=gcd(bi,bj)个循环,这一部分的贡献为 ∑ i = 1 k ∑ j = i + 1 k gcd ⁡ ( b i , b j ) \sum_{i=1}^k\sum_{j=i+1}^k\gcd(b_i,b_j) i=1kj=i+1kgcd(bi,bj)

设上面的总贡献为 s s s,那么 m s m^s ms即为不动点个数,但是还有一个问题,我们不能枚举置换,那就考虑每一种 b b b的贡献,也就是我们枚举 b 1 ≤ b 2 . . . . . ≤ b k b_1\leq b_2.....\leq b_k b1b2.....bk并且 ∑ b i = n \sum b_i=n bi=n,然后用多重集的排列可知方案数为 n ! ∏ b i ! \frac{n!}{\prod b_i!} bi!n!,还要考虑循环内部的方案数 ∏ ( b i − 1 ) ! \prod (b_i-1)! (bi1)!(看成环的排列),那方案数就是 n ! ∏ b i \frac{n!}{\prod b_i} bin!,但是这样还是会算重,因为我们要排除大小相等的循环之间的顺序,设一段有 c c c个大小相同的循环,那么把方案数除以 ∏ c ! \prod c! c!就算对了。

复杂度难以计算,但是比 O ( n ! ) O(n!) O(n!)快很多,比赛时可以打表算一下 b 1 ≤ b 2 . . . . . ≤ b k b_1\leq b_2.....\leq b_k b1b2.....bk并且 ∑ b i = n \sum b_i=n bi=n的方案数,然后就很容易知道会不会超时了,这种算法足以通过本题。

#include <cstdio>
#include <iostream>
using namespace std;
#define int long long
const int M = 55;
int read()
{
    int x=0,flag=1;
    char c;
    while((c=getchar())<'0' || c>'9') if(c=='-') flag=-1;
    while(c>='0' && c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x*flag;
}
int n,m,p,ans,n1,n2=1,t,st[M],inv[M],fac[M];
void init()
{
    inv[0]=inv[1]=fac[0]=fac[1]=1;
    for(int i=2;i<=n;i++) inv[i]=inv[p%i]*(p-p/i)%p;
    for(int i=2;i<=n;i++) fac[i]=fac[i-1]*i%p;
    for(int i=2;i<=n;i++) inv[i]=inv[i]*inv[i-1]%p;
}
int qkpow(int a,int b)
{
    int r=1;
    while(b>0)
    {
        if(b&1) r=r*a%p;
        a=a*a%p;
        b>>=1;
    }
    return r;
}
int gcd(int a,int b)
{
    return !b?a:gcd(b,a%b);
}
void dfs(int s,int mx,int c)
{
    if(!s)
    {
        ans=(ans+qkpow(m,n1)*n2)%p;
        return ;
    }
    int a=n1,b=n2;
    for(int i=1;i<=mx;i++)
    {
        st[++t]=i;
        n1=a+i/2;
        for(int j=1;j<t;j++) n1+=gcd(st[j],i);
        n2=b*inv[i]%p*fac[i-1]%p;
        if(i==st[t-1]) n2=n2*fac[c]%p*inv[c+1]%p;
        dfs(s-i,min(s-i,i),i==st[t-1]?c+1:1);
        t--;
    }
}
signed main()
{
    n=read();m=read();p=read();
    init();
    dfs(n,n,0);
    printf("%lld\n",ans);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值