离散对数 学习笔记
阶和原根
对于 \(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;
}
例题
观察规律:
移项:
大步小步即可。然而直接交会获得 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。
[[数学]]

浙公网安备 33010602011771号