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

一、同余:模运算的根基

1.1 定义

给定正整数 mm,若 m(ab)m \mid (a - b)(即 aba - bmm 的倍数),则称 aabbmm 同余,记作:

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

这意味着 aabb 除以 mm 的余数相同。等价地说,存在整数 kk 使得 a=b+kma = b + km

一个直觉:同余就是把整数”卷绕”到 0,1,,m10, 1, \ldots, m-1mm 个值上。想象一个标了 00m1m-1 的时钟,每个整数映射到它除以 mm 的余数——这就是模运算的几何图像。

1.2 同余是等价关系

同余关系 (modm)\equiv \pmod{m} 满足等价关系的三条性质:

性质内容为什么
自反性aa(modm)a \equiv a \pmod{m}m0m \mid 0,显然
对称性abbaa \equiv b \Rightarrow b \equiv am(ab)m(ba)m \mid (a-b) \Rightarrow m \mid (b-a)
传递性ab, bcaca \equiv b,\ b \equiv c \Rightarrow a \equiv cm(ab)m \mid (a-b)m(bc)m(ac)m \mid (b-c) \Rightarrow m \mid (a-c)

因为它是等价关系,整数被划分为 mm等价类(也叫剩余类):

r={r+km:kZ},r=0,1,,m1\overline{r} = \{r + km : k \in \mathbb{Z}\}, \quad r = 0, 1, \ldots, m-1

所有等价类的集合记作 Z/mZ\mathbb{Z}/m\mathbb{Z}(或 Zm\mathbb{Z}_m),共 mm 个元素。

二、模运算的代数性质

2.1 运算的封闭性

aa(modm)a \equiv a' \pmod{m}bb(modm)b \equiv b' \pmod{m},则:

a+ba+b(modm)a + b \equiv a' + b' \pmod{m} abab(modm)a - b \equiv a' - b' \pmod{m} abab(modm)a \cdot b \equiv a' \cdot b' \pmod{m}

这意味着加法、减法、乘法在模意义下是良定义的——不依赖等价类中代表元的选取。

但除法不行25(mod3)2 \equiv 5 \pmod{3},但 4/2=24/2 = 24/54/5 甚至不是整数。模意义下的”除法”需要特殊处理——这就是逆元的由来。

2.2 模的分配律

实际计算中,为避免溢出,随时可以取模:

(a+b)modm=((amodm)+(bmodm))modm(a + b) \bmod m = ((a \bmod m) + (b \bmod m)) \bmod m (ab)modm=((amodm)(bmodm))modm(a \cdot b) \bmod m = ((a \bmod m) \cdot (b \bmod m)) \bmod m

减法需要小心(ab)modm(a - b) \bmod m 在 C++ 中可能得负数,应写成:

((a - b) % m + m) % m

2.3 Z/pZ\mathbb{Z}/p\mathbb{Z} 是一个域

m=pm = p素数时,Z/pZ\mathbb{Z}/p\mathbb{Z} 不仅是环,还是——这意味着每个非零元素都有乘法逆元。这是素数模在竞赛中如此常见(109+710^9+7998244353998244353)的根本原因。

三、模逆元 — 模意义下的”除法”

3.1 定义

若存在整数 xx 使得 ax1(modm)ax \equiv 1 \pmod{m},则称 xxaamm乘法逆元,记作 a1a^{-1}a1a^{*-1}

存在条件a1a^{-1} 存在     \iff gcd(a,m)=1\gcd(a, m) = 1

m=pm = p 为素数且 a≢0a \not\equiv 0 时,逆元一定存在。

3.2 费马小定理

pp 为素数且 gcd(a,p)=1\gcd(a, p) = 1,则 ap11(modp)a^{p-1} \equiv 1 \pmod{p}

等式两边乘 a1a^{-1}

ap2a1(modp)a^{p-2} \equiv a^{-1} \pmod{p}

所以模素数 pp 下,aa 的逆元就是 ap2(modp)a^{p-2} \pmod{p}——直接用快速幂算。

long long inv(long long a, long long p) {
    return qpow(a, p - 2, p);  // 快速幂
}

3.3 为什么逆元 = 模意义下的除法?

因为 b/aba1(modm)b / a \equiv b \cdot a^{-1} \pmod{m}

验证:ba1ab1b(modm)b \cdot a^{-1} \cdot a \equiv b \cdot 1 \equiv b \pmod{m}。乘上去、再除回来,确实还原了。

四、模意义下的组合计数

4.1 阶乘与逆阶乘

要算大量的 (nk)\binom{n}{k},预处理阶乘和逆阶乘:

(nk)=n!k!(nk)!n!(k!)1((nk)!)1(modp)\binom{n}{k} = \frac{n!}{k! \cdot (n-k)!} \equiv n! \cdot (k!)^{-1} \cdot ((n-k)!)^{-1} \pmod{p}

预处理方式:

const int MOD = 998244353;
const int MAXN = 3000006;

long long fact[MAXN], inv_fact[MAXN];

void init(int n) {
    fact[0] = 1;
    for (int i = 1; i <= n; i++)
        fact[i] = fact[i - 1] * i % MOD;

    // 只需一次快速幂算 (n!)^{-1},然后倒推
    inv_fact[n] = qpow(fact[n], MOD - 2, MOD);
    for (int i = n - 1; i >= 0; i--)
        inv_fact[i] = inv_fact[i + 1] * (i + 1) % MOD;
}

long long C(int n, int k) {
    if (k < 0 || k > n) return 0;
    return fact[n] * inv_fact[k] % MOD * inv_fact[n - k] % MOD;
}

关键点:逆阶乘不需要逐个算快速幂,从 inv_fact[n]\text{inv\_fact}[n] 倒推只需 O(n)O(n),因为:

(k!)1=((k+1)!)1(k+1)(k!)^{-1} = ((k+1)!)^{-1} \cdot (k+1)

4.2 插一句:Vandermonde 恒等式

后面会用到的一个重要组合恒等式:

j(aj+r)(bj)=(a+bb+r)\sum_{j} \binom{a}{j+r}\binom{b}{j} = \binom{a+b}{b+r}

它是范德蒙德卷积 j(aj)(bnj)=(a+bn)\sum_j \binom{a}{j}\binom{b}{n-j} = \binom{a+b}{n} 的变形。本质上说的是:从 ABA \cup BA=a,B=b|A|=a, |B|=b)中选 b+rb+r 个元素,按”从 AA 中选多少”来分类求和,结果等于直接选。

五、实战:序列计数

问题:求长度为 X1+X2+X3X_1 + X_2 + X_3 的序列 AA 的个数,模 998244353998244353,满足:

  1. AA 含有 X1X_111X2X_222X3X_333
  2. 相邻元素之差的绝对值 1\le 1(即 ai+1ai1|a_{i+1} - a_i| \le 1

约束:1X1,X2,X31061 \le X_1, X_2, X_3 \le 10^6

5.1 约束的翻译

值只有 1,2,31, 2, 3ai+1ai1|a_{i+1} - a_i| \le 1 意味着禁止 131 \leftrightarrow 3 的直接跳转

所以问题的等价表述是:在由 1,2,31, 2, 3 组成的序列中,1133 永不相邻。

5.2 组合建模

核心操作:把 X2X_222 排成一排,它们自然地把序列分成 X2+1X_2 + 1 个”槽”:

槽 0 2 槽 1 2  2 槽 X2\underbrace{\Box}_{\text{槽 }0}\ 2\ \underbrace{\Box}_{\text{槽 }1}\ 2\ \cdots\ 2\ \underbrace{\Box}_{\text{槽 }X_2}

每个槽可以填入:

  • 若干个 11(每个 1\ge 1
  • 若干个 33(每个 1\ge 1
  • 什么都不填00 个元素)

但不能在一个槽里同时放 1133——因为槽内没有 22 来隔开它们,而 1133 相邻是禁止的。

为什么这样建模是对的? 因为 22 是唯一允许出现在 1133 之间的值。一旦固定了 22 的位置,所有非 22 的位置自然形成了被 22 分隔的连续段。在每个连续段内部,元素只能是纯 11 或纯 33,否则就出现了 11-33 相邻。

5.3 计数

ss 个槽分配给 11tt 个槽分配给 33s+tX2+1s + t \le X_2 + 1s1s \ge 1t1t \ge 1)。

四个独立的选择:

步骤做什么方案数
1X2+1X_2+1 个槽中选 ss 个放 11(X2+1s)\binom{X_2+1}{s}
2从剩余槽中选 tt 个放 33(X2+1st)\binom{X_2+1-s}{t}
3X1X_111 分到 ss 个槽(每槽 1\ge 1(X11s1)\binom{X_1-1}{s-1}
4X3X_333 分到 tt 个槽(每槽 1\ge 1(X31t1)\binom{X_3-1}{t-1}

步骤 3 和 4 用的是隔板法(Stars and Bars):将 nn 个相同物品分到 kk 个非空组,方案数为 (n1k1)\binom{n-1}{k-1}

所以:

Ans=s=1X1t=1X3(X2+1s)(X2+1st)(X11s1)(X31t1)\text{Ans} = \sum_{s=1}^{X_1}\sum_{t=1}^{X_3} \binom{X_2+1}{s}\binom{X_2+1-s}{t}\binom{X_1-1}{s-1}\binom{X_3-1}{t-1}

这是 O(X1X3)O(X_1 \cdot X_3) 的双重求和,X1,X3X_1, X_3 高达 10610^6——太慢了

5.4 用 Vandermonde 恒等式消一层求和

固定 ss,对 tt 求和:

t=1X3(X2+1st)(X31t1)\sum_{t=1}^{X_3}\binom{X_2+1-s}{t}\binom{X_3-1}{t-1}

j=t1j = t - 1

j=0X31(X2+1sj+1)(X31j)\sum_{j=0}^{X_3-1}\binom{X_2+1-s}{j+1}\binom{X_3-1}{j}

由 Vandermonde 恒等式的变形 j(aj+r)(bj)=(a+bb+r)\sum_j \binom{a}{j+r}\binom{b}{j} = \binom{a+b}{b+r},取 a=X2+1sa = X_2+1-sr=1r = 1b=X31b = X_3-1

=(X2+X3sX3)= \binom{X_2+X_3-s}{X_3}

一层求和消失了。 最终公式:

Ans=s=1min(X1,X2+1)(X2+1s)(X11s1)(X2+X3sX3)\boxed{\text{Ans} = \sum_{s=1}^{\min(X_1,\, X_2+1)} \binom{X_2+1}{s}\binom{X_1-1}{s-1}\binom{X_2+X_3-s}{X_3}}

O(min(X1,X2))O(\min(X_1, X_2)) 的单层求和,完全可行。

5.5 验证

X1=X3=1X_1 = X_3 = 1

  • 唯一的 s=1s = 1(X2+11)(00)(X21)=(X2+1)1X2\binom{X_2+1}{1}\binom{0}{0}\binom{X_2}{1} = (X_2+1) \cdot 1 \cdot X_2
  • 即答案为 X2(X2+1)X_2(X_2 + 1)

手动验证:N=X2+2N = X_2 + 2 个位置放一个 11、一个 33X2X_2221133 不相邻。总排列 (X2+2)(X2+1)(X_2+2)(X_2+1) 减去 11-33 相邻的 2(X2+1)2(X_2+1),等于 (X2+1)X2(X_2+1) \cdot X_2。✓

5.6 C++ 实现

#include <cstdio>
#include <algorithm>
using namespace std;

const int MOD = 998244353;
const int MAXN = 3000006;

long long fact[MAXN], inv_fact[MAXN];

long long qpow(long long a, long long b, long long mod) {
    long long res = 1;
    a %= mod;
    while (b) {
        if (b & 1) res = res * a % mod;
        a = a * a % mod;
        b >>= 1;
    }
    return res;
}

void init(int n) {
    fact[0] = 1;
    for (int i = 1; i <= n; i++)
        fact[i] = fact[i - 1] * i % MOD;
    inv_fact[n] = qpow(fact[n], MOD - 2, MOD);
    for (int i = n - 1; i >= 0; i--)
        inv_fact[i] = inv_fact[i + 1] * (i + 1) % MOD;
}

long long C(int n, int k) {
    if (k < 0 || k > n) return 0;
    return fact[n] * inv_fact[k] % MOD * inv_fact[n - k] % MOD;
}

int main() {
    int X1, X2, X3;
    scanf("%d%d%d", &X1, &X2, &X3);

    init(X1 + X2 + X3);

    long long ans = 0;
    int M = min(X1, X2 + 1);
    for (int s = 1; s <= M; s++) {
        long long term = C(X2 + 1, s) * C(X1 - 1, s - 1) % MOD;
        term = term * C(X2 + X3 - s, X3) % MOD;
        ans = (ans + term) % MOD;
    }

    printf("%lld\n", ans);
    return 0;
}