PyInfo
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.

Advanced6 min readdiscrete logbaby-step giant-stepmodular arithmeticmeet in the middle

Read first: Euler's Totient Function, Modular Multiplicative Inverse

The discrete logarithm problem asks for an integer xx such that

ax≡b(modm)a^x \equiv b \pmod m

It's the modular counterpart of log⁡ab\log_a b. 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 O(m)O(m) down to O(m)O(\sqrt m).

Baby-step giant-step (coprime case)

Assume gcd⁡(a,m)=1\gcd(a, m) = 1. The powers of aa are periodic with a period of at most φ(m)\varphi(m), so if a solution exists there is one with 0≤x<m0 \le x < m. Write x=np−qx = n p - q where n=⌈m⌉n = \lceil \sqrt m \rceil, p∈[1,n]p \in [1, n] and q∈[0,n)q \in [0, n) (every xx in [0,n2)[0, n^2) has such a form). The equation becomes

anp−q≡b  ⟺  anp≡b aq(modm)a^{n p - q} \equiv b \iff a^{n p} \equiv b\, a^{q} \pmod m

(we can multiply by aqa^q because aa is invertible.) Both sides are cheap to tabulate:

  1. Baby steps: compute b aqb\,a^q for every q=0,…,nq = 0, \dots, n and store it in a hash table (remember the largest qq for each value).
  2. Giant steps: for p=1,2,…,np = 1, 2, \dots, n compute anpa^{np} and look it up. On the first hit, x=np−qx = np - q.

Time and memory: O(m)O(\sqrt m).

python
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 7

Why the smallest: the giant step loop finds the smallest pp, and among equal table values the largest qq was stored, and np−qnp - q is then minimal.

Any aa and mm (not necessarily coprime)

If g=gcd⁡(a,m)>1g = \gcd(a, m) > 1, then ax≡b(modm)a^x \equiv b \pmod m with x≥1x \ge 1 forces g∣bg \mid b. Divide out gg once:

ag ax−1≡bg(modmg)\frac{a}{g}\,a^{x-1} \equiv \frac{b}{g} \pmod{\tfrac{m}{g}}

This is an equation of the form k⋅ax′≡b′(modm′)k \cdot a^{x'} \equiv b' \pmod{m'} with a known factor kk. Repeat until gcd⁡(a,m′)=1\gcd(a, m') = 1 (at most log⁡2m\log_2 m times; xx increases by 1 each time), then run the coprime algorithm with the extra factor kk on the left side. If at some point b≡kb \equiv k, then xx equals the number of steps already taken.

python
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 a<70a < 70, all b<mb < m and all m≤64m \le 64 passes, including the non-coprime cases.

A note on complexity and the hash table

The algorithm balances two costs: making the table takes nn steps and the search takes m/nm/n steps; the balance n=mn = \sqrt m minimizes the total. You can shift work between them: if you need many logarithms for the same aa and mm, use a bigger baby-step table once (a larger nn) so each query does fewer giant steps.

For mm of about 101210^{12} 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 xk≡ax^k \equiv a (discrete root) through a primitive root.
  • Finding the order of an element or checking whether bb is in the subgroup generated by aa.
  • Puzzles of the type "after how many steps does the sequence xi+1=axi mod mx_{i+1} = a x_i \bmod m first reach bb?".
python
# 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

Practice problems

This article is a Python adaptation of “Discrete Log” from cp-algorithms.com, licensed under CC BY-SA 4.0. The text was condensed and rewritten and the C++ code was reimplemented in Python; this adaptation is shared under the same license.