Number Theory for Competitive Programming
Interview-style DSA rarely asks you to compute or find every prime under ten million — but competitive programming asks for exactly that, constantly. This lesson is the number theory toolkit that shows up over and over in contests: it’s less about clever tricks and more about a handful of building blocks you’ll reuse in dozens of problems.
What you’ll learn
Section titled “What you’ll learn”- GCD, LCM, and Euclid’s algorithm — the foundation everything else builds on.
- Modular arithmetic rules for add/subtract/multiply, and why division needs special handling.
- Fast modular exponentiation — computing in instead of .
- Modular inverse via Fermat’s little theorem, so division under a modulus becomes multiplication.
- The Sieve of Eratosthenes for finding every prime up to , plus a pointer to the linear sieve.
- Prime factorization and combinatorics mod p with precomputed factorials — answering in per query.
The cue
Section titled “The cue”GCD, LCM, and Euclid’s algorithm
Section titled “GCD, LCM, and Euclid’s algorithm”The greatest common divisor of two numbers can be found without ever
listing their factors: gcd(a, b) == gcd(b, a % b), and the recursion
bottoms out the moment the second argument hits 0.
import math
def gcd(a, b):
while b:
a, b = b, a % b
return a
def lcm(a, b):
return a * b // gcd(a, b)
print("gcd(48, 18):", gcd(48, 18)) # 6
print("lcm(4, 6):", lcm(4, 6)) # 12
print("math.gcd matches:", math.gcd(48, 18)) # stdlib version -- use this in practice
print("math.lcm matches:", math.lcm(4, 6)) # Python 3.9+ graph LR
N0["gcd(48, 18)"] --> N1["gcd(18, 12)"]
N1 --> N2["gcd(12, 6)"]
N2 --> N3["gcd(6, 0) = 6"]
Each step replaces (a, b) with (b, a % b), and a % b is always
smaller than b — so the pair shrinks fast (logarithmically) no matter
how large a and b start out.
Modular arithmetic rules
Section titled “Modular arithmetic rules”Contest answers are usually taken for a large prime like , both to keep numbers bounded and to give a clean, unique answer. Addition, subtraction, and multiplication all distribute cleanly over the modulus:
The + m in the subtraction rule matters in Python less than in most other
languages (Python’s % always returns a non-negative result for a
positive modulus), but it’s worth keeping the habit — it’s what makes the
rule portable to any language.
Division is the odd one out: is not in general. Division under a modulus needs the modular inverse, covered below.
Fast modular exponentiation
Section titled “Fast modular exponentiation”Computing by multiplying a into an accumulator b times is
— far too slow when b is or more. Binary
exponentiation cuts this to by squaring the base and only
folding it into the result on the bits of b that are 1:
def power_mod(base, exp, mod):
result = 1
base %= mod
while exp > 0:
if exp & 1: # this bit of exp is 1 -> fold the current base in
result = (result * base) % mod
base = (base * base) % mod # square the base for the next bit
exp >>= 1 # move to the next bit
return result
print("3^13 mod 7:", power_mod(3, 13, 7))
print("built-in pow() does the same:", pow(3, 13, 7))Modular inverse via Fermat’s little theorem
Section titled “Modular inverse via Fermat’s little theorem”Division under a modulus works by multiplying by the modular inverse of the divisor instead. When the modulus is prime, Fermat’s little theorem gives a one-line formula:
MOD = 1_000_000_007
def mod_inverse(a, mod=MOD):
return pow(a, mod - 2, mod) # valid ONLY when mod is prime (Fermat's little theorem)
a = 123456
inv = mod_inverse(a)
print("inverse of", a, "mod", MOD, ":", inv)
print("check: a * inv mod p == 1 ->", (a * inv) % MOD == 1)Sieve of Eratosthenes
Section titled “Sieve of Eratosthenes”Finding every prime up to some limit n by trial-dividing each number
individually is O(n sqrt(n)). The sieve flips the approach: start every
number as “prime”, then cross off every multiple of each prime found,
starting from its square (smaller multiples were already crossed off by a
smaller prime).
def sieve_of_eratosthenes(limit):
is_prime = [True] * (limit + 1)
is_prime[0] = is_prime[1] = False
for i in range(2, int(limit ** 0.5) + 1):
if is_prime[i]:
for multiple in range(i * i, limit + 1, i):
is_prime[multiple] = False
return [i for i, prime in enumerate(is_prime) if prime]
primes = sieve_of_eratosthenes(50)
print("primes up to 50:", primes)
print("count:", len(primes))Prime factorization
Section titled “Prime factorization”Once you have primes (or even without a sieve, for a single number), trial division up to finds every prime factor and its exponent:
def prime_factorize(n):
factors = {}
d = 2
while d * d <= n:
while n % d == 0:
factors[d] = factors.get(d, 0) + 1
n //= d
d += 1
if n > 1: # whatever's left over is itself a prime factor
factors[n] = factors.get(n, 0) + 1
return factors
print(prime_factorize(360)) # {2: 3, 3: 2, 5: 1} -> 2^3 * 3^2 * 5 = 360
print(prime_factorize(97)) # {97: 1} -- 97 is primeCombinatorics mod p: nCr with precomputed factorials
Section titled “Combinatorics mod p: nCr with precomputed factorials”Computing this directly under a modulus needs the modular inverses of
k! and (n - k)! — and precomputing every factorial and inverse
factorial up to the largest n you’ll ever need turns each individual
query into an lookup, which matters when a problem asks for
thousands of nCr queries.
MOD = 1_000_000_007
MAX_N = 200_000
fact = [1] * (MAX_N + 1)
for i in range(1, MAX_N + 1):
fact[i] = fact[i - 1] * i % MOD
inv_fact = [1] * (MAX_N + 1)
inv_fact[MAX_N] = pow(fact[MAX_N], MOD - 2, MOD) # one Fermat inverse, at the top
for i in range(MAX_N, 0, -1):
inv_fact[i - 1] = inv_fact[i] * i % MOD # (n-1)!^-1 = n!^-1 * n, walking down
def n_choose_k(n, k):
if k < 0 or k > n:
return 0
return fact[n] * inv_fact[k] % MOD * inv_fact[n - k] % MOD
print("C(10, 3):", n_choose_k(10, 3)) # 120
print("C(1000, 500) mod 1e9+7:", n_choose_k(1000, 500))Only one expensive pow(..., MOD - 2, MOD) call is needed in the whole
setup — every other inverse factorial is derived from the next one with a
single multiplication, since inv_fact[i - 1] = inv_fact[i] * i % MOD
falls straight out of fact[i] = fact[i - 1] * i.
Dry run
Section titled “Dry run”Euclid — gcd(48, 18). Each step replaces (a, b) with (b, a mod b):
| step | a | b | a % b |
|---|---|---|---|
| 1 | 48 | 18 | 12 |
| 2 | 18 | 12 | 6 |
| 3 | 12 | 6 | 0 → stop, answer is a = 6 |
Three iterations for numbers up to 48. The bound is because a % b at least
halves a every two steps — if b ≤ a/2 the remainder is already below a/2, and if b > a/2
then a % b = a − b < a/2. Worst case is consecutive Fibonacci numbers, which is where the log base
turns out to be the golden ratio.
Binary exponentiation — 3^13 mod (10^9+7). 13 = 1101₂, so only the set bits contribute:
exp | bit | base (squared each round) | result |
|---|---|---|---|
| 13 | 1 | 3 | 3 |
| 6 | 0 | 9 | 3 — bit clear, result untouched |
| 3 | 1 | 81 | 243 |
| 1 | 1 | 6561 | 1594323 |
Four iterations instead of thirteen multiplications; at exp = 10^18 it is 60 instead of .
- The base is squared every round whether or not the bit is set. That is what makes
basehold at roundk, so the set bits pick out exactly the factors whose exponents sum to 13: . - Every multiplication is followed by
% mod. Skip it and the intermediates grow without bound — harmless in Python (arbitrary precision, just slow), catastrophic in C++ or Java where they overflow silently. - Python’s built-in
pow(base, exp, mod)does exactly this, in C. Use it in interviews and say what it does; write the loop only when asked to show the mechanism.
Sieve of Eratosthenes up to 30. Start at i*i, not 2i — every smaller multiple of i already
has a smaller prime factor and was crossed off earlier:
i | marks composite | why start at i*i |
|---|---|---|
| 2 | 4, 6, 8, …, 30 | — |
| 3 | 9, 12, …, 30 | 6 was already crossed by 2 |
| 5 | 25, 30 | 10, 15, 20 already gone |
Primes: 2 3 5 7 11 13 17 19 23 29. 24 marking operations in total, against roughly 99 division
tests for per-number trial division — and the gap widens fast, since the sieve is
against .
- The outer loop stops at . Any composite
≤ nhas a factor≤ √n, so it has already been marked by the timeipasses there. - is essentially a constant — about 3 for . Treat the sieve as linear when estimating.
Complexity
Section titled “Complexity”| Task | Naive | Optimized | Notes |
|---|---|---|---|
| GCD | — | Euclid’s algorithm | |
| Binary (fast) exponentiation | |||
| Modular inverse | — | Fermat’s little theorem, requires prime modulus | |
| Primes up to | trial division | Sieve of Eratosthenes ( with a linear sieve) | |
| , many queries | per query | precompute, per query | Precomputed factorials + inverse factorials |
The variant map
Section titled “The variant map”| Need | Tool | Cost |
|---|---|---|
gcd(a, b) | Euclid — or math.gcd, which is C-speed | |
lcm(a, b) | a // gcd(a, b) * b — divide before multiplying to avoid a huge intermediate | |
Solve ax + by = gcd(a,b) | extended Euclid | |
binary exponentiation, or pow(a, b, m) | ||
| , p prime | Fermat: pow(a, p - 2, p) | |
| , m composite | extended Euclid — Fermat is invalid here | |
All primes ≤ n | sieve of Eratosthenes | |
Smallest prime factor of every n | modified sieve storing the factor, not a boolean | , then per factorisation |
| Factorise one number | trial division to | |
| , many queries | precomputed factorials + inverse factorials | setup, per query |
| Divisor count / sum | from the prime factorisation: | |
“Is n prime”, single query | trial division to | — do not build a sieve |
Enormous n, primality | Miller–Rabin (deterministic for 64-bit with fixed bases) |
Pitfalls
Section titled “Pitfalls”- Forgetting
% MODon an intermediate. In Python the answer stays correct but the integers grow and everything crawls; in a fixed-width language it overflows silently. Apply the modulus after every multiplication and addition. - Using Fermat’s inverse with a composite modulus.
pow(a, m - 2, m)is only an inverse whenmis prime. With compositemit returns a number with no error and every downstream result is wrong. Use extended Euclid, and check thatgcd(a, m) == 1— otherwise no inverse exists at all. a * b // gcd(a, b)for LCM. Computea // gcd * binstead; the reordered version keeps the intermediate small, which matters outside Python.- Starting the sieve’s inner loop at
2*i. Correct but wasteful — every multiple belowi*ialready has a smaller prime factor. - Building a sieve for one primality test. setup to answer a question that trial division answers in .
- Negative modulo. Python’s
%returns a non-negative result for a positive modulus, so-7 % 5is3. C and Java give-2. Do not carry that assumption between languages. 0and1in the sieve. Neither is prime; both need explicit initialisation toFalse.- Overflowing the factorial table. Precompute factorials mod p as you build them, never as exact integers.
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Why is Euclid ?” | Whether you can justify it | Because a % b at least halves a every two steps: if b ≤ a/2 the remainder is already under a/2, and if b > a/2 then a % b = a − b < a/2. Worst case is consecutive Fibonacci numbers |
| “Why does the problem say mod ?” | Whether you understand the constraint | Because the true answer is astronomically large. is prime (so Fermat’s inverse works) and small enough that a product of two residues fits in 64 bits |
| “Compute with ” | Binary exponentiation | Square the base each round and multiply into the result on set bits of b: 60 iterations instead of . In Python, pow(a, b, m) does it natively |
| “How do you divide under a modulus?” | The key identity | You multiply by the modular inverse. For prime p, by Fermat’s little theorem. For composite moduli use extended Euclid, and note the inverse exists only when gcd(a, m) = 1 |
| “Primes up to ” | Choosing the right tool | Sieve — , essentially linear. Per-number trial division would be , about a billion operations |
| “Now factorise different numbers” | Extending the sieve | Store each number’s smallest prime factor during the sieve instead of a boolean. Each factorisation then costs divisions with no trial division at all |
| ” for queries” | Precomputation | Factorials and inverse factorials up to n once in , then each query is f[n] * inv[k] % p * inv[n-k] % p in |
| “The modulus is , not a prime” | The trap | Fermat no longer applies. Use extended Euclid, and be aware that inverses only exist for values coprime to the modulus — for that means odd numbers only |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”A sieve, a GCD in disguise, and a problem that is one theorem long once you recognise it.
LC 204 — Count Primes · Medium
Section titled “LC 204 — Count Primes · Medium”Problem. Return the number of prime numbers strictly less than n.
Constraints. 0 <= n <= 5 * 10**6.
Examples. n = 10 gives 4 (2, 3, 5, 7) · n = 0 gives 0 ·
n = 1 gives 0
Editorial · approach, complexity, follow-ups
Time — the sum of over primes below . Effectively linear, and the number to quote. Space bits.
Both optimisations have one-line justifications, and both get asked about:
-
Start at . Any multiple with has a prime factor smaller than , so it was already crossed out on an earlier pass.
-
Stop when . A composite below must have a prime factor at most , so every composite is already gone.
-
Strictly less than
n.n = 3gives 1 (just the 2), andn = 2gives 0. Off-by-one here is the most common wrong answer on the problem. -
n = 0andn = 1must not indexsieve[1], hence the early return. A bare[True] * nwithsieve[0] = sieve[1] = Falsecrashes onn = 0andn = 1. -
p * p < n, notp * p <= n— with indices running ton - 1, apwherep * p == nhas no multiple inside the array anyway. Either bound is correct here; the strict one matches the index range exactly. -
sum(sieve)works becauseTrueis 1. Cute, and fine to use.
In Python the inner loop is often written as a slice assignment,
sieve[p*p::p] = [False] * len(sieve[p*p::p]), which pushes the work into C and is
several times faster. Worth showing after the explicit loop.
Follow-ups you should expect: “List the primes, not the count?” — the same
sieve, then enumerate. “Primes up to ?” — a plain sieve needs too much
memory; use a segmented sieve, and mention Miller-Rabin for testing a single large
number. “Smallest prime factor of every number?” — store p instead of False
during the sieve; that gives factorisation per query afterwards.
“Prime factorise one number?” — trial division to is enough. “Count
primes in a range [a, b]?” — segmented sieve, or two prefix counts.
LC 1071 — Greatest Common Divisor of Strings · Easy
Section titled “LC 1071 — Greatest Common Divisor of Strings · Easy”Problem. A string t divides s if s is t repeated some number of
times. Return the longest string that divides both str1 and str2, or "" if
there is none.
Constraints. 1 <= len(str1), len(str2) <= 1000, uppercase English letters.
Examples. "ABCABC" and "ABC" gives "ABC" ·
"ABABAB" and "ABAB" gives "AB" · "LEET" and "CODE" gives ""
Editorial · approach, complexity, follow-ups
A number-theory identity wearing a string costume. Once you see that string concatenation behaves like addition of exponents over a common base, the whole problem is two lines.
Why commuting is the right test. If both strings are repetitions of a common
base t, then str1 + str2 and str2 + str1 are both t repeated
(len(str1) + len(str2)) / len(t) times, so they are equal. The converse is the
non-obvious direction — it is a standard result that xy = yx for strings forces
both to be powers of a common word. You are not expected to prove it at a
whiteboard, but you should state that you are using it rather than pretend it is
obvious.
Why the length is the GCD. Any common divisor’s length divides both lengths, so it divides their GCD; and given that a common divisor exists, the prefix of that GCD length is one. So it is the longest.
Time for the concatenation and comparison. Space for the two temporary strings.
- Return
"", notNone, when there is no common divisor. "LEET"and"CODE"have GCD length 4, so a length check alone would wrongly return"LEET". The commuting test is what rejects it — skipping it is the classic wrong solution that passes the first two examples.str1[:g]versusstr2[:g]— once the strings commute, both prefixes are identical, so either works.- Equal strings return the whole string, since the GCD of
nandnisn.
Follow-ups you should expect: “Prove it, or at least test it without the
theorem?” — take the candidate prefix of GCD length and verify it repeats to build
both strings; still and much easier to defend. “Repeated Substring
Pattern (LC 459)?” — the same family: s is a repetition if s appears in
(s + s)[1:-1]. “Shortest string both divide (the LCM)?” — exists exactly when a
GCD does, with length lcm(m, n). “Why does math.gcd suffice?” — it is
Euclid’s algorithm in C, .
LC 1250 — Check If It Is a Good Array · Hard
Section titled “LC 1250 — Check If It Is a Good Array · Hard”Problem. You may pick a subset of nums, multiply each chosen number by any
integer, and add the results. The array is good if some choice sums to
exactly 1. Return whether it is good.
Constraints. 1 <= len(nums) <= 10**5, 1 <= nums[i] <= 10**9.
Examples. [12,5,7,23] gives True · [29,6,10] gives True ·
[3,6] gives False
Editorial · approach, complexity, follow-ups
Bezout’s identity. For integers and , the set is exactly the set of multiples of . Extended to numbers: the reachable sums are precisely the multiples of the GCD of all of them. So 1 is reachable exactly when that GCD is 1 — when the numbers are setwise coprime.
Note this is setwise coprime, not pairwise. [6,10,15] has no two elements
coprime, yet the GCD of all three is 1, so it is good. Being able to draw that
distinction is most of what the problem is testing.
Time . Space .
- Seed with 0.
gcd(0, x) == x, so 0 is the correct identity element for folding. Seeding withnums[0]also works but needs a guard for an empty array. - The early exit is safe because
gcd(1, x) == 1always — once the running GCD is 1 it stays 1. Not required for correctness, but a natural thing to point out. [3,6]isFalse. Every reachable sum is a multiple of 3, and 1 is not.[1]isTrue— pick the 1 and multiply by 1.- The multipliers may be negative, which is what makes
[29,6,10]work: for example , then negate. If the multipliers were restricted to non-negative integers this would be the Frobenius coin problem, which is a genuinely different and much harder question.
Follow-ups you should expect: “Find the actual multipliers?” — the extended
Euclidean algorithm, folded across the array. “Reach some other target t?” — t
is reachable exactly when the GCD divides it. “Non-negative multipliers only?” —
the Chicken McNugget / Frobenius problem; no simple closed form beyond two
numbers. “Why is this rated Hard?” — purely for the recognition step; the code is
four lines and that gap is the lesson.
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 204Count PrimesmediumThe Sieve of Eratosthenes above, applied directly to count primes below a limit
- 264Ugly Number IImediumNot a sieve, but the same "multiples of small primes" thinking: merge three pointers tracking multiples of 2, 3, and 5
- 279Perfect SquaresmediumUsually solved with DP, but Lagrange's four-square theorem (every positive integer is a sum of at most four perfect squares) gives a number-theory shortcut most solvers never see
- 1390Four DivisorsmediumSum the divisors of every number that has exactly four of them; a direct application of prime factorization
Self-check
Section titled “Self-check”-
Why is Euclid's algorithm O(log min(a, b))?
The worst case is consecutive Fibonacci numbers, which is why the log base works out to the golden ratio. gcd(48,18) takes three steps.
pch.quizShowAnswer
B — Because `a % b` at least halves `a` every two steps — if b ≤ a/2 the remainder is already under a/2, and if b > a/2 then a % b = a − b < a/2 — The worst case is consecutive Fibonacci numbers, which is why the log base works out to the golden ratio. gcd(48,18) takes three steps.
-
A problem says 'return the answer modulo 10^9 + 7'. What does that tell you?
It is also chosen so that a product of two residues still fits in 64 bits. In Python skipping the mod is 'only' slow; in C++ it overflows silently.
pch.quizShowAnswer
B — That the true answer is astronomically large, so it must never be materialised — every multiplication and addition needs a % MOD, and the modulus being prime means Fermat's inverse is available — It is also chosen so that a product of two residues still fits in 64 bits. In Python skipping the mod is 'only' slow; in C++ it overflows silently.
-
In binary exponentiation, why is the base squared even when the current bit is 0?
For 3^13 = 3^8 · 3^4 · 3^1, the bases 3, 9, 81, 6561 are produced by unconditional squaring and bits 1101 pick three of them.
pch.quizShowAnswer
B — Because base must hold a^(2^k) at round k — the squaring is what builds the ladder, and the set bits select which rungs get multiplied in — For 3^13 = 3^8 · 3^4 · 3^1, the bases 3, 9, 81, 6561 are produced by unconditional squaring and bits 1101 pick three of them.
-
How do you divide under a modulus?
Applying Fermat with a composite modulus is the dangerous case: it returns a number with no error and every result downstream is wrong. The inverse exists only when gcd(a, m) = 1.
pch.quizShowAnswer
B — Multiply by the modular inverse — for prime p, a^(-1) = a^(p-2) by Fermat's little theorem; for composite moduli use extended Euclid — Applying Fermat with a composite modulus is the dangerous case: it returns a number with no error and every result downstream is wrong. The inverse exists only when gcd(a, m) = 1.
-
Why does the sieve's inner loop start at `i*i` rather than `2*i`?
When i = 5, the numbers 10, 15 and 20 were already marked by 2 and 3. The outer loop can also stop at √n, since any composite ≤ n has a factor ≤ √n.
pch.quizShowAnswer
B — Because every multiple of i below i*i has a smaller prime factor and was already crossed off by that smaller prime — When i = 5, the numbers 10, 15 and 20 were already marked by 2 and 3. The outer loop can also stop at √n, since any composite ≤ n has a factor ≤ √n.
-
You need one primality test for a single number around 10^12. Sieve?
Sieves are for bulk queries. Building one to answer a single question is the classic misapplication — and at 10^12 the array alone would need terabytes.
pch.quizShowAnswer
B — No — trial division to √n is about 10^6 operations, while a sieve of that size is impossible in memory. Miller–Rabin if n grows further — Sieves are for bulk queries. Building one to answer a single question is the classic misapplication — and at 10^12 the array alone would need terabytes.
Recall card
Section titled “Recall card”- Cue — “modulo ”, divisibility, primes, gcd, huge exponents, or a per-number question
asked for every value up to
n. - gcd — Euclid:
while b: a, b = b, a % b. . LCM =a // gcd * b(divide first). - — binary exponentiation: square the base each round, multiply in on set bits.
; Python’s
pow(a, b, m)is this in C. - Division under a modulus = multiply by the inverse. Prime
p:pow(a, p-2, p). Composite: extended Euclid, and only whengcd(a, m) = 1. - Sieve — inner loop starts at
i*i, outer stops at . , effectively linear. Store the smallest prime factor instead of a boolean and factorisation becomes per number. - One primality test — trial division to . Do not build a sieve.
nCrmod p — precompute factorials and inverse factorials once, per query.- Apply
% MODafter every operation, not at the end.
- GCD/LCM: Euclid’s algorithm shrinks
(a, b)togcdin ; usemath.gcdandmath.lcmin practice. - Modular arithmetic: add/subtract/multiply distribute cleanly over a modulus; division needs the modular inverse instead.
- Fast exponentiation: via squaring, or just call
pow(a, b, m). - Modular inverse:
pow(a, p - 2, p)for primep, via Fermat’s little theorem. - Sieve of Eratosthenes: to find every prime up to
n; a linear sieve reaches true when needed. - nCr mod p: precompute factorials and inverse factorials once, then answer every query in .
That closes out the sparse table and number theory building blocks — between static-array RMQ and this modular-arithmetic toolkit, you can now recognize and solve a large share of the “math-flavored” problems that show up throughout competitive programming.
Next: String Algorithms: KMP, Z, and Rabin-Karp — linear-time pattern matching, the other family of classic CP building blocks that shows up just as often as the number theory above.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading