思路来源
https://baike.baidu.com/item/%E5%8E%9F%E6%A0%B9/8103534?fr=aladdin
https://blog.csdn.net/zoro_n/article/details/78200742
https://www.cnblogs.com/Dance-Of-Faith/p/9905786.html
《数学的思维方式与创新》 丘维声著 北京大学出版社 2011年3月第一版
看到的一些和原根相关的比较好玩的东西
http://www.sohu.com/a/226489349_100125672
http://www.sohu.com/a/214094143_815436
概念
提示:如果下述出现一些符号不明白,可下翻到参考概念及预备知识部分
设m是正整数,a是整数,若a模m的阶等于φ(m)(即|a|==φ(m)),则称a为模m的一个原根。
*群的元素的阶
性质
①如果正整数(a,m) = 1和正整数 d 满足≡1(mod m),则 d 整除 φ(m)。
因此|a|整除φ(m)。当a= 3时,我们仅需要验证 3 的 1 、2、3 和 6 次方模 7 的余数即可。
证明:
显然,由欧拉定理,,结合下述命题2得证
从这个思路出发,这为寻找模m意义下的最小原根提供了方法论,
至少可以帮我们快速判定一个数不是原根,
性质1的方法论:
如果对于所有的素因子prime[i],模m均不为1,则|a|==φ(m),a为原根
证明:
上述证明引自博客https://www.cnblogs.com/Dance-Of-Faith/p/9905786.html
一点小说明,对于引理2,我们可以递归进行这个操作,且,从而继续往小求,直至
由于g是的一个约数,g<,则g至少比少一个素因子,那么一定是g的倍数,则得证
②,……,模 m 两两不同余,因此当a是模m的原根时,,……,构成模 m 的简化剩余系。
简化剩余系,1到m-1中所有与m互素的数的集合。
,……,这些数只能是可逆元,因此是可逆元的集合,不可能出现零因子,故均与m互素。
③模m有原根的充要条件是m= 1,2,4,,,其中p是奇质数,n是任意正整数。
常用:2的原根是1;4的原根是3;998244353的原根是3;1e9+7的原根是5
该证明由高斯在1801年完成,菜鸡表示不会证QAQ
④对正整数(a,m) = 1,如果 a 是模 m 的原根,那么 a 是整数模n乘法群(即加法群 Z/mZ的可逆元,也就是所有与 m 互素的正整数构成的等价类构成的乘法群)Zn的一个生成元。由于Zn有 φ(m)个元素,而它的生成元的个数就是它的可逆元个数,即 φ(φ(m))个,因此当模m有原根时,它有φ(φ(m))个原根。
百度百科证明实在是看不懂,就是φ(φ(m))让我找到的这本书,更为详尽的证明
证明:
其实就是群论拉格朗日定理的一个结论,然而不写成这个命题3的形式我还是证不出来
*循环群与生成元
由群,若存在原根a,则,
只需令,即有
由知,为群的生成元,
k的个数即为的生成元的个数,
也为满足即与互质的数的个数,
这显然就是,即可逆元的个数,证毕
故原根个数为0或
⑤一个数的最小原根的大小是O()的
详见百度百科最小正原根问题,由王元院士于1959年给出证明
⑥一个数n的全体原根乘积mod n==1
⑦一个数n的全体原根总和mod n==莫比乌斯函数μ(n-1)
⑧找到最小原根a后,所有与互素的数k,均为原根
其实求模m剩余循环群的所有生成元的过程,就是求m的原根的过程,
这二者是等价的,原根个数==2,即有两个生成元
例题
①poj1284 Primitive Roots 求奇素数p的原根个数
根据性质4,显然答案为
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int maxn=(1<<16)+10;
bool ok[maxn];
int prime[maxn],phi[maxn],cnt,n;
void sieve()
{
phi[1]=1;
for(ll i=2;i<maxn;++i)
{
if(!ok[i])
{
prime[cnt++]=i;
phi[i]=i-1;
}
for(int j=0;j<cnt;++j)
{
if(i*prime[j]>=maxn)break;
ok[i*prime[j]]=1;
if(i%prime[j]==0)
{
phi[i*prime[j]]=phi[i]*prime[j];//prime[j]是i的因子 prime[j]的素因子项包含在i的素因子项里
break;
}
else phi[i*prime[j]]=phi[i]*(prime[j]-1);//prime[j]与i互质 phi[i*prime[j]=phi[i]*phi[prime[j]]
}
}
}
int main()
{
sieve();
while(~scanf("%d",&n))
{
printf("%d\n",phi[phi[n]]);
}
return 0;
}
②hdu4992 Primitive Roots 求一个数n的所有原根(2<=n<1e6)
先根据性质3判是否存在原根,
若存在,再根据性质1的方法论从a==2开始往上找最小原根a,
找到原根a后,根据性质8,输出这些原根即可
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<cmath>
#include<algorithm>
using namespace std;
const int maxn=1e6+10;
typedef long long ll;
bool ok[maxn];
vector<ll>cal,rt;
ll p,phi[maxn],prime[maxn],cnt;
ll modpow(ll x,ll n,ll mod)
{
if(!n)return 1;
ll p=modpow(x,n/2,mod),q=p*p%mod;
if(n&1)q=q*x%mod;
return q;
}
void sieve()
{
phi[1]=1;
for(ll i=2;i<maxn;++i)
{
if(!ok[i])
{
prime[cnt++]=i;
phi[i]=i-1;
}
for(int j=0;j<cnt;++j)
{
if(i*prime[j]>=maxn)break;
ok[i*prime[j]]=1;
if(i%prime[j]==0)
{
phi[i*prime[j]]=phi[i]*prime[j];//prime[j]是i的因子 prime[j]的素因子项包含在i的素因子项里
break;
}
else phi[i*prime[j]]=phi[i]*(prime[j]-1);//prime[j]与i互质 phi[i*prime[j]=phi[i]*phi[prime[j]]
}
}
}
vector<ll>divisor(ll p)
{
vector<ll>tmp;
for(int i=0;i<cnt;++i)
{
if(p%prime[i]==0)
{
tmp.push_back(prime[i]);
while(p%prime[i]==0)
p/=prime[i];
}
}
if(p>1)tmp.push_back(p);
return tmp;
}
bool judge(ll p)
{
if(p%2==0)p/=2;
if(!ok[p])return 1;
if(p%2==0)return 0;
for(int i=1;i<cnt;++i)//得找奇素数 i=1很关键
{
if(p%prime[i]==0)
{
while(p%prime[i]==0)
p/=prime[i];
return p==1;
}
}
return 0;
}
vector<ll>solve(ll p)
{
vector<ll>t;
if(p==2)
{
t.push_back(1);
return t;
}
if(p==4)
{
t.push_back(3);
return t;
}
if(!judge(p))
{
t.push_back(-1);
return t;
}
ll root,now=1;//phi(p)
cal=divisor(phi[p]);//phi(p)质因子
int len=cal.size();
for(ll a=2;a<p;++a)
{
if(modpow(a,phi[p],p)!=1)continue;
bool flag=1;
for(int i=0;i<len&&flag;++i)
{
if(modpow(a,phi[p]/cal[i],p)==1)flag=0;
}
if(flag)
{
root=a;
break;
}
}
for(ll i=1;i<phi[p];++i)
{
now=(now*root)%p;
if(__gcd(i,phi[p])==1)t.push_back(now);//algorithm库函数
}
sort(t.begin(),t.end());
ll sz=unique(t.begin(),t.end())-t.begin();//可能有重复 如26
t.resize(sz);
return t;
}
int main()
{
sieve();
while(~scanf("%lld",&p))
{
rt=solve(p);
int sz=rt.size();
for(int i=0;i<sz;++i)
printf("%lld%c",rt[i],i==sz-1?'\n':' ');
}
return 0;
}
③51nod1135 求最小原根(保证输入是一个3<=p<=1e9的奇素数)
和上面的代码大同小异,由于题意中p一定是素数,直接把phi[p]的地方都换成p-1即可
然后找到root之后就返回即可,不用找剩下的原根了
如果p不是素数的话,根据性质3,有原根的p最多有两个素因子,
在judge函数里记录一下,是素数还是不是素数,有哪几个素因子,
这样如果有原根的话就能顺便把phi(p)也给求了
④URAL-1268 求最大原根(保证输入是一个3<=p<=65536的奇素数)
这题时限250ms,卡的比较紧,T了两发280ms
(1)把回答过的记忆化一下,毕竟不超过65536的奇素数估计也就4k,然而有6k多个询问
(2)是开vector预处理各个数的所有素因子,枚举素因子向vector里放,
每个数素因子不超过6个(2*3*5*7*11*13约等于3W),大概就是O(6*n)的叭,然后就卡过了
(3)从大到小枚举 比 从小到大找出原根再用gcd搞一个最大的原根 要快一些
(4)题目说是奇素了,把判是否存在原根的那些都杠掉
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<cmath>
#include<algorithm>
using namespace std;
const int maxn=(1<<16)+10;
typedef long long ll;
bool ok[maxn];
vector<ll>cal,rt;
vector<ll>divisor[maxn];
ll p,phi[maxn],prime[maxn],ans[maxn],cnt;
ll modpow(ll x,ll n,ll mod)
{
if(!n)return 1;
ll p=modpow(x,n/2,mod),q=p*p%mod;
if(n&1)q=q*x%mod;
return q;
}
void sieve()
{
phi[1]=1;
for(ll i=2;i<maxn;++i)
{
if(!ok[i])
{
prime[cnt++]=i;
phi[i]=i-1;
ans[i]=-1;
}
for(int j=0;j<cnt;++j)
{
if(i*prime[j]>=maxn)break;
ok[i*prime[j]]=1;
if(i%prime[j]==0)
{
phi[i*prime[j]]=phi[i]*prime[j];//prime[j]是i的因子 prime[j]的素因子项包含在i的素因子项里
break;
}
else phi[i*prime[j]]=phi[i]*(prime[j]-1);//prime[j]与i互质 phi[i*prime[j]=phi[i]*phi[prime[j]]
}
}
for(int k=0;k<cnt;++k)
{
ll &i=prime[k];
for(ll j=i;j<maxn;j+=i)
{
divisor[j].push_back(i);
}
}
}
bool judge(ll p)
{
if(p%2==0)p/=2;
if(!ok[p])return 1;
if(p%2==0)return 0;
for(int i=1;i<cnt;++i)//得找奇素数 i=1很关键
{
if(p%prime[i]==0)
{
while(p%prime[i]==0)
p/=prime[i];
return p==1;
}
}
return 0;
}
ll solve(ll p)
{
if(p==2)return 1;
if(p==4)return 3;
//if(!judge(p))return -1; 保证是素数
ll root,now=1,final=0;//phi(p)
cal=divisor[phi[p]];
int len=cal.size();
for(ll a=p-1;a>=2;--a)
{
if(modpow(a,phi[p],p)!=1)continue;
bool flag=1;
for(int i=0;i<len&&flag;++i)
{
if(modpow(a,phi[p]/cal[i],p)==1)flag=0;
}
if(flag)
{
root=a;
return root;
}
}
}
int main()
{
int t;
scanf("%d",&t);
sieve();
while(t--)
{
scanf("%lld",&p);
if(ans[p]!=-1)printf("%lld\n",ans[p]);
else printf("%lld\n",ans[p]=solve(p));
}
return 0;
}
应用
①如果模p意义下问一些数a[]的乘积的话,可以把a[]压缩成原根对应的值,变乘法为加法
如Codeforces Round #538 (Div. 2) F - Please, another Queries on Array?
如2018秦皇岛ccpc-camp Steins-Gate (原根+FFT)
②NTT,用最小原根的板子搞出原根来,才能做
③BSGS,当模的数p很大而需要被映射的值v很少的时候,
无法for循环一遍1-p-1,把值映射成原根对应的幂次,
用BSGS,对于每个v,是O(sqrt(p))的复杂度
参考概念及预备知识(上述证明符号不懂的看这里)
①模m剩余类(Zm)的概念
其实也就是离散学的一种同余关系,最普遍的那种模意义下的同余关系
②环的概念&&单位元的概念&&零因子的概念&&可逆元的概念
③可逆元与欧拉函数的关系
即ax==1(mod m)方程有解的充要条件是(a,m)==1
④可逆元的个数
⑤的概念&&群的概念&&群的阶
注意到的加法不封闭,乘法封闭,因此只有一种运算,
两种运算+blahblah是环,一种运算+blahblah是群
为可逆元的集合,元素个数为可逆元个数即