Template: Modular arithmetic library
Binary exponentiation + Fermat inverse + factorial caches
mod_pow, mod_inv, Combo struct with nCr. Your Olympiad toolkit in Rust.
Exercise
Build a small, reusable modular arithmetic toolkit for competitive programming. Contract:
mod_pow(base, exp, m)—base^exp mod mvia binary exponentiation.mod_inv(a, m)— multiplicative inverse ofamodulo a primem, via Fermat's little theorem.Combo::new(n, m)/Combo::ncr(n, k)— preallocate factorial and inverse-factorial tables up ton; returnnCr mod minO(1).
Target: a header-file-style library you drop into every contest and forget about. No dynamic dispatch, no allocation on the hot path, no panics on standard inputs.
Intuition
Three ideas:
- Binary exponentiation.
base^expcomputed inO(log exp)by halving the exponent. Every multiplication is done modmto prevent overflow. - Fermat's little theorem. For prime
mandgcd(a, m) = 1,a^(m-1) ≡ 1 mod m, soa^(m-2) ≡ a^(-1) mod m. This givesmod_invinO(log m). - Factorial caching. For many
nCrqueries with fixedm, precomputefact[0..=n]andinv_fact[0..=n]. ThennCr = fact[n] * inv_fact[k] * inv_fact[n-k] mod mis one multiplication triple.
The whole toolkit is ~30 lines and covers 90% of combinatorics contest problems.
Approach
Binary Exponentiation + Fermat + Combo
O(log m) per op, O(n) buildO(n)Standard binary exponentiation. Fermat for mod_inv (requires prime m). Combo precomputes factorials forward, then inverse factorials backward using the identity inv_fact[i-1] = inv_fact[i] * i mod m, avoiding n separate inverse computations.
pub fn mod_pow(mut base: u64, mut exp: u64, m: u64) -> u64 {
if m == 1 { return 0; }
base %= m;
let mut acc: u64 = 1;
while exp > 0 {
if exp & 1 == 1 { acc = acc * base % m; }
exp >>= 1;
base = base * base % m;
}
acc
}
pub fn mod_inv(a: u64, m: u64) -> u64 {
mod_pow(a, m - 2, m)
}
pub struct Combo {
fact: Vec<u64>,
inv_fact: Vec<u64>,
m: u64,
}
impl Combo {
pub fn new(n: usize, m: u64) -> Self {
let mut fact = vec![1u64; n + 1];
for i in 1..=n { fact[i] = fact[i - 1] * (i as u64) % m; }
let mut inv_fact = vec![1u64; n + 1];
inv_fact[n] = mod_inv(fact[n], m);
for i in (1..=n).rev() {
inv_fact[i - 1] = inv_fact[i] * (i as u64) % m;
}
Self { fact, inv_fact, m }
}
pub fn ncr(&self, n: usize, k: usize) -> u64 {
if k > n { return 0; }
self.fact[n] * self.inv_fact[k] % self.m * self.inv_fact[n - k] % self.m
}
}
// TS: use BigInt everywhere — (1e9 + 7) squared overflows Number.MAX_SAFE_INTEGER.
function modPow(base: bigint, exp: bigint, m: bigint): bigint {
if (m === 1n) return 0n;
let acc = 1n;
let b = base % m;
let e = exp;
while (e > 0n) {
if (e & 1n) acc = (acc * b) % m;
e >>= 1n;
b = (b * b) % m;
}
return acc;
}
function modInv(a: bigint, m: bigint): bigint {
return modPow(a, m - 2n, m);
}
class Combo {
private fact: bigint[];
private invFact: bigint[];
constructor(n: number, private m: bigint) {
this.fact = new Array(n + 1).fill(1n);
for (let i = 1; i <= n; i++) this.fact[i] = (this.fact[i - 1] * BigInt(i)) % m;
this.invFact = new Array(n + 1).fill(1n);
this.invFact[n] = modInv(this.fact[n], m);
for (let i = n; i >= 1; i--) this.invFact[i - 1] = (this.invFact[i] * BigInt(i)) % m;
}
ncr(n: number, k: number): bigint {
if (k > n) return 0n;
return ((this.fact[n] * this.invFact[k]) % this.m * this.invFact[n - k]) % this.m;
}
}Trace
mod_pow(3, 5, 100):
| step | exp binary | acc | base |
|---|---|---|---|
| init | 101 | 1 | 3 |
| exp & 1 = 1 → acc = 3; shift, square | 10 | 3 | 9 |
| exp & 1 = 0; shift, square | 1 | 3 | 81 |
| exp & 1 = 1 → acc = 3 × 81 % 100 = 43; shift, square | 0 | 43 | (stop) |
Result 43 — matches 243 mod 100.
Edge cases
m = 1— every residue is0; handle before the loop.exp = 0— return1immediately; the loop exits before multiplying, so the initialacc = 1is correct.- Non-prime
m—mod_invvia Fermat is wrong. Use the extended Euclidean algorithm for compositemand generalgcd(a, m) = 1. - Overflow — all intermediates must be
u64;(1e9 + 7)^2 ≈ 10^18fits comfortably inu64::MAX ≈ 1.8 × 10^19.i64also works but loses a bit for nothing.
For TypeScript, the operand range forces you to use BigInt. The perf hit is real (roughly 3× slower than u64 in Rust) but correctness is non-negotiable. There's no floating-point cheat that preserves exact modular semantics at the 10^9 scale.
Takeaway
Binary exponentiation, Fermat inverse, and factorial caches together are the 80/20 modular-arithmetic toolkit. They unlock every combinatorial DP — 62 (Unique Paths via C(m+n-2, m-1)), 1569, 1866, 2514 — and every number-theoretic contest problem that fits in a u64.
Keep this as a personal utility file and tune it once. Every subsequent contest or interview that asks for a big combinatorial answer is then a five-line problem.
More in Rust-Specific Practice
Build a SegTree<T: Monoid> that works for sum, min, max, and GCD.
Implement as a struct with rank and size. Make it reusable.
Use std::cmp::Reverse for min-heap. Handle the Rust BinaryHeap idioms.
Vec<TrieNode> with usize indices instead of Box pointers.