离散对数 学习笔记

阶和原根

对于 \(a\in\mathbb{Z},m\in\mathbb{N}^+,\gcd(a,m)=1\),定义 \(a\)\(m\) 的阶 \(\delta_m(a)\) 为满足 \(a^n\equiv 1\pmod{m}\) 的最小正整数 \(n\),记作 \(\delta_m(a)\)

首先 \(a,a^2,\cdots,a^{\delta_m(a)}\)\(m\) 互不相同。否则设 \(a^i\equiv a^j\pmod{m}\),则 \(|i-j|\) 才是 \(a^n\equiv1\pmod{m}\) 的最小解,矛盾。那么阶相当于不断把 \(a\) 自乘的循环节长度。

原根:你说得对,但是原根是一个数学符号。对于 \(g\in\mathbb{Z},m\in\mathbb{N}^+\),若 \(\gcd(g,m)=1\)\(\delta_m(g)=\varphi(m)\),则称 \(g\) 为模 \(m\) 的原根。这就是说 \(g\) 的幂次能遍历所有模 \(m\) 意义下与 \(m\) 互质的元素。

你的数学很差,我现在每天用原根都能做 1e5 次数据规模 1e6 的 NTT,每个月差不多 3e6 次卷积,也就是现实生活中 3e18 次乘法运算,换算过来最少也要算 1000 年。虽然我只有 14 岁,但是已经超越了中国绝大多数人(包括你)的水平,这便是原根给我的骄傲的资本。

原根判定定理:对于 \(m\geq3,\gcd(g,m)=1\)\(g\) 是模 \(m\) 的原根的充要条件是对于 \(\varphi(m)\) 的每个质因数 \(p\) 都有 \(g^{\frac{\varphi(m)}{p}}\not\equiv1\pmod{m}\)

感性理解:\(\gcd(g,m)=1\),可以把 \(g\) 写成最小原根的 \(k\) 次幂,\(g\) 不断自乘相当于每次在循环节上走 \(k\) 步。这个定理说的是 \(k\)\(\varphi(m)\) 互质,否则就不能遍历循环节上的所有节点导致阶较小。

原根个数定理:若 \(m\) 有原根,则 \(m\) 的原根个数为 \(\varphi(\varphi(m))\)

\(k\)\(\varphi(m)\) 互质,那么 \(k\)\(\varphi(\varphi(m))\) 个。

原根存在定理:一个数 \(m\) 存在原根的充要条件是 \(m=2,4,p^k,2p^k\)。其中 \(p\) 是奇质数,\(k\in\mathbb{N}^+\)

这个证明比较复杂,在此略过。

另外可以证明最小原根的数量级是 \(O(m^{\frac 1 4})\) 的。

关于找一个数的原根 P6091

先预处理质数和欧拉函数,并标记存在原根的数。求最小原根,从小到大枚举 \(g\),判断是否 \(\gcd(g,m)=1\)\(\forall k<\varphi(m),g^k\not\equiv 1\pmod{m}\)

然而不能枚 \(1\sim\varphi(m)\)。从循环节的角度理解,一次跳若干步说明循环的步数是环长的因数,所以只用枚 \(\varphi(m)\) 的真因数。事实上只用枚举 \(\frac{\varphi(m)}{p_i}\),其中 \(p_i\)\(m\) 的质因子,因为这枚举到了 \(m\) 的真因数的倍数。

求出最小原根 \(g\) 后枚举与 \(\varphi(m)\) 互质的 \(k\)\(g^k\) 为原根。

#include<bits/stdc++.h>
using namespace std;
int t,d,prime[1000005],phi[1000005],cnt,ans[1000005],num;
long long n;
bool a[1000005],vis[1000005];
template<typename T>T gcd(T m,T n){
  return n?gcd(n,m%n):m;
}
template<typename T>T qpow(T a,T b,T n,T ans=1){
  for(a%=n;b;b>>=1)b&1&&(ans=1ll*ans*a%n),a=1ll*a*a%n;
  return ans;
}
int phi_init(int n,int prime[],int phi[],bool a[],int cnt=0){
  phi[1]=1;
  for(int i=2;i<=n;i++)a[i]=1;
  for(int i=2;i<=n;i++){
    if(a[i])prime[++cnt]=i,phi[i]=i-1;
    for(int j=1;j<=cnt&&i*prime[j]<=n;j++){
      a[i*prime[j]]=0;
      if(i%prime[j]==0){
        phi[i*prime[j]]=phi[i]*prime[j];
        break;
      }
      else phi[i*prime[j]]=phi[i]*(prime[j]-1);
    }
  }
  return cnt;
}
long long getg(long long n,long long g=1){
  while(1){
    if(gcd(g,n)!=1)goto tag;
    for(int j=1;j<=cnt&&prime[j]<=phi[n];j++){
      if(phi[n]%prime[j]==0&&qpow(g,1ll*phi[n]/prime[j],n)==1)goto tag;
    }
    break;
    tag:g++;
  }
  return g;
}
int main(){
  cnt=phi_init(1000000,prime,phi,a),vis[2]=vis[4]=1;
  for(int i=2;i<=cnt;i++){
    for(long long j=prime[i];j<=1000000;j*=prime[i])vis[j]=1;
    for(long long j=prime[i];2*j<=1000000;j*=prime[i])vis[2*j]=1;
  }
  cin>>t;
  while(t--){
    cin>>n>>d;
    if(!vis[n]){
      puts("0"),putchar('\n');
      continue;
    }
    long long g=getg(n),t=g;
    for(int i=1;i<=phi[n];i++,t=t*g%n)if(gcd(i,phi[n])==1)ans[++num]=t;
    sort(ans+1,ans+num+1),cout<<num<<'\n';
    for(int i=d;i<=num;i+=d)cout<<ans[i]<<' ';
    putchar('\n'),num=0;
  }
  return 0;
}

原根的作用除了充当模意义下单位根,还有表示所有模 \(m\) 意义下与 \(m\) 互质的元素。可以用原根定义模意义下的对数,设 \(g^t\equiv a\pmod{m}\),则 \(a\) 在模 \(m\) 意义下以 \(g\) 为底的对数为 \(t\)

大步小步

用于解决离散对数问题,即形如 \(a^x\equiv b\pmod{p}\) 的同余方程。普通的大步小步算法要求 \(a,p\) 互质。

因为 \(a,p\) 互质,所以根据欧拉定理可以知道 \(x\leq\varphi(p)<p\)

考虑用类似 meet in the middle 的方法优化暴力。设一个阈值 \(m\),把 \(x\) 拆成 \(um-v\) 放到两边变成 \((a^u)^m\equiv ba^v\pmod{p}\)。先把右边的可能值存进哈希表,再枚举左边的值,如果相遇就返回。

\(m=\sqrt p+1\) 时复杂度最优,为 \(O(\sqrt p)\)\(u\)\(0\sim m-1\)\(v\)\(1\sim m\) 可以枚举完 \(1\sim p-1\)

template<typename T>T bsgs(T a,T b,T n){
  unordered_map<T,T>p;
  T m=sqrt(n)+1;
  a%=n,b%=n;
  if(b==1)return 0;
  for(T i=0,t=b;i<m;i++,t=t*a%n)p[t]=i;
  for(T i=1,t=a;i<m;i++)a=a*t%n;
  for(T i=1,t=a;i<=m;i++,t=t*a%n)if(p.count(t))return m*i-p[t];
  return -1;
}

拓展大步小步

如果没有 \(a,p\) 互质这一条件,考虑整个方程同除 \(d=\gcd(a,p)\),变成 \(\frac a d a^{x-1}\equiv\frac b d\pmod{\frac p d}\),即 \(a^{x-1}\equiv\frac b d\left(\frac a d\right)^{-1}\pmod{\frac p d}\),这相当于一个子问题。重复上述操作直到 \(a,\frac p d\) 互质,此时用普通 BSGS 求解。若 \(d\nmid b\) 则无解。

template<typename T>T gcd(T m,T n){
  return n?gcd(n,m%n):m;
}
template<typename T>void exgcd(T a,T b,T &x,T &y){
  b?(exgcd(b,a%b,y,x),y-=a/b*x):(x=1,y=0);
}
template<typename T>T inv(T a,T b){
  T x,y;
  exgcd(a,b,x,y);
  return (x%b+b)%b;
}
template<typename T>T bsgs(T a,T b,T n){
  unordered_map<T,T>p;
  T m=sqrt(n)+1;
  a%=n,b%=n;
  if(b==1)return 0;
  for(T i=0,t=b;i<m;i++,t=t*a%n)p[t]=i;
  for(T i=1,t=a;i<m;i++)a=a*t%n;
  for(T i=1,t=a;i<=m;i++,t=t*a%n)if(p.count(t))return m*i-p[t];
  return -1;
}
template<typename T>T exbsgs(T a,T b,T n){
  b%=n;
  if(b==1||n==1)return 0;
  T k=0,t=1;
  for(T d=gcd(a,n);d!=1;d=gcd(a,n)){
    if(b%d)return -1;
    k++,b/=d,n/=d,t=t*(a/d)%n;
    if(t==b)return k;
  }
  T ans=bsgs(a,b*inv(t,n)%n,n);
  return ans==-1?-1:ans+k;
}

例题

P3306

观察规律:

\[\begin{aligned}x_1&=x_1\\x_2&=ax_1+b\\x_3&=a^2x_1+ab+b\\x_4&=a^3x_1+a^2b+ab+b\\\vdots\\x_n&=a^{n-1}x_{1}+b\sum_{i=0}^{n-2}a^i\end{aligned} \]

移项:

\[\begin{aligned}t&\equiv a^{n-1}x_{1}+b\sum_{i=0}^{n-2}a^i\pmod{p}\\t&\equiv a^{n-1}x_1+b\frac{a^{n-1}-1}{a-1}\pmod{p}\\t&\equiv a^{n-1}x_1+\frac{ba^{n-1}}{a-1}-\frac{b}{a-1}\pmod{p}\\t&\equiv (x_1+\frac{b}{a-1})a^{n-1}-\frac{b}{a-1}\pmod{p}\\a^{n-1}&\equiv \frac{x_1+\frac{b}{a-1}}{t+\frac{b}{a-1}}\pmod{p}\\a^{n-1}&\equiv \frac{ax_1-x_1+b}{at-t+b}\pmod{p}\end{aligned} \]

大步小步即可。然而直接交会获得 0pts 的好成绩。

首先当 \(x_1=t\) 输出 \(1\)

注意上面的等比数列求和需要 \(a>1\),要特判。

\(a=0\),数列只有 \(x_1,b\) 两种值。

\(a=1\)\(x_i=x_1+b(i-1)\),用 exgcd 求解即可。还要特判 \(b=0\)

#include<bits/stdc++.h>
using namespace std;
template<typename T>T qpow(T a,T b,T n,T ans=1){
  for(a%=n;b;b>>=1)b&1&&(ans=ans*a%n),a=a*a%n;
  return ans;
}
template<typename T>T inv(T a,T b)
{
  return qpow(a,b-2,b);
}
template<typename T>T bsgs(T a,T b,T n){
  unordered_map<T,T>p;
  T m=sqrt(n)+1;
  a%=n,b%=n;
  if(b==1)return 0;
  for(T i=0,t=b;i<m;i++,t=t*a%n)p[t]=i;
  for(T i=1,t=a;i<m;i++)a=a*t%n;
  for(T i=1,t=a;i<=m;i++,t=t*a%n)if(p.count(t))return m*i-p[t];
  return -1;
}
int t;
long long p,a,b,x,c,ans;
int main(){
  cin>>t;
  while(t--){
    cin>>p>>a>>b>>x>>c;
    if(c==x)puts("1");
    else if(a==0)puts(c==b?"2":"-1");
    else if(a==1)cout<<(b?(c-x+p)%p*inv(b,p)%p+1:-1)<<'\n';
    else ans=bsgs(a,(a*c-c+b+p)%p*inv(a*x-x+b+p,p)%p,p),cout<<(ans==-1?-1:ans+1)<<'\n';
  }
  return 0;
}

科技:Pohlig-Hellman 算法

这个算法需要 \(p\) 为质数且 \(p-1\) 的质因数分解简单。

\(g^y=a,g^z=b\),那么 \(a^x\equiv b\pmod{p}\) 就成了 \(g^{xy}\equiv g^z\pmod{p}\),即 \(xy\equiv z\pmod{p-1}\)。exgcd 即可。

问题在于怎么求形如 \(g^x\equiv h\pmod{p}\)\(\log_g h\equiv x\pmod{p-1}\)。这可以分解成若干 \(\log_g h\equiv x\pmod{p^k}\) 再 CRT 合并。

\(x\) 进行 \(p\) 进制分解,设为 \(x=x_0+x_1p+x_2p^2+\cdots+x_{k-1}p^k\)。假设已知前 \(0\sim\sigma-1\) 项和,记为 \(r\)。此时等式两边乘 \(p^{k-\sigma-1}\),让 \(\sigma\) 项之后 \(p\) 的指数都大于等于了 \(p^k\),也就是消失了。此时有 \(p^{k-\sigma-1}\log_g h\equiv p^{k-\sigma-1}(r+x_\sigma p^\sigma)\pmod{p^k}\)

移项:\(p^{k-\sigma-1}(\log_g h-r)\equiv p^{k-1}x_\sigma\pmod{p^k}\)

两边同时放到 \(g\) 的指数上:\(\left(g^{p^{k-1}}\right)^{x_\sigma}\equiv\left(hg^{-r}\right)^{p^{k-\sigma-1}}\pmod{p^k}\)

发现这变成了一个模数很小的离散对数,可以暴力或 BSGS。

特殊模数下复杂度大概是两只 \(\log\),能跑 1e18。

[[数学]]

posted @ 2024-03-01 09:35  lgh_2009  阅读(26)  评论(0)    收藏  举报