In algorithmic competitions, an important part of number theory is modular arithmetic (modular arithmetic), that is, performing various integer operations modulo a fixed modulus. Besides basic arithmetic and exponentiation, one can also conveniently compute discrete logarithms, roots of various degrees, factorials, binomial coefficients, and more.
Modular arithmetic appears in all kinds of problems, not only in number theory. The actual answer to many problems may be very large, exceeding the storage range of common integer types. To avoid introducing big-integer arithmetic and printing long numbers, problems often ask for the answer modulo some number. This makes it important to master various modular-arithmetic techniques.
Integer Division and Remainder in C/C++
In C/C++, integer division and remainder do not follow the usual mathematical conventions for modular arithmetic and division.
For every standard version of C/C++, integer division is specified as follows:
- When the divisor is zero, the behavior is undefined.
- Otherwise, the result of
(a / b) * b + a % bis equal toa.
In other words, the sign of the remainder depends on how division rounds, and the way division rounds is implementation-defined (determined by the compiler).
Starting with the C99 and C++11 standards, the quotient is specified to round toward zero (discarding the fractional part), so the remainder has the same sign as the dividend. Therefore, the following assertions are guaranteed to be true:
assert(5 % 3 == 2);
assert(5 % -3 == 2);
assert(-5 % 3 == -2);
assert(-5 % -3 == -2);
ModInt Class
Modular arithmetic can be viewed as performing operations on congruence classes modulo a fixed number. If a structure is used to represent a congruence class, and addition, subtraction, multiplication, and other operations between congruence classes are encapsulated as structure methods or overloaded operators, modular arithmetic can naturally be implemented as a ModInt class. The following is a simple example supporting addition, subtraction, multiplication, and fast exponentiation of -bit signed integers for moduli :
A Simple ModInt Class
// A simple ModInt implementation.
template <int M>
struct ModInt {
struct skip_mod {};
ModInt(int v, skip_mod) : v(v) {}
int v;
ModInt() : v(0) {}
// Initialization: find remainder.
// Equivalent to: v = int((x % M + M) % M)
ModInt(long long x) {
x %= M;
if (x < 0) x += M;
v = int(x);
}
// Addition.
// Equivalent to: ModInt((l.v + r.v) % M)
friend ModInt operator+(ModInt l, ModInt r) {
int res = l.v + r.v;
if (res >= M) res -= M;
return ModInt(res, skip_mod{});
}
// Subtraction.
// Equivalent to: ModInt((l.v - r.v + M) % M)
friend ModInt operator-(ModInt l, ModInt r) {
int res = l.v - r.v;
if (res < 0) res += M;
return ModInt(res, skip_mod{});
}
// Multiplication.
friend ModInt operator*(ModInt l, ModInt r) {
return ModInt(1LL * l.v * r.v % M, skip_mod{});
}
// Exponentiation.
ModInt pow(long long b) const {
ModInt res{1}, po{*this};
for (; b; b >>= 1) {
if (b & 1) res = res * po;
po = po * po;
}
return res;
}
};
This implementation deliberately reduces the number of remainder operations, because remainder operations are usually much slower than ordinary addition, subtraction, multiplication, or comparison. The code comments provide equivalent, more direct implementations. The main idea of these simple optimizations is that when two integers in are added or subtracted, the result always lies in , so one addition or subtraction can bring it back into . The exponentiation implementation uses the binary exponentiation technique.
In addition to these basic operations, the following operations can be performed under various moduli:
- Multiplicative inverse
- Division
- Factorial
- Binomial coefficients
- Square roots
- Discrete logarithms
- Roots
These operations are usually easier for prime moduli. For composite moduli, extended versions of the corresponding algorithms and the Chinese remainder theorem are often needed. Most modular operations can be viewed as solving some kind of congruence equation. For general methods of solving congruence equations, see the congruence equations page.
Related Algorithms
This section introduces several methods for optimizing remainder, multiplication, and fast-exponentiation operations modulo a number. For most problems, the simple implementation given above is already efficient enough. However, when a problem imposes strict requirements on constant factors, these optimizations can reduce unnecessary computation and remainder operations, further lowering the running time.
Fast Multiplication
In primality testing and integer factorization, one often encounters modular multiplication with moduli in the long long range. To avoid integer overflow during the computation, this section introduces a “fast multiplication” method that handles moduli in the long long range without using __int128, in time. This algorithm requires the judging system to represent long double with at least -bit extended precision floating point1.
Assume . We want to compute . Notice that:
Using the natural overflow of unsigned long long:
As long as the quotient can be computed, the multiplication and subtraction in the rightmost expression can both be evaluated directly using unsigned long long.
It remains to consider how to compute . The solution is to first use long double to compute and then multiply by . Since long double is being used, there will inevitably be precision error. Suppose that long double is an -bit extended-precision floating-point type (with a -bit sign, a -bit exponent, and a -bit significand). Then long double can represent at most significant bits exactly2. Therefore, may first become wrong at bit , with an error range3 of . After multiplying by the -bit signed integer , the error range is . To simplify the discussion, we can first add and then round; the final error range is .
Finally, substituting into the expression above requires multiplication by , so the final error range is . Since is in the long long range, when the result lies in , return directly; otherwise return .
The implementation is as follows:
Reference Implementation
long long mul(long long a, long long b, long long m) {
long long c = (unsigned long long)a * b -
(unsigned long long)((long double)a / m * b + 0.5L) * m;
return c < 0 ? c + m : c;
}
Most judging systems now provide C/C++ compilers supporting the __int128 type4, so the multiplicand type can also be promoted to __int128 before taking the remainder:
Reference Implementation
long long mul(long long a, long long b, long long m) {
return (__int128)a * b % m;
}
Of course, taking the remainder of __int128 values is not particularly fast. If further constant-factor optimization is needed, the methods in the next two sections can be considered.
Barrett Reduction
As mentioned above, division and remainder operations are usually slower than the other four arithmetic operations. To reduce the cost of remainder operations, some algorithms can obtain the same result without directly taking a remainder. The Barrett reduction algorithm introduced in this section is one of them.
Let be a fixed modulus, and suppose we need to compute for different positive values . By the division algorithm:
The key is computing the quotient . Let be a constant. Then5
If we choose , then in the right-hand side, can be precomputed, and division by can be performed by a shift operation. Thus, computing the quotient with the right-hand side requires only one multiplication and one shift. Substituting it into the expression for gives the desired estimate of the remainder.
Now analyze the error. The floor function has the property that for all , we have . Therefore, the error is
As long as , the error does not exceed . Since , the estimate can only be or . After obtaining the estimate, it is sufficient to subtract an extra when to ensure the answer is correct.
The Barrett reduction computation uses only two multiplications, one shift, and at most two subtractions to perform an integer remainder. This efficiency improvement is not free: the intermediate variables involved in Barrett reduction are often longer than the input variables. It is easy to see that the longest intermediate variable in Barrett reduction is . Let be the bit length of the binary representation of integer . Then:
Because the choice of must satisfy , this length is at least . However, when a remainder is needed, we usually have , so this intermediate variable may be longer than the input length . For example, if a -bit integer is reduced modulo a -bit integer, the intermediate variable actually needs bits.
One application of Barrett reduction is computing the remainder of the product . If one multiplicand is fixed, for example when is fixed, we can use:
to make a similar estimate; it is only necessary to precompute the value of . This fixed- case is sometimes called Shoup modular multiplication6.
The more common case is that are both variable. First compute , then use Barrett reduction to obtain . For example, when implementing modular multiplication, compute for . In this case, the chosen must satisfy . By the analysis above, the longest intermediate variable has length . When , this length is . In other words, to implement modular multiplication for -bit integers using Barrett reduction, the intermediate variable needs bits. This is one limitation of Barrett reduction in practical algorithm-contest applications.
As an example, a reference implementation of modular multiplication for signed 32-bit integers using Barrett reduction is:
Reference Implementation
// Modular multiplication of int32_t using Barrett reduction.
class Barrett {
int32_t m;
uint64_t r;
public:
Barrett(int32_t m) : m(m), r((uint64_t)(-m) / m + 1) {}
// Barrett reduction: a % m.
int32_t reduce(int64_t a) const {
int64_t q = (__int128)a * r >> 64;
a -= q * m;
return a >= m ? a - m : a;
}
// Modular multiplication: (a * b) % m;
// Assume that 0 <= a, b < m.
int32_t mul(int32_t a, int32_t b) const { return reduce((int64_t)a * b); }
};
The implementation uses 128-bit integers4.
Montgomery Modular Multiplication
Montgomery modular multiplication serves a purpose very similar to Barrett reduction: it also reduces the cost of remainder operations during modular arithmetic. Unlike the previous two algorithms, which approximate the quotient, Montgomery multiplication maps all integers into Montgomery space, where operations are relatively easy, thereby reducing the overall computational cost.
Let the modulus be odd, and choose . Then the Montgomery form corresponding to the congruence class is
Because , there is a bijection between the congruence class and its Montgomery form . Therefore, integers can be converted to Montgomery form, several operations modulo can be performed, and the resulting Montgomery form can then be converted back to an integer; the result is always correct.
Montgomery form makes many modular operations convenient. As noted above, whether two congruence classes are equal can be checked by comparing their Montgomery forms. Also,
so addition and subtraction of congruence classes correspond to addition and subtraction of their Montgomery forms. However, multiplication of congruence classes cannot be performed by directly multiplying the two Montgomery forms. Indeed,
Thus, when multiplying two Montgomery forms, we need to apply the following Montgomery reduction operation to their product :
Using this operation, the Montgomery form of the product is . Montgomery reduction is the core operation of Montgomery modular multiplication:
- Converting to its Montgomery form is .
- Converting the Montgomery form of back to is .
- The Montgomery form of the modular inverse is .
Now discuss how to implement the Montgomery reduction operation . When computing , we always assume , which holds in all the cases above. Since , Bézout’s identity gives integers such that
Therefore, let . Then
Since and ,
That is, this quotient differs from by at most one . Adding when the quotient is negative gives . Computing this quotient requires only two integer multiplications, one integer subtraction, and two bit operations (taking the remainder modulo and dividing by ). Thus, Montgomery reduction can be performed efficiently.
To perform Montgomery modular multiplication, a series of constants must be precomputed. First, Montgomery reduction uses , which can be computed with the Newton–Hensel method introduced below. In addition, converting different operations into Montgomery reductions involves constants such as . To obtain it, compute once, then add it to itself to obtain . Next, regard it as the Montgomery form of and perform fast exponentiation directly to obtain .
As an example, Montgomery modular multiplication for signed -bit integers is implemented as follows:
Reference Implementation
// Montgomery modular multiplication.
// The modulus m must be odd. The constant r is 2^32.
class Montgomery {
int32_t m;
uint32_t mm, r2;
public:
Montgomery(int32_t m) : m(m), mm(1), r2(-m) {
// Compute mm as inv(m) mod r.
for (int i = 0; i < 5; ++i) {
mm *= 2 - mm * m;
}
// Compute r2 as r * r mod m.
// If allowed to use modular operation for uint64_t, simply use:
// r2 = (uint64_t)(-m) % m;
r2 %= m;
r2 <<= 1;
if (r2 >= (uint32_t)m) r2 -= m;
for (int i = 0; i < 5; ++i) {
r2 = mul(r2, r2);
}
}
// Montgomery reduction: x * inv(r) % m.
// Also used to transform x from Montgomery space to the normal space.
int32_t reduce(int64_t x) {
uint32_t u = (uint32_t)x * mm;
int32_t ans = (x - (int64_t)m * u) >> 32;
return ans < 0 ? ans + m : ans;
}
// Multiplication in Montgomery space: x * y * inv(r) % m.
int32_t mul(int32_t x, int32_t y) { return reduce((int64_t)x * y); }
// Transform x from the normal space to Montgomery space.
int32_t init(int32_t x) { return mul(x, r2); }
};
Compared with implementing modular multiplication using Barrett reduction, Montgomery multiplication involves several steps, including conversion, multiplication in Montgomery form, and inverse conversion. Therefore, conversion and inverse-conversion costs can be amortized only when enough modular operations are performed between them, leading to high overall efficiency. However, because the Montgomery implementation uses only intermediate variables of length , it is more flexible. For example, multiplication of -bit integers requires only an intermediate variable of bits. Thus, if a modular-integer class is needed for various number-theoretic computations, Montgomery multiplication is more suitable.
Integer Classes Modulo Powers of 2
This section discusses implementing a modular-integer class when the modulus is a power of . In this special case, division and remainder operations can be implemented with bit operations, making computation very efficient. Barrett reduction and Montgomery multiplication both exploit this property when is used as divisor and modulus. In particular, when the modulus is exactly or , an unsigned integer of the corresponding bit width combined with natural overflow can implement a modular-integer class without any explicit remainder operations. Even when the modulus is not exactly one of these values, it can be converted to one of these cases. For example, when the modulus is , intermediate computations can be performed modulo , and the result can finally be reduced modulo . Besides the convenience of taking remainders, modular-integer classes modulo have many other special implementations. This section focuses on implementing inverses and exponentiation.
First, consider inversion: given odd and modulus , compute . Common methods include the extended Euclidean algorithm and fast exponentiation. The extended Euclidean algorithm involves taking remainders modulo a general modulus; ordinary fast exponentiation requires computing , which takes integer multiplications. A more efficient method is the Newton–Hensel method. Specifically, apply the following fact:7
Thus, starting from and repeatedly applying , we obtain after iterations.
As an example, a reference implementation for inversion modulo is:
Reference Implementation
// Compute 1/v mod 2^32 for odd v.
uint32_t inv_mod_2_32(uint32_t v) {
uint32_t x = 1;
for (int i = 0; i != 5; ++i) {
x *= 2 - v * x;
}
return x;
}
Next, consider exponentiation: given and modulus , compute , where is odd. From the analysis of the multiplicative structure modulo , we know that can always be written in the form 8, and the minus sign appears exactly when . In this case, replace with and multiply the final result by . Thus, assume from now on that . The core idea of the algorithm is to write as , then compute the desired power as .
Computing means computing the discrete logarithm . Note that as long as , can always be written in the following form:
where . This is because expanding the product directly shows that the binary representation of has as its second-lowest set bit, at position (with bit indices starting from ), so this representation can be found recursively. By the properties of discrete logarithms,
Because the modulus of the discrete logarithm is the order , multiply the entire congruence by here so that the computation can be carried out in the residue classes modulo . Therefore, precomputing, for every , the quantity makes it possible to compute quickly.
Conversely, it is also easy to obtain the value of from . By the binomial theorem, for every ,
Therefore, . By the properties of orders,
Thus, . This shows that , where . Therefore, the least significant bit equal to in the binary representation of is exactly bit (with bit indices starting from ). Hence, the same binary-representation recursion can decompose into a sum of terms of the form . This gives the value of .
There are several further optimizations in the concrete implementation. First, division is still needed when decomposing into a product. It is more convenient to compute the decomposition of , that is, find such that
holds. Again, determine by finding the second-lowest bit equal to , but to remove the factor from , it is enough to multiply by , which can be done with bit operations. Since , subtraction must replace addition when accumulating . Second, for a specially chosen base , iteration need not continue to ; it only needs to reach . For this, choose such that
For , we have
Therefore, induction starting from shows that for every . Consequently, as long as , we have
and
Therefore, after processing all binary bits with , the remaining discrete logarithm can be obtained directly without computing bit by bit. With the first optimization, the entire exponentiation requires additions/subtractions and bit operations and multiplication; with the second optimization, about half of the additions/subtractions and bit operations can be omitted, at the cost of an additional multiplication.
As an example, exponentiation modulo is implemented as follows:
Reference Implementation
// Store 4L(a) for a = 2^d + 1, where L(a) is disc. log. base 388251981.
// The first two values are never used and thus set to zero.
// The base is chosen such that 4L(2^16+1) = 2^16.
constexpr uint32_t log_table[16] = {
0x00000000, 0x00000000, 0xbba0267c, 0x49b9d1e8, 0xf0026f90, 0xd6e17e20,
0xe78bf840, 0x039fe080, 0xaf7f8100, 0x60fe0200, 0xd1f80400, 0x23e00800,
0x47801000, 0x8e002000, 0x18004000, 0x20008000,
};
// Compute 4L(v).
uint32_t log_mod_2_32(uint32_t x, uint32_t v) {
for (int i = 2; i != 16; ++i) {
if ((v >> i) & 1) {
v += v << i;
x -= log_table[i];
}
}
x += v ^ 1;
return x;
}
// Compute x*a for 4L(a) = v.
uint32_t exp_mod_2_32(uint32_t x, uint32_t v) {
for (int i = 2; i != 16; ++i) {
if ((v >> i) & 1) {
x += x << i;
v -= log_table[i];
}
}
x *= v ^ 1;
return x;
}
// Compute x*a^b for odd a.
uint32_t pow_odd_mod_2_32(uint32_t a, uint32_t b, uint32_t x) {
if (a & 2) {
a = -a;
if (b & 1) {
x = -x;
}
}
return exp_mod_2_32(x, log_mod_2_32(0, a) * b);
}
// Compute x*a^b mod 2^32.
uint32_t pow_mod_2_32(uint32_t a, uint32_t b, uint32_t x = 1) {
if (!a) return b == 0 ? x : 0;
auto d = __builtin_ctz(a);
if ((uint64_t)d * b >= 32) return 0;
return pow_odd_mod_2_32(a >> d, b, x) << (d * b);
}
The discrete-logarithm preprocessing can be done with the Pohlig–Hellman algorithm. The base can be chosen as
References and Notes
- Fast Modular Multiplication by orz - Codeforces
- Barrett Reduction - Wikipedia
- Barrett Reduction - A41
- Barrett Reduction Principle and Correctness Proof by Chen - Zhihu
- Montgomery Multiplication - CP Algorithms
- Montgomery Modular Multiplication by Chen - Zhihu
- Binary Exponentiation by Factoring - CP Algorithms
- Barrett, Paul. "Implementing the Rivest Shamir and Adleman public key encryption algorithm on a standard digital signal processor." In Conference on the Theory and Application of Cryptographic Techniques, pp. 311-323. Berlin, Heidelberg: Springer Berlin Heidelberg, 1986.
- Becker, Hanno, Vincent Hwang, Matthias J. Kannwischer, Bo-Yin Yang, and Shang-Yi Yang. "Neon NTT: Faster Dilithium, Kyber, and Saber on Cortex-A72 and Apple M1." IACR Transactions on Cryptographic Hardware and Embedded Systems (2022): 221-244.
- Montgomery, Peter L. "Modular multiplication without trial division." Mathematics of computation 44, no. 170 (1985): 519-521.
Footnotes
-
This applies to GCC or Clang compilers on most 64-bit systems. ↩
-
Here we use the condition , that is, . ↩
-
In current mainstream compilation environments, only MSVC on Windows does not support the
__int128type. To write code compatible with multiple platforms, use the macro_MSC_VERto detect the MSVC compilation environment, and include the<intrin.h>header under that condition, using its provided intrinsic functions (such as_umul128and others) to implement 128-bit integer arithmetic indirectly (available only on 64-bit platforms). ↩ ↩2 -
Here can also be replaced with other integer estimates of , such as the ceiling function and the nearest-integer rounding function , as long as the corresponding error correction step is adjusted. ↩
-
Shoup implemented this extension of Barrett reduction in his number-theoretic computation library NTL, hence the name. ↩
-
Direct verification: from , write . Then . ↩
-
The page cited in the text only proves that can be chosen as . In fact, repeating that proof completely shows that can be any integer congruent to modulo . The method for choosing is discussed below. ↩