首先回憶一下初等代數里的對數。如果$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 \times a^m\equiv 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; }
參考鏈接: