大步小步算法

大步小步算法(baby step giant step,BSGS)

是一种用来求解离散对数(即模意义下对数)的算法,即给出 \(a^x \equiv b \pmod m\)\(a,b,m\) 的值(这里保证 \(a\)\(m\) 互质,求解 \(m\)

既然保证了 \(a\)\(m\) 互质,那么很容易联想到欧拉定理,我们知道 \(a^{\phi(m)} \equiv 1 \pmod m\),也就说明 \(a^x\) 在模 \(m\) 意义下有一个长度为 \(\phi(m)\) 的循环节。既然后面都是循环的,我们只需要考虑 $x\leq\phi(m) $ 的情形就可以了。这就有了离散对数的朴素算法:暴力枚举。复杂度 \(O(\phi(m))\),因为当 \(m\) 是质数时 \(\phi(m)=m-1\) ,最坏时间复杂度就是 \(O(m)\)

实际上,大步小步算法就是对暴力枚举的一个简单的改进, 我们把 \(x\) 拆成 \(At-B\) ,则原式化为 \(a^{At-B} \equiv b \pmod m\) ,即 \(a^{At} \equiv ba^B \pmod m\) 。然后我们预计算出右侧所有可能的取值,再固定一个 \(t\) ,计算出左边可能的值,当发现某个值已经在右边出现过,这时的 \(At-B\) 就是我们要求的 \(x\)

B可能的取值有 \(\phi(m) \:\: mod \:\: t\) 个,A可能的取值有 \(\lfloor \frac{\phi(m)}{t} \rfloor\) 个,不难看出,取 \(t= \lceil \sqrt {\phi(m)} \rceil\) 是最好的,当然为了避免计算欧拉函数,我们直接取 \(t=\lceil \sqrt{m} \rceil\) ,不难验证,此时取 $A,B\in [1,t] $ 可以保证把 \(x\in[1,m-1]\) 全部枚举一遍。时间复杂度为 \(O(\sqrt m)\)

ll bsgs(ll a,ll b, ll m, ll k = 1){
	unordered_map<ll, ll>hs;
	ll cur=1,t=sqrt(m)+1;
	for(int B=1;B<=t;B++){
		(cur*=a)%=m;
		hs[b*cur%m]=B; // 哈希表中存B的值
	}
	ll now=cur*k%m;
	for(int A=1;A<=t;A++){
		auto it=hs.find(now);
		if(it!=hs.end())return A*t - it->second;
		(now*=cur)%=m;
	}
	return -inf; 
}

例题

1.G - Sequence in mod P
题意

p是质数

题解:
不妨设 \(i=A*t-B, f(X)=(aX+b)%P\)
根据题目
\(f^{At-B}(S) \equiv G \pmod P \Rightarrow f^{At}(S)\equiv f^B(G) \pmod P\)
因此我们只要取 \(t=\lceil \sqrt P \rceil\) 来进行大步小步算法
如何计算\(f^t(X)\),因为递归是线性的,设 \(f^t(X)=A^{(t)}X+B^{(t)}\)
由递归式可知
\(A^{(t)} = A^{(t-1)}*a\)
\(B^{(t)} = a*B^{(t-1)}+b\)

点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("-1");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e5+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}

void solve(){
	ll p=read(),a=read(),b=read(),s=read(),g=read();
	ll ans=-1;
	if(s==g){puts("0");return ;}
	if(a==0){
		if(b==g)puts("1");
		else puts("-1");
		return ;
	}
	ll aa=1,bb=0,cur=g,t=sqrt(p)+1;
	unordered_map<ll,ll>hs;
	for(int B=1;B<=t;B++){
		aa=aa*a%p;
		bb=(bb*a+b)%p;
		cur=(cur*a+b)%p;
		hs[cur]=B;
	}
	cur=(aa*s+bb)%p; 
	for(int A=1;A<=t;A++){
		if(hs.count(cur)){
			ans=A*t-hs[cur];
			break;
		}
		cur=(aa*cur+bb)%p;
	}
	printf("%lld\n",ans);
	return ;
}
int main(){
	int t=read();
	while(t--)solve();
    return 0;
}

拓展大步小步算法

上面的算法只针对 \(a\)\(m\) 互质的情形,如果不互质,又该如何计算?我们尝试把它转化成 \(a\)\(m\) 互质的情形。

\(x>0\) 时, \(a^x\equiv b \pmod m\) 等价于 \(a^{x-1}a+nm=b\) ,由斐蜀定理可知,有解的条件是 \(b\)\(gcd(a,m)\) 的倍数。

\(d=gcd(a,m)\) ,则 \(\frac{a^{x-1}a}{d} + \frac{nm}{d} =\frac{b}{d}\) ,即 \(\frac{a}{d} a^{x-1} \equiv \frac{b}{d} \pmod {\frac{m}{d} }\) 。这时如果 \(gcd(a,\frac{m}{d})=1\) ,就可以直接用大步小步算法求解了(左侧多了一个系数,但这显然影响不大,把大步小步算法稍作修改即可),解出的 \(x\) 加上 \(1\) 即是原方程的一个解。否则,可以继续递归下去,直到互质为止。

注意一下\(特判\),本来BSGS是不需要特判的,但是我们的递归条件只对 \(x>0\) 时成立。所以如果 \(b\equiv 1 \pmod m\) 直接返回 \(x=0\) 即可。

为了进一步加快速度,我们用非递归而非递归的方式实现这个算法。这里很容易做到,而且写起来比递归版本更加简洁。

struct BSGS{ 
	//a^x=b(mod m)
	/*
		a^{At}=b*a^{B} (mod m)
	*/
	ll bsgs(ll a,ll b, ll m, ll k = 1){
	    unordered_map<ll, ll>hs;
	    ll cur=1,t=sqrt(m)+1;
	    for(int B=1;B<=t;B++){
	        (cur*=a)%=m;
	        hs[b*cur%m]=B; // 哈希表中存B的值
	    }
	    ll now=cur*k%m;
	    for(int A=1;A<=t;A++){
	        auto it=hs.find(now);
	        if(it!=hs.end())return A*t - it->second;
	        (now*=cur)%=m;
	    }
	    return -inf; // 这里因为要多次加1,要返回更小的负数
	}
	ll exBSGS(ll a,ll b,ll m,ll k=1){
	    ll A=a%=m,B=b%=m,M=m;
	    if(b==1)return 0;
	    ll cur=1%m;
	    for(int i=0;;i++){
	        if(cur==B)return i;
	        cur=cur*A%M;
	        ll d=gcd(a,m);
	        if(b%d)return -inf;
	        if(d==1)return bsgs(a,b,m,k*a%m)+i+1;
	        k=k*a/d%m,b/=d,m/=d; 	// 相当于在递归求解exBSGS(a, b / d, m / d, k * a / d % m)
	    }
	}
}S;
posted @ 2022-09-28 23:03  I_N_V  阅读(209)  评论(0编辑  收藏  举报