(扩展)BSGS与高次同余方程

不打扰是莪最后的温柔 提交于 2020-01-30 07:05:46

前言:

今天更BSGS算法。俗称大步小步算法(Big-Step G…-Step),又称拔山盖世、北上广深、白色狗屎
 
 

问题:

求解指数同余方程:axb(mod  p)a^{x}\equiv b(mod\; p)的最小自然数解。
 
 

BSGS算法:模板题

这个算法只考虑gcd(a,p)=1gcd(a,p)=1的情况。
 
那么,怎么做呢。
其实做法很简单,就是枚举
 
先来一种暴力枚举
就是枚举xx[0,p1][0,p-1],然后检验答案是否正确。
知道欧拉定理的应该知道只要枚举到φ(p)1\varphi(p)-1,但pp为质数时欧拉函数仍为p1p-1
看过阶与原根的应该知道只要枚举到pp的阶即可,但算pp的阶仍然要暴力去枚举。
所以这里只要一个一个枚举过去便好。
 
但是o(p)o(p)的暴力枚举配不上这么高大上的名字,所以还有优化的余地。
我们可以考虑折半枚举
怎么折半,就是这个算法的精髓了。
我们令x=kn+ix=kn+i,其中nn为我们设置的一个定值,kk和iii均为变量。
这样原式就变为akn+ib(mod  p)a^{kn+i}\equiv b(mod\;p)
由于gcd(a,p)=1gcd(a,p)=1,我们可以将aia^{i}的逆元乘到右边,变成aknb(ai)1(mod  p)a^{kn}\equiv b(a^{i})^{-1}(mod\;p)
这样我们可以枚举所有的ii,将其对应右边的值存入哈希表(也可用unordered_map)。然后枚举kk的过程中,去表中找满足的ii,找到后kn+ikn+i就是答案。当然因为要找最小的,枚举过程中动点手脚就好了。
那么这个nn的设置嘛,利用分块的思想,nn设为p\sqrt{p}就好了,这样kk只用枚举到p\sqrt{p}ii也只用枚举到p\sqrt{p}。总的复杂度为n\sqrt{n}

上代码:

#include <bits/stdc++.h>
#include <unordered_map>
using namespace std;
typedef long long ll;
const ll mod=1e9+7;

ll fpow(ll a,ll n,ll mod)
{
    ll sum=1,base=a%mod;
    while(n!=0)
    {
        if(n%2)sum=sum*base%mod;
        base=base*base%mod;
        n/=2;
    }
    return sum;
}

ll ex_gcd(ll a,ll b,ll& x,ll& y)
{
    if(b==0)
    {
        x=1;y=0;
        return a;
    }
    ll ans=ex_gcd(b,a%b,x,y);
    ll tmp=x;
    x=y;
    y=tmp-a/b*y;
    return ans;
}

ll inv(ll a,ll mod)//存在逆元条件:gcd(a,mod)=1
{
    ll x,y;
    ll g=ex_gcd(a,mod,x,y);
    if(g!=1)return -1;
    return (x%mod+mod)%mod;
}

ll BSGS(ll a,ll b,ll p)
{
    b%=p;
    if(b==1||p==1)return 0;
    ll n=sqrt(p);
    static unordered_map<ll,ll>Bmp;
    Bmp.clear();
    ll inva=inv(fpow(a,n-1,p),p)*b%p;
    for(ll i=n-1;i>=0;i--)
    {
        Bmp[inva]=i;inva=inva*a%p;
    }
    ll ta=1,powa=fpow(a,n,p);
    for(ll k=0;k<=p;k+=n)
    {
        if(Bmp.count(ta))return k+Bmp[ta];
        ta=ta*powa%p;
    }
    return -1;
}

int main()
{
    ll p,b,n;
    scanf("%lld%lld%lld",&p,&b,&n);
    ll ans=BSGS(b,n,p);
    if(ans==-1)printf("no solution\n");
    else printf("%lld\n",ans);
    return 0;
}

扩展BSGS:模板题

既然BSGS只能解决gcd(a,p)=1gcd(a,p)=1的情况,那么当然还有gcd(a,p)gcd(a,p)可能不等于11的情况。
那么这个时候就需要对算法进行扩展了。
 
因为gcd(a,p)=1gcd(a,p)=1,所以不能再像原来一样逆元了。
所以我们考虑把gcdgcd先提取出来。
我们令d=gcd(a,p)d=gcd(a,p)
我们将原式axb(mod  p)a^{x}\equiv b(mod\;p)展开成ax+kp=ba^{x}+kp=b
然后同时除以dd变成axd+kpd=bd\frac{a^{x}}{d}+k\frac{p}{d}=\frac{b}{d}
再同时取模pd\frac{p}{d}就得到了axdbd(mod  pd)\frac{a^{x}}{d}\equiv \frac{b}{d}(mod\;\frac{p}{d})
容易发现bb必须是dd的倍数,否则直接返回无解。
然后此时gcd(ad,pd)=1gcd(\frac{a}{d},\frac{p}{d})=1,所以可以乘上它的逆元。
变成ax1bd(ad)1(mod  pd)a^{x-1}\equiv \frac{b}{d}(\frac{a}{d})^{-1}(mod\;\frac{p}{d})
就变成了一个新的问题。
那么观察此时gcd(a,pd)gcd(a,\frac{p}{d})是否一定为11
当然不一定,举例a=2,p=8a=2,p=8,那么d=2d=2,将pp除以dd后,仍能除以dd,没事,那再除一次嘛。
笔者懒,写了递归版,这个过程可以一步到位的,递归可能会慢一些,但是比起固有p\sqrt{p}的复杂度,多个loglog也无伤大雅。
 
那么递归的思路就很清晰了。
如果gcd(a,p)=1gcd(a,p)=1,那么用BSGS求解。
如果gcd(a,p)1gcd(a,p)\neq 1,那么用上面的办法将模pp的问题变为模pgcd(a,p)\frac{p}{gcd(a,p)}的问题,然后再递归调用这个函数。
 
非递归思路:
如果gcd(a,p)=1gcd(a,p)=1,那么用BSGS求解。
如果gcd(a,p)1gcd(a,p)\neq 1,那么用上面的方法给pp除以gcd(a,p)gcd(a,p),只要除以多少个弄清楚,就可以一次弄好了。
 
最后是我的递归版代码:

#include <bits/stdc++.h>
#include <unordered_map>
using namespace std;
typedef long long ll;
const ll mod=1e9+7;

ll fpow(ll a,ll n,ll mod)
{
    ll sum=1,base=a%mod;
    while(n!=0)
    {
        if(n%2)sum=sum*base%mod;
        base=base*base%mod;
        n/=2;
    }
    return sum;
}

ll ex_gcd(ll a,ll b,ll& x,ll& y)
{
    if(b==0)
    {
        x=1;y=0;
        return a;
    }
    ll ans=ex_gcd(b,a%b,x,y);
    ll tmp=x;
    x=y;
    y=tmp-a/b*y;
    return ans;
}

ll inv(ll a,ll mod)//存在逆元条件:gcd(a,mod)=1
{
    ll x,y;
    ll g=ex_gcd(a,mod,x,y);
    if(g!=1)return -1;
    return (x%mod+mod)%mod;
}

ll gcd(ll a,ll b)
{
    return b?gcd(b,a%b):a;
}

ll BSGS(ll a,ll b,ll p)
{
    b%=p;
    if(b==1||p==1)return 0;
    ll n=sqrt(p);
    static unordered_map<ll,ll>Bmp;
    Bmp.clear();
    ll inva=inv(fpow(a,n-1,p),p)*b%p;
    for(ll i=n-1;i>=0;i--)
    {
        Bmp[inva]=i;inva=inva*a%p;
    }
    ll ta=1,powa=fpow(a,n,p);
    for(ll k=0;k<=p;k+=n)
    {
        if(Bmp.count(ta))return k+Bmp[ta];
        ta=ta*powa%p;
    }
    return -1;
}

ll exBSGS(ll a,ll b,ll p)
{
    b%=p;
    if(a==0&&b==0)return 1;
    else if(a==0&&b!=0)return -1;
    if(b==1||p==1)return 0;
    ll d=gcd(a,p);
    if(b%d!=0)return -1;
    p=p/d;
    b=b/d*inv(a/d,p)%p;
    if(d!=1)
    {
        ll ans=exBSGS(a,b,p);
        if(ans==-1)return -1;
        return ans+1;
    }
    ll ans=BSGS(a,b,p);
    if(ans==-1)return -1;
    return ans+1;
}

int main()
{
    ll a,p,b;
    while(scanf("%lld%lld%lld",&a,&p,&b)!=EOF)
    {
        if(a==0&&p==0&&b==0)break;
        ll ans=exBSGS(a,b,p);
        if(ans==-1)printf("No Solution\n");
        else printf("%lld\n",ans);
    }
    return 0;
}

最后再补充一个例题,Mod Tree。注意题目中的取模后相同并非模意义下相同。
 
 

高次同余方程:

不知道原根的出门左转。
最后再将一个BSGS的应用。
求解方程xna(mod  p)x^{n}\equiv a(mod\;p)
 
我们只考虑pp为素数的求解办法。
做法:
我们先求出pp的原根gg
根据原根的性质,当pp为素数时,gg的幂次可以得到一个完全剩余系。
所以我们可以令x=gkx=g^{k},同样可以令a=gx0a=g^{x_{0}}
此时,我们只要求解kk,就可以求解xx了。
方程变为gkngx0(mod  p)g^{kn}\equiv g^{x_{0}}(mod\;p)
等价于方程knx0(mod  p1)kn\equiv x_{0}(mod\;p-1)。(这个可以根据费马小定理,也可以根据ordpg=p1ord_{p}g=p-1
因为不能确保gcd(n,p1)=1gcd(n,p-1)=1,所以不能乘逆元。
其实方程等价于求方程nk+(p1)y=x0nk+(p-1)y=x_{0},用扩展欧几里得即可。
到这一步答案就解出来了。
 
嗯?我要说的BSGS呢?
噢,你会发现这个x0x_{0}还没求。
gx0a(mod  p)g^{x_{0}}\equiv a(mod\;p),所以求x0x_{0}就是求指数同余方程了。
到这里,求毕。
 
这个没写代码,我懒了,想都别想。
 
 
那么关于pp不为素数的。
其实我不会,但是我还可以鬼扯,可能可行也可能不行,往下阅读时请保留自己的理智,免得被笔者误导。

 
考虑分解合数p=p1k1p2k2pnknp=p_{1}^{k_{1}}p_{2}^{k_{2}}…p_{n}^{k_{n}}
噢,好像不可行。
但是当p=p1p2pnp=p_{1}p_{2}…p_{n},即每个素数的次数都为11次时,还是有办法可行的。
我们只要求解出对于每个模数的方程xna(mod  pi)x^{n}\equiv a(mod\;p_{i})的解xa0(mod  p)x\equiv a_{0}(mod\;p)
然后用中国剩余定理合并即可。
这样想的话,如果有办法求解xna(mod  piki)x^{n}\equiv a(mod\;p_{i}^{k_{i}})的话,就可以解决任意模数的问题了,暂时还没想好。
扯毕。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!