Garner's Algorithm (Mixed-Radix CRT Reconstruction) — Junior Level¶
One-line summary: Garner's algorithm rebuilds a single big number
xfrom its remaindersx mod p_1, x mod p_2, …, x mod p_k(the primes pairwise coprime). It writes the answer in mixed-radix formx = a_1 + a_2·p_1 + a_3·p_1·p_2 + …and solves for the digitsa_1, a_2, …one at a time using small modular inverses, so every intermediate computation stays a single-residue size — no giant products.
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Big-O Summary
- Real-World Analogies
- Pros & Cons
- Step-by-Step Walkthrough
- Code Examples
- Coding Patterns
- Error Handling
- Performance Tips
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Cheat Sheet
- Visual Animation
- Summary
- Further Reading
Introduction¶
Imagine you measured the same unknown number x three different ways and only wrote down the remainders:
xdivided by 3 leaves remainder 2,xdivided by 5 leaves remainder 3,xdivided by 7 leaves remainder 2.
Can you recover the original x? The Chinese Remainder Theorem (sibling topic 05-crt) promises that as long as the divisors 3, 5, 7 are pairwise coprime (share no common factor), there is exactly one answer x in the range 0 ≤ x < 3·5·7 = 105. CRT is the existence theorem: it says "yes, a unique x exists." Garner's algorithm is the constructive recipe that actually computes that x.
There are two famous ways to reconstruct x from its residues. The "classic CRT formula" (covered in 05-crt) adds up k big terms, each of which involves the giant product P = p_1·p_2·…·p_k divided by one prime. Those terms are huge. Garner's algorithm takes a smarter route: it expresses x in a mixed-radix (variable-base) numeral system
and finds the "digits" a_1, a_2, …, a_k one at a time. The beautiful part: solving for each digit a_i is a tiny computation done modulo a single prime p_i — never modulo the giant product. That is why Garner avoids huge intermediate numbers and is the method of choice when x is a true big integer.
This idea shows up everywhere big numbers must be handled without overflow:
- Multi-prime NTT (sibling
13-ntt): you compute a convolution modulo several primes, then Garner-combine the results into the real (large) coefficient. - Residue Number Systems (RNS) for big-integer arithmetic: store a number as a tuple of residues, add/multiply componentwise (fast, parallel), and Garner-reconstruct only at the end.
- Avoiding overflow: split a too-big computation across several machine-word-sized primes.
For this junior file we focus on the reconstruct-from-residues problem itself, the mixed-radix idea, and a small worked example you can verify by hand.
Prerequisites¶
Before reading this file you should be comfortable with:
- Modular arithmetic —
a mod m, and thatx mod p_iis the remainder ofxdivided byp_i. See sibling02-modular-arithmetic. - The Chinese Remainder Theorem (existence) — that a system of congruences with pairwise-coprime moduli has a unique solution mod the product. See sibling
05-crt. - Modular inverse — the number
ywitha·y ≡ 1 (mod m). Computed via the extended Euclidean algorithm (06-extended-euclidean-modular-inverse) or Fermat (04-fermat-euler). - Coprime / pairwise coprime — two numbers are coprime if
gcd = 1; "pairwise coprime" means every pair is coprime. All distinct primes are pairwise coprime. - Big-O notation —
O(k²), wherekis the number of primes.
Optional but helpful:
- A glance at positional numeral systems (base-10, base-2), since mixed-radix is a generalization where each position has its own base.
- Familiarity with 64-bit overflow, which is the practical problem Garner sidesteps.
Glossary¶
| Term | Meaning |
|---|---|
| Residue | A remainder: r_i = x mod p_i. The "fingerprint" of x under prime p_i. |
Modulus / prime p_i | One of the pairwise-coprime divisors. Often actual primes; only coprimality is required. |
| Pairwise coprime | Every pair (p_i, p_j) with i ≠ j has gcd(p_i, p_j) = 1. Required for CRT/Garner to work. |
| CRT (Chinese Remainder Theorem) | The theorem that a unique x in [0, P) matches all the residues, where P = ∏ p_i. |
| Mixed-radix representation | A numeral system where each digit position has its own base. Here the "weights" are the prefix products 1, p_1, p_1 p_2, …. |
Mixed-radix digit a_i | The i-th coefficient in x = a_1 + a_2 p_1 + a_3 p_1 p_2 + …. Always satisfies 0 ≤ a_i < p_i. |
| Prefix product | P_i = p_1·p_2·…·p_i (with P_0 = 1). The weight of digit a_{i+1} is P_i. |
Modular inverse inv(a, p) | The y with a·y ≡ 1 (mod p); lets us "divide" mod p. |
| RNS (Residue Number System) | Storing a number as its residue tuple (x mod p_1, …, x mod p_k) and computing componentwise. |
| Garner's algorithm | The O(k²) procedure that solves for the mixed-radix digits a_i sequentially using inverses of prefix products mod p_i. |
Core Concepts¶
1. The Reconstruct-from-Residues Problem¶
You are given k pairwise-coprime primes p_1, …, p_k and the residues r_1, …, r_k. You must find the unique x with
CRT (05-crt) guarantees this x exists and is unique. Garner constructs it.
2. The Mixed-Radix Idea¶
A normal base-10 number 573 means 3 + 7·10 + 5·100. The bases are all powers of 10. A mixed-radix number uses a different base at each step. Garner writes x as
The weights 1, p_1, p_1 p_2, … are the prefix products. Each digit obeys 0 ≤ a_i < p_i. This representation is unique (just like a base-10 number is unique) — that is what makes the digit-by-digit solve work.
3. Solving for the First Digit a_1¶
Reduce the whole equation mod p_1. Every term after the first contains the factor p_1, so they vanish mod p_1:
But we know x ≡ r_1 (mod p_1). Therefore a_1 = r_1. The first digit is just the first residue — free.
4. Solving for the Second Digit a_2¶
Now reduce mod p_2. Every term with weight p_1 p_2 … (i.e. a_3 onward) vanishes, leaving
We know x ≡ r_2 (mod p_2), so a_1 + a_2·p_1 ≡ r_2 (mod p_2). Solve for a_2:
Here (p_1)^{-1} is the modular inverse of p_1 modulo p_2 — a tiny number you compute once. Notice every operation here is mod p_2, a single-prime size, even though x itself may be enormous.
5. The General Digit a_i¶
In general, reducing mod p_i keeps only the first i terms, and we solve
where P_{i-1} = p_1·…·p_{i-1} is the prefix product. The inner sum is the partial reconstruction using digits already found, evaluated mod p_i. Each step needs one modular inverse and a handful of mod-p_i operations.
6. Assembling the Final x¶
Once all digits a_1, …, a_k are known, plug them into the mixed-radix formula and add up using big integers (or reduce mod some other modulus M if that is all you need). Because the digits are small and the prefix products are built incrementally, this final assembly is straightforward.
Big-O Summary¶
| Operation | Time | Space | Notes |
|---|---|---|---|
Solve one mixed-radix digit a_i | O(i) | O(1) | Evaluate partial sum mod p_i, one inverse. |
Full Garner reconstruction (k digits) | O(k²) | O(k) | Dominant cost; classic Garner. |
One modular inverse mod p_i | O(log p_i) | O(1) | Extended Euclid; can be precomputed. |
| Final assembly into a big integer | O(k · (size of x)) | O(size of x) | Big-int adds/multiplies. |
Reconstruct only x mod M (no big int) | O(k²) | O(k) | All arithmetic mod small numbers. |
The headline number is O(k²) in the number of primes k. Crucially, all the modular arithmetic is on single-prime-sized numbers — the algorithm never forms the giant product as a single big multiply during the digit solve.
Real-World Analogies¶
| Concept | Analogy |
|---|---|
Residues r_i | Several partial "fingerprints" of a person; no single one identifies them, but together they pin down exactly one person. |
| Pairwise coprime primes | Independent measuring rulers (centimeters, inches, a custom unit) that don't redundantly overlap, so each adds genuinely new information. |
| Mixed-radix digits | Like reading a clock as "2 days, 3 hours, 15 minutes" — each unit has its own base (7 days/week, 24 hours/day, 60 min/hour). |
Solving digit a_i mod p_i | Figuring out the "minutes" part using only minute-level info, before worrying about hours. |
| Avoiding the giant product | Counting change as "$3, two dimes, a nickel" instead of converting everything to pennies first. |
| RNS arithmetic | Doing your bookkeeping in several small ledgers in parallel, only adding them up at the very end. |
Where the analogy breaks: real clocks have fixed bases known in advance; Garner's bases are your chosen primes, and the "carry" logic is replaced by modular inverses.
Pros & Cons¶
| Pros | Cons |
|---|---|
| Never forms huge intermediate products — each digit solved mod a single prime. | O(k²) in the number of primes (classic CRT formula is also O(k²) but with bigger constants per term). |
| Naturally incremental — add a new prime/residue and extend with one more digit. | Requires pairwise-coprime moduli (a hard requirement; non-coprime needs a different combine). |
| Mixed-radix digits are small and exact — great for big-integer / RNS use. | Solving digits is inherently sequential (digit a_i needs a_1…a_{i-1}). |
Can stop early and reconstruct only x mod M for any target modulus M. | Modular inverses of prefix products must exist (they do, given coprimality). |
Standard tool for combining multi-prime NTT results (13-ntt). | A forgotten inverse or wrong modulus silently corrupts the result. |
When to use: reconstructing a big integer from residues, combining multi-prime NTT/modular convolution results, RNS-based bignum arithmetic, or any time you need the exact value (or its value mod another modulus) that is known only by its remainders.
When NOT to use: when you only ever needed the residues (don't reconstruct unnecessarily), when the moduli are not coprime (use a generalized combine), or when a single 64-bit modulus already suffices (no need for multiple primes at all).
Step-by-Step Walkthrough¶
Solve the opening example: find x with
Primes p = [3, 5, 7], residues r = [2, 3, 2]. Product P = 105, so the answer is unique in [0, 105).
Digit a_1 (mod 3). From x ≡ a_1 (mod 3) and x ≡ 2 (mod 3):
So far the partial reconstruction is x ≈ a_1 = 2.
Digit a_2 (mod 5). Weight of a_2 is p_1 = 3. We need a_1 + a_2·3 ≡ r_2 (mod 5):
The inverse of 3 mod 5 is 2 (since 3·2 = 6 ≡ 1). So a_2 ≡ 1·2 = 2 (mod 5), giving a_2 = 2.
Partial reconstruction: x ≈ a_1 + a_2·p_1 = 2 + 2·3 = 8. Check: 8 mod 3 = 2 ✓, 8 mod 5 = 3 ✓.
Digit a_3 (mod 7). Weight of a_3 is p_1·p_2 = 15. We need the running value 8 plus a_3·15 to be ≡ r_3 = 2 (mod 7):
8 + 15·a_3 ≡ 2 (mod 7)
1 + a_3 ≡ 2 (mod 7) (since 8 ≡ 1 and 15 ≡ 1 mod 7)
a_3 ≡ 1 · (15)^{-1} (mod 7).
15 ≡ 1 (mod 7), so (15)^{-1} ≡ 1 (mod 7), and a_3 ≡ (2 − 1)·1 = 1 (mod 7), giving a_3 = 1.
Assemble x. Mixed-radix:
Verify. 23 mod 3 = 2 ✓, 23 mod 5 = 3 ✓, 23 mod 7 = 2 ✓, and 0 ≤ 23 < 105. The reconstruction is correct, and we never multiplied anything bigger than the prefix product 15.
Code Examples¶
Example: Garner Reconstruction into a Big Integer¶
Solve a system of congruences with pairwise-coprime moduli and return the unique x.
Go¶
package main
import (
"fmt"
"math/big"
)
// modInverse returns a^{-1} mod m using big.Int (extended Euclid under the hood).
func modInverse(a, m int64) int64 {
A := big.NewInt(a)
M := big.NewInt(m)
A.Mod(A, M)
inv := new(big.Int).ModInverse(A, M)
if inv == nil {
panic("no inverse: moduli not coprime?")
}
return inv.Int64()
}
// garner reconstructs x from residues r modulo pairwise-coprime primes p.
func garner(r, p []int64) *big.Int {
k := len(p)
a := make([]int64, k) // mixed-radix digits
for i := 0; i < k; i++ {
// compute partial value mod p[i] using digits found so far
var partial int64 = 0
var prefix int64 = 1 // prefix product mod p[i]
for j := 0; j < i; j++ {
partial = (partial + a[j]*prefix) % p[i]
prefix = (prefix * p[j]) % p[i]
}
// a[i] = (r[i] - partial) * prefix^{-1} mod p[i]
diff := ((r[i]-partial)%p[i] + p[i]) % p[i]
a[i] = (diff * modInverse(prefix, p[i])) % p[i]
}
// assemble x = a_1 + a_2 p_1 + a_3 p_1 p_2 + ...
x := big.NewInt(0)
weight := big.NewInt(1)
for i := 0; i < k; i++ {
term := new(big.Int).Mul(big.NewInt(a[i]), weight)
x.Add(x, term)
weight.Mul(weight, big.NewInt(p[i]))
}
return x
}
func main() {
r := []int64{2, 3, 2}
p := []int64{3, 5, 7}
fmt.Println(garner(r, p)) // 23
}
Java¶
import java.math.BigInteger;
public class Garner {
static long modInverse(long a, long m) {
BigInteger inv = BigInteger.valueOf(((a % m) + m) % m)
.modInverse(BigInteger.valueOf(m));
return inv.longValue();
}
static BigInteger garner(long[] r, long[] p) {
int k = p.length;
long[] a = new long[k];
for (int i = 0; i < k; i++) {
long partial = 0, prefix = 1;
for (int j = 0; j < i; j++) {
partial = (partial + a[j] * prefix) % p[i];
prefix = (prefix * p[j]) % p[i];
}
long diff = ((r[i] - partial) % p[i] + p[i]) % p[i];
a[i] = (diff * modInverse(prefix, p[i])) % p[i];
}
BigInteger x = BigInteger.ZERO, weight = BigInteger.ONE;
for (int i = 0; i < k; i++) {
x = x.add(BigInteger.valueOf(a[i]).multiply(weight));
weight = weight.multiply(BigInteger.valueOf(p[i]));
}
return x;
}
public static void main(String[] args) {
long[] r = {2, 3, 2};
long[] p = {3, 5, 7};
System.out.println(garner(r, p)); // 23
}
}
Python¶
def mod_inverse(a, m):
return pow(a % m, -1, m) # Python 3.8+: modular inverse
def garner(r, p):
k = len(p)
a = [0] * k # mixed-radix digits
for i in range(k):
partial, prefix = 0, 1
for j in range(i):
partial = (partial + a[j] * prefix) % p[i]
prefix = (prefix * p[j]) % p[i]
diff = (r[i] - partial) % p[i]
a[i] = (diff * mod_inverse(prefix, p[i])) % p[i]
# assemble x = a_1 + a_2 p_1 + a_3 p_1 p_2 + ...
x, weight = 0, 1
for i in range(k):
x += a[i] * weight
weight *= p[i]
return x
if __name__ == "__main__":
print(garner([2, 3, 2], [3, 5, 7])) # 23
What it does: computes the mixed-radix digits a = [2, 2, 1] for the worked example, then assembles x = 23. Run: go run main.go | javac Garner.java && java Garner | python garner.py
Coding Patterns¶
Pattern 1: Brute-Force Reconstruction (the oracle you test against)¶
Intent: Before trusting Garner, find x by scanning every candidate in [0, P) and checking all congruences. Slow, but obviously correct on tiny inputs.
def brute_crt(r, p):
from math import prod
P = prod(p)
for x in range(P):
if all(x % p[i] == r[i] for i in range(len(p))):
return x
return None # should never happen with pairwise-coprime p
This is O(P) — only usable when the product P is small. Garner must give the same answer.
Pattern 2: Incremental Two-at-a-Time Combine¶
Intent: Garner can also be phrased as repeatedly merging two congruences into one (the way 05-crt often presents CRT). Both reach the same x; Garner's mixed-radix form keeps the numbers smallest.
Pattern 3: Reconstruct Only x mod M¶
Intent: If you only need x mod M (e.g. the final answer of a problem is itself taken mod 10^9+7), assemble the mixed-radix sum modulo M and skip big integers entirely.
Error Handling¶
| Error | Cause | Fix |
|---|---|---|
Wrong x | Moduli not pairwise coprime. | Verify gcd(p_i, p_j) = 1 for all pairs first. |
nil / exception from inverse | Inverse of a prefix product doesn't exist (non-coprime). | Same as above; coprimality guarantees the inverse exists. |
Negative digit a_i | (r_i − partial) went negative before mod. | Normalize: ((diff % p) + p) % p. |
Overflow in a[j]*prefix | Products of residues exceed 64-bit before % p. | Use 128-bit/big.Int for the product, or keep p_i small. |
x out of range | Residues larger than their modulus. | Reduce inputs: r_i = r_i mod p_i before starting. |
| Off-by-one weights | Prefix product weight mismatched with digit index. | Weight of a_i is p_1·…·p_{i-1} (with a_1 weight 1). |
Performance Tips¶
- Precompute inverses. If the same primes are reused across many reconstructions (common in NTT), precompute
inv(p_1·…·p_{i-1}, p_i)once and reuse. - Reduce inputs first. Take
r_i mod p_iat the start so all values are minimal. - Reduce mod
p_iinside the inner loop, not at the end, to keep the partial sum from overflowing. - Use the right integer width. With primes up to ~
30bits, products of two residues fit in 64 bits before reduction; with larger primes use 128-bit or big-int multiply. - Stop early if you only need
x mod M— never build the big integer if a single residue is all you want.
Best Practices¶
- Always reduce each residue:
r_i ← r_i mod p_ibefore running Garner. - Assert the moduli are pairwise coprime once, at the top; this is the silent-failure trap.
- Keep
modInversea small, separately tested helper — most bugs hide there. - State whether you want the full big integer
xor justx mod M, and write the assembly accordingly. - Test Garner against the brute-force oracle (Pattern 1) on random small inputs before trusting it.
- Document that digit
a_ilives in[0, p_i)and weight is the prefix productp_1·…·p_{i-1}.
Edge Cases & Pitfalls¶
- Single prime (
k = 1) — the answer is justx = r_1; the loop produces one digit and stops. - Residue equal to 0 — perfectly fine;
a_imay be 0. r_i ≥ p_i— reduce first; an un-reduced residue corrupts the very first digit.- Non-coprime moduli — Garner as written fails (no inverse). Detect with
gcdand use a generalized combine if needed (covered in05-crt). - The product
Poverflows 64 bits — exactly the case Garner is built for; use big integers only at final assembly, not during the digit solve. - Confusing the classic CRT formula with Garner — both reconstruct
x; Garner uses mixed-radix and stays small.
Common Mistakes¶
- Forgetting to reduce residues — using
r_i ≥ p_ipoisons digita_1. - Wrong prefix-product weight — the weight of digit
a_iisp_1·…·p_{i-1}, notp_1·…·p_i. - Negative remainder — in Go/Java
%can be negative; normalize before multiplying by the inverse. - Assuming any moduli work — they must be pairwise coprime, or the inverse step breaks.
- Building the giant product to "divide" it — that defeats the purpose; Garner never divides by
P. - Overflow in the inner product —
a[j]*prefixcan exceed 64 bits for big primes; widen or reduce. - Confusing existence (CRT) with construction (Garner) —
05-crtprovesxexists; Garner computes it.
Cheat Sheet¶
| Question | Tool | Formula |
|---|---|---|
| First digit | direct | a_1 = r_1 |
i-th digit | inverse of prefix product | a_i = (r_i − partial)·(P_{i-1})^{-1} mod p_i |
Weight of digit a_i | prefix product | P_{i-1} = p_1·…·p_{i-1} (with P_0 = 1) |
| Final value | mixed-radix sum | x = Σ a_i·P_{i-1} |
Range of x | CRT | 0 ≤ x < P = ∏ p_i |
| Range of each digit | by construction | 0 ≤ a_i < p_i |
Core algorithm:
garner(r[], p[]):
for i = 0 .. k-1:
partial = 0; prefix = 1 # both mod p[i]
for j = 0 .. i-1:
partial = (partial + a[j]*prefix) mod p[i]
prefix = (prefix * p[j]) mod p[i]
a[i] = (r[i] - partial) * inv(prefix, p[i]) mod p[i]
x = 0; weight = 1
for i = 0 .. k-1:
x += a[i] * weight # big integer (or mod M)
weight *= p[i]
return x
# cost: O(k^2)
Visual Animation¶
See
animation.htmlfor an interactive visualization.The animation demonstrates: - The list of residues
r_iand primesp_iyou can edit - Solving each mixed-radix digita_iin turn (subtract the partial, multiply by the prefix-product inverse modp_i) - The running reconstructed value accumulating asa_1 + a_2 p_1 + a_3 p_1 p_2 + …- Play / Step / Reset controls to watch the digits appear one at a time
Summary¶
Garner's algorithm is the constructive side of the Chinese Remainder Theorem: given residues r_i modulo pairwise-coprime primes p_i, it rebuilds the unique x by writing it in mixed-radix form x = a_1 + a_2 p_1 + a_3 p_1 p_2 + … and solving for the digits a_1, a_2, … one at a time. Each digit a_i is found with a single modular inverse, working only modulo p_i, so no giant intermediate products ever appear — the reason Garner is the go-to method for big-integer reconstruction, multi-prime NTT combining (13-ntt), and RNS arithmetic. The whole reconstruction costs O(k²). Where 05-crt proves the answer exists, Garner computes it, digit by digit, staying small the whole way.
Further Reading¶
- Sibling topic
05-crt— the Chinese Remainder Theorem (existence) and the classic combining formula. - Sibling topic
06-extended-euclidean-modular-inverse— computing the inverses Garner needs. - Sibling topic
13-ntt— multi-prime NTT whose results are Garner-combined. - Sibling topic
27-bigint-arithmetic— big-integer assembly of the finalx. - cp-algorithms.com — "Garner's Algorithm" and "Chinese Remainder Theorem".
- Knuth, The Art of Computer Programming, Vol. 2 — mixed-radix and modular (residue) arithmetic.