Loading [MathJax]/extensions/TeX/mathchoice.js

Description

将正 n 边形的 n 个顶点用 n 种颜色染色,问有多少种方案(旋转相同算一种),答案对给定质数 p 取模。

Solution

之前写的,太烂了但是舍不得删。

怎么说,这里是记录一个玄学优化吧。

先列出 Polya 定理吧:

ans=1|G|gk=1mc(σk)

这道题只有旋转,于是我们的 |G|==g 就是 n 啦,而表示颜色种数的 m 也是 n 啦。

但是康康 n 的范围:1e9 艹!

于是我们调转枪头,我们发现循环节的长度相同时,此时的 i 不一定相同,这就是我们优化的突破口。

设 L 为循环节长度,易得 L=ngcd(i,n),其中 gcd(i,n) 是循环节的个数。

再设 cnt=gcd(i,n)=nL

因为 cnt|i,所以可令 i=tcnt。(因为 1<=i<=n,所以 1<=t<=ncnt=L

所以有:gcd(i,n)==gcd(tcnt,Lcnt)==cnt

上面这个式子就是使循环节长度为 L 的条件。

易得当 gcd(t,L)==1 时满足条件。

L 是可以以 O(n) 的复杂度枚举的,求出此时合法的 t 的个数就转换成了求 phi(L)

然后直接套上去就行了。

注意的是我们这里用两种求 phi 的方法为了降低点运行时间。直接算 phi 的那个方法可以证明我们之前筛的素数是够用的,因为我们先前筛的素数是 1e6 范围内,而 n 的范围是 1e9 之内,可以保证超过我们预处理范围的素数至多只有一种。

就是 Polya 定理的一个板子,答案就是 1ngGnc(g)=1nn1i=0ngcd(i,n).

接下来证明正 n 边形顺时针移动 i 位时,形成的互不相交的 "环" 的个数为 gcd(i,n).

假设对于某个 "环" 的开头为 x,那么这个 "环" 包含 x,x+i,x+2i,...,x+ki,不妨假设在 x+ki第一次 回到开头,那么有 x\equiv x+ki\pmod n,从而 ki\equiv 0\pmod n.

那么 k=\frac{\text{lcm}(i,n)}{i}=\frac{n}{\gcd(n,i)},我们注意到 k 实际是每个 "环" 的长度,所以互不相交的 "环" 的个数为 n/k=\gcd(i,n).

但是 n 的范围很大,所以需要继续优化。一个很自然的想法是枚举 n 的约数即 \gcd(i,n),它是 \mathcal O(\sqrt n) 级别的。

\begin{align} \text{Ans}&=\frac{1}{n}\cdot \sum_{i=0}^{n-1}n^{\gcd(i,n)}\\ &=\frac{1}{n}\cdot \sum_{i=1}^{n}n^{\gcd(i,n)}\\ &=\frac{1}{n}\cdot \sum_{d\mid n}n^d\sum_{i=1}^n [\gcd(i,n)=d]\\ &=\sum_{d\mid n}n^{d-1}\cdot \varphi(n/d) \end{align}

推导在我的莫反博客里有提及,这里就不再赘述。

具体实现时,可以预处理 10^6 之内的欧拉函数值,再拼上 \mathcal O(\sqrt n) 求欧拉函数。

\frak{Code}

#include <cstdio>
#define print(x,y) write(x),putchar(y)

template <class T>
inline T read(const T sample) {
	T x=0; char s; bool f=0;
	while((s=getchar())>'9' || s<'0')
		f |= (s=='-');
	while(s>='0' && s<='9')
		x = (x<<1)+(x<<3)+(s^48),
		s = getchar();
	return f?-x:x;
}

template <class T>
inline void write(T x) {
	static int writ[50],tp=0;
	if(x<0) putchar('-'),x=-x;
	do writ[++tp] = x-x/10*10, x /= 10; while(x);
	while(tp) putchar(writ[tp--]^48);
}

const int maxn = 1e6+5;

bool is[maxn];
int n,mod,pc;
int p[maxn],phi[maxn];

void sieve() {
    phi[1] = 1;
    for(int i=2;i<=maxn-5;++i) {
        if(!is[i])
            p[++pc] = i,
            phi[i] = i-1;
        for(int j=1; j<=pc && i*p[j]<=maxn-5; ++j) {
            is[i*p[j]] = 1;
            if(i%p[j]==0) {
                phi[i*p[j]] = phi[i]*p[j];
                break;
            } else phi[i*p[j]] = phi[i]*(p[j]-1);
        }
    }
}

int qkpow(int x,int y) {
    int r=1; 
    while(y) {
        if(y&1) r=1ll*r*x%mod;
        x=1ll*x*x%mod; y>>=1;
    }
    return r;
}

inline int inc(int x,int y) {
    return x+y>=mod?x+y-mod:x+y;
}

int Phi(int x) {
    if(x<=maxn-5) return phi[x]%mod;
    int ans=x;
    for(int i=1; i<=pc && p[i]*p[i]<=x; ++i)
        if(x%p[i]==0) {
            ans -= ans/p[i];
            while(x%p[i]==0) x/=p[i];
        }
    if(x>1) ans -= ans/x;
    return ans%mod;
}

int Polya() {
    int ans=0;
    for(int i=1;i*i<=n;++i) {
        if(n%i) continue;
        ans = inc(ans,1ll*Phi(i)*qkpow(n,n/i-1)%mod);
        if(i*i==n) break;
        ans = inc(ans,1ll*Phi(n/i)*qkpow(n,i-1)%mod);
    }
    return ans;
}

int main() {
    sieve();
    for(int T=read(9);T;--T) {
        n=read(9),mod=read(9);
        print(Polya(),'\n');
    }
    return 0;
}
posted on   Oxide  阅读(126)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· 什么是nginx的强缓存和协商缓存
· 一文读懂知识蒸馏
· Manus爆火,是硬核还是营销?



点击右上角即可分享
微信分享提示