MathIntermediate

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 divisibilityUnderstanding 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 algorithmsThese 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 functionKnowing when m is prime or composite determines invertibility and which theorems apply.
  • C++ integer types and overflow behaviorCorrect implementation requires avoiding overflow and handling negatives with the % operator.

Detailed Explanation

Tap terms for definitions

01Overview

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

For an integer m 2, define an equivalence relation on the integers by a b if and only if m (a - b), i.e., a and b differ by a multiple of m. The equivalence classes under this relation are called residue classes modulo m, and there are exactly m such classes, often represented by the set {0, 1, 2, , m-1}. Addition and multiplication of residue classes are well-defined: [a] + [b] := [a + b] and [a][b] := [ab], where brackets denote classes. The resulting structure (integers modulo m) is a commutative ring with identity [1]. An element [a] in has a multiplicative inverse (i.e., there exists [x] such that [a][x] = [1]) if and only if (a, m) = 1; these invertible elements form the multiplicative group of units . When m is prime p, every nonzero class [a] has an inverse, and is a finite field. Important theorems include Fermat’s little theorem: for prime p and a not divisible by p, 1 , implying ; and Euler’s theorem: for (a, m) = 1, 1 , where is Euler’s totient function. Algorithmically, the extended Euclidean algorithm produces integers x, y such that ax + my = (a, m); when the gcd is 1, x is a modular inverse of a modulo m.

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

Basic modular addition, subtraction, normalization, and a single multiplication each run in O(1) time and O(1) space. However, care must be taken to avoid overflow: if operands are up to 64 bits, direct multiplication can overflow before taking modulo. Casting to 128-bit (e.g., using __int128 in GCC/Clang) preserves correctness with only constant overhead. Fast modular exponentiation (binary exponentiation) computes mod m in O(log e) time and O(1) auxiliary space, repeatedly squaring and multiplying while reducing modulo m at every step. The extended Euclidean algorithm runs in O(log min(a, m)) time due to the properties of the Euclidean GCD algorithm, and returns coefficients that can be used to construct modular inverses when gcd(a, m) = 1. Solving a linear congruence ax ≡ b (mod m) reduces to one inverse modulo m/g when solvable, and enumerating all solutions then takes O(g) time where , m), with O(1) space beyond the output. For array-based tasks like computing sums or detecting a subarray whose sum is divisible by m using prefix sums modulo m, the time is O(n) and space is O(min(n, m)) when using hashing to store seen remainders. Precomputing factorials and inverse factorials modulo a prime up to N is O(N) time and O(N) space, enabling O(1) per-query binomial coefficients. Across all cases, the crucial performance strategy is to reduce modulo m after each arithmetic operation to maintain small intermediate values and to use 128-bit intermediates for safe multiplication where needed.

Code Examples

Safe modular addition, subtraction, multiplication, and normalization
1#include <iostream>
2#include <cstdint>
3
4// Normalize a into the range [0, m-1]
5long 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
12long 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]
21long 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)
30long 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
35int 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.

Time: O(1) per operationSpace: O(1)
Fast modular exponentiation and modular inverse (Fermat and Extended Euclid)
1#include <iostream>
2#include <tuple>
3#include <cstdint>
4
5long long norm(long long a, long long m) {
6 long long r = a % m; if (r < 0) r += m; return r;
7}
8
9long 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
15long 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)
27std::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)
36std::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
44long long inv_mod_fermat(long long a, long long m_prime) {
45 return pow_mod(a, m_prime - 2, m_prime);
46}
47
48int 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.

Time: pow_mod: O(log e); inv_mod_euclid: O(log m)Space: O(1)
Subarray with sum divisible by m using prefix sums modulo m
1#include <bits/stdc++.h>
2using namespace std;
3
4// Normalize to [0, m-1]
5static 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
10bool 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
22int 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.

Time: O(n) expected (hashing)Space: O(min(n, m))
Solve linear congruence ax ≡ b (mod m) with Extended Euclid (all solutions)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Extended Euclidean: returns (g, x, y) with ax + by = g = gcd(a, b)
5tuple<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
13long 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].
16vector<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
37int 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].

Time: O(log m) to find one base solution; O(g) to enumerate all solutionsSpace: O(1) beyond the output