Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 6

Preventing Overflow with Modular Arithmetic

Recommended resources

Hello! Welcome to the second module of our course. In the previous module, we built a solid foundation in core number theory, culminating in the lesson on LCM where you saw firsthand how a clever implementation detail—rearranging division and multiplication—is crucial for preventing integer overflow. That idea is the perfect bridge to our new topic.

This lesson kicks off our exploration of modular arithmetic, a concept that is absolutely fundamental to competitive programming. Many problems involve calculations with numbers so large they would overflow even a long long data type. Modular arithmetic is the elegant solution to this problem. Today, you'll learn how to apply the properties of modular addition, subtraction, and multiplication. This will allow you to keep intermediate results within a manageable range, enabling you to solve a whole new class of problems.

The Problem with Large Numbers

In competitive programming, you'll often encounter problems that ask for a count of something—combinations, permutations, paths on a grid, etc. The final answer can be astronomical. For example, calculating 100! (100 factorial) results in a number with 158 digits, far too large for any standard integer type in C++.

To test your algorithmic logic without requiring complex big integer libraries, problem setters ask for the answer "modulo some number M". A very common choice is . This number is large enough to avoid many accidental collisions in results, small enough that the product of two numbers less than M fits into a 64-bit integer (long long), and it's a prime number, which will be important for division later on.

The famous competitive programmer Errichto provides an excellent overview of why this is necessary.

Computations Modulo P in Competitive Programming

This video, "Computations Modulo P in Competitive Programming," explains the two main issues with huge numbers: they cause overflows in languages like C++, and operations on them become very slow even in languages with built-in support for big integers.

Watch the first two minutes of the video, from the beginning. He clearly lays out the motivation for using modular arithmetic.


Modular Arithmetic: Thinking on a Clock

At its heart, modular arithmetic is "clock arithmetic". If it's 9 o'clock and you add 4 hours, it becomes 1 o'clock, not 13 o'clock. This is because we are working "modulo 12". The calculation is .

This image illustrates modular addition. Adding 4 hours to 9 o'clock results in 1 o'clock. The sum 13 "wraps around" the 12-hour clock, leaving a remainder of 1.

Formally, the expression gives the remainder when is divided by . This remainder is always in the range . This "wrapping around" behavior is the key to keeping our numbers small.

The following reading from a Codeforces blog post formally introduces the terminology.

Modular Arithmetic for Beginners

This article provides a concise introduction to modular arithmetic, defining key terms and highlighting an important distinction between the mathematical definition and the behavior of the % operator in some programming languages.

Read the introduction and the section on terminology. Pay special attention to the note about how different languages handle negative numbers with the % operator.


The Core Properties

The power of modular arithmetic comes from a set of properties that allow us to perform operations within the modular system without ever needing to compute the full, gigantic numbers. For any integers , , and modulus :

  1. Addition:
  2. Multiplication:
  3. Subtraction:

Notice the + m in the subtraction formula. We'll return to why that's crucial shortly.

These identities mean we can take the modulo at every intermediate step of a calculation. This ensures that the numbers we are working with never exceed the modulus, completely preventing overflow.

Let's dive into some resources that explain and prove these properties.

Number Theory for Competitive Programming | Topic Stream 9

This clip from a number theory lecture by Colin Galen provides a clear explanation of these identities, including a brief proof for the multiplication property.

Watch the section on basic identities. The proof demonstrates that since any number a can be written as cm + d (where d is a mod m), the cm terms can always be discarded when working modulo m.

Modulo 10^9+7 (1000000007) - GeeksforGeeks

This GeeksforGeeks article provides further context on why we need modulo and gives a very practical example of applying the properties correctly versus incorrectly.

First, read the sections Why do we need modulo and How modulo is used. Then, carefully study the example that calculates (a*b*c)%m. It shows how Method 1 (multiplying first) fails due to overflow, while Method 2 (taking modulo at each step) works perfectly.


C++ Implementation and Pitfalls

Knowing the properties is one thing; implementing them correctly is another. There are two main pitfalls in C++.

1. Intermediate Multiplication Overflow

Consider the expression (a * b) % m. If a and b are both integers close to m (say, ), their product a * b will be around . This fits within a long long. However, if your variables a and b are of type int, the multiplication a * b will be performed using 32-bit arithmetic, causing an overflow before the result is assigned to a long long and before the modulo is taken.

The safe way to compute modular multiplication is to cast one of the operands to long long first:
long long result = (1LL * a * b) % m;

Errichto demonstrates this exact point in his video.

Computations Modulo P in Competitive Programming

This segment shows a practical example of multiplying large numbers and how applying the modulo at each step prevents overflow.

Watch the part from the explanation of multiplication. He correctly points out that even with long long, you must take the modulo of the components first to ensure the intermediate product (a % m) * (b % m) fits.

2. Negative Results from Subtraction

This is a classic bug in C++ competitive programming code. The % operator can return negative values. For example, (5 - 8) % 10 evaluates to -3 % 10, which in C++ is -3. However, a proper modular result should be in the range . The correct answer is 7.

To fix this, we use the property (a - b) % m = (a - b + m) % m.
By adding m before the final modulo, we guarantee the result is positive.

int result = ((a - b) % m + m) % m;

If (a - b) % m is already positive, adding m and taking another modulo doesn't change the result (e.g., (3 + 10) % 10 = 3). If it's negative, it shifts it into the correct positive range (e.g., (-3 + 10) % 10 = 7).

This crucial detail is explained in both the GeeksforGeeks article and Errichto's video.

Modulo 10^9+7 (1000000007) - GeeksforGeeks

This section directly addresses the issue with negative numbers from the modulo operator in C/C++ and provides the standard one-line fix.

Read the section beginning with the note about negative numbers and look at the C++ code snippet that follows.


Putting It All Together

Here are simple, robust C++ functions you can add to your programming template. They handle the casting and negative number issues correctly.

const int MOD = 1e9 + 7;

// Modular addition
long long add(long long a, long long b) {
    return (a + b) % MOD;
}

// Modular multiplication
long long mul(long long a, long long b) {
    return (1LL * a * b) % MOD;
}

// Modular subtraction
long long sub(long long a, long long b) {
    // The inner (a - b) % MOD can be negative,
    // so we add MOD and take modulo again.
    return ((a - b) % MOD + MOD) % MOD;
}

These three functions are the building blocks for almost all calculations in modular arithmetic.


Conclusion

In this lesson, we've established the "why" and "how" of basic modular arithmetic. It's not a trick to make problems harder, but an essential tool to make them solvable by managing the size of numbers.

Here are the key takeaways:

  • Modular arithmetic is used in competitive programming to prevent integer overflow when dealing with very large results.
  • The fundamental properties are:
  • The key strategy is to apply the modulo operator at every intermediate step of a calculation.
  • In C++, be mindful of casting to long long for multiplications and adding the modulus in subtractions to handle potential negative results.

You now have the tools to perform chains of additions, subtractions, and multiplications without fear of overflow. But what about exponentiation, like calculating ? Doing multiplications would be too slow if is large. In our next lesson, we will tackle this exact problem by learning binary exponentiation, an efficient algorithm that computes modular powers in time.