高次剩余学习笔记 / P5668 - 【模板】N次剩余 题解

考前学算法,就是这么浪。


\(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\neq0\)。若 \(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\)\(\beta<\alpha\),那么 \(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\!\left(\sqrt {p^\alpha}\right)\),而 \(|X|\) 上限是有保证的)。直接暴力枚举 \([0,2^\alpha)\) 内所有 \(x\) 显然会 T,我们可以剪枝呀!考虑对 \(\alpha\) 进行迭代,假设 \(x^a\equiv b\pmod{2^{\alpha-1}}\) 的解集已经算出为 \(X_{\alpha-1}\),那么 \(x\)\(x^a\equiv b\pmod{2^{\alpha}}\) 的解的必要条件是满足 \(\alpha-1\) 的方程,即 \(x\in X_{\alpha-1}\pmod{2^{\alpha-1}}\),而对于每个 \(x_0\in X_{\alpha-1}\),在 \(\bmod 2^\alpha\) 意义下有两个对应值,都快速幂判一下即可。这样最坏复杂度依然是跑满的,但是剪枝效果很好(可以通过模板题),毕竟 \(2^\beta\) 时减掉一个枝相当于直接砍掉 \(2^{\alpha-\beta}\) 个可能的答案。

upd:被这个帖子 hack 掉了。还以为是什么大问题,没想到是个偷袭(雾)。设分解到的每个质因子的解数为 \(X_i\),那么总共要做 \(\mathrm O(X=\prod X_i)\) 次 CRT,而 CRT 不会出现无法合并,也就是总解数也是 \(\mathrm O(X)\),而题目保证 \(X\) 不会太大,所以复杂度是对的。但有一个漏洞:有 \(X_i=0\) 的话,「只要做 \(\mathrm O(X)\) 次 CRT」便不成立,这里在开始 CRT 之前特判一下就好了。

代码(已经换码风了,还要适应之前的码风好烦):

#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;
    vector<vector<int>> vv;
    for(int i=0;i<dsr.size();i++){
        vector<int> v=solve(a,b,dsr[i].X,dsr[i].Y);
        vv.pb(v);
        if(v.empty())return puts("0"),void();
    }
    for(int i=0;i<dsr.size();i++){
        auto v=vv[i];
        vector<int> 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;
}
posted @ 2021-10-22 21:09  ycx060617  阅读(495)  评论(1编辑  收藏  举报