离散对数问题——pohlig-hellman算法讲解(有例子)

写在前面

一切都要从去年8月的多校开始说起。
笔者在多校某场比赛中,遇到一个离散对数的题:HDU 6632
给定 a , b , p ( p ∈ p r i m e , 65537 ≤ p ≤ 1 e 18 , 2 ≤ a , b ≤ p − 1 a,b,p(p∈prime,65537\leq p \leq 1e18,2\leq a,b\leq p-1 a,b,p(pprime,65537p1e18,2a,bp1 p − 1 p-1 p1的质因子只能由 2 2 2(和/或) 3 3 3组成),求
a x ≡ b ( m o d   p ) a^x\equiv b(mod\ p) axb(mod p)的最小正整数解,若无解则输出 − 1 -1 1

因为质数 p p p很大,且通常使用的 B S G S BSGS BSGS复杂度为 O ( n ) O(\sqrt n) O(n ),显然无法通过正常方法解题。于是很理所应当地翻车了。
随后出题人给的题解中提到了pohlig-hellman算法,想去学,但国内对于该算法的相关资料实在是少之又少。于是在理应不存在的404网站们找到了相关视频学习后才醒悟(一醒就是半年,咕咕咕)。于是决定整理一下这个问题。
注:笔者是一名十八线蒟蒻ACMer,因此下列文章可能有许多细节上的问题,请您指正,我将尽快回复和改正,非常感谢!

前置技能

快速幂
基础数论知识
扩展gcd
欧拉函数
原根

中国剩余定理(CRT)
质因子分解

pohlig-hellman算法

需要注意的是,pohlig-hellman算法的复杂度在一般情况下比 B S G S BSGS BSGS高!
因此,使用pohlig-hellman的场合只能是较为特殊的情况,即:
1.p是质数,且p-1包含的质因子较少&较小。
B S G S BSGS BSGS算法一样,pohlig-hellman算法也是用于解决离散对数问题(也有很多文献提到是解决椭圆曲线之类的)。
即给定a,b,p,求 a x ≡ b ( m o d   p ) a^x \equiv b(mod\ p) axb(mod p)
在开始讲算法前,首先要提一下欧拉定理。

( a , p ) = 1 , 那么 a φ ( p ) ≡ 1 ( m o d   p ) ⋅ ⋅ ⋅ ⋅ ⋅ ⋅ ( ∗ ) (a,p)=1,那么a^{φ(p)} \equiv1(mod\ p)······(*) (a,p)=1,那么aφ(p)1(mod p)⋅⋅⋅⋅⋅⋅()

证明略。
如果 p p p是质数,那 φ ( p ) = p − 1 φ(p)=p-1 φ(p)=p1,即费马小定理。

对于 a x ≡ b ( m o d   p ) a^x\equiv b(mod\ p) axb(mod p),记其原根为 g g g
a = g a i , b = g b i a=g^{a_i},b=g^{b_i} a=gai,b=gbi(原根的次幂可以在[1,p-1]中一一对应,故 a i , b i a_i,b_i ai,bi必定存在)
g a i x ≡ g b i ( m o d   p ) g^{a_ix}\equiv g^{b_i}(mod\ p) gaixgbi(mod p)
所以 a i x ≡ b i ( m o d   p − 1 ) a_ix\equiv b_i(mod\ p-1) aixbi(mod p1)
(因为 g p − 1 ≡ 1 ( m o d   p ) g^{p-1}\equiv 1(mod\ p) gp11(mod p), p − 1 p-1 p1是最小循环节,即)
故利用扩展gcd求出 x x x即可。
于是我们的问题就变成了如何求 a i , b i a_i,b_i ai,bi
接下来,开始将pohlig-hellman算法的步骤(此处 a a a是原根, p p p是质数)。
以下步骤建议读者自行手写一遍!
以下步骤建议读者自行手写一遍!
以下步骤建议读者自行手写一遍!
重要的话说三遍!

1.将 φ φ φ ( p ) (p) (p)=p-1质因子分解
即 p − 1 = p 1 k 1 p 2 k 2 . . . p m k m ( m 为质因子个数 ) 即p-1=p^{k_1}_1p^{k_2}_2...p^{k_m}_m(m为质因子个数) p1=p1k1p2k2...pmkm(m为质因子个数)

2.对于每个质因子 p i p_i pi,列出方程
x = p i 0 a 0 + p i 1 a 1 + p i 2 a 2 . . . + p i k i − 1 a k i − 1 ( m o d   p i k i ) x=p^0_ia_0+p^1_ia_1+p^2_ia_2...+p^{k_{i-1}}_ia_{k_{i-1}}(mod\ p_i^{k_i}) x=pi0a0+pi1a1+pi2a2...+piki1aki1(mod piki)
(即把x写成 p i p_i pi进制,每个系数都小于 p i p_i pi)。

3. 令 r = 1 , 求 ( a x ) p − 1 p i r ≡ b p − 1 p i r ( m o d   p ) 令r=1,求(a^x)^{\frac{p-1}{p_i^r}}\equiv b^{\frac{p-1}{p_i^r}}(mod\ p) r=1,(ax)pirp1bpirp1(mod p)
(注意这个 p − 1 p i r {\frac{p-1}{p_i^r}} pirp1,这是我们求出 a i a_i ai各项系数的关键!)

4.展开 x x x,得到 ( a p i 0 a 0 + p i 1 a 1 + p i 2 a 2 . . . + p i k i − 1 a k i − 1 ) p − 1 p i r ≡ b p − 1 p i r ( m o d   p ) (a^{p^0_ia_0+p^1_ia_1+p^2_ia_2...+p^{k_{i-1}}_ia_{k_{i-1}}})^{\frac{p-1}{p_i^r}}\equiv b^{\frac{p-1}{p_i^r}}(mod\ p) (api0a0+pi1a1+pi2a2...+piki1aki1)pirp1bpirp1(mod p)

5.展开,得(以下以r=1为例)
a a 0 ∗ p − 1 p i ∗ a ( p − 1 ) a 1 ∗ ( a p − 1 ) p a 2 ∗ ( a p − 1 ) p 2 a 3 . . . ( a p − 1 ) p ( k i − 2 ) a k i − 1 ≡ b p − 1 p i r ( m o d   p ) a^{a_0*\frac{p-1}{p_i}}*a^{(p-1)a_1}*(a^{p-1})^{pa_2}*(a^{p-1})^{p^2a_3}...(a^{p-1})^{p^{(k_{i}-2)}a_{k_{i-1}}}\equiv b^{\frac{p-1}{p_i^r}}(mod\ p) aa0pip1a(p1)a1(ap1)pa2(ap1)p2a3...(ap1)p(ki2)aki1bpirp1(mod p)

6.注意到从第二项开始,后面每一项都是1(因为由欧拉定理得 a p − 1 ≡ 1 ( m o d   p ) a^{p-1}\equiv1(mod\ p) ap11(mod p),1的任何次方都是1),故我们可以得到式子

a a 0 ∗ p − 1 p i ≡ b p − 1 p i r ( m o d   p ) a^{a_0*\frac{p-1}{p_i}} \equiv b^{\frac{p-1}{p_i^r}}(mod\ p) aa0pip1bpirp1(mod p)
因为a_i的所有系数∈ [ 0 , p i − 1 ] [0,p_i-1] [0,pi1](前文提到了 p i p_i pi进制),故可以在 O ( p i ) O(p_i) O(pi)的时间内暴力求出 a 0 a_0 a0

7.得到 a 0 a_0 a0后,令 r = r + 1 r=r+1 r=r+1,回到步骤3。
直到所有a_i被求出后,可得到:
x = p i 0 a 0 + p i 1 a 1 + p i 2 a 2 . . . + p i k i − 1 a k i − 1 ( m o d   p i k i ) 。 x=p^0_ia_0+p^1_ia_1+p^2_ia_2...+p^{k_{i-1}}_ia_{k_{i-1}}(mod\ p_i^{k_i})。 x=pi0a0+pi1a1+pi2a2...+piki1aki1(mod piki)
( 注意模数不是 p ! ) (注意模数不是p!) (注意模数不是p)

8.在算出m个关于x的式子后,来一发CRT(中国剩余定理)即可(不需要扩展CRT,因为模数互质)。
需要注意!CRT方程组必定有解,因此,如果原根变换后的最后的扩展gcd无解,也说明原方程无解!!!

举例

下面,我们通过一个例子来帮助理解以上的步骤。
eg1: 7 x ≡ 12 ( m o d   41 ) 7^x\equiv12(mod\ 41) 7x12(mod 41)
解:首先 41 41 41的最小原根是 6 6 6
故令 7 = 6 x 1 , 12 = 6 x 2 7=6^{x_1},12=6^{x_2} 7=6x1,12=6x2
先计算 6 x ≡ 7 ( m o d   41 ) 6^{x}\equiv 7(mod\ 41) 6x7(mod 41)
分解 φ ( 41 ) = 40 = 2 3 5 。 φ(41)=40=2^35。 φ(41)=40=235
对于质因子2,我们首先列出方程
x = 2 0 a 0 + 2 1 a 1 + 2 2 a 2 ( a i ∈ [ 0 , 1 ] ) x=2^0a_0+2^1a_1+2^2a_2(a_i∈[0,1]) x=20a0+21a1+22a2(ai[0,1])
在等式两边同乘以 p − 1 2 = 20 \frac{p-1}{2}=20 2p1=20,得
6 x 20 ≡ 7 20 ( m o d   41 ) 。 6^{x^{20}}\equiv 7^{20}(mod\ 41)。 6x20720(mod 41)

x x x展开,得

6 ( 2 0 a 0 + 2 1 a 1 + 2 2 a 2 ) 20 ≡ 7 20 ( m o d   41 ) 6^{(2^0a_0+2^1a_1+2^2a_2)^{20}}\equiv 7^{20}(mod\ 41) 6(20a0+21a1+22a2)20720(mod 41)

化简,得

6 20 a 0 ∗ 6 40 a 1 ∗ 6 8 0 2 a ∗ ≡ 7 20 ( m o d   41 ) 6^{20a_0}*6^{40a_1}*6^{80^a_2}*\equiv 7^{20}(mod\ 41) 620a0640a16802a720(mod 41)

由费马小定理/欧拉定理得后面两项都是1,故:

6 20 a 0 ≡ 7 20 ( m o d   41 ) 6^{20a_0} \equiv 7^{20}(mod\ 41) 620a0720(mod 41)

快速幂优化,得

4 0 a 0 ≡ 40 ( m o d   41 ) 40^{a_{0}}\equiv 40(mod\ 41) 40a040(mod 41)

[ 0 , 1 ] [0,1] [0,1]内枚举 a 0 a_0 a0,得到 a 0 = 1 。 a_0=1。 a0=1

有了 a 1 a_1 a1后,我们将原式同乘以 p − 1 2 2 = 10 \frac{p-1}{2^2}=10 22p1=10,得到 6 x 10 ≡ 7 10 ( m o d   41 ) 。 6^{x^{10}}\equiv 7^{10}(mod\ 41)。 6x10710(mod 41)

重复上述步骤,得

6 ( 1 ∗ 2 1 + a 1 + 2 2 a 2 ) 10 ≡ 7 10 ( m o d   41 ) 6^{(1*2^1+a_1+2^2a_2)^{10}}\equiv 7^{10}(mod\ 41) 6(121+a1+22a2)10710(mod 41)

化简,得

6 10 ∗ 6 20 a 1 ∗ 6 40 a 2 ≡ 7 10 ( m o d   41 ) 6^{10}*6^{20a_1}*6^{40a_2}\equiv 7^{10}(mod\ 41) 610620a1640a2710(mod 41)

同样道理,后面一项是1,得

6 10 ∗ 6 2 0 a 1 ≡ 7 10 ( m o d   41 ) 6^{10}*6^{20^{a_1}}\equiv 7^{10}(mod\ 41) 610620a1710(mod 41)

快速幂优化,得

32 ∗ 4 0 a 1 ≡ 9 ( m o d   41 ) 32*40^{a_1}\equiv9(mod\ 41) 3240a19(mod 41)

[ 0 , 1 ] [0,1] [0,1]内枚举 a 1 a_1 a1,得 a 1 = 1 。 a_1=1。 a1=1

同理我们得到 a 2 = 1 a_2=1 a2=1

于是 x ≡ 2 0 ∗ 1 + 2 1 ∗ 1 + 2 2 ∗ 1 ≡ 7 ( m o d   8 ) x\equiv2^0*1+2^1*1+2^2*1\equiv7(mod\ 8) x201+211+2217(mod 8)

即 x ≡ 7 ( m o d   8 ) 即x\equiv7(mod\ 8) x7(mod 8)

对于质因子 5 5 5,同理,设 x = 5 0 a 0 ( a i ∈ [ 0 , 4 ] ) x=5^0a_0(a_i∈[0,4]) x=50a0(ai[0,4]),得到 a 0 = 4 a_0=4 a0=4
x = 4 ( m o d   5 ) 。 x=4(mod\ 5)。 x=4(mod 5)
综上所述,我们得到:
{ x ≡ 7 ( m o d   8 ) x ≡ 4 ( m o d   5 ) \begin{cases} x\equiv 7(mod\ 8)\\ x\equiv 4(mod\ 5)\\ \end{cases} {x7(mod 8)x4(mod 5)
由中国剩余定理可以得到 x ≡ 39 ( m o d   40 ) 。 x\equiv39(mod\ 40)。 x39(mod 40)
6 39 ≡ 7 ( m o d   41 ) 6^{39}\equiv 7(mod\ 41) 6397(mod 41)
同理可以求出 6 27 ≡ 12 ( m o d   41 ) 6^{27}\equiv12(mod\ 41) 62712(mod 41)
因此 6 3 9 x ≡ 6 27 ( m o d   41 ) 6^{39^x}\equiv 6^{27}(mod\ 41) 639x627(mod 41)
39 x ≡ 27 ( m o d   40 ) 39x\equiv27(mod\ 40) 39x27(mod 40)
解得 x ≡ 13 ( m o d   40 ) x\equiv13(mod\ 40) x13(mod 40)
7 x ≡ 12 ( m o d   41 ) 7^x\equiv 12(mod\ 41) 7x12(mod 41)的解是 x ≡ 13 ( m o d   40 ) x\equiv13(mod\ 40) x13(mod 40)

代码

需要用快速乘优化快速幂,对于模数是1e18左右时还要考虑到先除后乘等防溢出的操作。
UPD:代码内的质数因子是提前筛出来的,可以用质数筛法存入prime数组内或提前筛出写入数组。

#include<bits/stdc++.h>
using namespace std;
const long long numprime=2;//因子个数
long long prime[numprime+1]={0,2,3};
//质因子,需要提前筛出,此处只写了两个,请根据需要进行添加或跑一遍任意筛法存储。
long long factor[numprime+1]={0};//因子次数
long long xishu[200]={0};//系数
long long pow_prime[200];//存储pi^k
long long numc=0LL;//crt方程总数
long long crta[numprime+1],crtb[numprime+1];//a是余数,b是模数
long long quick_mul (long long a,long long b,long long c)//快速乘
{
    return (a*b-(long long)((long double)a*b/c)*c+c)%c;
}
long long quick_pow (long long a,long long b,long long c)//快速幂
{
    long long ans=1,base=a;
    while (b!=0)
    {
        if (b&1)
            ans=quick_mul (ans,base,c);
        base=quick_mul (base,base,c);
        b>>=1;
    }
    return ans%c;
}
void fenjie(long long p)//质因子分解
{
    long long p0=p-1;
    for (int i=1;i<=numprime;i++)
    {
        factor[i]=0;
        while (p0%prime[i]==0)
        {
            factor[i]++;
            p0/=prime[i];
        }
    }
}
bool check(long long a,long long p)//判断是否是原根
{
    for(int i=1;i<=numprime;i++)
    {
        if(quick_pow (a,(p-1)/prime[i],p)==1)
            return 0;
    }
    return 1;
}
long long Root (long long p)//遍历寻找原根
{
    for (long long g=2;g<p;g++)
    {
        if(check(g,p))
        return g;
    }
}
long long extended_gcd (long long a,long long b,long long &x,long long &y)
//扩展gcd
{
    long long r,t;
    if (b==0)
    {
        x=1;
        y=0;
        return a;
    }
    r=extended_gcd (b,a%b,x,y);
    t=x;
    x=y;
    y=t-(a/b)*y;
    return r;
}
long long Inv (long long a,long long mod)//逆元
{
    long long p,x,y;
    p=extended_gcd (a,mod,x,y);//p是gcd=1
    long long t=mod;
    if (x>=0)
        x=x%t;
    else
    {
        while (x<0)
            x+=t;
        x%t;
    }
    if (x==0)
        x+=t;
    return x;
}
long long CRT (long long numc,long long a[],long long b[],long long p)
//中国剩余定理
{
    long long ans=0;
    long long mod=p-1;
    for (long long i=1;i<=numc;i++)
    {
        long long x1=quick_mul (a[i]%mod,mod/b[i]%mod,mod);
        x1=quick_mul (x1%mod,Inv (mod/b[i],b[i])%mod,mod);
        ans=(ans+x1+mod)%mod;
    }
    return ans;
}
long long pohlig_hellman (long long g,long long b,long long p)
{
    numc=0;
    //g^x=b(mod p)
    memset (crta,0,sizeof (crta));
    memset (crtb,0,sizeof (crtb));
    for (int i=1;i<=numprime;i++)//枚举每个质因子
    {
        memset (xishu,0,sizeof (xishu));
        pow_prime[0]=1;
        for (int j=1;j<=factor[i]+1;j++)
        {
            pow_prime[j]=pow_prime[j-1]*prime[i];//预处理
        }
        long long sum=1;
        long long yushu=0,moshu=pow_prime[factor[i]];
        for (int j=1;j<=factor[i];j++)//求出每个系数
        {
            long long a0=quick_pow (g,(p-1)/prime[i],p);
            long long b0=quick_pow (b,(p-1)/pow_prime[j],p);
            for (long long x=0;x<=prime[i]-1;x++)//遍历找出系数x
            {
                if (quick_mul (sum,quick_pow (a0,x,p),p)==b0)
                {
                    xishu[j]=x;
                    long long xs=0;//已求出的系数按权求和
                    for (long long k=1;k<=j;k++)
                    {
                        xs=(xs+(pow_prime[k-1]*xishu[k]%(p-1))+p-1)%(p-1)+p-1;
                    }
                    sum=quick_pow (g,quick_mul (xs,(p-1)/pow_prime[j+1],p-1)+p-1,p);
                    break;
                }
            }
            yushu+=pow_prime[j-1]*xishu[j];
        }
        if (factor[i])
        {
            numc++;
            crta[numc]=yushu;
            crtb[numc]=moshu;
        }
    }
    return CRT (numc,crta,crtb,p);
}
int main ()
{
    int t;
    scanf ("%d",&t);
    while (t--)
    {
        long long a,b,p,g,xx,yy,rr,x1,x2;
        scanf ("%lld%lld%lld",&p,&a,&b);
        fenjie (p);
        if(check(a,p))// 如果a是原根,那只要做一遍pohlig-hellman
        {
            printf("%lld\n",pohlig_hellman (a,b,p));
            continue;
        }
        g=Root (p);
//        printf ("%lld's Primitive Root :%lld\n",p,g);
        x1=pohlig_hellman (g,a,p);
        x2=pohlig_hellman (g,b,p);
//        printf("solve %lldx=%lld(mod %lld)\n",x1,x2,p-1);//x1x=x2(mod p-1)
        rr=extended_gcd (x1,p-1,xx,yy);
//        printf("gcd=%lld\n",rr);
        if (x2%rr!=0)
        {
            printf ("-1\n");
            continue;
        }
        long long tt=(p-1)/rr;
        if (xx>=0)
        {
            xx=quick_mul (xx,(x2/rr),tt);
        }
        else
        {
            xx=xx%tt+tt;
            xx=quick_mul (xx,(x2/rr),tt);
        }
        printf ("%lld\n",xx);
    }
}

后记

写这篇文章的动机很简单:我没看懂国内关于pohlig-hellman算法的文章(肯定是我语文差),因此在某些网站上学懂之后想着自己写一篇文章,一来是方便日后复习,二来帮助下和我语文一样差的程序猿…
顺便一提,模数是有原根的合数时,可能也适用,具体还需要进一步确认,先咕咕咕。
UPD:感谢群聚njust-poorwill的指点。
UPD2:添加了部分描述,避免产生理解上的问题
DrGilbert 2020.1.21

  • 56
    点赞
  • 144
    收藏
    觉得还不错? 一键收藏
  • 19
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值