Why this matters when you build with AI
Number theory answers look correct for small inputs and fail quietly for large ones. A tool that writes (a - b) % p in C++ gets a negative number when a is smaller than b, and then prints it as the answer. A tool that multiplies three numbers before reducing them overflows a 64-bit integer, and the result is a wrong number with no error message. Both bugs pass a test with the sample input, because the sample numbers are small.
Here is a real case from this round. The problem asks for the sum of a_i - a_j over pairs, reduced into the range from 0 to p - 1, with p = 1000000007. For the input 3 and 5 -2 7 the right answer is 1000000003. The naive program prints -4, which is the true sum before reduction, with the reduction lost. The fix is one function, and the way to catch the bug is a brute force that reduces each pair on its own.
Ask for the reduction points before the code. This prompt makes the tool show where every value is reduced:
Write a C++17 solution with modulus p = 1000000007. Before the code, list:
1. every value that can be negative, and where it is reduced into [0, p);
2. every product, and whether both factors are already reduced;
3. every value that can pass 10^18.
Then write a Python brute force for n up to 30 that uses the same definition, so I can compare the two.A good answer lists the negative coefficients and reduces them, and it writes the brute force from the definition, not from the fast formula. Check that the brute force does not reuse the same shortcut as the solution, because a shared mistake makes the comparison useless.
How the round works
This is a practice round you run on your own. The 90-minute timer is a choice made for this lesson, not a rule of any site. Follow these steps.
- Start the timer. Read all five statements first, for about five minutes, and pick an order.
- For each problem, write three lines as a comment before the code: the identity you use, the reduction points, and the complexity.
- Test each solution against a brute force on small inputs. For formulas that are only interesting at large n, also check one large case against an independent method, such as exact big-integer arithmetic.
- Stop at 90 minutes. Read the editorials after the timer, not before.
- Fill in the post-round log (template in the section "Post-round log").
Terms used in this lesson:
- The modulus p is the number you divide by to keep results small. We reduce a value into the range 0 to p - 1, and the reduced value is the remainder.
- A prime is a number with exactly two divisors, 1 and itself. For a prime p, Fermat's little theorem says that a^(p - 2) mod p is the inverse of a, the number that multiplied by a gives 1 mod p, when a is not a multiple of p.
- The divisor count d(k) is how many numbers divide k.
- Euler's totient phi(d) is how many numbers from 1 to d share no factor with d.
- The gcd of two numbers is their largest common divisor. The lcm is the smallest number both divide.
- A sieve fills a table of values for all numbers up to a limit, by marking multiples.
- A binomial coefficient C(n, k) counts the ways to pick k items from n.
- Inclusion-exclusion counts a union by adding and subtracting overlaps: add the single sets, subtract the pairs, add the triples, and so on.
The idea: reduce at every step, and check the sign
Take problem E of this round. For the sample 3 and 5 -2 7, the pair differences are:
pair (1, 2): 5 - (-2) = 7
pair (1, 3): 5 - 7 = -2
pair (2, 3): -2 - 7 = -9
sum = -4The answer must be -4 reduced into [0, p). That is 1000000007 - 4 = 1000000003. A quick way to get the same answer without the pairs is a coefficient for each value. Position k (counting from 0) is added to the sum once for each later position and subtracted once for each earlier position. So its coefficient is (n - 1) - 2k. For n = 3 the coefficients are 2, 0, -2, and the sum is 5 * 2 + (-2) * 0 + 7 * (-2) = 10 - 14 = -4, the same value.
The coefficients are negative, so the reduction matters. In C++ the % operator keeps the sign of its left side, so -4 % p is -4, not 1000000003. Python's % returns a value with the sign of the modulus. The next program is input-free, and I ran it to check the values:
Its output: 500000004, then 2, then 22. The first line is the inverse of 2 mod p, because 2 * 500000004 = 1000000008, which is 1 mod p. The second line is Python's answer for -7 mod 3. The third line counts the numbers from 1 to 30 divisible by 2, 3 or 5.
The same C++ rule catches the sign in a separate small program. This check compiled with g++ 15.2.0 at -O2 -std=c++17:
(3 - 11) % 7 = -1
(-7) % 3 = -1Python gives 6 for (3 - 11) % 7. The same expression has different answers in the two languages, so a Python brute force can disagree with a C++ solution for reasons that have nothing to do with the algorithm. Check the sign rule in both languages before you compare them.
The round
Five original problems. Each sample was run through the solution and the brute force, and both printed the same answer. The modulus is p = 1000000007 in every problem.
Problem A: The divisor total
Compute d(1) + d(2) + ... + d(n), where d(k) is the number of divisors of k, reduced modulo p.
Limits: 1 <= n <= 10^12.
Input: one line with n. Sample input 10, sample output 27.
Problem B: Choose from a row
There is a number N and Q queries. Each query gives n and k. Output C(n, k) mod p for each query. If k is less than 0 or greater than n, output 0.
Limits: 1 <= N <= 200000, 1 <= Q <= 200000, and every n is at most N.
Input: a first line N Q, then Q lines n k.
Sample input:
6 5
5 2
6 0
6 6
4 -1
3 2Sample output: 10, 1, 1, 0, 3, one per line.
Problem C: Numbers that dodge the moduli
There are k numbers m_1 to m_k. Count the integers x from 1 to N that are divisible by none of them. The count is exact, with no modulo.
Limits: 1 <= k <= 15, 1 <= N <= 10^18, 1 <= m_i <= 10^6.
Input: a first line k N, then a line with the k moduli.
Sample input:
3 30
2 3 5Sample output: 8 (the numbers 1, 7, 11, 13, 17, 19, 23 and 29).
Problem D: The gcd table
Compute the sum of gcd(i, j) over all pairs (i, j) where both i and j are from 1 to n, reduced modulo p.
Limits: 1 <= n <= 10^6.
Input: one line with n. Sample input 4, sample output 24.
Problem E: Pair differences
You have n integers a_1 to a_n, each with absolute value at most 10^9, and they can be negative. Compute the sum of a_i - a_j over all pairs where the first index is smaller than the second, reduced into the range 0 to p - 1.
Limits: 1 <= n <= 200000.
Input: a line with n, then a line with the n integers.
Sample input:
3
5 -2 7Sample output: 1000000003.
Editorial A: the divisor total
Identity: the sum of d(k) for k from 1 to n equals the sum of floor(n / i) for i from 1 to n. To see why, count the pairs (i, q) of positive integers with i * q at most n. Each k up to n is counted once for each way to write k = i * q, which gives d(k). Counting by i instead gives floor(n / i) pairs for each i.
State: the value of floor(n / i) is the same for a block of consecutive i. There are only about 2 * sqrt(n) distinct blocks. Transition: for a block [i, r] with q = n / i, add (r - i + 1) * q.
The product (r - i + 1) * q is at most n, because the block length is at most n / q and q is at most n / i. So it fits in 64 bits even for n = 10^12. The sum over all blocks is about n * ln(n), near 3 * 10^13. The mod is therefore a safety step, not a fix for overflow. I checked that: the program gives 452254591 for n = 10^12, and an exact Python computation of the same sum gives the same value after the mod. For n = 10^6 the program and a sieve brute force both give 13970034.
The state that blows up is "the value of d(k) for every k up to n". A sieve for that needs n entries, so at n = 10^12 it needs far more memory than any computer has. Trial division for each k costs about n * sqrt(n) steps, which is also far too slow. The block method costs O(sqrt(n)), which is about 10^6 steps at the limit.
// Input: n (1 <= n <= 10^12). Output S(n) = d(1) + d(2) + ... + d(n) mod p, p = 1e9 + 7,
// where d(k) is the number of divisors of k.
// Identity: S(n) = sum over i of floor(n / i), because each pair (i, q) with i * q <= n is one divisor count.
// State: blocks [i, r] of values i that share the same q = n / i.
// Transition: for a block, add (r - i + 1) * q. The product is at most about n, since the block length
// is at most n / q and q is at most n / i, so it fits in 64 bits even for n = 10^12.
// The sum over all blocks is about n * ln(n), near 3 * 10^13, so the mod is a safety step, not a necessity.
#include <bits/stdc++.h>
using namespace std;
const long long P = 1000000007LL;
int main() {
long long n;
scanf("%lld", &n);
long long ans = 0;
for (long long i = 1; i <= n;) {
long long q = n / i;
long long r = n / q;
ans = (ans + ((r - i + 1) % P) * (q % P)) % P;
i = r + 1;
}
printf("%lld\n", ans);
return 0;
}Output on the sample 10: 27. Check it by hand: floor(10 / i) for i = 1 to 10 is 10, 5, 3, 2, 2, 1, 1, 1, 1, 1, and they add up to 27.
Editorial B: binomials modulo a prime
Identity: C(n, k) = n! / (k! * (n - k)!). Modulo a prime p, division becomes multiplication by an inverse. So C(n, k) mod p = f[n] * inv[k] * inv[n - k] mod p, where f[i] = i! mod p and inv[i] = (i!)^(-1) mod p.
State: f[i] and inv[i] for every i up to N. Base case: f[0] = 1. The inverse of the top factorial comes from Fermat's little theorem, inv[N] = f[N]^(p - 2) mod p, and the other inverses follow from the top one by going down, since inv[i - 1] = inv[i] * i.
The state that blows up is Pascal's triangle, the table of all C(n, k) for n up to N. That is about N^2 / 2 entries, which is 2 * 10^10 at N = 2 * 10^5, far too many. Computing each query's product of k factors costs O(k) per query, which is up to 4 * 10^10 steps for 2 * 10^5 queries. The factorial table costs O(N) once, plus one fast power, and each query is then O(1).
Overflow matters here too. Without the mod, the factorial 21! is 51090942171709440000, which is above the largest 64-bit signed value, 9223372036854775807. So each multiplication must reduce mod p. The program reduces at every step.
// Input: first line "N Q" (N is at least the largest n asked), then Q lines "n k".
// Output C(n, k) mod p for each query, p = 1e9 + 7 (a prime), or 0 when k < 0 or k > n.
// State: f[i] = i! mod p and inv[i] = (i!)^(-1) mod p, for every i up to N, computed once.
// Transition: C(n, k) = f[n] * inv[k] * inv[n - k] mod p.
// The inverse of i! comes from Fermat's little theorem: for a prime p and x not divisible by p,
// x^(p - 2) is the inverse of x mod p. Then the inverses of smaller factorials follow from the top one.
#include <bits/stdc++.h>
using namespace std;
const long long P = 1000000007LL;
long long power(long long b, long long e) {
long long r = 1;
b %= P;
while (e > 0) {
if (e & 1) r = r * b % P;
b = b * b % P;
e >>= 1;
}
return r;
}
int main() {
int N, Q;
scanf("%d %d", &N, &Q);
vector<long long> f(N + 1), inv(N + 1);
f[0] = 1;
for (int i = 1; i <= N; i++) f[i] = f[i - 1] * i % P;
inv[N] = power(f[N], P - 2);
for (int i = N; i > 0; i--) inv[i - 1] = inv[i] * i % P;
while (Q--) {
long long n, k;
scanf("%lld %lld", &n, &k);
if (k < 0 || k > n) {
printf("0\n");
continue;
}
printf("%lld\n", f[n] * inv[k] % P * inv[n - k] % P);
}
return 0;
}Output on the sample: 10, 1, 1, 0, 3. The brute force uses exact big-integer binomials from Python's math.comb, reduced mod p at the end. The two agreed on 200 random query sets with N = 300. The check at N = 300 does not test the Fermat step for large factorials, so a second check at N = 2 * 10^5 is worth doing with a different method, for example a random query compared with math.comb for one n around 10^5.
Editorial C: inclusion-exclusion over the moduli
Identity: the count of x in [1, N] divisible by none of the moduli is the sum over all subsets S of the moduli of (-1)^|S| * floor(N / lcm(S)). The empty set has lcm 1 and gives N. For a number x that at least one modulus divides, the signed terms of its divisible moduli add up to zero, so only the numbers that no modulus divides survive the sum.
State: the depth i in the list of moduli, the lcm L of the subset chosen so far, and the parity of the subset size. Transition: skip m_i, or take it and move to lcm(L, m_i) = L / gcd(L, m_i) * m_i.
The state that blows up is the value of x itself. Looping over x up to N = 10^18 is impossible. The subsets are fine, since 2^15 = 32768 is small. The overflow danger is in the lcm. The product of the moduli can be far above 10^18, so the program computes L / g * m_i in 128-bit arithmetic and stops when the lcm passes N, because every bigger subset then contributes 0.
I checked this with a naive version, which computes the lcm in plain 64-bit arithmetic. On the input 8, 1000000000000000000, with eight moduli near 10^6 (999983, 999979, 999961, 999959, 999953, 999931, 999929 and 999917), the naive program fails. Compiled with -fsanitize=undefined, it printed:
c_naive.cpp:19:26: runtime error: signed integer overflow: 999813011478769747 * 999917 cannot be represented in type 'long long int'
999991999639979904The correct solution prints 999991999639979828, and an exact big-integer computation of the same sum gives the same value. The naive program printed a value that is 76 too large, and only the sanitizer message tells you why. That is why the sanitizer belongs in your checklist.
// Input: first line "k N" (k moduli, 1 <= k <= 15, 1 <= N <= 10^18), then k moduli m_i (1 <= m_i <= 10^6).
// Output how many integers x in [1, N] are divisible by none of the m_i (an exact count, no modulo).
// Identity (inclusion-exclusion): count = sum over subsets S of (-1)^|S| * floor(N / lcm(S)),
// where lcm(empty set) = 1.
// State: depth i in the list of moduli, the lcm L of the subset chosen so far (always at most N here),
// and the parity of the subset size.
// Transition: skip m_i, or take it and move to lcm(L, m_i). The lcm is computed in __int128 so that
// L / g * m_i cannot overflow 64 bits; when it is above N, every superset contributes 0, so we stop.
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int k;
ll N;
vector<ll> m;
ll ans = 0;
void dfs(int i, ll L, int sz) {
if (i == k) {
ll term = N / L;
ans += (sz % 2 == 0) ? term : -term;
return;
}
dfs(i + 1, L, sz);
ll g = std::gcd(L, m[i]);
__int128 nl = (__int128)(L / g) * m[i];
if (nl > (__int128)N) return;
dfs(i + 1, (ll)nl, sz + 1);
}
int main() {
scanf("%d %lld", &k, &N);
m.resize(k);
for (auto &x : m) scanf("%lld", &x);
dfs(0, 1, 0);
printf("%lld\n", ans);
return 0;
}Output on the sample 3 30 / 2 3 5: 8. The brute force tests every x from 1 to N directly. The two agreed on 200 random inputs with up to 6 moduli and N up to 2000.
Editorial D: the gcd table through the totient
Identity: gcd(i, j) equals the sum of phi(d) over all d that divide both i and j. So the sum over all pairs is the sum over d of phi(d) times the number of pairs where d divides both, which is floor(n / d)^2. The total is therefore the sum over d of phi(d) * floor(n / d)^2.
Check it at n = 2: the formula gives phi(1) * 2^2 + phi(2) * 1^2 = 4 + 1 = 5. The direct sum is gcd(1,1) + gcd(1,2) + gcd(2,1) + gcd(2,2) = 1 + 1 + 1 + 2 = 5. The sample n = 4 gives 24 both ways.
State: phi(d) for every d up to n, from a linear sieve. Each composite number is visited once, from its smallest prime factor, so the sieve runs in O(n). Transition: add phi(d) * (n / d)^2 mod p.
The state that blows up is the double loop over pairs (i, j), which is n^2 gcd calls. At n = 10^6 that is 10^12 calls, far too slow. The formula needs n steps for the sieve and n steps for the sum. Overflow is small here: floor(n / d)^2 is at most 10^12, and phi(d) is at most 10^6, so their product is below 10^18 and fits in 64 bits, but the code reduces each factor first anyway.
// Input: n (1 <= n <= 10^6). Output G(n) = sum of gcd(i, j) over all 1 <= i, j <= n, modulo p = 1e9 + 7.
// Identity: gcd(i, j) = sum of phi(d) over every d that divides both i and j,
// where phi(d) is Euler's totient: how many of 1..d are coprime to d.
// So G(n) = sum over d of phi(d) * floor(n / d)^2, since there are floor(n / d)^2 pairs with d | i and d | j.
// State: phi[d] from a linear sieve up to n (each composite is visited once, from its smallest prime factor).
// Transition: add phi[d] * (n / d)^2 mod p for every d.
#include <bits/stdc++.h>
using namespace std;
const long long P = 1000000007LL;
int main() {
int n;
scanf("%d", &n);
vector<int> phi(n + 1, 0), primes;
vector<char> comp(n + 1, 0);
phi[1] = 1;
for (int i = 2; i <= n; i++) {
if (!comp[i]) {
primes.push_back(i);
phi[i] = i - 1;
}
for (int p : primes) {
long long x = (long long)i * p;
if (x > n) break;
comp[x] = 1;
if (i % p == 0) {
phi[x] = phi[i] * p;
break;
}
phi[x] = phi[i] * (p - 1);
}
}
long long ans = 0;
for (int d = 1; d <= n; d++) {
long long q = n / d;
long long term = (long long)phi[d] % P * ((q * q) % P) % P;
ans = (ans + term) % P;
}
printf("%lld\n", ans);
return 0;
}Output on the sample 4: 24. The brute force adds gcd(i, j) for every pair with exact integers and reduces once at the end. The two agreed on 200 random inputs with n up to 150.
Editorial E: pair differences with a coefficient
Identity (0-based positions): a_k is added once for each later position and subtracted once for each earlier position. So its coefficient is (n - 1 - k) - k = (n - 1) - 2k, and the sum is the sum of a_k * ((n - 1) - 2k) over all k. Check it on the sample: the coefficients for n = 3 are 2, 0, -2, and the sum is -4, as computed in the section "The idea".
State: the running sum mod p. Transition: add a_k * c_k, where both factors are first reduced into [0, p). The coefficient c_k can be negative, and a_k can be negative, so each factor needs the reduction ((x % p) + p) % p.
The state that blows up is the pair loop, which costs n^2 / 2 steps. At n = 2 * 10^5 that is about 2 * 10^10 steps. The coefficient form costs n steps. The naive version in the next block is the one I ran, and it prints -4 on the sample, the value before the reduction. The correct program prints 1000000003.
// Input: n (1 <= n <= 2 * 10^5), then n integers a_i with |a_i| <= 10^9 (they can be negative).
// Output S = sum over i < j of (a_i - a_j), reduced into the range [0, p), p = 1e9 + 7.
// Identity (0-based positions): a_k is added once for each j > k and subtracted once for each i < k,
// so its coefficient is (n - 1 - k) - k = (n - 1) - 2k. Coefficients can be negative.
// State: the running sum mod p. Transition: add a_k * c_k, both reduced into [0, p) first.
// C++ % keeps the sign of its left operand, so a negative value needs ((x % p) + p) % p.
#include <bits/stdc++.h>
using namespace std;
const long long P = 1000000007LL;
long long md(long long x) {
x %= P;
if (x < 0) x += P;
return x;
}
int main() {
int n;
scanf("%d", &n);
long long ans = 0;
for (int k = 0; k < n; k++) {
long long a;
scanf("%lld", &a);
long long c = (long long)(n - 1) - 2LL * k;
ans = (ans + md(a) * md(c)) % P;
}
printf("%lld\n", ans);
return 0;
}Output on the sample 3 / 5 -2 7: 1000000003. The brute force looks at every pair (i, j) where i is before j, reduces each difference with Python's %, which is never negative, and adds them. The two agreed on 300 random arrays with n up to 30 and values between -10^9 and 10^9. That loop is the stress check in the next section.
The naive version, which reduces only the product with C++ %, prints the following on the same sample:
-4Common mistakes
Mistakes 1 and 2 were reproduced on this machine with g++ 15.2.0 (Linux, WSL2), and their output lines are real. The other items are checks to make before you submit.
-
Negative remainder. The naive pair-difference program prints -4 where the correct answer is 1000000003. The symptom is a negative number in a problem that asks for a value in [0, p). The fix is the function
mdin Editorial E, applied to every factor with a possible sign. -
Overflow in an lcm or a product. The naive inclusion-exclusion program printed 999991999639979904 with a sanitizer error about
999813011478769747 * 999917, and the correct value is 999991999639979828. A wrong answer with no crash is the usual symptom. Compile with-fsanitize=undefinedwhile you test, and treat every sanitizer message as a failed test. -
Factorials without reduction. The factorial of 21 is already above the 64-bit limit. Reduce mod p after every multiplication, as Editorial B does.
-
Negative or out-of-range binomial arguments. When k is negative or larger than n, the answer is 0, and the program must check that before it indexes the factorial table. The sample contains
4 -1, which needs this check. -
Sign of inclusion-exclusion. The sign is (-1) raised to the size of the subset, not to the depth in the list. A brute force over x up to N, run on small inputs, catches a wrong sign at once.
-
Assuming the formula holds without checking it. The identity for gcd sums and the coefficient for pair differences both look right, but each needs a small hand check, such as n = 2 for gcd sums, before you trust it.
Checklist
- Write the identity in one line before the code, and check it by hand on n = 2 or n = 3.
- Reduce every product mod p, and every difference with the fixed
mdfunction. - Check the sign of a value before you reduce it, and check the sign rule of your language.
- Check that the binomial arguments are in range, and that a k larger than n gives 0.
- Compile with
-fsanitize=undefinedonce, and read every message. - Compare the solution with a brute force on at least 200 random small inputs, using a brute force that reduces by a different route.
- For a formula that matters only at large n, check one large case with exact big integers.
Post-round log
Fill one line per problem right after the round, before you read the editorial.
Problem | solved (yes/no) | minutes used | identity (one line) | reduction points | first wrong idea | bugs found
A | | | | | |
B | | | | | |
C | | | | | |
D | | | | | |
E | | | | | |Count how often "reduction points" or "bugs found" are empty. If they are empty for a problem you got wrong, the reduction step is the habit to practise.
A stress loop for your own solutions
This loop compares the solution for problem E with its Python brute force on 300 random arrays. I ran it on the machine used for this lesson. It needs the compiled program e_sol and the file e_brute.py, which is the brute force described in Editorial E.
import random
import subprocess
rng = random.Random(42)
bad = 0
for t in range(300):
n = rng.randint(1, 30)
vals = [rng.randint(-10**9, 10**9) for _ in range(n)]
txt = f"{n}\n" + " ".join(map(str, vals)) + "\n"
a = subprocess.run(["./e_sol"], input=txt, capture_output=True, text=True, timeout=10).stdout.strip()
b = subprocess.run(["python3", "-I", "e_brute.py"], input=txt, capture_output=True, text=True, timeout=10).stdout.strip()
bad += a != b
print("E: 300 tests, mismatches:", bad)Output on the machine used for this lesson: E: 300 tests, mismatches: 0.
Summary
The modulus problems in this round have two traps that a sample does not reveal: a negative value reduced by C++ %, and a product of large values that overflows before the reduction. The formulas are short, and the fast ones are worth the time to derive, but each formula needs a hand check on a tiny n and a brute force on small random inputs. The sanitizer and the brute force find the two traps quickly, so use both on every submission.
Practice
Practice 1: divisor total by hand
Compute the sum of d(k) for k from 1 to 10 by using the identity with floor(10 / i).
Show the answer
The values of floor(10 / i) for i = 1 to 10 are 10, 5, 3, 2, 2, 1, 1, 1, 1, 1. They add to 27, which the program prints for the input 10.
Practice 2: count the multiples
Problem C asks for the numbers from 1 to 30 that are divisible by none of 2, 3 and 5. Change the question to the numbers that are divisible by at least one of them. What is the answer?
Show the answer
The answer is 22, because 30 - 8 = 22, where 8 is the count of numbers divisible by none of them.
Practice 3: inverse of 2
Find the inverse of 2 modulo p = 1000000007 without a computer, using Fermat's little theorem. Then check it.
Show the answer
The inverse is 2^(p - 2) mod p, which equals 500000004. Check: 2 * 500000004 = 1000000008, and 1000000008 mod p is 1.
Practice 4: the same expression in two languages
What does C++ print for (3 - 11) % 7, and what does Python print for the same expression? Explain the difference in one sentence.
Show the answer
C++ prints -1, because it keeps the sign of the left operand. Python prints 6, because its result has the sign of the modulus. A reduction that is correct in Python can be wrong in C++.
Practice 5: a small gcd sum
Compute the sum of gcd(i, j) over all pairs (i, j) with both i and j from 1 to 2, first directly, then with the totient identity.
Show the answer
Directly: gcd(1,1) + gcd(1,2) + gcd(2,1) + gcd(2,2) = 1 + 1 + 1 + 2 = 5. With the identity: phi(1) * 2^2 + phi(2) * 1^2 = 4 + 1 = 5. Both give 5.