【8】原根学习笔记

前言

多校联训 2025 暑假 Day 1

有些性质没有用到或很显然,等以后用到了再记证明。其实是不会证

前置知识:【7】同余学习笔记【7】欧拉函数学习笔记

概念

定义:给定 \(a,m\in\mathbb{N^+}\),满足 \(a^n\equiv1\pmod m\) 的最小的正整数 \(n\) 叫做 \(a\)\(m\),记作 \(\delta_m(a)\)\(\text{ord}_m(a)\)

性质 \(1\)\(a,a^2,\dots,a^{\delta_m(a)}\)\(m\) 两两不同余。


考虑反证,假设存在 \(1\le i\lt j\le\delta_m(a)\) 使 \(a^i\equiv a^j\pmod{m}\),则 \(a^{j-i}=1\pmod{m}\)。显然 \(j=i\lt\delta_m(a)\),与阶的最小性矛盾。原结论得证。


性质 \(2\):若 \(a^n\equiv1\pmod{m}\),则 \(\delta_m(a)\mid n\)


\(n\) 做带余除法,设 \(n=\delta_m(a)q+r\)。带回原式。

\[a^n\equiv a^{\delta_m(a)q+r}\equiv(a^{\delta_m(a)})^qa^r\equiv a^r\equiv1\pmod{m} \]

由带余除法 \(r\lt\delta_m(a)\),与阶的最小性矛盾。原结论得证。


推论 \(1\):若 \(a^p\equiv a^q\pmod m\),则 \(p\equiv q\pmod{\delta_m(a)}\)

性质 \(3\):若 \(\gcd(a,m)=\gcd(b,m)=1\),则 \(\delta_m(ab)=\delta_m(a)\delta_m(b)\) 的充要条件是 \(\gcd(\delta_m(a),\delta_m(b))=1\)

性质 \(4\):若 \(\gcd(a,m)=1\),则 \(\delta_m(a^k)=\frac{\delta_m(a)}{\gcd(\delta_m(a),k)}\)

求法

考虑使用试除法,由阶的性质 \(2\),阶一定是 \(\varphi(m)\) 的因数。枚举每一个质因数,除掉这个质因数后快速幂判断是否模意义下等于 \(1\)。正确性显然。

代码中 \(d\) 数组为 \(\varphi(m)\) 去重后质因数的集合。使用朴素分解质因数复杂度为 \(O(\sqrt{n}+\log^2m)\),使用 Pollard-rho 可以做到 \(O(n^{0.25}\log n+\log^2m)\)

long long order(long long x,long long mod)
{
	long long now=phi;
	for(int i=1;i<=cnt;i++)
	    while(now%d[i]==0&&power(x,now/d[i],mod)==1)now/=d[i];
	return now;
} 

原根

概念

定义\(m\in\mathbb{N^+},g\in\mathbb{Z}\),若 \(\gcd(g,m)=1\),且 \(\delta_m(g)=\varphi(m)\),则称 \(g\)\(m\)原根

性质:若一个数 \(m\) 有原根,则 \(g^1,g^2,\dots,g^{\varphi(m)}\)\(m\) 互不相等且构成 \(m\) 的简化剩余系。

推论:若 \(m\) 为质数且有原根,则 \(g^1,g^2,\dots,g^{m-1}\)\(m\) 互不相等且构成 \(m\) 的简化剩余系。

这性质和推论十分重要。它的作用包括但不限于定义离散对数,取离散对数降次,NTT。

原根存在定理

一个数存在原根当且仅当 \(m=2,4,p^{\alpha},2p^{\alpha}\),其中 \(p\) 为奇质数,\(\alpha\in\mathbb{N^+}\)

原根判定定理

\(m\ge 2\),则正整数 \(g\)\(m\) 原根的充要条件是 \(\gcd(g,m)=1\) 且对于 \(\varphi(m)\) 的每一个素因数 \(p\) 都有 \(g^{\frac{\varphi(m)}{p}}\not\equiv 1\)


必要性显然。考虑证明充分性。

假设存在满足要求的数 \(g\) 不是原根,则由欧拉定理有 \(g^{\varphi(m)}\equiv1\pmod m\)

又因为 \(g\) 不是原根,由阶和原根的定义得 \(\delta_m(g)\lt\varphi(m)\)。由阶的性质 \(2\)\(\delta_m(g)\mid\varphi(m)\)

因此存在一个 \(\varphi(m)\) 的素因数 \(p\) 满足 \(\delta_m(g)\mid\frac{\varphi(m)}{p}\)。则 \(g^{\frac{\varphi(m)}{p}}\equiv(g^{\delta_m(g)})^k\equiv g^{\delta_m(g)}\equiv1\pmod m\)。因此 \(g\) 是原根,矛盾。原结论成立。


原根数量

\(m\) 有原根,则 \(m\) 的原根数量为 \(\varphi(\varphi(m))\)

最小原根的范围估计

最小原根的范围为 \(\Omega(n^{0.25})\),应用时可以看作 \(O(n^{0.25})\)

求法

我们先来实现判断一个数是否为原根,直接套用原根判定定理。

代码中 a 表示 \(g\)n 表示 \(\varphi(m)\)mod 表示 \(m\)y 数组表示 \(\varphi(m)\) 的素因数集合。利用 power(a,n,mod)!=1 判断 \(\gcd(g,m)\) 是否等于 \(1\)

bool check(long long a,long long n,long long mod)
{
	if(power(a,n,mod)!=1)return 0;
	for(int i=1;i<=tol;i++)
	    if(power(a,n/y[i],mod)==1)return 0;
	return 1;
}

根据最小原根的范围,我们可以在求出 \(\varphi(m)\) 的素因数集合后暴力遍历每个数求出最小原根。

求出最小原根 \(g\) 后,求其他原根是容易的。根据原根的定义,我们枚举 \(g\) 的次幂对 \(m\) 取模可以遍历整个剩余系,且 \(g\) 自乘之后原根判定定理的第一个条件依旧满足。\(g\) 的幂次与 \(\varphi(i)\) 互质时第二个条件也满足,且 \(g\) 的幂次与 \(\varphi(i)\) 不互质时无法计算得到 \(1\),因此直接判断 \(g\) 的幂次与 \(\varphi(i)\) 是否互质就可以判断是否为原根,且可以求出所有原根。

代码中 a[i] 表示数 \(i\) 是否为质数,若 \(a[i]=0\) 表示 \(i\) 为质数。

void getroot(long long n)
{
	long long mi=1,ans=0,num=0,b=n,now=1;
	tol=0;
	if(!r[n])
	  {
	  	printf("0\n\n");
	  	return;
	  }
	n=phi[n];
	if(!a[n])y[++tol]=n;
	for(int i=2;i*i<=n;i++)
	    if(n%i==0)
		    {
		    	if(!a[i])y[++tol]=i;
		    	if(n/i!=i&&!a[n/i])y[++tol]=n/i;
		    }
	while(!check(mi,n,b))mi++;
	for(long long i=1;i<=n;i++)
	    {
	    now=now*mi%b;
	    if(gcd(i,n)==1)ans++,rt[++num]=now;
		}
}

复杂度为 \(O(n^{0.25}\log n)\)

离散对数

定义:取模数 \(m\) 的原根 \(g\),则对满足 \(\gcd(a,m)=1\) 的整数 \(a\),则必然存在唯一的整数 \(0\le k\lt\varphi(m)\) 使 \(g^k\equiv a\pmod m\),那我们称 \(k\) 为以 \(g\) 为底,模 \(m\)离散对数,记作 \(k=\text{ind}_{g}\;a\),在不混淆的情况下可以记作 \(\text{ind }a\)

显然 \(\text{ind}_{g}\;1=0,\text{ind}_{g}\;g=1\)

性质 \(1\)\(\text{ind}_{g}\;ab\equiv\text{ind}_{g}\;a+\text{ind}_{g}\;b\pmod {\varphi(m)}\)


\[g^{\text{ind}_{g}\;ab}\equiv ab\equiv g^{\text{ind}_{g}\;a}g^{\text{ind}_{g}\;b}=g^{\text{ind}_{g}\;a+\text{ind}_{g}\;b}\pmod {m} \]

由原根的性质,得 \(\text{ind}_{g}\;ab\equiv\text{ind}_{g}\;a+\text{ind}_{g}\;b\pmod {\varphi(m)}\)。结论得证。


推论 \(1\)\(\text{ind}_{g}\;a^k\equiv k\text{ind}_{g}\;a\pmod {\varphi(m)}\)

性质 \(2\):若 \(g'\) 也是 \(m\) 的原根,则有 \(\text{ind}_{g}\;a\equiv\text{ind}_{g'}\;a\times\text{ind}_{g}\;g'\pmod {\varphi(m)}\)

性质 \(3\)\(a\equiv b\pmod m\)\(\text{ind}_{g}\;a= \text{ind}_{g}\;b\) 可以互相推导。


\(a\equiv b\pmod m\) 可以推出 \(g^{\text{ind}_{g}\;a}\equiv g^{\text{ind}_{g}\;b}\pmod m\),由原根的性质得 \(\text{ind}_{g}\;a=\text{ind}_{g}\;b\)。上述每步步步可逆,结论得证。


在实际应用时,离散对数不一定会取到 \(0\le k\lt\varphi(m)\),所以我们有时需要把后面的式子改写为 \(\text{ind}_{g}\;a\equiv\text{ind}_{g}\;b\pmod{\varphi(m)}\)。特别注意,对离散对数的取模都是模 \(\varphi(m)\)

例题

例题 \(1\)

P6091 【模板】原根

原根模板题,线性筛个质数就做完了。

#include <bits/stdc++.h>
using namespace std;
long long t,n,d,y[1000010],pri[1000010],phi[1000010],r[2000010],rt[2000010],cnt=0,tol=0;
bool a[1000010];
void init(long long mx)
{
	a[1]=1,phi[1]=1;
	for(int i=2;i<=mx;i++)
	    {
	    	if(!a[i])pri[++cnt]=i,phi[i]=i-1;
	    	for(int j=1;j<=cnt&&i*pri[j]<=mx;j++)
	    	    {
	    	    	a[i*pri[j]]=1;
	    	    	if(i%pri[j]==0)
	    	    	   {
	    	    	   phi[i*pri[j]]=phi[i]*pri[j];
					   break;
				       }
				    phi[i*pri[j]]=phi[i]*(pri[j]-1);
				}
		}
	r[2]=r[4]=1;
	for(int i=2;i<=cnt;i++)
	    for(long long j=pri[i];j<=mx;j*=pri[i])r[j]=r[j*2]=1;
}

long long gcd(long long x,long long y)
{
	if(y==0)return x;
	else return gcd(y,x%y);
}

long long power(long long a,long long p,long long mod)
{
	long long ans=1,x=a;
	while(p)
	   {
	   	if(p&1)ans=ans*x%mod;
	   	p>>=1;
	   	x=x*x%mod;
	   }
	return ans;
}

bool check(long long a,long long n,long long mod)
{
	if(power(a,n,mod)!=1)return 0;
	for(int i=1;i<=tol;i++)
	    if(power(a,n/y[i],mod)==1)return 0;
	return 1;
}

void getroot(long long n,long long d)
{
	long long mi=1,ans=0,num=0,b=n,now=1;
	tol=0;
	if(!r[n])
	  {
	  	printf("0\n\n");
	  	return;
	  }
	n=phi[n];
	if(!a[n])y[++tol]=n;
	for(int i=2;i*i<=n;i++)
	    if(n%i==0)
		    {
		    	if(!a[i])y[++tol]=i;
		    	if(n/i!=i&&!a[n/i])y[++tol]=n/i;
		    }
	while(!check(mi,n,b))mi++;
	for(long long i=1;i<=n;i++)
	    {
	    now=now*mi%b;
	    if(gcd(i,n)==1)ans++,rt[++num]=now;
		}
	sort(rt+1,rt+num+1);
	printf("%lld\n",ans);
	for(int i=d;i<=num;i+=d)printf("%lld ",rt[i]);
	printf("\n");
}

int main()
{
	init(1000000);
	scanf("%lld",&t);
	while(t--)
	   {
	   	scanf("%lld%lld",&n,&d);
	   	getroot(n,d);
	   }
	return 0;
}

例题 \(2\)

P11175 【模板】基于值域预处理的快速离散对数

记录这个小技巧,顺便展示一下离散对数的用法。本题中 \(\text{ind}\) 省略了底数 \(g\)

先考虑求询问 \(1\sim\sqrt{p}+1\) 内的数的答案。根据离散对数的性质 \(1\),如果一个数 \(x\) 可以分解为两个数 \(a,b\) 的乘积,则有 \(\text{ind }x=\text{ind }a+\text{ind }b\),可以在线性筛的时候顺便维护出来。

于是我们只需要对 \(1\sim\sqrt{p}+1\)\(y\) 为质数的情况求 BSGS。设 BSGS 分块的阈值为 \(B\),由于本题是预处理 \(O(B)\) 查询 \(O(\pi(\sqrt{p})\frac{p}{B})\)\(B\) 需要取为 \(\sqrt{p\sqrt{p}\log\sqrt{p}}\) 复杂度均衡。

若查询 \(1\sim\sqrt{p}+1\) 的数,直接返回答案。否则,我们把 \(p\) 对输入的数 \(y\) 做带余除法,有 \(p=vy+r\)。由于 \(y\gt\sqrt{p}+1\),所以 \(v\le\sqrt{p}\)

变形一下有 \(y=\frac{p-r}{v}\),两边取离散对数有 \(\text{ind }y\equiv\text{ind }(p-r)-\text{ind }v\)。由离散对数性质 \(3\),有 \(\text{ind }y\equiv\text{ind }(p-1)+\text{ind }r-\text{ind }v\)

我们又有 \(p=(v+1)y+r-y\),变形一下有 \(y=\frac{p+y-r}{v+1}\),两边取以 \(g\) 为底的离散对数有 \(\text{ind }y\equiv\text{ind }(p+y-r)-\text{ind }(v+1)\)。由离散对数性质 \(3\),有 \(\text{ind }y\equiv\text{ind }(y-r)-\text{ind }(v+1)\)

观察两式,\(\text{ind }v,\text{ind }(v+1)\) 因为 \(v\le\sqrt{p}\) 已经被求出,而 \(\text{ind }(p-1)\) 可以预处理,因此不需要管它们。而 \(\min(r,y-r)\le\frac{y}{2}\),因此如果每次我们选择 \(r\)\(y-r\) 较小的一项的递归下去,每次 \(y\) 至少减半,直到 \(\sqrt{p}+1\),可以在 \(\log\) 的时间范围内求出结果。

时间复杂度预处理 \(O(\frac{p^{0.75}}{\sqrt{\log p}})\),单次询问 \(O(\log p)\)。注意离散对数的值是模 \(\varphi(p)\)

#include <bits/stdc++.h>
using namespace std;
int p,g,q,y,h,b,pr[100000],lg[100000],cnt=0,ap=0;
bool vis[100000];
unordered_map<int,int>t;
int power(int a,int q)
{
	int ans=1,x=a;
	while(q)
	   {
	   	if(q&1)ans=1ll*ans*x%p;
	   	q>>=1;
	   	x=1ll*x*x%p;
	   }
	return ans;
}

int bsgs(int y)
{
	int inv=power(power(g,b),p-2),siv=y;
	for(int i=0;i<=p/b;i++)
	    {
	    	if(t.count(siv))return i*b+t[siv]-1;
	    	siv=1ll*inv*siv%p;
		}
	return -1;
}

void init(int mx)
{
	int now=1;
	for(int i=0;i<b;i++)
	    {
	    if(!t[now])t[now]=i+1;
	    else t[now]=min(t[now],i+1);
	    now=1ll*now*g%p;
	    }
	vis[1]=1;
	for(int i=1;i<=mx;i++)
	    {
	    	if(!vis[i])pr[++cnt]=i,lg[i]=bsgs(i);
	    	for(int j=1;j<=cnt&&i*pr[j]<=mx;j++)
	    	    {
	    	    	vis[i*pr[j]]=1,lg[i*pr[j]]=(lg[i]+lg[pr[j]])%(p-1);
	    	    	if(i%pr[j]==0)break;
				}
		}
}

int fdl(int y)
{
	if(y==p-1)return ap;
	if(y<=h+1)return lg[y];
	int v=p/y,r=p-v*y;
	if(r<=y-r)return ((fdl(p-1)+fdl(r))%(p-1)+p-1-fdl(v))%(p-1);
	return (fdl(y-r)+p-1-fdl(v+1))%(p-1);
}

int main()
{
	scanf("%d%d%d",&p,&g,&q);
	h=sqrt(p),b=sqrt(1ll*p*h/log(h));
	init(h+1),ap=bsgs(p-1);
	while(q--)
	   {
	   	scanf("%d",&y);
	   	printf("%d\n",fdl(y));
	   }
	return 0;
}

例题 \(3\)

P5605 小 A 与两位神仙

由于 \(m\) 是奇质数的幂,所以存在原根 \(g\)

两边取以 \(g\) 为底的离散对数,利用离散对数的推论 \(1\) 降次。由于左边乘了一个 \(a\),右边的范围不一定在 \(0\le k\lt\varphi(m)\) 范围内,所以我们用同余式来表述。注意是模 \(\varphi(m)\)。本题中 \(\text{ind}\) 省略了底数 \(g\)

\[a\text{ind }x\equiv\text{ind }y\pmod{\varphi(m)} \]

由于题目问是否存在 \(a\),又因为同余式可以被转化为裴蜀等式,裴蜀等式可以判有无解,考虑转化为裴蜀等式。

\[a\text{ind }x+b\varphi(m)=\text{ind }y \]

由裴蜀定理,有解需要满足如下条件。

\[\gcd(\text{ind }x,\varphi(m))\mid\text{ind }y \]

先考虑左边怎么求。\(\gcd(\text{ind }x,\varphi(m))\) 可以由 \(\text{lcm}(\text{ind }x,\varphi(m))\) 变形得到,而 \(\text{lcm}\) 是与最小性和倍数有关。联想到阶的定义与性质 \(2\),我们考虑利用阶来构造 \(\gcd(\text{ind }x,\varphi(m))\)

由阶的定义,有 \(x^{\delta_m(x)}\equiv1\pmod m\)。由离散对数的定义,有 \(g^{\text{ind }x}\equiv x\pmod m\)。把后面式子带入前面得如下式子。

\[g^{\text{ind }x\delta_m(x)}\equiv1\pmod m \]

由原根的定义,有 \(\delta_m(g)=\varphi(m)\)。由阶的性质 \(2\),有如下式子。

\[\varphi(m)\mid\text{ind }x\delta_m(x) \]

由于 \(\delta_m(x)\) 的最小性,得到 \(\delta_m(x)=\frac{\text{lcm}(\varphi(m),\text{ind }x)}{\text{ind }x}\)。展开成 \(\gcd\) 移项得到最终的表达式。

\[\gcd(\varphi(m),\text{ind }x)=\frac{\varphi(m)}{\delta_m(x)} \]

回到本来的式子,考虑应用这个结论,我们发现 \(\gcd(\text{ind }x,\varphi(m))\mid\text{ind }y\) 其实等价于 \(\gcd(\text{ind }x,\varphi(m))\mid\gcd(\text{ind }y,\varphi(m))\),同理得右边为 \(\frac{\varphi(m)}{\delta_m(y)}\)

Pollard-rho 分解质因数算 \(\varphi\) 和阶即可。时间复杂度 \(O(m^{0.25}\log m+n\log^2m)\)

#include <bits/stdc++.h>
using namespace std;
long long m,n,x,y,st[10000],d[10000],cnt=0,phi=0;
mt19937 rd(time(NULL));
long long gcd(long long x,long long y)
{
	if(y==0)return x;
	return gcd(y,x%y);
}

long long power(long long a,long long p,long long mod)
{
	long long x=a%mod,ans=1;
	while(p)
	   {
	   	if(p&1)ans=(__int128)ans*x%mod;
	   	p>>=1;
	   	x=(__int128)x*x%mod;
	   }
	return ans;
}

bool check(long long x,long long bas)
{
	long long p=x-1,top=1;
	while(!(p&1))p>>=1,top++;
	st[top]=power(bas,p,x);
	for(int i=top-1;i>=1;i--)st[i]=(__int128)st[i+1]*st[i+1]%x;
	if(st[1]!=1)return 0;
	for(int i=2;i<=top;i++)
	    if(st[i]==x-1)return 1;
	    else if(st[i]!=1&&st[i]!=x-1)return 0;
	return 1;
}

bool miller_rabin(long long x)
{
	if(x==1)return 0;
	long long pr[12]={2,3,5,7,11,13,17,19,23,29,31,37};
	for(int i=0;i<12;i++)
	    if(x==pr[i])return 1;
	for(int i=0;i<12;i++)
	    if(x%pr[i]==0)return 0;
	for(int i=0;i<12;i++)
	    if(!check(x,pr[i]))return 0;
	return 1;
}

long long pollard_rho(long long x)
{
	long long now=0,d=rd()%(x-1)+1,lim=1;
	while(lim)
	   {
	   	long long pr=now,sum=1;
	   	for(int i=1;i<=lim;i++)
	   	    {
	   	        now=((__int128)now*now%x+d)%x,sum=(__int128)sum*abs(now-pr)%x;
	   	        if(i%127==0&&gcd(sum,x)>1)return gcd(sum,x);
			}
	   	lim<<=1;
	   	if(gcd(sum,x)>1)return gcd(sum,x);
	   }
	return x;
}

void divide(long long x)
{
	long long q[500],l=1,r=0;
	q[++r]=x,cnt=0;
	while(l<=r)
	   {
	   	if(miller_rabin(q[l]))d[++cnt]=q[l];
	   	else
	   	   {
	   	   long long d=pollard_rho(q[l]);
		   while(d==q[l])d=pollard_rho(q[l]);
		   q[++r]=d,q[++r]=q[l]/d;	
		   }
	   	l++;
	   }
}

long long order(long long x,long long mod)
{
	long long now=phi;
	for(int i=1;i<=cnt;i++)
	    while(now%d[i]==0&&power(x,now/d[i],mod)==1)now/=d[i];
	return now;
} 

int main()
{
	scanf("%lld%lld",&m,&n);
	divide(m),sort(d+1,d+cnt+1),cnt=unique(d+1,d+cnt+1)-d-1,phi=m;
	for(int i=1;i<=cnt;i++)phi=phi/d[i]*(d[i]-1);
	divide(phi),sort(d+1,d+cnt+1),cnt=unique(d+1,d+cnt+1)-d-1;
	for(int i=1;i<=n;i++)
	    {
	    	scanf("%lld%lld",&x,&y);
	    	long long g1=phi/order(x,m),g2=phi/order(y,m);
	    	if(g2%g1==0)printf("Yes\n");
	    	else printf("No\n");
		}
	return 0;
}

后记

OI 会用到的初等数论,应该,都,学完了吧?

灼日之矢 与心门轻擦

寒月旧盟 重逢与月下

三界之外 斗战自洒踏

倒打一耙 倾力冲垮

posted @ 2025-07-23 16:55  w9095  阅读(8)  评论(0)    收藏  举报