Number Theory & Algebra contents
Discrete Logarithm
Solve a^x ≡ b (mod m) with baby-step giant-step in O(√m), including the case where a and m are not coprime.
Read first: Euler's Totient Function, Modular Multiplicative Inverse
The discrete logarithm problem asks for an integer such that
It's the modular counterpart of . There is no known fast general algorithm (the difficulty of the problem underlies Diffie-Hellman key exchange), but a meet in the middle trick brings it from down to .
Baby-step giant-step (coprime case)
Assume . The powers of are periodic with a period of at most , so if a solution exists there is one with . Write where , and (every in has such a form). The equation becomes
(we can multiply by because is invertible.) Both sides are cheap to tabulate:
- Baby steps: compute for every and store it in a hash table (remember the largest for each value).
- Giant steps: for compute and look it up. On the first hit, .
Time and memory: .
from math import gcd, isqrt
def dlog_coprime(a, b, m):
"""Smallest x >= 0 with a^x = b (mod m), assuming gcd(a, m) == 1; None if no solution."""
a %= m
b %= m
n = isqrt(m) + 1
a_n = pow(a, n, m)
baby = {}
cur = b
for q in range(n + 1):
baby[cur] = q # later (larger) q overwrites: gives the smallest x below
cur = cur * a % m
cur = 1
for p in range(1, n + 1):
cur = cur * a_n % m
if cur in baby:
return n * p - baby[cur]
return None
assert dlog_coprime(2, 3, 5) == 3 # 2^3 = 8 = 3 (mod 5)
assert dlog_coprime(3, 13, 17) == 4 # 3^4 = 81 = 13 (mod 17)
assert dlog_coprime(2, 5, 7) is None # 5 is not a power of 2 mod 7Why the smallest: the giant step loop finds the smallest , and among equal table values the largest was stored, and is then minimal.
Any and (not necessarily coprime)
If , then with forces . Divide out once:
This is an equation of the form with a known factor . Repeat until (at most times; increases by 1 each time), then run the coprime algorithm with the extra factor on the left side. If at some point , then equals the number of steps already taken.
def dlog(a, b, m):
"""Smallest x >= 0 with a^x = b (mod m), for any a, b, m >= 1; None if there is none."""
if m == 1:
return 0
a %= m
b %= m
k, add = 1, 0
while True:
g = gcd(a, m)
if g == 1:
break
if b == k:
return add
if b % g:
return None
b //= g
m //= g
add += 1
k = k * (a // g) % m
n = isqrt(m) + 1
a_n = pow(a, n, m)
baby = {}
cur = b % m
for q in range(n + 1):
baby[cur] = q
cur = cur * a % m
cur = k % m
for p in range(1, n + 1):
cur = cur * a_n % m
if cur in baby:
return n * p - baby[cur] + add
return None
def dlog_brute(a, b, m):
for x in range(0, 2 * m + 2):
if pow(a, x, m) == b % m:
return x
return None
for m in range(1, 65):
for a in range(0, 70):
for b in range(0, m):
assert dlog(a, b, m) == dlog_brute(a, b, m), (a, b, m)The exhaustive comparison over all , all and all passes, including the non-coprime cases.
A note on complexity and the hash table
The algorithm balances two costs: making the table takes steps and the search takes steps; the balance minimizes the total. You can shift work between them: if you need many logarithms for the same and , use a bigger baby-step table once (a larger ) so each query does fewer giant steps.
For of about the table has a million entries: fine in Python. For a prime modulus of hundreds of bits (cryptography) the algorithm is hopeless, which is exactly the point.
Applications
- Solving (discrete root) through a primitive root.
- Finding the order of an element or checking whether is in the subgroup generated by .
- Puzzles of the type "after how many steps does the sequence first reach ?".
# after how many steps does x -> 7x mod 1000003 starting from 1 first reach 123456?
steps = dlog(7, 123456, 1000003)
assert steps is not None and pow(7, steps, 1000003) == 123456