```mermaid graph LR A[Mermaid Keeper] --> B[保持激活] ```

前言

你打开一道算法竞赛题,读完题意,最后一行写着:“答案对 109+710^9+7 取模。”

这句话几乎出现在每一道计数题里。为什么?因为很多计数问题的答案大得离谱——比如”从 100 个人里选 50 个组成委员会”,答案是 (10050)1029\binom{100}{50} \approx 10^{29},29 位数,long long 存不下。但题目只需要输出它除以 109+710^9+7 的余数,这个余数在 00109+610^9+6 之间,轻松存下。

问题是:你不能等算完再取模——那样早就溢出了。你需要在计算的每一步都取模,最终结果还得是对的。

这就需要同余理论。好消息是,模意义下的加法、减法、乘法和你熟悉的普通运算几乎一样——只是每步多加一个 % MOD。但也有陷阱:负数取模、中间值溢出……这些都是竞赛里的常客。

下一篇 02-04 快速幂与逆元 会讲模意义下的乘方和除法(逆元)。

问题的本质

同余:余数相同就是”同类”

今天星期三,10 天后是星期几?3+10=133 + 10 = 1313mod7=613 \bmod 7 = 6,星期六。100 天后呢?3+100=1033 + 100 = 103103mod7=5103 \bmod 7 = 5,星期五。

你不关心”过了多少天”,只关心”星期几”——也就是天数除以 7 的余数。10 天和 100 天的余数不同,所以对应不同的星期。但 10 天和 17 天的余数相同(都是 3),所以都是星期六。

这就是同余的直觉:两个数除以 mm 的余数相同,它们就是”同类”。数学记号:

ab(modm)    m(ab)a \equiv b \pmod{m} \iff m \mid (a - b)

比如 173(mod7)17 \equiv 3 \pmod{7},因为 173=14=2×717 - 3 = 14 = 2 \times 7

一个有用的公式:amodma \bmod m 可以写成 ama/ma - m \cdot \lfloor a/m \rfloor。这告诉我们:取模本质上就是”减去尽可能多的 mm。比如 17mod7=177×2=317 \bmod 7 = 17 - 7 \times 2 = 3

这个公式也解释了为什么负数取模有陷阱:3mod7=37×3/7-3 \bmod 7 = -3 - 7 \times \lfloor -3/7 \rfloor。这里 3/7=1\lfloor -3/7 \rfloor = -1(向下取整),所以 37×(1)=3+7=4-3 - 7 \times (-1) = -3 + 7 = 4。但 C++ 的 % 对负数的行为是向零取整(3)%7=3(-3) \% 7 = -3,不是 4。Python 的 % 则返回非负数(3)%7=4(-3) \% 7 = 4。这是语言差异,做题时务必注意。

为什么可以”边算边取模”?

核心性质:同余关系在加法、减法、乘法下保持。 也就是说:

如果 aa(modm), bb(modm)\text{如果 } a \equiv a' \pmod{m},\ b \equiv b' \pmod{m} 那么 a+ba+b(modm)\text{那么 } a+b \equiv a'+b' \pmod{m}

减法和乘法同理。

这意味着:你可以在计算的任何一步把大数替换成它除以 mm 的余数,不影响最终结果的余数。先取模再算先算完再取模,余数一样。

但前提是:只有加、减、乘。除法不行——下一篇讲。

更具体地说,以下操作不能直接套用同余替换:

操作能否替换反例
a+ba + b
aba - b
a×ba \times b
a/ba / b(整除)4/22,4/414/2 \equiv 2, 4/4 \equiv 1,但 2≢4(mod3)2 \not\equiv 4 \pmod{3},结果不同
ana^nnn 是常数)2381(mod7)2^3 \equiv 8 \equiv 1 \pmod{7}937291(mod7)9^3 \equiv 729 \equiv 1 \pmod{7}29(mod7)2 \equiv 9 \pmod{7},结果一致
aba^bbb 是变量)a1=2,b1=1,a2=2,b2=4a_1 = 2, b_1 = 1, a_2 = 2, b_2 = 4,模 3:212,2412^1 \equiv 2, 2^4 \equiv 11≢4(mod3)1 \not\equiv 4 \pmod{3} 但结果不同
a!a!(阶乘)1!1(mod2)1! \equiv 1 \pmod{2}3!0(mod2)3! \equiv 0 \pmod{2}13(mod2)1 \equiv 3 \pmod{2} 但阶乘结果不同

记住这张表就能避免踩坑。加、减、乘、常数次幂可以在任何一步取模;除法、变量次幂、阶乘不行。

理论 + 代码

模加法、模减法、模乘法

const long long MOD = 1e9 + 7;

// 模加法
long long add(long long a, long long b) {
    return (a + b) % MOD;
    // a, b 都在 [0, MOD) 内,a+b 最大约 2×10^9,long long 没问题
}

// 模减法
long long sub(long long a, long long b) {
    return (a - b + MOD) % MOD;
    // ① 为什么 +MOD?因为 a-b 可能是负数!
    // C++ 中 (-3) % 7 = -3,不是 4(讲义也特别提醒了这一点)
    // 加一个 MOD 再取模,保证结果非负
}

// 模乘法
long long mul(long long a, long long b) {
    return a * b % MOD;
    // ② a, b 都 < MOD ≈ 10^9,a*b 最大约 10^18
    // long long 最大约 9.2×10^18,刚好没溢出
}
  • 负数取模是最常见的坑。C++ 的 % 对负数的结果也是负数:(3)%7=3(-3) \% 7 = -3。但我们需要的是 [0,m)[0, m) 范围内的余数,即 (3)mod7=4(-3) \bmod 7 = 4(因为 3=1×7+4-3 = -1 \times 7 + 4)。所以加一个 MOD 再取模:(3+7)%7=4%7=4(-3 + 7) \% 7 = 4\%7 = 4。即使 a - b 不是负数,加 MOD 再取模也不影响结果(因为 (x + MOD) % MOD = x % MOD)。

  • 乘法溢出。如果 a,ba, b 都接近 10910^9,那 a×b1018a \times b \approx 10^{18}long long 最大 9.2×10189.2 \times 10^{18},所以刚好没溢出。但如果 MOD 更大(比如 101810^{18}),乘法就会溢出——那种情况需要 __int128 或特殊处理。

常见陷阱总结

陷阱错误写法正确写法原因
负数取模(a-b) % MOD(a-b%MOD+MOD) % MODC++ 负数取模得负数
乘法溢出int a * blong long a * bint 只到 2×1092 \times 10^9
忘记取模ans += valans = (ans + val) % MOD不取模就溢出
先乘后除a * b / ca / c * b(整除时)乘完可能溢出

为什么模数总是 109+710^9+7998244353998244353

计数题末尾那句话——“答案对 MM 取模”——里面的 MM 几乎总是下面两个数之一:

模数特点
109+710^9+71000000007素数。约 10910^9,两个模内数之积不超 long long。M&A 演習問題集统一用这个。
998244353998244353998244353素数=119×223+1= 119 \times 2^{23} + 1,支持 NTT(数论变换),AtCoder ABC/ARC 中极其常见

它们都是素数,所以每个非零元素都有乘法逆元(下一篇讲)。约 10910^9 的大小也刚刚好——足够大避免答案冲突(不同答案对应不同余数),又足够小使得两个模内数之积不超出 long long109×1091018<9.2×101810^9 \times 10^9 \approx 10^{18} < 9.2 \times 10^{18})。

实际做题时一定要看清楚题目给的是哪个模数! 不要想当然地写 const int MOD = 1000000007;——AtCoder ABC 中 998244353998244353 出现频率同样极高(例如 abc456_d Not Adjacent 2)。

本系列例题主要来自 M&A 演習問題集,统一使用 109+710^9+7,所以后续代码中 MOD = 1000000007。但理论完全适用于 998244353998244353 或任何素数模数。

例题

例题 1:M&A 005 — Modulo 100(★1)

题目:给定 NN 个整数 A1,A2,,ANA_1, A_2, \ldots, A_N,输出它们的和对 100 取模的结果。

数据范围1N10001 \le N \le 10001Ai1051 \le A_i \le 10^5

—— AtCoder M&A 005

思路:累加后取模。但更安全的做法是每加一个数就取模——防止 int 溢出。

代码

#include <cstdio>
using namespace std;

int main() {
    int N;
    scanf("%d", &N);
    int sum = 0;
    for (int i = 0; i < N; i++) {
        int a;
        scanf("%d", &a);
        sum += a;
        sum %= 100;    // ① 每加一次就取模,防止溢出
    }
    printf("%d\n", sum);
}

逐行解析

  • ① 如果不取模,N=1000N = 1000 个数每个 10510^5,总和 108<2×10910^8 < 2 \times 10^9int 范围),这里不会溢出。但养成好习惯:每步取模。

例题 2:M&A 049 — Fibonacci Easy (mod 10⁹+7)(★1)

题目:斐波那契数列 a1=1,a2=1,an=an1+an2a_1 = 1, a_2 = 1, a_n = a_{n-1} + a_{n-2}。求 aNmod(109+7)a_N \bmod (10^9+7)

数据范围3N1073 \le N \le 10^7

—— AtCoder M&A 049

思路:递推即可。每步取模 109+710^9+7N=107N = 10^7O(N)O(N) 没问题。

代码

#include <cstdio>
using namespace std;

const int MOD = 1000000007;

int main() {
    int N;
    scanf("%d", &N);

    int a = 1, b = 1;  // a_1, a_2
    for (int i = 3; i <= N; i++) {
        int c = (a + b) % MOD;  // ① a_n = (a_{n-1} + a_{n-2}) % MOD
        a = b;                   // ② 滚动更新
        b = c;
    }
    printf("%d\n", b);
}

逐行解析

  • ① 取模的位置:在加法之后立刻取模。(a+b)<2×109(a + b) < 2 \times 10^9,不超过 int 范围,所以不需要 long long
  • ② 只保留最近两项,空间 O(1)O(1)

例题 3:M&A 050 — Power(★1)

题目:给定 aabb,求 abmod(109+7)a^b \bmod (10^9+7)

数据范围1a1001 \le a \le 1001b1091 \le b \le 10^9

—— AtCoder M&A 050

思路bb 最大 10910^9,不能循环 bb 次。快速幂 O(logb)O(\log b)——这是同余+模运算章节的核心技巧。

代码

#include <cstdio>
using namespace std;

const long long MOD = 1000000007;

int main() {
    long long a, b;
    scanf("%lld%lld", &a, &b);

    long long base = a, ans = 1;
    while (b > 0) {
        if (b % 2 == 1)                // ① 当前二进制位是 1
            ans = ans * base % MOD;    // ② 乘到结果上
        base = base * base % MOD;      // ③ 底数平方
        b /= 2;
    }
    printf("%lld\n", ans);
}

逐行解析

  • ①②③ 标准快速幂。把 bb 写成二进制,逐位处理。每次 base 自乘(平方),如果当前位是 1 就乘到 ans 上。全部取模。

例题 4:M&A 053 — Sum of 4^N(★2)

题目:给定正整数 NN,求 40+41++4Nmod(109+7)4^0 + 4^1 + \cdots + 4^N \bmod (10^9+7)

数据范围1N10181 \le N \le 10^{18}

—— AtCoder M&A 053

思路:等比数列求和公式:i=0N4i=4N+1141=4N+113\sum_{i=0}^{N} 4^i = \frac{4^{N+1} - 1}{4 - 1} = \frac{4^{N+1} - 1}{3}

在模 pp 下除以 3 等价于乘以 3 的逆元。所以答案 = (4N+11)×31modp(4^{N+1} - 1) \times 3^{-1} \bmod p

4N+14^{N+1} 用快速幂,31modp3^{-1} \bmod p 用费马小定理(因为 pp 是素数):31=3p2modp3^{-1} = 3^{p-2} \bmod p

代码

#include <cstdio>
using namespace std;

const long long MOD = 1000000007;

long long power(long long base, long long exp) {
    long long ans = 1;
    while (exp > 0) {
        if (exp % 2 == 1) ans = ans * base % MOD;
        base = base * base % MOD;
        exp /= 2;
    }
    return ans;
}

int main() {
    long long N;
    scanf("%lld", &N);

    long long num = (power(4, N + 1) - 1 + MOD) % MOD;  // ① 分子 = 4^{N+1} - 1
    long long inv3 = power(3, MOD - 2);                  // ② 3 的逆元
    printf("%lld\n", num * inv3 % MOD);                   // ③ 分子 × 逆元
}

逐行解析

  • power(4, N+1) - 1:可能变成负数(模运算后),所以 + MOD% MOD 保证非负。
  • ② 费马小定理:313p2(modp)3^{-1} \equiv 3^{p-2} \pmod{p}
  • ③ 模意义下的除法 → 乘以逆元。

例题 5:M&A 042 — Sum of Divisors(★2)

题目:设 f(X)f(X)XX 的正约数个数。求 K=1NK×f(K)\sum_{K=1}^{N} K \times f(K)

数据范围1N1071 \le N \le 10^7

—— AtCoder M&A 042

思路:如果对每个 KK 单独求 f(K)f(K),总时间 O(NN)3×1010O(N\sqrt{N}) \approx 3 \times 10^{10},太慢。换个角度:枚举约数 dd,看 dd 是多少个数的约数——答案 dd 的贡献是 d×N/dd \times \lfloor N/d \rfloor(因为 11NNdd 的倍数有 N/d\lfloor N/d \rfloor 个)。

所以 K=1NK×f(K)=d=1Nd×N/d\sum_{K=1}^{N} K \times f(K) = \sum_{d=1}^{N} d \times \lfloor N/d \rfloor

代码

#include <cstdio>
using namespace std;

int main() {
    int N;
    scanf("%d", &N);

    long long ans = 0;
    for (int d = 1; d <= N; d++) {
        ans += (long long)d * (N / d);  // ① d 的贡献 = d × (1~N中d的倍数个数)
    }
    printf("%lld\n", ans);
}

逐行解析

  • ① 对每个 ddK[1,N]K \in [1, N] 中有 N/d\lfloor N/d \rfloor 个数包含 dd 作为约数。dd 被每个这样的 KK 贡献了一次 K×dK \times d(不对,更准确地说:K=1NK×f(K)=d=1Nd×\sum_{K=1}^N K \times f(K) = \sum_{d=1}^N d \timesdd 是多少个 KK 的约数” =d=1Nd×N/d= \sum_{d=1}^N d \times \lfloor N/d \rfloor)。

验证N=4N = 4f(1)=1,f(2)=2,f(3)=2,f(4)=3f(1)=1, f(2)=2, f(3)=2, f(4)=3=1+4+6+12=23\sum = 1 + 4 + 6 + 12 = 23

用公式:1×4+2×2+3×1+4×1=4+4+3+4=15231 \times 4 + 2 \times 2 + 3 \times 1 + 4 \times 1 = 4 + 4 + 3 + 4 = 15 \ne 23

不对!公式应该是 d=1Nd×\sum_{d=1}^{N} d \times(以 dd 为约数的 KK 的个数)。但题目是 K×f(K)\sum K \times f(K),不是 d×f(d)\sum d \times f(d)

重新推导:K=1NK×f(K)=K=1NdKK=d=1NK=1dKNK\sum_{K=1}^N K \times f(K) = \sum_{K=1}^N \sum_{d|K} K = \sum_{d=1}^N \sum_{\substack{K=1 \\ d|K}}^N K

对固定的 ddKKd,2d,,N/ddd, 2d, \ldots, \lfloor N/d \rfloor \cdot d。所以 dKK=d(1+2++N/d)=dm(m+1)2\sum_{d|K} K = d(1 + 2 + \cdots + \lfloor N/d \rfloor) = d \cdot \frac{m(m+1)}{2},其中 m=N/dm = \lfloor N/d \rfloor

代码应该用这个公式。让我重写:

#include <cstdio>
using namespace std;

int main() {
    long long N;
    scanf("%lld", &N);

    long long ans = 0;
    for (long long d = 1; d <= N; d++) {
        long long m = N / d;    // ① d 的倍数有多少个
        ans += d * m * (m + 1) / 2;  // ② d × (1+2+...+m)
    }
    printf("%lld\n", ans);
}

验证:N=4N = 4

  • d=1d=1: m=4m=4, 1×4×5/2=101 \times 4 \times 5/2 = 10
  • d=2d=2: m=2m=2, 2×2×3/2=62 \times 2 \times 3/2 = 6
  • d=3d=3: m=1m=1, 3×1×2/2=33 \times 1 \times 2/2 = 3
  • d=4d=4: m=1m=1, 4×1×2/2=44 \times 1 \times 2/2 = 4
  • 总计 10+6+3+4=2310+6+3+4=23。✓

参考文献

理论讲义 — AtCoder Algorithm Lectures

教材讲解 — 競技プログラミングの鉄則 第 5 章

基础练习 — アルゴリズムと数学 演習問題集


系列索引

第零章 基础工具

第一章 搜索技术

第二章 数学基础

第三章 数据结构

第四章 图论

第五章 动态规划

第六章 贪心

第七章 字符串

第八章 进阶