[模板] [另解] 扩展BSGS

[题解] [模板] [另解] 扩展BSGS

零.前言

​ 今天听了一点课,然后开始整活,目前没有在题解里面看到这种做法,所以来发一篇。此篇涉及到一点点的群论定义,但是只是皮皮皮皮毛,很好理解,提供理论支撑,与代码无关。

​ 理论上来讲,看这篇题解,从学会 BSGS 到它的扩展版,只需要 5分钟(若你对群没有丝毫了解,则需 10 分钟),并且也有不用群论的数论解释,前提你要知道扩展欧拉定理。

一.半群

​ 觉得看不懂的可以不看,看之后的数论解释,而且多半也没什么用

​ 半群是最简单、最自然的一类代数系统。一个非空集合S连同定义在它上面的一个结合的(即满足结合律的)二元运算 (cdot) 的代数系统 $(S,cdot) $ 称为一个半群。(|S|) 称为这个半群的大小

​ 很显然我们定义一个集合 S 为(S={x|xequiv a^y pmod p}),而定义的乘法也满足结合律,为二元运算,这样组成了一个半群,而我们的算法的理论基础就建立在这个上面。

​ 理论上任意的半群都可以拿来跑 BSGS ,而且时间复杂度均为 (O(sqrt{|S|}))

二.数论解释

​ 首先将 "乘以 a " 看为一条出边,连接向结果,基于扩展欧拉定理,会有循环节。更具体的说,将起点设为 1 ,那么会形成一个形如 ( ho) 的图,即在某一刻进入循环节,然后再环上不断跑。

​ 为了方便,我们称循环节的部分为 环,非循环节的部分为 柄。

​ 容易知道这个图的大小(点数)不会超过 (2*varphi(p)) ,这个是依据扩展欧拉定理来的,柄和环分别都是上界为 (varphi(p)) .所以理论复杂度是 (O(sqrt{varphi(p)})) ,好耶!并且环的大小必然为(varphi(p)) 的因子。

​ 当然实际会有哈希表和乘法定义的复杂度,这里就八仙过海了。

三.具体操作

​ 还是像普通BSGS,将大步的大小 (M) 算为 (sqrt{(2*varphi(p))}) 预处理出大步和小步(懂得都懂吧,小步就是 (b*a^x) ,大步就是 (a^M,a^{2M}dots)) ,只是不同于不同 BSGS 的有两点。

  • 哈希表里面存的大步而非小步,这样具有十分优美的形态。
  • 每一个地方都需要存两个最小的对应指标,理由待会再说。

然后直接大力跑就行了。若是不一定逆元,最后需要检验答案,即 (a^{kM-q}equiv b) 这个式子是否成立。检验一定要检验两个,即拿最小得两个可能是答案得 (k) 拿去检验。检验是因为逆元不一定存在,至于两个的原因,现在来说。

​ 考虑到 (kM-q) 的意义相当于在图上倒退 q 步。那么如果是在环上第一圈,则可能倒退回柄上,若非第一圈则倒退至环上,也就是可能有两个地方。而最小和次小之间最小差一整圈,所以不会出现在两个都在柄上的情况。

​ 下面给出代码。

​ 由于我们所有可能的都考虑了,是不会漏解的,只会有增根,所以要检验,非要类比就好像是解方程时给根式平方了,然后有一个负数的增根。

CODE

#define int long long
#define fe(i,a,b) for(int i=a;i<=b;++i)
inline int read(){
	...
}
inline int get_phi(int x){
	int res=x;
	for(int i=2;i*i<=x;++i){
		if(x%i==0){
			res=res/i*(i-1);
			while(x%i==0)x/=i;
		}
	}
	if(x!=1)res=res/x*(x-1);
	return res;
}
#define pii pair<int,int>
#define mp(a,b) make_pair(a,b)
inline int KSM(int a,int b,int mod){
	int res=1;
	for(;b;b>>=1,a=(a*a)%mod)if(b&1)res=(res*a)%mod;
	return res;
}
unordered_map<int,pii> ha;
int ans,Baby[100000],a,b,p,Gaint[100000];
inline bool check(int c,int siz,int i){
	return (b%p)==(Gaint[c-1]*Baby[siz-i]%p);
}
signed main(){
	while(1){
		a=read(),p=read(),b=read();
		if(!a&&!b&&!p)break;
		int phi=get_phi(p);
		int siz=ceil(sqrt(2*phi)),alf=KSM(a,siz,p),temp=1;
		ha.clear(),ans=-1;
		fe(i,0,siz){
			Gaint[i]=temp;
			pii u=ha[temp];
			if(u.first==0)u.first=i;
			else if(u.first>i)u.second=u.first,u.first=i;
			else if(u.second>i||u.second==0)u.second=i;
			ha[temp]=u;
			temp=(temp*alf)%p;
		}
		Baby[0]=1;
		fe(i,1,siz)Baby[i]=(Baby[i-1]*a)%p;
		fe(i,0,siz-1){
			pii u=ha[(b*Baby[i])%p];
			if(u.first!=0&&check(u.first,siz,i))ans=(ans==-1||ans>u.first*siz-i)?u.first*siz-i:ans;
			else if(u.second!=0&&check(u.second,siz,i))ans=(ans==-1||ans>u.second*siz-i)?u.second*siz-i:ans;
		}
		if(ans==-1)puts("No Solution");
		else printf("%lld
",ans);
	}
	return 0;
}

​ 考场实现当然可以手写一个 hash 表,每个位置也只用挂两个,(机房卷老写了两个哈希表,说好写一些)这里偷懒写了map。

​ 然后存小步的写法我还没怎么思考,感觉不如存大步优美,好像还要逆元什么的,多半不好写。而且存大步可以解决对于同一个模数与底数的多次询问,会舒服一些。

原文地址:https://www.cnblogs.com/clockwhite/p/14432701.html