Abstract
本文将探讨与下列三个同余方程有关的问题,并进行一些数论相关知识的拓展。
ax≡bmodp
a
x
≡
b
mod
p
ax≡bmodp
a
x
≡
b
mod
p
xa≡bmodp
x
a
≡
b
mod
p
分别求出
x
x
的最小值。
注:如非特别说明,则本文中所有的 均表示为素数,且涉及到的数均为正整数。
ax≡bmodp a x ≡ b mod p
Euler Theorem: aφ(n)≡1modn,∀(a,n)=1. a φ ( n ) ≡ 1 mod n , ∀ ( a , n ) = 1.
根据欧拉定理和欧拉函数的定义,则有费马定理:
Fermat Theorem:
ap−1≡1modp
a
p
−
1
≡
1
mod
p
Theorem 1:当且仅当 (a,n)∣b ( a , n ) ∣ b , ax≡bmodn a x ≡ b mod n 有解。
PROOF(不严谨)
易知
ax≡bmodn⇒ax+ny=b
a
x
≡
b
mod
n
⇒
a
x
+
n
y
=
b
.
根据 Bézout’s identity:
ax+ny=b
a
x
+
n
y
=
b
存在整数解当且仅当
(a,n)∣b
(
a
,
n
)
∣
b
.
所以当且仅当
(a,n)∣b
(
a
,
n
)
∣
b
,
ax≡bmodn
a
x
≡
b
mod
n
有解.
Q.E.D
所以该同余方程一定有解。
Theorem 2:若
(a,n)∣b
(
a
,
n
)
∣
b
,则
ax≡bmodn
a
x
≡
b
mod
n
有恰有
(a,n)
(
a
,
n
)
个不同的解.
Conclusion 1:对任意
n>1
n
>
1
,如果
(a,n)=1
(
a
,
n
)
=
1
,则
ax≡bmodn
a
x
≡
b
mod
n
对模
n
n
有唯一解.
所以该同余方程只有唯一解。
.
有了 Fermat Theorem ,我们就知道了
ap−2≡a−1modp
a
p
−
2
≡
a
−
1
mod
p
。
用快速幂直接计算
bap−2
b
a
p
−
2
就是
x
x
的最小值了。
这里我们需要用到一个算法:大步小步(Baby Step Giant Step).
它是用于解决
ax≡bmodp
a
x
≡
b
mod
p
这类问题的。
根据 Fermat Theorem,我们知道
x
x
一定在 之间。
所以令
x=A⌈p‾√⌉−B
x
=
A
⌈
p
⌉
−
B
,其中
A,B
A
,
B
均在
0∼⌈p‾√⌉
0
∼
⌈
p
⌉
。
则有
aA⌈p√⌉−B≡bmodp⇒aA⌈p√⌉≡baBmodp
a
A
⌈
p
⌉
−
B
≡
b
mod
p
⇒
a
A
⌈
p
⌉
≡
b
a
B
mod
p
。
于是我们可以得到这样的一个(类似双向搜索的)做法,先枚举 B B 算出所有的 ,用数据结构维护,再逐一计算 aA⌈p√⌉ a A ⌈ p ⌉ ,寻找是否有与之相等的 baB b a B ,从而就可以找到最小值了。
在这儿我用了 Hash表 来维护,且从小到大枚举 A,B A , B ,复杂度为 O(q‾√) O ( q ) 。
//BZOJ 3122 这题还要推一下式子
#include <cstdio>
#include <cstring>
#include <cmath>
#define Min(_A, _B) (_A < _B ? _A : _B)
#define R register
const int Mod = 1000003;
namespace Steaunk
{
struct Hash
{
int Point[Mod], Next[31630], To[31630], W[31630], q;
void Add(R int u, R int v)
{ W[++q] = u; u %= Mod; Next[q] = Point[u]; Point[u] = q; To[q] = v; }
int check(R int u)
{
for(R int j = Point[u % Mod]; j; j = Next[j])
if(W[j] == u) return To[j];
return 2139062143;
}
void clear(){ memset(Point, 0, sizeof(Point)); q = 0; }
} M;
int p, a, b, k, t;
int Pow(R int A, R int K = p - 2)
{
R int d = 1;
while(K)
{
if(K & 1) d = 1ll * d * A % p;
K >>= 1;
A = 1ll * A * A % p;
}
return d;
}
void main()
{
scanf("%d %d %d %d %d", &p, &a, &b, &k, &t);
if(a == 0 || k == t || (k == 0 && b == 0))
{
if(k == t) puts("1");
else if(b == t) puts("2");
else puts("-1");
return ;
}
if(a == 1 && b != 0)
{
t = (t - k + p) % p;
t = 1ll * t * Pow(b) % p;
printf("%d\n", t + 1);
return ;
}
M.clear();
R int tmp = 1ll * b * Pow(a - 1) % p;
t = t + tmp;
tmp += k;
t = 1ll * t * Pow(tmp) % p;
R int w = sqrt(p) + 1.5;
R int v = 1;
for(R int i = 0; i < w; i++)
{
tmp = 1ll * t * v % p;
M.Add(tmp, -i + 1);
v = 1ll * v * a % p;
}
R int o = Pow(a, w); v = o;
for(R int i = 1; i <= w; i++)
{
R int t = M.check(v);
if(t != 2139062143)
{
printf("%d\n", i * w + t);
return ;
}
v = 1ll * v * o % p;
}
puts("-1");
}
}
int main()
{
R int T;
scanf("%d", &T);
while(T--) Steaunk::main();
return 0;
}
xa≡bmodp x a ≡ b mod p
练习题:BZOJ-1319
要想解决这个问题,需要先搞懂 ax≡bmodp a x ≡ b mod p 是怎么解决的。
显然, b=0 b = 0 时 x=0 x = 0 .
然后我们假设能找到一个数
g
g
,
满足 且
y≠z
y
≠
z
,
gy≠gzmodp
g
y
≠
g
z
mod
p
,
即
gc (0<c<p−1)
g
c
(
0
<
c
<
p
−
1
)
与
1∼p−1
1
∼
p
−
1
的数是一一对应的。
那么 xa≡bmodp⇒(gc)a≡bmodp⇒(ga)c≡bmodp x a ≡ b mod p ⇒ ( g c ) a ≡ b mod p ⇒ ( g a ) c ≡ b mod p
其中 c c 是变量, 是常量,似曾相识?看看前文的式子 ax≡bmodp a x ≡ b mod p ,问题就这样被划归为第二问了。
那么怎么找到这样一个
g
g
呢?
先定义一下。
Defination 1:若 ,
(a,n)=1
(
a
,
n
)
=
1
, 则使得
at≡1modn
a
t
≡
1
mod
n
,成立的最小正整数
t
t
叫作 模
n
n
的次数;当 时,我们称
a
a
为模 的原根。
Conclusion 2:在模
n
n
意义下,如果 是原根,则对于任意
x<n
x
<
n
都可以表示为
a
a
的一个幂。
所以我们要找的数 就是模 p p 的原根。
Theorem 3:对于所有的素数 和正整数 e e ,只有当 有原根。
所以显然我们是可以找到 g g 的。
Theorem 4: 为 a a 模 的次数,那么对于任意整数 d d ,,那么 t∣d t ∣ d .
PROOF
令
d=tq+r (0≤r<t)
d
=
t
q
+
r
(
0
≤
r
<
t
)
.
ad≡atq+r≡(at)qar≡ar≡1modn
a
d
≡
a
t
q
+
r
≡
(
a
t
)
q
a
r
≡
a
r
≡
1
mod
n
.
根据 Defination 1,
r=0
r
=
0
.
∴d=tq⇒t∣d
∴
d
=
t
q
⇒
t
∣
d
.
Q.E.D
看到这我们就可以有所启发了。
由 Fermat Theorem 知,
gp−1≡1modp
g
p
−
1
≡
1
mod
p
,
再根据 Theorem 4 我们明白了,如果存在
t
t
使得 ,那么
t
t
一定是 的因数。
那么若
g
g
是模 的原根,那么
t=p−1
t
=
p
−
1
。
综上,我们得到了一个基于原根的分布的算法,我们从 2 2 到 的去枚举 g g , 且 x≠p x ≠ p 满足 gx≡1modp g x ≡ 1 mod p ,那么 g g 一定不是原根。
复杂度为 (你可以认为 NA N A 是一个小常数)。
Theorem 5:若 g g 为模 的原根,则模 m m 的原根的个数为 ,并且
看上去是不是数量挺多的,且好像也很随机的样子(匿了)…
update 2018.7.14:我今天去看了一下wekipidia,发现它跟我一样口糊…
至此我们将第三问化归为了第二问,也找到了一个原根,该问题圆满解决!
//BZOJ 1319
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#define R register
const int Mod = 1000003;
int a, b, p, g, fac[70010], tot, S[100010], cnt, V[100010];
struct Hash
{
int Point[Mod], Next[100010], To[100010], W[100010], q;
void Add(R int u, R int v){ W[++q] = u; Next[q] = Point[u %= Mod]; Point[u] = q; To[q] = v; }
void Push(R int u, R int v)
{
R int t = u % Mod;
for(R int j = Point[t]; j; j = Next[j])
if(u == W[j]) S[++cnt] = v - To[j];
}
} M;
int Pow(R int A, R int K)
{
R int d = 1;
while(K)
{
if(K & 1) d = 1ll * d * A % p;
K >>= 1;
A = 1ll * A * A % p;
}
return d;
}
bool check(R int A)
{
for(R int i = 1; i <= tot; i++) if(Pow(A, fac[i]) == 1) return 0;
return 1;
}
int main()
{
scanf("%d %d %d", &p, &a, &b);
for(R int i = 2; i * i < p; i++)
{
if((p - 1) % i == 0)
{
fac[++tot] = i;
if(i * i != p - 1) fac[++tot] = (p - 1) / i;
}
}
for(R int i = 2; i < p; i++)
if(check(i))
{
g = i;
break;
}
R int G = Pow(g, a);
R int w = sqrt(p) + 1.5, d = b;
for(R int i = 0; i < w; i++) M.Add(d, i), d = 1ll * d * G % p;
d = 1; R int U = Pow(G, w);
for(R int i = 0; i <= w; i++) M.Push(d, i * w), d = 1ll * d * U % p;
tot = 0;
for(R int i = 1; i <= cnt; i++) if(S[i] >= 0 && S[i] < p - 1) V[++tot] = Pow(g, S[i]);
std::sort(V + 1, V + 1 + tot);
R int Ans = 0;
for(R int i = 1; i <= tot; i++) if(V[i] != V[i - 1]) Ans++;
printf("%d\n", Ans);
for(R int i = 1; i <= tot; i++) if(V[i] != V[i - 1]) printf("%d\n", V[i]);
return 0;
}
参考文献
[1] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introduction to Algorithms.
[2] Miskcoo. 扩展大步小步法解决离散对数问题. http://blog.miskcoo.com/2015/05/discrete-logarithm-problem.
[3] 夏静波, 张四兰, 陈建华. 高效的原根生成算法.
[4] etc.