【学习笔记】原根

定义

  • 若满足 an1(modp) 的最小正整数 n 存在,则称 nap,记为 δp(a)ordp(a)
    • 由欧拉定理可知对于 aZ,pNgcd(a,p)=1aφ(p)1(modp),此时有 δp(a)φ(p)
  • 类似地,若满足 an1(modp) 的最小正整数 n 存在,则称 nap半阶,记为 δp(a)

性质

  • 性质 1i,j(ij)[1,δp(a)],aiaj(modp)
  • 性质 2:若 an1(modp),则有 δp(a)|n
    • 证明
      • 假设 δp(a)n,不妨设 n=kδp(a)+r(1r<δp(a))
      • 此时有 an=(aδp(a))karar1(modp),由阶最小性可知 r=0 与假设矛盾。
  • 性质 3:若 auav(modp) ,则有 uv(modp)
  • 性质 4:对于 a,bZ,pN,δp(ab)=δp(a)δp(b) 的充要条件为 gcd(δp(a),δp(b))=1
    • 证明
      • 必要性
        • aδp(a)1(modp),bδp(b)1(modp) 联立得到 (ab)lcm(δp(a),δp(b))1(modp)
        • 由性质 2δp(ab)=δp(a)δp(b)|lcm(δp(a),δp(b)),得到 gcd(δp(a),δp(b))=1
      • 充分性
        • {1(ab)δp(ab)δp(b)aδp(ab)δp(b)1(ab)δp(ab)δp(a)bδp(ab)δp(a),得到 {δp(a)|δp(ab)δp(b)δp(b)|δp(ab)δp(a),进一步得到 δp(a),δp(b)|δp(ab),即 δp(a)δp(b)|δp(ab)
        • 类似地,有 (ab)δp(a),δp(b)(aδp(a))δp(b)(bδp(b))δp(a)1(modp),得到 δp(ab)|δp(a)δp(b)
        • δp(ab)=δp(a)δp(b)
  • 性质 5δp(ak)=δp(a)gcd(δp(a),k)
    • 证明
      • 1(ak)δp(ak)akδp(ak)(modp) 可知 δp(a)|kδp(ak),移项得到 δp(a)gcd(δp(a),k)|δp(ak)
      • 又因为 (ak)δp(a)gcd(δp(a),k)=(aδp(a))kgcd(δp(a),k)1(modp),可知 δp(ak)|δp(a)gcd(δp(a),k)
      • δp(ak)=δp(a)gcd(δp(a),k)

原根

定义

  • gZ,pN,gcd(g,p)=1δp(g)=φ(p),则称 a 为模 p原根
  • p 为质数时由于 φ(p)=p1 化简得到 i,j(ij)[1,p1],gigj(modp)

原根判定定理

  • g 是模 p 的原根当且仅当对于 φ(p) 的每个素因子 α 都有 gφ(p)α1(modp)
    • 证明
      • 必要性由阶的最小性可知。
      • 充分性
        • 假设在满足后面条件的情况下存在一个 g 不是模 p 的原根。
        • 由欧拉定理可知 gφ(p)1(modp),此时有 δp(g)|φ(p)δp(g)<φ(p),说明存在素因子 α|φ(p) 使得 δp(g)|φ(p)α,代入得到 gφ(p)α1(modp),与假设不符。

原根个数

  • p 有原根,则在模意义下它原根的个数为 φ(φ(p))
    • 证明
      • g 为模 p 的原根,则有 δp(gk)=δp(g)gcd(δp(g),k)=φ(p)gcd(φ(p),k)
      • gk 为模 p 的原根当且仅当 gcd(φ(p),k)=1,可行的 k 只有 φ(φ(p)) 个。

原根存在定理

  • p 有原根当且仅当 p=2,4,αβ,2αβ,其中 α 为奇素数,βN
    • 证明我不会,建议去看 OI Wiki

最小原根的范围估计

  • 王元和 Burgess 证明了素数 p 的最小原根 gp=O(p0.25+ϵ),其中 ϵ>0。事实上,由大量试验数据可以发现,对于足够大的 p,其最小正原根的大小不是多项式级别的。
  • Fridlander 和 Salié证明了素数 p 的最小原根 gp=Ω(logp)
  • 一般情况下暴力找一个数的最小原根的时间复杂度是可以接受的。

常用素数原根

例题

luogu P6091 【模板】原根

  • 一种方法是线筛预处理出 φ 并标记出是否存在原根后,暴力找一个数的最小原根然后求出所有原根。

    点击查看代码
    ll prime[1000010],vis[1000010],phi[1000010],pr[1000010],len=0;
    vector<ll>result,ans;
    void isprime(ll n)
    {
    	memset(vis,0,sizeof(vis));
    	phi[1]=1;
    	for(ll i=2;i<=n;i++)
    	{
    		if(vis[i]==0)
    		{
    			len++;  prime[len]=i;
    			phi[i]=i-1;
    		}
    		for(ll j=1;j<=len&&i*prime[j]<=n;j++)
    		{
    			vis[i*prime[j]]=1;
    			if(i%prime[j]==0)
    			{
    				phi[i*prime[j]]=phi[i]*prime[j];
    				break;
    			}
    			else  phi[i*prime[j]]=phi[i]*(prime[j]-1);
    		}
    	}
    	pr[2]=pr[4]=1;
    	for(ll i=2;i<=len;i++)
    	{
    		for(ll j=1;j*prime[i]<=n;j*=prime[i])  pr[j*prime[i]]=1;
    		for(ll j=2;j*prime[i]<=n;j*=prime[i])  pr[j*prime[i]]=1;
    	}
    }
    ll qpow(ll a,ll b,ll p)
    {
    	ll ans=1;
    	while(b)
    	{
    		if(b&1)  ans=ans*a%p;
    		b>>=1;
    		a=a*a%p;
    	}
    	return ans;
    }
    void divide(ll n)
    {
    	result.clear();
    	for(ll i=1;i<=len&&prime[i]*prime[i]<=n;i++)
    	{
    		if(n%prime[i]==0)
    		{
    			result.push_back(prime[i]);
    			while(n%prime[i]==0)  n/=prime[i];
    		}
    	}
    	if(n>1)  result.push_back(n);
    }
    bool check(ll x,ll p)
    {
    	if(qpow(x,phi[p],p)!=1)  return false;
    	for(ll i=0;i<result.size();i++)
    	{
    		if(qpow(x,phi[p]/result[i],p)==1)  return false;
    	}
    	return true;
    }
    ll min_pr(ll p)
    {
    	if(pr[p]==0)  return -1;
    	for(ll i=1;i<=p-1;i++)
    	{
    		if(check(i,p)==true)  return i;
    	}
    	return -1;
    }
    void all_pr(ll p,ll g)
    {
    	ans.clear();
    	if(g==-1)  return;
    	for(ll i=1,x=g;i<=phi[p];i++,x=x*g%p)
    	{
    		if(__gcd(i,phi[p])==1)  ans.push_back(x);
    	}
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	ll t,p,d,i,j;
    	cin>>t;
    	isprime(1000000);
    	for(j=1;j<=t;j++)
    	{
    		cin>>p>>d;
    		divide(phi[p]);
    		all_pr(p,min_pr(p));
    		sort(ans.begin(),ans.end());
    		cout<<ans.size()<<endl;
    		for(i=1;i<=ans.size()/d;i++)
    		{
    			cout<<ans[i*d-1]<<" ";
    		}
    		cout<<endl;
    	}
    	return 0;
    }
    
  • 另一种方法是注意到原根比较密集,且在模意义下我们只需要找到任意一个原根即可。又因为 φ(φ(n))n 在可行范围内不会很大,不妨直接将原来 O(p0.25+ϵ) 找最小原根改为在 [1,n1] 范围内随机选择若干个数判断,可以证明随着选择次数的增多找不到原根的概率越来越接近 0

    点击查看代码
    ll prime[1000010],vis[1000010],phi[1000010],pr[1000010],len=0;
    vector<ll>result,ans;
    void isprime(ll n)
    {
    	memset(vis,0,sizeof(vis));
    	phi[1]=1;
    	for(ll i=2;i<=n;i++)
    	{
    		if(vis[i]==0)
    		{
    			len++;  prime[len]=i;
    			phi[i]=i-1;
    		}
    		for(ll j=1;j<=len&&i*prime[j]<=n;j++)
    		{
    			vis[i*prime[j]]=1;
    			if(i%prime[j]==0)
    			{
    				phi[i*prime[j]]=phi[i]*prime[j];
    				break;
    			}
    			else  phi[i*prime[j]]=phi[i]*(prime[j]-1);
    		}
    	}
    	pr[2]=pr[4]=1;
    	for(ll i=2;i<=len;i++)
    	{
    		for(ll j=1;j*prime[i]<=n;j*=prime[i])  pr[j*prime[i]]=1;
    		for(ll j=2;j*prime[i]<=n;j*=prime[i])  pr[j*prime[i]]=1;
    	}
    }
    ll qpow(ll a,ll b,ll p)
    {
    	ll ans=1;
    	while(b)
    	{
    		if(b&1)  ans=ans*a%p;
    		b>>=1;
    		a=a*a%p;
    	}
    	return ans;
    }
    void divide(ll n)
    {
    	result.clear();
    	for(ll i=1;i<=len&&prime[i]*prime[i]<=n;i++)
    	{
    		if(n%prime[i]==0)
    		{
    			result.push_back(prime[i]);
    			while(n%prime[i]==0)  n/=prime[i];
    		}
    	}
    	if(n>1)  result.push_back(n);
    }
    bool check(ll x,ll p)
    {
    	if(qpow(x,phi[p],p)!=1)  return false;
    	for(ll i=0;i<result.size();i++)
    	{
    		if(qpow(x,phi[p]/result[i],p)==1)  return false;
    	}
    	return true;
    }
    ll arb_pr(ll p)
    {
    	if(pr[p]==0)  return -1;
    	while(1) //如果担心会一直循环下去可以限制枚举次数
    	{
    		ll g=rand()%(p-1)+1;
    		if(check(g,p)==true)  return g;
    	}
    	return -1;
    }
    void all_pr(ll p,ll g)
    {
    	ans.clear();
    	if(g==-1)  return;
    	for(ll i=1,x=g;i<=phi[p];i++,x=x*g%p)
    	{
    		if(__gcd(i,phi[p])==1)  ans.push_back(x);
    	}
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	srand(time(0));
    	ll t,p,d,i,j;
    	cin>>t;
    	isprime(1000000);
    	for(j=1;j<=t;j++)
    	{
    		cin>>p>>d;
    		divide(phi[p]);
    		all_pr(p,arb_pr(p));
    		sort(ans.begin(),ans.end());
    		cout<<ans.size()<<endl;
    		for(i=1;i<=ans.size()/d;i++)
    		{
    			cout<<ans[i*d-1]<<" ";
    		}
    		cout<<endl;
    	}
    	return 0;
    }
    
  • 同时可以对求出所有原根时求 O(n)gcd 进行优化,具体地,类似欧拉筛的写法通过 φ(n) 的所有素因子进行标记,具体详见 题解 P6091 【【模板】原根】 | 【模板】原根 题解

Gym103428C Assign or Multiply

  • 比较经典的套路是通过原根及模意义下的离散对数将若干次原数的乘法运算转化成对数的加法运算。具体来说,设模 p 意义下分别要乘以 a1,a2,,ak,可以转化为模 φ(p) 意义下 logpga1(modp),logpga2(modp),,logpgak(modp) 的相加最后再通过 pg 的若干次幂得到原式。
  • 其他部分详见 2025省选模拟5 T2 HZTG5844. A Dance of Fire and Ice

T711. 随

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

  • log 是完全加性函数,考虑线筛求出 π(p)=O(nlnn) 个素数处的离散对数值。

  • 求解离散对数值考虑 BSGS。设 x=loggα=Bi+j[0,p),其中 j[0,B),i[0,p1B]。将 gloggα=gx=gBi+jα(modp) 移项得到 gjαgBi(modp)

  • 对于一般的 BSGS 来说插入和查询的次数同阶故常取 B=O(p) 用于平衡复杂度。但在本题中只需要插入 1 次,但需要查询 π(p) 次,故取 B=O(pπ(p))

  • p 值域较大,直接线筛空间复杂度不可接受,需要进一步优化。

  • 先处理出 p 内所有数的离散对数。对于每次询问,

    • bp 则直接回答。
    • 否则设 p=vb+r,0rp,0rb1
      • p=vb+r 可知 b=prv 进一步得到 loggblogg(r)loggv=logg(1)+loggrloggvlogg(p1)+loggrloggv(modφ(p))
      • p=(v+1)b+rb 可知 b=pr+bv+1 进一步得到 loggblogg(br)logg(v+1)(modφ(p))
      • logg(p1),loggv,logg(v+1) 容易通过预处理得到。
      • min(r,br)b2 每次选择较小的进行迭代使 b 的规模减半直至 bp
  • 时间复杂度为 O(p0.75log0.5p+Tlogp)

    点击查看代码
    int p,g,phi,lg[32010],prime[32010],vis[32010],lg_base,klen,len;
    int qpow(int a,int b,int p)
    {
    	int ans=1;
    	while(b)
    	{
    		if(b&1)  ans=1ll*ans*a%p;
    		b>>=1;
    		a=1ll*a*a%p;
    	}
    	return ans;
    }
    struct BSGS
    {
    	unordered_map<int,int>vis;
    	int k,base;
    	void init(int g,int p)
    	{
    		vis.clear();
    		k=sqrt(1ll*p*sqrt(p)/log(p))+1;
    		for(int i=0,mi=1;i<=k-1;i++,mi=1ll*mi*g%p)  vis[mi]=i;
    		base=qpow(qpow(g,k,p),p-2,p);
    	}
    	int query(int b)
    	{
    		for(int i=0,mi=b;i*k<=phi;i++,mi=1ll*mi*base%p)
    		{
    			if(vis.find(mi)!=vis.end())  return i*k+vis[mi];
    		}
    		return -1;
    	}
    }B;
    void isprime(int n)
    {
    	lg[1]=0;
    	for(int i=2;i<=n;i++)
    	{
    		if(vis[i]==0)
    		{
    			len++;
    			prime[len]=i;
    			lg[i]=B.query(i);
    		}
    		for(int j=1;j<=len&&i*prime[j]<=n;j++)
    		{
    			vis[i*prime[j]]=1;
    			lg[i*prime[j]]=(lg[i]+lg[prime[j]])%phi;
    			if(i%prime[j]==0)  break;
    		}
    	}
    }
    int solve(int x)
    {
    	if(x<=klen)  return lg[x];
    	int v=p/x,r=p%x;
    	if(r<x-r)  return ((lg_base+solve(r))%phi-lg[v]+phi)%phi;
    	else  return (solve(x-r)-lg[v+1]+phi)%phi;
    }
    int main()
    {
    // #define Isaac
    #ifdef Isaac
    	freopen("in.in","r",stdin);
    	freopen("out.out","w",stdout);
    #endif
    	int t,b,i;
    	scanf("%d%d%d",&p,&g,&t);
    	klen=sqrt(p)+1;  phi=p-1;
    	B.init(g,p);  isprime(klen);
    	lg_base=B.query(p-1);
    	for(i=1;i<=t;i++)
    	{
    		scanf("%d",&b);
    		printf("%d\n",solve(b));
    	}
    	return 0;
    }
    
    

LibreOJ 6542. 离散对数

BZOJ1420 Discrete Root

BZOJ2219 数论之神

参考资料

posted @   hzoi_Shadow  阅读(22)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
扩大
缩小
点击右上角即可分享
微信分享提示