一切都要从去年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(p∈prime,65537≤p≤1e18,2≤a,b≤p−1且 p − 1 p-1 p−1的质因子只能由 2 2 2(和/或) 3 3 3组成),求
a x ≡ b ( m o d p ) a^x\equiv b(mod\ p) ax≡b(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算法的复杂度在一般情况下比 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) ax≡b(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)=p−1,即费马小定理。
对于 a x ≡ b ( m o d p ) a^x\equiv b(mod\ p) ax≡b(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) gaix≡gbi(mod p)
所以 a i x ≡ b i ( m o d p − 1 ) a_ix\equiv b_i(mod\ p-1) aix≡bi(mod p−1)
(因为 g p − 1 ≡ 1 ( m o d p ) g^{p-1}\equiv 1(mod\ p) gp−1≡1(mod p), p − 1 p-1 p−1是最小循环节,即阶)
故利用扩展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为质因子个数) 即p−1=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...+piki−1aki−1(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)pirp−1≡bpirp−1(mod p)
(注意这个 p − 1 p i r {\frac{p-1}{p_i^r}} pirp−1,这是我们求出 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...+piki−1aki−1)pirp−1≡bpirp−1(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) aa0∗pip−1∗a(p−1)a1∗(ap−1)pa2∗(ap−1)p2a3...(ap−1)p(ki−2)aki−1≡bpirp−1(mod p)
6.注意到从第二项开始,后面每一项都是1(因为由欧拉定理得 a p − 1 ≡ 1 ( m o d p ) a^{p-1}\equiv1(mod\ p) ap−1≡1(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) aa0∗pip−1≡bpirp−1(mod p)
因为a_i的所有系数∈ [ 0 , p i − 1 ] [0,p_i-1] [0,pi−1](前文提到了 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...∗piki−1aki−1(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) 7x≡12(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) 6x≡7(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 2p−1=20,得
6 x 20 ≡ 7 20 ( m o d 41 ) 。 6^{x^{20}}\equiv 7^{20}(mod\ 41)。 6x20≡720(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)20≡720(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) 620a0∗640a1∗6802a∗≡720(mod 41)
由费马小定理/欧拉定理得后面两项都是1,故:
6 20 a 0 ≡ 7 20 ( m o d 41 ) 6^{20a_0} \equiv 7^{20}(mod\ 41) 620a0≡720(mod 41)
快速幂优化,得
4 0 a 0 ≡ 40 ( m o d 41 ) 40^{a_{0}}\equiv 40(mod\ 41) 40a0≡40(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 22p−1=10,得到 6 x 10 ≡ 7 10 ( m o d 41 ) 。 6^{x^{10}}\equiv 7^{10}(mod\ 41)。 6x10≡710(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(1∗21+a1+22a2)10≡710(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) 610∗620a1∗640a2≡710(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) 610∗620a1≡710(mod 41)
快速幂优化,得
32 ∗ 4 0 a 1 ≡ 9 ( m o d 41 ) 32*40^{a_1}\equiv9(mod\ 41) 32∗40a1≡9(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) x≡20∗1+21∗1+22∗1≡7(mod 8)
即 x ≡ 7 ( m o d 8 ) 即x\equiv7(mod\ 8) 即x≡7(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} {x≡7(mod 8)x≡4(mod 5)
由中国剩余定理可以得到 x ≡ 39 ( m o d 40 ) 。 x\equiv39(mod\ 40)。 x≡39(mod 40)。
即 6 39 ≡ 17 ( m o d 41 ) 6^{39}\equiv 17(mod\ 41) 639≡17(mod 41)
同理可以求出 6 27 ≡ 12 ( m o d 41 ) 6^{27}\equiv12(mod\ 41) 627≡12(mod 41)
因此 6 3 9 x ≡ 6 27 ( m o d 41 ) 6^{39^x}\equiv 6^{27}(mod\ 41) 639x≡627(mod 41)
39 x ≡ 27 ( m o d 40 ) 39x\equiv27(mod\ 40) 39x≡27(mod 40)
解得 x ≡ 13 ( m o d 40 ) x\equiv13(mod\ 40) x≡13(mod 40)
故 7 x ≡ 12 ( m o d 41 ) 7^x\equiv 12(mod\ 41) 7x≡12(mod 41)的解是 x ≡ 13 ( m o d 40 ) x\equiv13(mod\ 40) x≡13(mod 40)
需要用快速乘优化快速幂,对于模数是1e18左右时还要考虑到先除后乘等防溢出的操作。
#include
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
{
while (xx<0)
{
xx+=tt;
}
xx=quick_mul (xx,(x2/rr),tt);
}
if (xx==0)
xx=tt;
printf ("%lld\n",xx);
}
}
写这篇文章的动机很简单:我没看懂国内关于pohlig-hellman算法的文章(肯定是我语文差),因此在某些网站上学懂之后想着自己写一篇文章,一来是方便日后复习,二来帮助下和我语文一样差的程序猿…
顺便一提,模数是有原根的合数时,可能也适用,具体还需要进一步确认,先咕咕咕。
UPD:感谢群聚njust-poorwill的指点。
DrGilbert 2020.1.21