首先回忆一下初等代数里的对数。如果$a^x=b$,就是$x = log_ab$,即x是以a为底b的对数。在模算术中,也有类似的概念,但要比初等代数里的复杂一些。简单起见,这里只考虑一种最简单的情况,即当n为素数时,解模方程$a^x equiv b(mod n)$。因为n为素数,只要a不为0,一定存在逆$a^{-1}$。
根据欧拉定理,只需检查$x=1,2, cdots,n-1$是不是解即可,因为$a^{n-1} equiv 1(mod n)$,当x超过n-1时$a^x$就开始循环了。我们先检查前m项(m的取值稍后叙述),即$a^0,a^1, cdots, a^{m-1}$模n的值是否为b,并把$a^i mod n$保存在$e_i$里,求出$a^m$的逆$a^{-m}$.
下面考虑$a^m,a^{m+1},cdots,a^{2m-1}$.这次不用一一检查,因为如果它们中有解,则相当于存在$i$,使得$e_i imes a^mequiv b(mod n)$,两边左乘$a^{-m}$得${b}' equiv a^{-m}b(mod n)$。这样只需检查是否真存在$e_i$等于这个${b}'$即可。为了方便,可以把$e_i$保存在一个$STL$集合中,但考虑还需要得到具体的下标$i$,我们用$map<int,int>x$,其中$x[j]$表示满足$e_i = j$的最小的$i$(因为可能有多个$e_i$的值相同)。在下面的代码中,${b}'$采用递推计算,覆盖了输入参数$b$。
//求解模方程a^x=b(mod n)。n为素数,无解返回-1 int log_mod(int a, int b, int n) { int m, v, e = 1, i; m = (int)sqrt(n + 0.5); v = inv(pow_mod(a, m, n), n); map<int, int>x; x[1] = 0; for (i = 1; i < m; i++) { e = mul_mod(e, a, n); if (!x.count(e)) x[e] = i; } for (i = 0; i < m; i++) //m*i < n,当m=√n,i < m { if (x.count(b)) return i * m + x[b]; b = mul_mod(b, v, n); } return -1; }
上面的代码中,$m=n^{1/2}$,这是为什么呢?因为计算e数组需要$O(mlogm)$时间,以后每一轮需要$O(logm)$时间,一共$O(n/m)$轮,因此总的时间复杂度为$O((m+n/m)logm)$,当$m$和$n/m$接近,即$m=n^{1/2}$时总时间最短,为$O(n^{1/2}logn)$。这个算法称为Shank的大步小步算法$({Shank}'s Baby-Step-Giant-Step Algorithm)$.
当$n$不是素数时,还可以把这个算法加以扩展。
易只,BSGS有两种形式:
$$a^{km+t}equiv(mod p), a^{km-t}equiv b(mod p)$$
第一种形式是经典的BSGS,并可以应用到EXBSGS中,而第二种形式的优点在于不需要求逆元,比如 $a, b$ 是矩阵时。
按照BSGS的思路,可化成 $a^{km}equiv b*a^t(mod p)$,
于是我们便可以把 $b*a^t(mod p)$ 存到 map 中,然后枚举 $k$ 的取值来查询。
map<int,int>mp; int bsgs(int a, int b, int p){ //a^x = b (mod P),(a,p)=1,返回x,x>=1; 无解返回-1 int m=sqrt(p)+1;mp.clear(); for(register int i=0,res=b;i<m;++i,res=1ll*res*a%p)mp[res]=i; for(register int i=1,tmp=qpow(a,m,p),res=tmp;i<=m+1;++i,res=1ll*res*tmp%p) if(mp.count(res))return i*m-mp[res]; return -1; }
参考链接: