polya 定理

发布时间 2023-04-23 17:34:40作者: gtm1514

我不知道啥是群论。毕竟我不懂抽代。

啥是置换之类的东西不再说了。

任意一个置换都可以分解为若干不相交的循环置换的乘积。有时候作为套路出现。

Burnside 引理

通俗的解释一下就是等价类个数 = 所有不同置换中不动点个数的平均值。

不动点就是一个置换中没动的点。字面意思。

知道这个就可以做题了。

polya 定理是什么东西可以自己看。可能没啥用。

P4980 【模板】Pólya 定理

题意:\(n\) 个点的环 \(n\) 染色方案,旋转同构。

很简单,不过我去年刚刚接触这些东西的时候很难理解。主要是不理解啥叫不动点。现在懂了不动点个数就是置换之后染色方案不变的方案数。

套一下 Burnside 引理:

  1. 所有不同置换:把环转一个点、两个点……一直到 \(n\) 个点。有 \(n\) 种。

然后我们现在要算一个置换的所有染色方案的不动点个数。

首先经典结论旋转 \(k\) 个点即形成 \(\gcd(n,k)\) 个环。然后如果置换后染色方案不变那么每个环都是相同颜色,方案数就是 \(n^{\gcd(n,k)}\)。于是答案就是

\[\frac 1n\sum_{i=1}^nn^{\gcd(n,k)} \]

简单莫反一下得到答案就是

\[\frac 1n\sum_{d|n}n^d\varphi(\frac nd) \]

#include <cstdio>
#include <algorithm>
#include <iostream>
using namespace std;
const int mod=1000000007;
int n;
int qpow(int a,int b){
	int ans=1;
	while(b){
		if(b&1)ans=1ll*a*ans%mod;
		a=1ll*a*a%mod;
		b>>=1;
	}
	return ans;
}
int getphi(int x){
	int phi=x;
	for(int i=2;i*i<=x;i++){
		if(x%i==0){
			phi=phi/i*(i-1);
			while(x%i==0)x/=i;
		}
	}
	if(x!=1)phi=phi/x*(x-1);
	return phi;
}
int main(){
	int tim;scanf("%d",&tim);
	while(tim--){
		scanf("%d",&n);
		int ans=0;
		for(int i=1;i*i<=n;i++){
			if(n%i==0){
				ans=(ans+1ll*getphi(n/i)*qpow(n,i))%mod;
				if(i*i!=n)ans=(ans+1ll*getphi(i)*qpow(n,n/i))%mod;
			}
		}
		printf("%d\n",1ll*ans*qpow(n,mod-2)%mod);
	}
}

[SDOI2013]项链

莫反还套个 polya。

首先套路一下,设长 \(n\) 的环不考虑循环同构答案是 \(F(n)\),则总的答案就是

\[\frac 1n\sum_{d|n}F(d)\varphi(\frac nd) \]

然后算 \(F(n)\)。发现我们首先要算不同的珠子有多少个。\(\gcd=1\),考虑莫比乌斯反演一下。设 \(f(d)\)\(\gcd\)\(d\) 的方案,\(g(d)\)\(\gcd\)\(d\) 的倍数的方案,则 \(f(1)=\sum_{d=1}^a\mu(d)g(d)\)

考虑算 \(g(n)\)。能放进去的元素有 \(m=\left\lfloor\dfrac an\right\rfloor\) 个。发现这玩意也是等价类。那么套个 burnside。对于所有三元排列:

  1. \(3\) 个置换环:只有 \((1,2,3)\) 一个。
  2. \(1\) 个置换环:\((2,3,1),(3,1,2)\) 两个。
  3. \(2\) 个置换环:剩下三个。
    于是 \(g(n)=\frac 16(2m+3m^2+m^3)\)。那就可以算出珠子个数 \(k=f(1)\)

然后算 \(F(n)\),即长 \(n\) 环染 \(k\) 种颜色相邻不同色的方案。考虑递推:

  1. 第一个和第 \(n-1\) 个是同色:相当于第一个和第 \(n-2\) 个不同色,然后放上一个同色的再加一个隔开,方案是 \((k-1)F(n-2)\)
  2. 不同色:显然 \((k-2)F(n-1)\)

这是个二阶递推式,可以直接特征根解一元二次方程得到通项,是 \(F(n)=(k-1)^{n}+(-1)^n(k-1)\)

然后发现 \(n\) 太大了直接算所有 \(d\)\(\varphi(d)\) 会爆炸。可以给 \(n\) 分解质因数然后 dfs 质因子算 \(\varphi\)。复杂度是 \(n\) 唯一分解的所有指数 \(+1\) 相乘。确实是能过的。

然后一个坑是 \(n\) 可能是模数的倍数,这时候要把模数变成 \(mod^2\) 然后最后答案除个 \(mod\)

血泪教训不要用 int128 做下标。

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
#define int __int128
using namespace std;
int mod=1000000007,inv6=166666668;
int k,ans;
long long n,a;
signed p[10000010],miu[10000010];
bool v[10000010];
void get(int n){
    miu[1]=1;
    for(int i=2;i<=n;i++){
        if(!v[i])p[++p[0]]=i,miu[i]=-1;
        for(int j=1;j<=p[0]&&i*p[j]<=n;j++){
            v[i*p[j]]=true;
            if(i%p[j]==0)break;
            miu[i*p[j]]=-miu[i];
        }
    }
    for(int i=1;i<=n;i++)miu[i]+=miu[i-1];
}
int qpow(int a,int b){
    int ans=1;
    while(b){
        if(b&1)ans=1ll*ans*a%mod;
        a=1ll*a*a%mod;
        b>>=1;
    }
    return ans;
}
int g(int n){
    return (2*n+3*n*n+n*n%mod*n)%mod*inv6%mod;
}
int f(int n){
    return (qpow(k-1,n)+((n&1)?mod-1:1)*(k-1))%mod;
}
signed tag;
int fac[110],cnt[110];
void dfs(int x,int now,int phi){
    if(x>tag){
        ans=(ans+1ll*phi%mod*f(n/now))%mod;
        return;
    }
    dfs(x+1,now,phi);
    for(int i=1;i<=cnt[x];i++){
        now*=fac[x];
        if(i==1)phi*=fac[x]-1;
        else phi*=fac[x];
        dfs(x+1,now,phi);
    }
}
signed main(){
    long long tim;scanf("%lld",&tim);
    get(10000000);
    while(tim--){
        scanf("%lld%lld",&n,&a);ans=k=tag=0;
        int mod2=0,inv62=833333345000000041;
        if(n%mod==0){
            mod2=mod;
            mod=mod*mod;swap(inv6,inv62);
        }
        for(int l=1,r;l<=a;l=r+1){
            r=a/(a/l);
            k=(k+1ll*(miu[r]-miu[l-1]+mod)%mod*g(a/l))%mod;
        }
        int ret=n;
        for(int i=2;i*i<=ret;i++){
            if(ret%i==0){
                fac[++tag]=i;cnt[tag]=0;
                while(ret%i==0){
                    ret/=i,cnt[tag]++;
                }
            }
        }
        if(ret!=1)fac[++tag]=ret,cnt[tag]=1;
        dfs(1,1,1);
        if(mod2){
            mod=mod2;inv6=inv62;
            ans=ans/mod*qpow(n/mod,mod-2)%mod;
        }
        else ans=1ll*ans*qpow(n%mod,mod-2)%mod;
        printf("%lld\n",(long long)ans);
    }
    return 0;
}

P4916 [MtOI2018]魔力环

首先特判 \(n=m\)。然后套路变成 \(\dfrac 1n\sum_{d|n}f(d)\varphi(\frac nd)\),直接来算 \(f(d)\)

如果 \(d\) 不是 \(m\) 的因子显然没贡献,所以枚举 \(d\) 可以直接枚举 \(\gcd(n,m)\) 的因子。

然后设 \(f(n,m)\) 为不考虑循环同构的方案。首先特判 \(m\le k\) 的答案为 \(\dbinom nm\)

断环为链,枚举分界上的黑珠子个数 \(i\in[0,k]\),方案是 \(i+1\)。然后强制两边是白的。现在就是将 \(m-i\) 个黑珠子用 \(n-m-2\) 个白珠子分段(可以空),每段长度不超过 \(k\) 的方案。这个简单容斥一下,强制一些段超过 \(k\),容易得到答案: \(n\) 个珠子划 \(m\) 段的方案数

\[\sum_{i=0}^{\min(m,\lflor\frac n{k+1}\rfloor)}(-1)^i\binom mi\binom{n-m(i+1)+m-1}{m-1} \]

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <vector>
#include <cstring>
using namespace std;
const int mod=998244353;
int n,m,k,ans,p[100010],phi[100010],jc[100010],inv[100010];
bool v[100010];
int qpow(int a,int b){
    int ans=1;
    while(b){
        if(b&1)ans=1ll*ans*a%mod;
        a=1ll*a*a%mod;
        b>>=1;
    }
    return ans;
}
void get(int n){
    phi[1]=1;
    for(int i=2;i<=n;i++){
        if(!v[i])p[++p[0]]=i,phi[i]=i-1;
        for(int j=1;j<=p[0]&&i*p[j]<=n;j++){
            v[i*p[j]]=true;
            if(i%p[j]==0){
                phi[i*p[j]]=phi[i]*p[j];break;
            }
            phi[i*p[j]]=phi[i]*phi[p[j]];
        }
    }
}
int gcd(int a,int b){
    return b?gcd(b,a%b):a;
}
int C(int n,int m){
    if(n<m)return 0;
    return 1ll*jc[n]*inv[m]%mod*inv[n-m]%mod;
}
int calc(int n,int m){
    int ans=0;
    for(int i=0;i<=min(n,m/(k+1));i++){
        if(i&1)ans=(ans-1ll*C(n,i)*C(n+m-i*(k+1)-1,n-1)%mod+mod)%mod;
        else ans=(ans+1ll*C(n,i)*C(n+m-i*(k+1)-1,n-1))%mod;
    }
    return ans;
}
int f(int n,int m){
    int ans=0;
    if(m<=k)return C(n,m);
    for(int i=0;i<=min(m,k);i++)ans=(ans+1ll*(i+1)*calc(n-m-1,m-i))%mod;
    return ans;
}
int main(){
    scanf("%d%d%d",&n,&m,&k);jc[0]=inv[0]=1;
    get(n);
    for(int i=1;i<=n;i++)jc[i]=1ll*jc[i-1]*i%mod;
    inv[n]=qpow(jc[n],mod-2);
    for(int i=n-1;i>=1;i--)inv[i]=1ll*inv[i+1]*(i+1)%mod;
    if(n==m){
        if(n<=k)puts("1");
        else puts("0");
    }
    int d=gcd(n,m),ans=0;
    for(int i=1;i*i<=d;i++){
        if(d%i==0){
            ans=(ans+1ll*f(n/i,m/i)*phi[i])%mod;
            if(i*i!=d)ans=(ans+1ll*f(n/(d/i),m/(d/i))*phi[d/i])%mod;
        }
    }
    ans=1ll*ans*qpow(n,mod-2)%mod;
    printf("%d\n",ans);
    return 0;
}

[SHOI2006] 有色图

[HNOI2009]图的同构计数 的加强版。那个相当于只有两种颜色。

首先第一步来点色图。我先挂个空链接在这

然后仍然考虑置换。对于一个点的置换,考虑计算它的边等价类个数,然后贡献就是 \(m\) 的个数次方。

考虑两种边:同一个循环置换内的和不同循环置换间的边。

对于第一类,显然长度为 \(n\) 的循环中所有长度相等的边都是等价的,即最后会有 \(\left\lfloor\dfrac n2\right\rfloor\) 个等价类。

对于第二类,设两个端点所在的循环大小分别为 \(n_1,n_2\),则它们经过 \(\text{lcm}(n_1,n_2)\) 次旋转会变成自己,即在一个大小为 \(\text{lcm}(n_1,n_2)\) 的等价类中。于是个数为 \(\dfrac{n_1n_2}{\text{lcm}(n_1,n_2)}=\gcd(n_1,n_2)\)

然后对所有置换统计。容易发现我们只要枚举整数分拆然后算有多少置换拆成循环置换满足这个整数分拆。

设这个整数分拆为 \(\{p_1,p_2,\cdots,p_k\}\),把 \(n\) 个点分到这些循环置换里边,方案是个多重集的组合数。然后每个循环内部是个环排列,方案是 \(\prod_{i=1}^k(p_i-1)!\)。然后相同大小的 \(p_i\) 会算出 \(cnt_{p_i}!\) 种,于是除一下。

跑的实际还行,最大的点跑了 0.5s。

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
int n,m,mod,ans,jc[70],inv[70],a[70],cnt[70];
int qpow(int a,int b){
    int ans=1;
    while(b){
        if(b&1)ans=1ll*ans*a%mod;
        a=1ll*a*a%mod;
        b>>=1;
    }
    return ans;
}
void dfs(int x,int mx){
    if(!x){
        int ret=1,num=0;
        for(int i=1;i<=n;i++)cnt[i]=0;
        for(int i=1;i<=a[0];i++)cnt[a[i]]++,ret=1ll*ret*inv[a[i]]%mod*jc[a[i]-1]%mod,num+=a[i]>>1;
        for(int i=1;i<=n;i++)ret=1ll*ret*inv[cnt[i]]%mod;
        for(int i=1;i<=a[0];i++){
            for(int j=i+1;j<=a[0];j++)num+=__gcd(a[i],a[j]);
        }
        ret=1ll*ret*qpow(m,num)%mod;
        ans=(ans+ret)%mod;
        return;
    }
    for(int i=1;i<=min(mx,x);i++){
        a[++a[0]]=i;dfs(x-i,min(mx,i));a[0]--;
    }
}
int main(){
    scanf("%d%d%d",&n,&m,&mod);jc[0]=inv[0]=1;
    for(int i=1;i<=n;i++)jc[i]=1ll*jc[i-1]*i%mod;
    inv[n]=qpow(jc[n],mod-2);
    for(int i=n-1;i>=1;i--)inv[i]=1ll*inv[i+1]*(i+1)%mod;
    dfs(n,n);
    printf("%d\n",ans);
    return 0;
}

以后有时间整点计数专题?