1. 程式人生 > 其它 >高次剩餘學習筆記 / P5668 - 【模板】N次剩餘 題解

高次剩餘學習筆記 / P5668 - 【模板】N次剩餘 題解

數論小雜燴:CRT + 原根 + BSGS + exgcd + 暴力剪枝

考前學演算法,就是這麼浪。


\(x^a\equiv b\pmod p\)\([0,p)\) 內的所有解。

核心思想是通過原根將冪變成指數。但是普通的模數 \(p\)​ 不一定有原根啊,那考慮使用分解質因數 \(p=\prod p_i^{\alpha_i}\)​,奇質數的冪是有原根的。拆開的理論依據是什麼?換個元,令 \(y=x^a\)​​,那麼 \(y\equiv b\pmod p\)​ 可以由若干個 \(y\equiv b\pmod{p_i^{\alpha_i}}\)​ 聯立而成的方程組等價表達,根據的是模數兩兩互質的 ExCRT(其實就是 CRT,可惜我不會寫)。那麼我們得到 \(y\equiv b\pmod{p_i^{\alpha_i}}\)

​ 的解集 \(X_i\)​ 後,其實就是 \(x\)​ 是原方程的解當且僅當(方程和方程組的等價性已經用 CRT 證明)對所有 \(i\)​ 都有 \(x\in X_i\pmod{p_i^{\alpha_i}}\),於是我們可以對每個 \(X_i\) 各挑一個出來用 CRT 合併,得到 \(\prod|X_i|\)\([0,p)\) 內的 \(x\) 便是原方程的所有解。這裡使用了兩次 CRT,正著一次反著一次,步步為等價變換。

下面討論 \(x^a\equiv b\pmod{p^\alpha}(p\in\mathbb P)\)​​​​​​ 怎麼解。注意到當 \(p\neq 2\)​​​​​​ 時 \(p^{\alpha}\)

​​​​​​ 必有原根,我們分成 \(p\overset{?}=2\)​​​​​​ 兩種情況討論,先考慮 \(p\neq 2\)​​​​​​。首先如果 \(b=0\)​​​​​​,那麼顯然解集為 \(p^{\left\lceil\frac \alpha a\right\rceil}\mid x\)​​​​​​,下面 \(b=0\)​​​​​​。若 \(b\perp p^{\alpha}\)​​​​​​,求得 \(p^{\alpha}\)​​​​​​ 的任意一個原根(可以用 1/4 次 log 的演算法求最小原根)\(g\)​​​​​​,然後用 BSGS 求出 \(b=g^\beta\)​​​​​​ 的 \(\left[0,\varphi=\varphi\!\left(p^\alpha\right)\right)\)
​​​​​​ 內唯一 \(\beta\)​​​​​​。此時顯然 \(x\)​​​​​​ 是解僅當 \(x\perp p^\alpha\)​​​​​​,於是令 \(x=g^y(y\in[0,\varphi))\)​​​​​​,求出 \(y\)​​​​​​ 在 \(\bmod\varphi\)​​​​​​ 意義下所有解,就相當於求出了 \(x\)​​​​​​ 的所有解。原方程即 \(g^{ay}\equiv g^\beta\pmod{p^\alpha}\)​​​​​​,跟據原根的冪週期為 \(\varphi\)​​​​​​,這顯然等價於 \(ay\equiv\beta\pmod{\varphi}\)​​​​​​。線性同餘方程就沒什麼講的必要了吧,exgcd 找特解然後列舉通解在 \([0,\varphi]\)​​​​​​ 內所有取值即可。

如果 \(b\not\perp p^\alpha\) 呢?此時 \(b\) 在模意義下沒有原根。設 \(b=p^\beta q\),令 \(x=p^\gamma r\),那麼原方程等價於 \(p^{a\gamma}r^a\equiv p^\beta q\pmod{p^\alpha}\)。此時我們發現,如果由於 \(b\neq 0\),也就是 \(\beta<+\infty\),那麼 \(p^{\beta} q\) 所在剩餘系中都恰好包含 \(\beta\) 個質因子 \(p\),那麼我們需要 \(a\gamma=\beta\)(並不是模意義下,而是嚴格相等!)!那麼判一手 \(a\mid\beta\),直接得到 \(\gamma=\dfrac\beta a\),它現在是常數了。跟據同餘式的性質將三邊的 \(p^\beta\) 約掉,得到 \(r^a\equiv q\pmod{p^{\alpha-\beta}}\),此時有 \(p\nmid q\),於是歸約到了 \(b\perp p^\alpha\) 的情況。求出 \(r\bmod p^{\alpha-\beta}\) 的所有可能值後,對於其中每一個,我們要找出對應的所有 \(\left[0,p^\alpha\right)\) 內的 \(p^\gamma r\),那麼找到所有 \(r\in\left[0,p^{\alpha-\gamma}\right)\) 即可,隨便遍歷一波,有 \(p^{(\alpha-\gamma)-(\alpha-\beta)}=p^{\beta-\gamma}\) 個值。

最後我們來考慮 \(p=2\) 的情形。由於沒有原根,很可惜,這種情況只能暴力(有原根的情況下複雜度顯然是 \(|X|+\mathrm O(\sqrt p)\),而 \(|X|\) 上限是有保證的)。直接暴力列舉 \([0,p)\) 內所有 \(x\) 顯然會 T,我們可以剪枝呀!考慮對 \(\alpha\) 進行迭代,假設 \(x^a\equiv b\pmod{p^{\alpha-1}}\) 的解集已經算出為 \(X_{\alpha-1}\),那麼 \(x\)\(x^a\equiv b\pmod{p^{\alpha}}\) 的解的必要條件是滿足 \(\alpha-1\) 的方程,即 \(x\in X_{\alpha-1}\pmod{p^{\alpha-1}}\),而對於每個 \(x_0\in X_{\alpha-1}\),在 \(\bmod p^\alpha\) 意義下有兩個對應值,都快速冪判一下即可。這樣最壞複雜度依然是跑滿的,但是剪枝效果很好(可以通過模板題),畢竟 \(p^\beta\) 時減掉一個枝相當於直接砍掉 \(p^{\alpha-\beta}\) 個可能的答案。

code(寫起來非常爽啊)
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pb push_back
#define mp make_pair
#define X first
#define Y second
int qpow(int x,int y,int p){
	int res=1;
	while(y){
		if(y&1)res=res*x%p;
		x=x*x%p;
		y>>=1;
	}
	return res;
}
int exgcd(int a,int b,int &x,int &y){
	if(!b)return x=1,y=0,a;
	int d=exgcd(b,a%b,y,x);
	return y-=a/b*x,d;
}
int inv(int a,int p){
	int x,y,d=exgcd(a,p,x,y);
	if(d==1)return 0;
	return (x%p+p)%p;
}
int excrt(int a1,int a2,int p1,int p2){
	int k1,k2,d=exgcd(p1,p2,k1,k2);
	assert(d==1);
	k1=(k1%(p1*p2)*(a2-a1)%(p1*p2)+p1*p2)%(p1*p2);
	return (a1+p1*k1)%(p1*p2);
}
vector<int> solve2(int a,int b,int alpha){
	b%=1<<alpha;
	if(alpha==0)return vector<int>({0});
	vector<int> v=solve2(a,b,alpha-1),ret;
	for(int i=0;i<v.size();i++){
		if(qpow(v[i],a,1<<alpha)==b)ret.pb(v[i]);
		if(qpow(v[i]+(1<<alpha-1),a,1<<alpha)==b)ret.pb(v[i]+(1<<alpha-1));
	}
	return ret;
}
int proot(int p,int alpha,int palpha){
	int phi=palpha/p*(p-1);
	vector<int> dsr;
	for(int i=2;i*i<=phi;i++)if(phi%i==0){
		dsr.pb(i);
		while(phi%i==0)phi/=i;
	}
	if(phi>1)dsr.pb(phi);
	phi=palpha/p*(p-1);
	for(int i=2;;i++)if(i%p){
		bool flg=true;
		for(int j=0;j<dsr.size();j++)flg&=qpow(i,phi/dsr[j],palpha)!=1;
		if(flg)return i;
	}
}
int bsgs(int g,int b,int p){
	unordered_map<int,int> mmp;
	int B=sqrt(p)+1,now=1,now0=1;
	for(int i=1;i<=B;i++)now=now*g%p,mmp[now*b%p]=i;
	for(int i=1;i<=B;i++){
		now0=now0*now%p;
		int j=mmp[now0];
		if(j)return i*B-j;
	}
}
vector<int> solve(int a,int b,int p,int alpha){
	int palpha=qpow(p,alpha,1e12);
//	cout<<palpha<<"!!\n";
	b%=palpha;
	if(p==2)return solve2(a,b,alpha);
	if(b==0){
		int div=(alpha+a-1)/a,fst=qpow(p,div,1e12);
		vector<int> ret;
		for(int i=0;i<palpha;i+=fst)ret.pb(i);
		return ret;
	}
	if(b%p==0){
		int beta=0,q=b;
		while(q%p==0)beta++,q/=p;
		if(beta%a)return vector<int>();
		vector<int> v=solve(a,q,p,alpha-beta);
		int gamma=beta/a;
		vector<int> ret;
		int lim=qpow(p,beta-gamma,1e12),mul=qpow(p,alpha-beta,1e12),pgamma=qpow(p,gamma,1e12);
		for(int i=0;i<v.size();i++)for(int j=0;j<lim;j++)ret.pb((v[i]+j*mul)%palpha*pgamma%palpha);
		return ret;
	}
	int g=proot(p,alpha,palpha),beta=bsgs(g,b,palpha),phi=palpha/p*(p-1);
	int x,y,d=exgcd(a,phi,y,x);
	if(beta%d)return vector<int>();
	int mod=phi/d,gap=qpow(g,mod,palpha);y=(y*(beta/d)%mod+mod)%mod;
	int now=qpow(g,y,palpha);
	vector<int> ret;
	for(int i=0;y+i*mod<phi;i++)ret.pb(now),now=now*gap%palpha;
	return ret;
}
void mian(){
	int a,p,b;
	cin>>a>>p>>b;
//	cerr<<a<<" "<<b<<" "<<p<<":\n";
	vector<pair<int,int> > dsr;
	for(int i=2;i*i<=p;i++)if(p%i==0){
		int cnt=0;
		while(p%i==0)cnt++,p/=i;
		dsr.pb(mp(i,cnt));
	}
	if(p>1)dsr.pb(mp(p,1));
	vector<int> ans;ans.pb(0);
	int now=1;
	for(int i=0;i<dsr.size();i++){
		vector<int> v=solve(a,b,dsr[i].X,dsr[i].Y),nw;
		for(int j=0;j<ans.size();j++)for(int k=0;k<v.size();k++)nw.pb(excrt(ans[j],v[k],now,qpow(dsr[i].X,dsr[i].Y,1e12)));
		ans.swap(nw),now*=qpow(dsr[i].X,dsr[i].Y,1e12);
	}
	sort(ans.begin(),ans.end());
	cout<<ans.size()<<"\n";
	for(int i=0;i<ans.size();i++)printf("%lld%c",ans[i]," \n"[i+1==ans.size()]);
}
signed main(){
//	freopen("P5668_1.in","r",stdin);freopen("out.out","w",stdout);
	int t;
	cin>>t;
	while(t--)mian();
	return 0;
}
珍愛生命,遠離抄襲!