Modular Arithmetic Basics
Key Points
- •Modular arithmetic is arithmetic with wrap-around at a fixed modulus m, like numbers on a clock.
- •You can safely compute (a + b) mod m, (a − b) mod m, and (a × b) mod m by reducing operands first and normalizing negatives.
- •In C++, use ((a % m) + m) % m to ensure results are in the range 0 to m−1 even if a is negative.
- •Modular division is not ordinary division; it requires multiplying by the modular inverse when it exists.
- •A modular inverse of a modulo m exists if and only if gcd(a, m) = 1; compute it with extended Euclid or Fermat’s little theorem when m is prime.
- •Avoid overflow by casting to __int128 before multiplication or by reducing early; then take modulo.
- •Fast modular exponentiation (binary exponentiation) computes mod m in O(log e) time.
- •Always reapply modulo after each operation to prevent numbers from growing and to keep results consistent.
Prerequisites
- →Integer arithmetic and divisibility — Understanding remainders, divisibility, and integer operations is fundamental to modular arithmetic.
- →Greatest common divisor (GCD) — The existence of modular inverses and solvability of linear congruences depend on gcd properties.
- →Euclidean and extended Euclidean algorithms — These algorithms compute gcd and coefficients to obtain modular inverses for general moduli.
- →Binary exponentiation (fast power) — Efficiently computes large powers modulo m and is essential for Fermat-based inverses.
- →Prime numbers and Euler’s totient function — Knowing when m is prime or composite determines invertibility and which theorems apply.
- →C++ integer types and overflow behavior — Correct implementation requires avoiding overflow and handling negatives with the % operator.
Detailed Explanation
Tap terms for definitions01Overview
Modular arithmetic is a way of doing arithmetic where numbers wrap around after reaching a certain value called the modulus m. Instead of tracking exact integer results, we only care about their remainders when divided by m. This is incredibly useful in programming contests and computer science because it keeps numbers small and manageable, avoids overflow when used properly, and often appears directly in problem statements (for example, asked to output answers modulo 1,000,000,007). The basic operations—addition, subtraction, and multiplication—work naturally with modulo: you can reduce operands first and then apply the operation. Division, however, is not the same as in normal arithmetic; you may only divide by a number that has a modular inverse with respect to m. Modular inverses exist exactly when the number is coprime with m (their greatest common divisor is 1). In competitive programming, we frequently use tools like fast modular exponentiation to compute large powers efficiently, and the extended Euclidean algorithm or Fermat’s little theorem to compute modular inverses. Care must be taken in languages like C++ because the % operator can yield negative results when the left operand is negative; we must normalize using ((a % m) + m) % m. Also, multiplication can overflow 64-bit integers, so we either cast to __int128 before taking modulo or reduce operands appropriately.
02Intuition & Analogies
Think of a 12-hour clock. If it is 9 o’clock now and 5 hours pass, the hand points to 2, not 14. That’s because the clock wraps around after 12; it keeps only the remainder upon division by 12. This is exactly what modular arithmetic does: it records where you land on a number circle with m equally spaced points labeled 0 to m−1. Addition moves you forward around the circle; subtraction moves you backward. If you go past 0, you wrap around to the top again—just like counting time zones or days of the week. For example, -1 mod 7 is like stepping one tick backwards from 0 on a 7-tick dial, which lands you on 6. Multiplication is repeated addition: 3 × 5 mod 12 means taking 5 steps, three times, wrapping around each time you pass 12. Division is different: on the clock, ‘dividing by 2’ means ‘finding a number that when doubled gives you your target position’, which only works if that doubling step is reversible for your modulus. That reversibility is what we call having a modular inverse. If the modulus shares no common factors with your step size, then you can uniquely walk backwards by that step; otherwise, some positions are unreachable or ambiguous. Practically, modular arithmetic is a tool to keep numbers small: when computing huge powers like 2^1,000,000, we never form the giant number; we just keep the remainder after each multiplication, like constantly rechecking our hand’s position on the clock after each move. The wrap-around behavior not only prevents overflow but also captures the exact information many problems care about: remainders, periodicity, and equivalence up to multiples of m.
03Formal Definition
04When to Use
Use modular arithmetic whenever a problem asks for answers modulo m or when values can grow too large to store exactly. Common use cases include combinatorics (counting arrangements, dynamic programming counts), number-theoretic problems (powers, inverses, linear congruences), hashing (working with polynomial rolling hashes modulo large primes), and algebraic structures over finite fields (e.g., matrix operations modulo a prime). In competitive programming, typical moduli are 10^9+7 or 998244353 (both primes), making modular inverses easy via Fermat’s little theorem. Modular arithmetic is also central to problems using prefix sums modulo m to detect divisibility of subarray sums, to compute cycles or periodic behavior, or to reduce large exponents using properties like Euler’s theorem. When multiplying large 64-bit numbers with m up to about 10^{18}, use __int128 to avoid overflow before taking modulo. Choose modular division (i.e., multiply by the modular inverse) only when the inverse exists; otherwise, reformulate the problem (e.g., solve a linear congruence ax \equiv b \pmod{m} with the extended Euclidean algorithm and handle multiple solutions). If m is not prime, be careful: not all numbers have inverses, but you can still use modular addition, subtraction, multiplication, and exponentiation safely with reduction after each operation.
⚠️Common Mistakes
Common pitfalls include: (1) Forgetting to normalize negatives in C++. The expression a % m can be negative when a < 0. Always write ((a % m) + m) % m to force the result into [0, m-1]. (2) Overflow before modulo. Even if the final result fits modulo m, intermediate operations can overflow 64-bit integers. Cast to __int128 for products: ( (__int128)a * b ) % m, and consider reducing a and b first. (3) Misusing division. There is no direct division modulo m. Replace division by multiplication with the modular inverse, which exists only when gcd(denominator, m) = 1. Don’t apply Fermat’s little theorem if m is composite. (4) Using floating-point pow or std::pow for modular exponentiation. These operate in doubles and cause precision loss or overflow. Instead, implement integer binary exponentiation (fast power) with modulus at each step. (5) Forgetting to take modulo after each addition/subtraction/multiplication, causing numbers to grow and overflow or produce wrong residues. (6) Assuming all residues have inverses under composite moduli, or not checking the gcd before trying to invert. (7) Mixing signed and unsigned types can produce surprising wrap-around; prefer signed 64-bit (long long) with explicit casts to __int128 for multiplication. (8) Not considering that subtraction can go negative: implement sub_mod as (a - b) mod m properly by adding m before taking modulo, or by normalizing the final result. Avoid these by using small, well-tested helper functions for normalize, add_mod, sub_mod, mul_mod, pow_mod, and inv_mod, and by documenting preconditions (e.g., modulus must be prime for Fermat-based inverse).
Key Formulas
Definition of congruence
Explanation: Two integers a and b are congruent modulo m if their difference is divisible by m. This defines when numbers are considered equivalent under modulo m.
Modular addition
Explanation: You can reduce both operands before adding and then take modulo again. This keeps numbers small and produces the correct remainder.
Modular subtraction
Explanation: Subtract first using reduced operands, then normalize to the 0..m−1 range. In code, add m if the result is negative before taking modulo again.
Modular multiplication
Explanation: Reduce operands before multiplying to help avoid overflow and then take modulo. In C++, also consider using 128-bit multiplication.
Normalization of negative remainders
Explanation: If a % m is negative in C++, adding m and taking modulo again ensures the result lies in the standard nonnegative range of residues.
Existence of modular inverse
Explanation: An inverse modulo m exists exactly when a and m are coprime. If gcd(a, m) > 1, no inverse exists.
Extended Euclid identity
Explanation: The extended Euclidean algorithm finds integers x and y that satisfy this identity. When the gcd is 1, x is a modular inverse of a modulo m.
Fermat’s little theorem
Explanation: For prime modulus p, raising any non-multiple of p to the power p−1 yields 1 modulo p. This implies (mod p).
Euler’s theorem
Explanation: For any modulus m, if a is coprime to m then a raised to Euler’s totient is congruent to 1 modulo m. It generalizes Fermat’s theorem.
Binomial coefficient modulo a prime
Explanation: When p is prime, factorial denominators are inverted using modular inverses (via Fermat or precomputed inverses). This enables O(1) per-query combinations after O(n) preprocessing.
Complexity Analysis
Code Examples
1 #include <iostream> 2 #include <cstdint> 3 4 // Normalize a into the range [0, m-1] 5 long long norm(long long a, long long m) { 6 long long r = a % m; // In C++, this can be negative if a < 0 7 if (r < 0) r += m; // Ensure nonnegative representative 8 return r; 9 } 10 11 // (a + b) mod m, avoiding overflow 12 long long add_mod(long long a, long long b, long long m) { 13 long long x = norm(a, m); 14 long long y = norm(b, m); 15 long long s = x + y; 16 if (s >= m) s -= m; // Equivalent to (x + y) % m but faster and avoids extra modulo 17 return s; 18 } 19 20 // (a - b) mod m, return in [0, m-1] 21 long long sub_mod(long long a, long long b, long long m) { 22 long long x = norm(a, m); 23 long long y = norm(b, m); 24 long long d = x - y; 25 if (d < 0) d += m; // Normalize after subtraction 26 return d; 27 } 28 29 // (a * b) mod m using 128-bit to avoid overflow (GCC/Clang extension) 30 long long mul_mod(long long a, long long b, long long m) { 31 __int128 x = (__int128)norm(a, m) * (__int128)norm(b, m); 32 return (long long)(x % m); 33 } 34 35 int main() { 36 long long a = -17, b = 25, m = 1000000007LL; // Example values 37 38 std::cout << "a mod m = " << norm(a, m) << "\n"; // Expect m-17 39 std::cout << "b mod m = " << norm(b, m) << "\n"; 40 41 std::cout << "(a + b) mod m = " << add_mod(a, b, m) << "\n"; 42 std::cout << "(a - b) mod m = " << sub_mod(a, b, m) << "\n"; 43 std::cout << "(a * b) mod m = " << mul_mod(a, b, m) << "\n"; 44 45 // Demonstrate large multiplication safety 46 long long x = 1000000000000000000LL; // 1e18 47 long long y = 1000000000000000000LL; // 1e18 48 long long mm = 1000000007LL; 49 std::cout << "(1e18 * 1e18) mod 1e9+7 = " << mul_mod(x, y, mm) << "\n"; 50 51 return 0; 52 } 53
This program provides small, reusable helpers for modular arithmetic in C++. It normalizes negative remainders, adds and subtracts with wrap-around, and multiplies safely by casting to __int128 to avoid overflow before taking modulo. The helpers reduce operands first, preventing growth and ensuring correctness even for large 64-bit inputs.
1 #include <iostream> 2 #include <tuple> 3 #include <cstdint> 4 5 long long norm(long long a, long long m) { 6 long long r = a % m; if (r < 0) r += m; return r; 7 } 8 9 long long mul_mod(long long a, long long b, long long m) { 10 __int128 x = (__int128)norm(a, m) * (__int128)norm(b, m); 11 return (long long)(x % m); 12 } 13 14 // a^e mod m using binary exponentiation 15 long long pow_mod(long long a, long long e, long long m) { 16 long long base = norm(a, m); 17 long long res = 1 % m; 18 while (e > 0) { 19 if (e & 1) res = mul_mod(res, base, m); 20 base = mul_mod(base, base, m); 21 e >>= 1; 22 } 23 return res; 24 } 25 26 // Extended Euclidean Algorithm: returns (g, x, y) such that ax + by = g = gcd(a, b) 27 std::tuple<long long, long long, long long> ext_gcd(long long a, long long b) { 28 if (b == 0) return {a >= 0 ? a : -a, a >= 0 ? 1 : -1, 0}; 29 auto [g, x1, y1] = ext_gcd(b, a % b); 30 long long x = y1; 31 long long y = x1 - (a / b) * y1; 32 return {g, x, y}; 33 } 34 35 // Modular inverse via Extended Euclid; returns (exists, inverse) 36 std::pair<bool, long long> inv_mod_euclid(long long a, long long m) { 37 auto [g, x, y] = ext_gcd(a, m); 38 if (g != 1 && g != -1) return {false, 0}; 39 long long inv = norm(x, m); 40 return {true, inv}; 41 } 42 43 // Modular inverse via Fermat's little theorem; m must be prime and gcd(a, m) = 1 44 long long inv_mod_fermat(long long a, long long m_prime) { 45 return pow_mod(a, m_prime - 2, m_prime); 46 } 47 48 int main() { 49 const long long P = 1000000007LL; // prime modulus 50 51 // Fast power example 52 std::cout << "2^1000 mod P = " << pow_mod(2, 1000, P) << "\n"; 53 54 // Inverse via Fermat (valid since P is prime and 3 != 0 mod P) 55 std::cout << "inv(3) mod P = " << inv_mod_fermat(3, P) << "\n"; 56 // Check: 3 * inv(3) mod P == 1 57 std::cout << "check: 3 * inv(3) % P = " << ( ( (__int128)3 * inv_mod_fermat(3, P) ) % P ) << "\n"; 58 59 // Inverse via Extended Euclid under composite modulus 60 long long a = 7, m = 26; // gcd(7,26)=1, inverse exists 61 auto [ok1, inv1] = inv_mod_euclid(a, m); 62 std::cout << "inv(7) mod 26 exists? " << (ok1 ? "yes" : "no") << ", value = " << (ok1 ? inv1 : -1) << "\n"; 63 64 // Non-invertible example: gcd(6,15)=3 != 1 65 auto [ok2, inv2] = inv_mod_euclid(6, 15); 66 std::cout << "inv(6) mod 15 exists? " << (ok2 ? "yes" : "no") << "\n"; 67 68 return 0; 69 } 70
This program implements binary exponentiation to compute a^e mod m efficiently and two ways to compute modular inverses: Fermat’s little theorem for prime moduli and the extended Euclidean algorithm for any modulus when gcd(a, m) = 1. It demonstrates both success and failure cases for inverses.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Normalize to [0, m-1] 5 static inline long long norm(long long a, long long m) { 6 long long r = a % m; if (r < 0) r += m; return r; 7 } 8 9 // Returns true if there exists a non-empty subarray with sum % m == 0 10 bool has_divisible_subarray(const vector<long long>& a, long long m) { 11 unordered_set<long long> seen; 12 long long prefix = 0; 13 seen.insert(0); // prefix sum 0 at position -1 ensures single-prefix divisible case is captured 14 for (long long x : a) { 15 prefix = norm(prefix + norm(x, m), m); 16 if (seen.count(prefix)) return true; // two equal prefix sums modulo m imply subarray divisible by m 17 seen.insert(prefix); 18 } 19 return false; 20 } 21 22 int main() { 23 vector<long long> arr = {5, -2, 3, 1, -4, 6}; 24 long long m = 7; 25 26 cout << boolalpha; 27 cout << "Has subarray with sum divisible by " << m << "? " << has_divisible_subarray(arr, m) << "\n"; 28 29 // As an extra, compute the total sum and product modulo m safely 30 long long sum = 0, prod = 1 % m; 31 for (long long x : arr) { 32 sum = (sum + norm(x, m)) % m; 33 __int128 t = (__int128)prod * norm(x, m); 34 prod = (long long)(t % m); 35 } 36 cout << "sum(arr) mod m = " << sum << "\n"; 37 cout << "product(arr) mod m = " << prod << "\n"; 38 39 return 0; 40 } 41
Using prefix sums modulo m, if two prefixes have the same remainder, their difference (a subarray) has sum divisible by m. We maintain a hash set of seen remainders, normalizing at each step. The example also shows safe sum and product modulo m handling negative numbers and overflow.
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 // Extended Euclidean: returns (g, x, y) with ax + by = g = gcd(a, b) 5 tuple<long long, long long, long long> ext_gcd(long long a, long long b) { 6 if (b == 0) return {a >= 0 ? a : -a, a >= 0 ? 1 : -1, 0}; 7 auto [g, x1, y1] = ext_gcd(b, a % b); 8 long long x = y1; 9 long long y = x1 - (a / b) * y1; 10 return {g, x, y}; 11 } 12 13 long long norm(long long a, long long m) { long long r = a % m; if (r < 0) r += m; return r; } 14 15 // Solve ax ≡ b (mod m). Returns all solutions in [0, m-1]. 16 vector<long long> solve_linear_congruence(long long a, long long b, long long m) { 17 auto [g, x, y] = ext_gcd(a, m); 18 if (b % g != 0) return {}; // no solutions 19 // Reduce to coprime case 20 long long a1 = a / g, b1 = b / g, m1 = m / g; 21 // x is inverse of a1 modulo m1 22 long long inv_a1 = norm(x, m1); 23 long long x0 = ( (__int128)inv_a1 * (b1 % m1 + m1) ) % m1; // base solution modulo m1 24 // All solutions: x = x0 + k*m1 for k = 0..g-1 25 vector<long long> sols; 26 sols.reserve((size_t)g); 27 for (long long k = 0; k < g; ++k) { 28 long long sol = x0 + k * m1; 29 sol = norm(sol, m); // ensure in [0, m-1] 30 sols.push_back(sol); 31 } 32 sort(sols.begin(), sols.end()); 33 sols.erase(unique(sols.begin(), sols.end()), sols.end()); 34 return sols; 35 } 36 37 int main() { 38 // Example 1: 14x ≡ 30 (mod 100) 39 long long a = 14, b = 30, m = 100; 40 auto sols1 = solve_linear_congruence(a, b, m); 41 cout << "Solving " << a << "x ≡ " << b << " (mod " << m << ")\n"; 42 if (sols1.empty()) cout << "No solution\n"; 43 else { 44 cout << "Solutions (" << sols1.size() << "): "; 45 for (auto v : sols1) cout << v << ' '; 46 cout << "\n"; 47 } 48 49 // Example 2: No-solution case: 6x ≡ 5 (mod 15), since gcd(6,15)=3 ∤ 5 50 a = 6; b = 5; m = 15; 51 auto sols2 = solve_linear_congruence(a, b, m); 52 cout << "Solving " << a << "x ≡ " << b << " (mod " << m << ")\n"; 53 if (sols2.empty()) cout << "No solution\n"; 54 55 return 0; 56 } 57
This program solves ax ≡ b (mod m) using the extended Euclidean algorithm. When gcd(a, m) = g divides b, there are exactly g solutions modulo m: x = x0 + k·(m/g). Otherwise no solution exists. The code returns all solutions normalized into [0, m−1].