【模板】原根 题解
我来讲一下我做这道题的思路,并且给出一个
我的这个做法充满人类智慧,只需要知道原根的定义、阶的定义以及知道一个原根之后怎么求别的原根即可。其他题解的复杂度证明几乎都是错的。看一下这个帖子。
我的原根学习笔记。以下主要讲本题解法。
假设我们只求一次(求
1.O(n\log^2 n) 解法
假设我们要求整数
- 求出
n 的欧拉函数\varphi(n) 。 - 将
\varphi(n) 质因数分解。 - 依照定义,找到最小原根
g 。如果找不到,那就无解。 - 找到其余原根,也就是
g 的x 次方,其中\gcd(\varphi(n),x)=1 。 - 排序后输出。
分析一下时间复杂度以及实现细节。
步骤 1 最坏情况下要枚举到
步骤 2 最坏情况下要枚举到
步骤 3 要枚举最小原根
步骤 4 直接枚举
步骤 5 将答案集合排个序,由于有
所以总时间复杂度是
以下是代码,用
AC 记录,开了 O2 最慢点 1.53 s,总共 9.92 s。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll m,c,s[20],ans[1000010],n,g,k,ca,T;
inline ll ksm(ll a,ll b){
ll r=1;
while(b){if(b&1)r=r*a%m;a=a*a%m;b>>=1;}
return r;
}
inline void pri(ll x){
c=0;
for(ll i=2;i*i<=x;i++)
if(x%i==0){s[++c]=i;while(x%i==0)x/=i;}
if(x>1)s[++c]=x;
}
inline ll phi(ll x){
pri(x);
for(ll i=1;i<=c;i++)x=x/s[i]*(s[i]-1);
return x;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>T;
while(T--){
ca=g=0;
cin>>m>>k;
pri(n=phi(m));
for(ll i=1;i<m;i++)
if(ksm(i,n)==1){
bool fl=1;
for(ll j=1;j<=c;j++)
if(ksm(i,n/s[j])==1)fl=0;
if(fl){g=i;break;}
}
if(!g){cout<<"0\n\n";continue;}
for(ll i=1,p=g;i<=n;i++,p=p*g%m)
if(__gcd(i,n)==1)ans[++ca]=p;
sort(ans+1,ans+ca+1);
cout<<ca<<'\n';
for(ll i=1;i<=ca/k;i++)cout<<ans[i*k]<<' ';
cout<<'\n';
}
return 0;
}
2.O(n\log n) 解法
我们发现复杂度瓶颈是步骤 3。注意到王元并没有证明任何数的原根是
但是注意到原根比较密集,也就是如果
然后我写出了如下代码,求解
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll _=1000010;
ll c=0,p[_],e[_],ans=0;
bitset<_>v;
double mn=1e6;
int main(){
v.set();
for(ll i=2;i<_;i++){
if(v[i])p[++c]=i,e[i]=i-1;
for(ll j=1;j<=c&&i*p[j]<_;j++){
v[i*p[j]]=0;
if(i%p[j])e[i*p[j]]=e[i]*(p[j]-1);
else{e[i*p[j]]=e[i]*p[j];break;}
}
}
for(ll i=3;i<_;i++)
if(double(e[e[i]])/double(i)<mn){
mn=double(e[e[i]])/double(i);
ans=i;
}
printf("%.6f %lld\n",mn,ans);
return 0;
}
那我们随机大概 200 次就几乎肯定能找到一个原根,因为此时依然找不到原根的概率小于
所以此时步骤 3 时间复杂度变为
于是总复杂度变为
所以我们可以将时间复杂度简化为
AC 记录,可以看到运行时间显著减少,开了 O2 后最慢的点仅 1.01 s,总共 5.70 s。代码长度 945b。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll m,c,s[20],ans[1000010],n,g,k,ca,T;
inline ll ksm(ll a,ll b){
ll r=1;
while(b){if(b&1)r=r*a%m;a=a*a%m;b>>=1;}
return r;
}
inline void pri(ll x){
c=0;
for(ll i=2;i*i<=x;i++)
if(x%i==0){s[++c]=i;while(x%i==0)x/=i;}
if(x>1)s[++c]=x;
}
inline ll phi(ll x){
pri(x);
for(ll i=1;i<=c;i++)x=x/s[i]*(s[i]-1);
return x;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>T;
while(T--){
ca=g=0;
cin>>m>>k;
pri(n=phi(m));
for(ll i=1,r;i<=200;i++)
if(ksm((r=rand()%(m-1)+1),n)==1){
bool fl=1;
for(ll j=1;j<=c;j++)
if(ksm(r,n/s[j])==1)fl=0;
if(fl){g=r;break;}
}
if(!g){cout<<"0\n\n";continue;}
for(ll i=1,p=g;i<=n;i++,p=p*g%m)
if(__gcd(i,n)==1)ans[++ca]=p;
sort(ans+1,ans+ca+1);
cout<<ca<<'\n';
for(ll i=1;i<=ca/k;i++)cout<<ans[i*k]<<' ';
cout<<'\n';
}
return 0;
}
3. O(n) 做法
我们发现此时步骤 0、1、2、3 都是线性或者亚线性,只有步骤 4、5 是
对于步骤 5,我们注意到所有的答案都在
对于步骤 4,我们发现它时间复杂度较大是因为要求
一种解决方案是快速
我们发现已经将
我改进了一下,使得它在依然是线性的同时拥有这个功能。
注意代码中的
for(ll i=1;i<=c;i++)
for(ll j=1;j<s[i]&&s[i]*j<=n;j++)u[s[i]*j]=0;
for(ll i=1;i<=n;i++)
for(ll j=1;j<=c&&s[j]<=i&&i*s[j]<=n;j++){
u[i*s[j]]=0;
if(i%s[j]==0)break;
}
-
对于正确性的证明:第一部分解决了所有
s_i 乘上一个比它小的数的情况,第二部分解决了所有s_j 乘上一个比它大的数的情况。 -
对于线性时间复杂度的证明:容易知道一个数在第一部分一定是被 最大质数×其余部分 筛掉,在第二部分一定是被 最小质数×其余部分 筛掉,所以在两个部分各至多被筛一次。所以时间复杂度
O(n) 。
于是我们就解决了这道题的
AC 记录,最慢的点仅 139 ms,总时间 793 ms,是
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll _=1000010;
ll m,c,s[20],ans[_],n,g,k,cp,ca,T;
bitset<_>u,v;
inline ll ksm(ll a,ll b){
ll r=1;
while(b){if(b&1)r=r*a%m;a=a*a%m;b>>=1;}
return r;
}
inline void pri(ll x){
c=0;
for(ll i=2;i*i<=x;i++)
if(x%i==0){s[++c]=i;while(x%i==0)x/=i;}
if(x>1)s[++c]=x;
}
inline ll phi(ll x){
pri(x);
for(ll i=1;i<=c;i++)x=x/s[i]*(s[i]-1);
return x;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>T;
while(T--){
ca=g=0;u.set();v.reset();
cin>>m>>k;
pri(n=phi(m));
for(ll i=1,r;i<=200;i++)
if(ksm((r=rand()%(m-1)+1),n)==1){
bool fl=1;
for(ll j=1;j<=c;j++)
if(ksm(r,n/s[j])==1)fl=0;
if(fl){g=r;break;}
}
if(!g){cout<<"0\n\n";continue;}
for(ll i=1;i<=c;i++)
for(ll j=1;j<s[i]&&s[i]*j<=n;j++)u[s[i]*j]=0;
for(ll i=1;i<=n;i++)
for(ll j=1;j<=c&&s[j]<=i&&i*s[j]<=n;j++){
u[i*s[j]]=0;
if(i%s[j]==0)break;
}
for(ll i=1,p=g;i<=n;i++,p=p*g%m)
if(u[i])v[p]=1;
for(ll i=1;i<=m;i++)
if(v[i])ans[++ca]=i;
cout<<ca<<'\n';
for(ll i=1;i<=ca/k;i++)cout<<ans[i*k]<<' ';
cout<<'\n';
}
return 0;
}
三、总结
在这道题里,我将原来
四、鸣谢
感谢出题人 ix35 提供题目。
感谢 codecode 的讲解。
感谢 jijidawang 提供论文链接。
感谢 chenxia25 提供随机化做法。
感谢 管理员 审核一篇这么长的题解。