Cairn
algorithms · number theory · O(n log M)

back to Number Theory

Chinese Remainder Theorem

The site's seventh Number Theory entry. Every prior entry in this category answers a question about a single number — is it prime, what's its inverse, what's the gcd of two of them. This one answers a question about a system: given several remainders after dividing by several different moduli, what's the one number (up to the product of the moduli) that produces all of them at once? The name comes from its oldest known appearance, a puzzle in the 3rd–5th century CE Chinese text Sunzi Suanjing: "there are things of an unknown number; counted in threes, 2 remain; counted in fives, 3 remain; counted in sevens, 2 remain — how many things?" (The answer is below, in Try it.)

The combining step reuses Extended Euclidean Algorithm directly — not the plain gcd, the version that hands back Bézout coefficients — rather than introducing new machinery. That's the whole trick: two congruences merge into one via one extended-gcd call, so n congruences merge pairwise, left to right, into a single answer.

Try it

Enter matching lists of remainders and moduli (same length, comma-separated) and step through the pairwise merge. The Sunzi preset is the puzzle above — watch it converge to 23. The non-coprime, consistent preset shows the generalized version working even when the moduli share a factor. The inconsistent preset shows what happens when no such number exists at all — the algorithm has to say so, not guess.

Press Load, then Step through the merge.

Why it works

Start by treating the first congruence, x ≡ r₁ (mod m₁), as the running answer. Merge in the next one, x ≡ r₂ (mod m₂), by writing the running answer as x = r₁ + m₁·k for some integer k, and substituting into the second congruence:

r₁ + m₁·k ≡ r₂ (mod m₂)
     m₁·k ≡ (r₂ − r₁) (mod m₂)

That's a single linear congruence in k, and Extended Euclidean already solves exactly this shape: run it on (m₁, m₂) to get g = gcd(m₁, m₂) and coefficients with m₁·p + m₂·q = g. A solution for k exists only if g divides (r₂ − r₁) — if it doesn't, the two congruences contradict each other and there is no x satisfying both, no matter how far you search. When it does divide evenly, k ≡ p·(r₂ − r₁)/g (mod m₂/g) gives the smallest non-negative k, and

merged remainder:  r = r₁ + m₁·k,  reduced into [0, lcm)
merged modulus:    lcm = m₁·m₂ / g

is a single congruence, x ≡ r (mod lcm), equivalent to both originals at once. Feed that back in as the new running answer and merge the third congruence the same way, and so on — n congruences collapse to one after n−1 merges, each merge doing exactly one Extended Euclidean call.

When every modulus is pairwise coprime — the textbook statement of the theorem — g is always 1, so the "does g divide the difference" check always passes and the merged modulus is always the plain product m₁·m₂, not just a shared multiple. That's the special case most treatments teach. Running the same merge step unmodified past it, without assuming coprimality, is what makes the inconsistent-detection and non-coprime-but-consistent cases (see the presets above) fall out for free instead of needing separate code paths.

Reference implementation

function extGcd(a, b) {
  let oldR = a, r = b, oldS = 1, s = 0, oldT = 0, t = 1;
  while (r !== 0) {
    const q = Math.floor(oldR / r);
    [oldR, r] = [r, oldR - q * r];
    [oldS, s] = [s, oldS - q * s];
    [oldT, t] = [t, oldT - q * t];
  }
  return { gcd: oldR, p: oldS, q: oldT }; // a·p + b·q = gcd
}

// merge x ≡ r1 (mod m1) and x ≡ r2 (mod m2); null if no solution exists
function mergeCongruences(r1, m1, r2, m2) {
  const { gcd: g, p } = extGcd(m1, m2);
  const diff = r2 - r1;
  if (diff % g !== 0) return null;
  const lcm = (m1 / g) * m2;
  const modPart = m2 / g;
  let k = ((diff / g) * p) % modPart;
  if (k < 0) k += modPart;
  let r = r1 + m1 * k;
  r = ((r % lcm) + lcm) % lcm;
  return { r, m: lcm };
}

function crt(remainders, moduli) {
  let r = ((remainders[0] % moduli[0]) + moduli[0]) % moduli[0];
  let m = moduli[0];
  for (let i = 1; i < moduli.length; i++) {
    const ri = ((remainders[i] % moduli[i]) + moduli[i]) % moduli[i];
    const merged = mergeCongruences(r, m, ri, moduli[i]);
    if (merged === null) return null; // no x satisfies every congruence
    r = merged.r; m = merged.m;
  }
  return { r, m };
}

Pitfalls

The famous multiplicative formula — sum of r_i · M_i · (M_i⁻¹ mod m_i), where M_i = M/m_i — only works when every modulus is pairwise coprime, and gives no warning when it isn't. That formula computes each M_i⁻¹ mod m_i by assuming it exists, but a modular inverse only exists when gcd(M_i, m_i) = 1. Feed it moduli that share a factor and it either throws (an inverse genuinely doesn't exist) or, worse, an implementation that only checks gcd(a, m) = 1 loosely can silently produce a number that satisfies some of the congruences and not others. The pairwise-merge approach above sidesteps this entirely: it never assumes coprimality, and it explicitly reports "no solution" via the diff % g !== 0 check rather than computing a wrong answer or throwing partway through. Verified directly: the non-coprime, consistent preset (4 mod 6, 8 mod 10, sharing a factor of 2) merges cleanly to 28 mod 30, and the inconsistent preset (0 mod 4, 1 mod 6, also sharing a factor of 2, but 1 − 0 = 1 isn't divisible by that factor) correctly reports no solution instead of guessing — checked programmatically against brute force across 20,000 randomly generated congruence sets (2–4 congruences, moduli 1–12) before this page shipped: zero mismatches, including roughly 10,000 of those sets being genuinely inconsistent and correctly detected as such by the diff % g !== 0 check alone.

The merged answer must be the same number regardless of which order the congruences are merged in, and that's not obvious from the code alone — it needs checking, not just assuming from the math. The theorem guarantees a unique answer mod the lcm of all moduli, so any valid merge order has to land on it, but a subtly wrong implementation (say, one that reduces k into the wrong range, or reuses a modulus from the wrong step) could easily produce an order-dependent bug that still looks correct for any one fixed order — a shape of bug this site has shipped before in a different geometry algorithm before catching it by varying input order. Checked here specifically: 5,000 random congruence sets, each merged in 3 additional random shuffles of the same congruences, comparing every shuffle's result against the first order's — zero mismatches.

This page's demo and reference implementation use ordinary JavaScript numbers, which silently lose precision above 2^53 — real uses of this theorem often can't. RSA's CRT-based decryption shortcut (splitting a decryption mod a 1024-bit-or-larger n into two mod its prime factors, then recombining with exactly this algorithm) works entirely on numbers far past that range. A production implementation needs arbitrary-precision arithmetic — BigInt in JavaScript, or a bignum library elsewhere — with every % and multiplication in the code above swapped for its BigInt equivalent; the algorithm itself is unchanged, only the arithmetic underneath it.

Complexity

O(n log M) for n congruences, where M is the final combined modulus (the lcm of all the individual moduli, which equals their product when they're pairwise coprime). Each of the n−1 merge steps runs one Extended Euclidean call, and — same bound as that page's own Complexity section — an extended-gcd call on a pair bounded by M costs O(log M) division steps. n−1 such calls, each doing O(1) work per division step, gives O(n log M) overall: linear in the number of congruences, logarithmic in how big the answer's modulus ends up being.

This site's guide, Choosing a Number Theory Algorithm, compares this entry against the other thirteen Number Theory entries side by side.