- admin 的博客
原根&离散对数相关
- @ 2026-7-10 9:01:19
更新于 2026/7/1 16:54:02 作者
command_block
-
离散对数问题
求解模方程
著名的数论难题,目前还没有的做法。
如果是质数,这就是经典BSGS.
考虑费马小定理 : ,所以幂是有循环节的。
我们考虑分块,即,
预处理作为,此时要求.
将放入Hash表中。
然后预处理并在Hash表中查询即可。
取即可得到复杂度为,Hash表可能带\log.
- 模板题 : P3846 [TJOI2007]可爱的质数
由于是质数,这就是个经典离散对数问题。质数真的非常可爱啊qwq。
#include<algorithm>
#include<cstdio>
#include<map>
#define ll long long
using namespace std;
ll mod,T;
ll powM(ll a,int t=mod-2)
{
ll ret=1;
while(t){
if (t&1)ret=ret*a%mod;
a=a*a%mod;t>>=1;
}return ret;
}
ll x,y;
map<int,int> sav;
int main()
{
scanf("%lld%lld%lld",&mod,&x,&y);
while(T*T<=mod)T++;
ll buf=powM(x),pro=1;
for (int i=0;i<T;i++){
sav[pro*y%mod]=i;
pro=pro*buf%mod;
}buf=powM(pro);pro=1;
for (int i=0;i<T;i++){
if (sav.count(pro)){
printf("%d\n",i*T+sav[pro]);
return 0;
}pro=pro*buf%mod;
}puts("no solution");
return 0;
}
附送 :
-
P2485 [SDOI2011]计算器 (一些特判)
-
在递推式上的扩展 : stO Itst!
-
当模数不是质数 : P4195 【模板】exBSGS/Spoj3105 Mod
由鸽笼原理容易得知,每个数的幂的循环节不大于,当的时候,直接使用普通BSGS。
问题在于,当中的时候,我们就不能求逆元了。
考虑求,让方程两边除以,这样能产生互质。
问题在于可能,由于的幂膜上之后一定残余的倍数,可得时无解。判掉。
现在我们成功除了一下,变成
此时,有,我们就可以求逆了。这里需要EXgcd算法。
$a^{x-1}=\frac{c}{d}(\frac{a}{d})^{-1}\pmod{\frac{p}{d}}$
注意,我们习惯,但是这里不一定和互质,于是我们就进入了子问题。
如此重复直到为止,此时我们就可以正常求逆了。
注意可能在转化过程中就已有,这时要及时停下来。
每次至少减半,所以这部分复杂度为可以不计。
注意还原子问题以及其他细节,具体可以看代码。
#include<algorithm>
#include<cstdio>
#include<map>
#define ll long long
using namespace std;
ll gcd(ll a,ll b)
{return !b ? a : gcd(b,a%b);}
void exgcd(ll a,ll b,ll &x,ll &y){
if (a==1&&b==0){x=1;y=0;return ;}
exgcd(b,a%b,y,x);y-=x*(a/b);
}
ll inv(ll a,int mod){
ll x,y;exgcd(a,mod,x,y);
return (x+mod)%mod;
}
map<int,int> sav;
ll BSGS(ll a,ll c,ll mod)
{
sav.clear();
int T=1;while(T*T<mod)T++;
ll buf=1,xp=inv(a,mod);
for (int i=0;i<T;i++){
if (!sav.count(buf*c%mod))
sav[buf*c%mod]=i;
buf=buf*xp%mod;
}xp=inv(buf,mod);buf=1;
for (int i=0;i<=T;i++){
if (sav.count(buf))
return i*T+sav[buf];
buf=buf*xp%mod;
}return -1000;
}
ll solve(ll a,ll c,int mod)
{
if (a==c)return 1;
ll d=gcd(a,mod);
if (d==1)return BSGS(a,c,mod);
if (c%d)return -1000;
mod/=d;
return solve(a%mod,(c/d)*inv(a/d,mod)%mod,mod)+1;
}
ll mlog(ll a,ll c,int mod)
{
if (c==1)return 0;
if (!a)return !c ? 1 : -1;
return solve(a,c,mod);
}
int main()
{
ll a,c;int mod;
while(1){
scanf("%lld%d%lld",&a,&mod,&c);
if (!mod)break;
a%=mod;c%=mod;
ll ret=mlog(a,c,mod);
printf(ret<0
? "No Solution\n"
: "%lld\n",ret
);
}return 0;
}
-
原根和阶
-
阶 : 在模 意义下的阶是 : 最小的整数 使得 .
对于的情况,阶为,或认为不存在。
说白了就是幂的最小循环节,根据欧拉定理这个上界是.
记为在模下的阶。
-
原根
满足的(达到上界),我们称之为的原根。
模板题 : P6091 【模板】原根
形如的数才有原根。
一旦我们求出了其中一个原根,我们可以这样构造出其余的原根 :
所有的当,这样恰好构造出个,而且能够证明没有遗漏。
理解 : 当时,在的环上每次走步能够遍历整个环。
这也告诉我们,所有的都能表示为原根的幂次,这一点很重要。
还能推出,原根是相当稠密的,另一个已被证明的结论是,最小原根的大小是的。
现在问题变成了求出最小原根,我们可以从小到大枚举。
如果按照定义求阶,每次的复杂度是的,显然不优。
-
引理 : 模的阶如果存在,一定是的约数。
证明 : 设这个数有阶,则被表示成的形式。
能够在大小的环上得到一个大小的环。
这就是它的阶,而且显然为的约数。
所以,我们每次计算阶的时候,只需要验证的约数即可。
当然,我们不需要确切地计算出阶,我们只需要查看阶是否等于而已。
注意到当,那么。
我们可以取出的质因数,分别判定即可。
因为这能够不遗漏地覆盖所有其它约数的倍数,而又达不到本身。可以视为高维空间的可重复差分。
复杂度是
求出原根之后,构造并排序,总复杂度.
讲道理,直接求最小原根不就好了,哪有题目需要同时使用很多个原根啊?
注意特判2.
#include<algorithm>
#include<cstdio>
#define MaxN 1005000
#define ll long long
#define pf printf
using namespace std;
inline int read(){
int X=0;char ch=0;
while(ch<48||ch>57)ch=getchar();
while(ch>=48&&ch<=57)X=X*10+(ch^48),ch=getchar();
return X;
}
int mod;
ll powM(ll a,int t=mod-2)
{
ll ret=1;
while(t){
if (t&1)ret=ret*a%mod;
a=a*a%mod;t>>=1;
}return ret;
}
int gcd(int a,int b)
{return !b ? a : gcd(b,a%b);}
int getphi(int n)
{
int ret=n,p=2;
while(p*p<=n){
if (n%p==0){
while(n%p==0)n/=p;
ret=ret/p*(p-1);
}p++;
}if (n>1)ret=ret/n*(n-1);
return ret;
}
int d[105],tn;
void getft(int n)
{
int sav=n,p=2;
while(p*p<=n){
if (n%p==0){
while(n%p==0)n/=p;
d[++tn]=sav/p;
}p++;
}if (n>1)d[++tn]=sav/n;
}
bool check(int n)
{
for (int i=1;i<=tn;i++)
if (powM(n,d[i])==1)
return 0;
return 1;
}
int ans[MaxN];
void solve()
{
mod=read();
int dr=read(),phi=getphi(mod),g=0;
if (mod==2){
puts("1");
if (dr==1)printf("1");
puts("");
return ;
}
tn=0;getft(phi);
for (int i=1;i<=200;i++)
if (gcd(i,mod)==1&&check(i))
{g=i;break;}
tn=0;
if (g){
for (int i=1;i<phi;i++)
if (gcd(i,phi)==1)
ans[++tn]=powM(g,i);
sort(ans+1,ans+tn+1);
}
printf("%d\n",tn);
for (int i=dr;i<=tn;i+=dr)
printf("%d ",ans[i]);
puts("");
}
int main()
{
int T=read();
while(T--)solve();
return 0;
}
-
例题
-
这道题让我发现我的数论在这个分支有多么渣,所以我才来写了这篇文章。
题意 : 给出模数,保证其是奇质数的幂。
有组,保证都与互质,询问是否存在自然数使得
,时限,允许
__int128.
设的原根为,由于与互质,可以分别设为.
方程变为 ,也即
暴力BSGS求离散对数就能拿到部分分了。
根据斐蜀定理,这个方程有解的充要条件是
观察的幂组成的集合,相当于在的环上,每次跳步,则有个元素。
这个集合的大小正是的阶,这就能够得到
简单变化一下,就能得到原来的条件等价于,现在问题就是快速求阶。
前面介绍过,阶必定是的约数。由此我们可以求解。
在以内,就算有素数的限制,也能够造出,看起来会超时的样子。
注意到当,那么,这能够查看阶是否是某个数的约数。
初始的阶为,对于其每个质因数,逐次降低次数来判定,就能摸到阶在这个质数上的最高次数。
需要Pollard-Rho来求出以及其所有质因数。
每次求阶的复杂度为,总的复杂度为
#include<algorithm>
#include<cstdio>
#include<cmath>
#define ll long long
#define sf scanf
using namespace std;
inline ll mul(ll a,ll b,ll m){
ll r=((long double)a/m*b+0.5);
r=a*b-r*m;
return r<0?r+m:r;
}
ll powM(ll a,ll t,ll m)
{
ll ans=1;
while(t){
if (t&1)ans=mul(ans,a,m);
a=mul(a,a,m);t>>=1;
}return ans;
}
bool mr(ll n,ll a)
{
ll t=n-1;
while(!(t&1))t>>=1;
ll buf=powM(a,t,n);
if (buf==1||buf==n-1)return 0;
while((t<<=1)<n-1){
buf=mul(buf,buf,n);
if (buf==n-1)return 0;
}return 1;
}
const int testp[8]={2,3,5,7,13,19,61,233};
bool ptest(ll n)
{
if (n<2)return 0;
for (int i=2;i*i<=min(n,10000ll);i++)
if (n%i==0)return 0;
if (n<=10000)return 1;
for (int i=0;i<8;i++)
if (mr(n,testp[i]))return 0;
return 1;
}
inline ll gcd(ll a,ll b){
if(a==0)return b;
if(a<0)a=-a;
ll t;
while(b){t=a%b;a=b;b=t;}
return a;
}
ll sav[150];int tot;
const int lim=128;
ll prho(ll n,ll c)
{
ll x1=(c+1)%n,x2=(mul(x1,x1,n)+c)%n,buf;
tot=0;
while(1){
buf=mul(x1-x2,buf,n);
sav[++tot]=x1-x2;
if (tot==lim){
if (gcd(buf,n)>1)break;
tot=0;
}
x1=mul(x1,x1,n)+c;
x2=mul(x2,x2,n)+c;
x2=mul(x2,x2,n)+c;
}
for (int i=1;i<=tot;i++){
buf=gcd(sav[i],n);
if (buf>1)return buf;
}return n;
}
ll p[105];int tn;
void solve(ll n)
{
if (ptest(n)){p[++tn]=n;return ;}
ll sav=prho(n,rand()%(n-1)+1);
while(sav==n)sav=prho(n,rand()%(n-1)+1);
solve(sav);solve(n/sav);
}
ll getphi(ll n)
{
tn=1;
for (int i=60;i;i--){
ll v=pow(n,1.0/i);
if (v-1&&!powM(v-1,i,n)){p[1]=v-1;break;}
if (v &&!powM(v ,i,n)){p[1]=v ;break;}
if (v+1&&!powM(v+1,i,n)){p[1]=v+1;break;}
}if (!p[1])p[1]=n;
ll sav=p[1]-1;
for (int i=2;i<=100;i++){
if (sav%i==0){
p[++tn]=i;
while(sav%i==0)sav/=i;
}
}
if (sav>1)solve(sav);
sav=p[1]-1;
for (int i=2;i<=tn;i++){
if (sav%p[i]==0){
while(sav%p[i]==0)sav/=p[i];
}else p[i]=0;
}return n/p[1]*(p[1]-1);
}
ll mod,phi;
ll ord(ll u)
{
ll ans=1;
for (int i=1;i<=tn;i++)if (p[i]){
ll sav=phi;int c=0;
while(sav%p[i]==0){sav/=p[i];c++;}
for (ll x=1;c;x*=p[i],c--)
if (powM(u,sav*x,mod)>1)
ans*=p[i];
}return ans;
}
int T;
int main()
{
sf("%lld%d",&mod,&T);
phi=getphi(mod);
for (int i=1;i<=T;i++){
ll x,y;sf("%lld%lld",&x,&y);
puts(ord(x)%ord(y)==0 ? "Yes" : "No");
}return 0;
}