NOIP 算法笔记全部章节
本章目录

第 7 章 数学与计数

本章定位:2021~2025 这五届 NOIP,“求方案数、对一个质数取模”的题出现了 6 次(2021 T2 数列、2022 T1 种花、2022 T3 建造军营、2024 T2 遗失的赋值、2024 T3 树的遍历、2025 T2 清仓甩卖),2024 年一届就有两道。计数题的正解要靠想,但它前几档部分分很实在:n 很小,就把所有方案枚举出来数一数;再大一点,写一个 O(n²) 的 DP。前提是取模、快速幂、组合数这三样不出错——取模错一处,后面全错,而且小样例往往看不出来。7.1~7.7 是重点。

7.1 取模的规矩 【必会】

一句话:方案数大到存不下,题目让你输出它除以某个数的余数;过程中每一步都要取模,而且加减乘除各有各的写法。 识别信号:题面出现“对 10⁹+7 取模”“对 998244353 取模”。

const int MOD = 1000000007;                     // 写成常量,数值从题面复制
long long a, b, c;                              // 参与运算的量一律 long long,并且始终保持在 [0, MOD) 里

long long sum     = (a + b) % MOD;              // 加
long long diff    = (a - b + MOD) % MOD;        // 减:a - b 可能是负数,先加上 MOD
long long product = a * b % MOD;                // 乘:两个小于 MOD 的数相乘不到 1e18,long long 装得下
long long triple  = a * b % MOD * c % MOD;      // 连乘:每乘一次就模一次
long long ratio   = a * power(b, MOD - 2) % MOD;// 除:乘以 b 的逆元(7.5 节),不能写 a / b

易错点

  • 两个 int 相乘先溢出、后取模。 int a, b; 时 a * b % MOD 是错的,要写 1LL * a * b % MOD。最省事的办法是计数题里所有变量都用 long long。
  • 连乘不能攒着。 a * b * c % MOD 里,a * b 已经接近 10¹⁸,再乘 c 就超出 long long 了。
  • 减法会出负数。 C++ 里 -7 % 3 等于 -1,不是 2。只要有减法,就写 (x - y + MOD) % MOD。读进来的数可能是负的,就先 (x % MOD + MOD) % MOD。
  • 不能只在最后取一次模。 中间量早就溢出了,最后再模也是错的。
  • 取过模的数不能比大小。 题目要“最大值对 MOD 取模”时,必须用没取模的真实值去比较。取过模的数也不能拿去做除法、当下标。
  • 指数不能取模。 求 a^b mod p 时,a 可以先 % p,b 不行。
  • 看清模数是不是质数。 10⁹+7 和 998244353 都是质数,可以用 7.5 节的办法求逆元。如果模数是输入给的、题面没说是质数,就不能这样求逆元,组合数改用杨辉三角。

7.2 快速幂 【必会】

一句话:O(log b) 算出 a^b mod p。 识别信号:“每个位置有 k 种选法,共 n 个位置”→ kⁿ;2ⁿ;求逆元;n 大到 10⁹ 以上却要算一个幂。 复杂度:O(log b)。

// 快速幂:O(log b) 求 a^b mod p。洛谷 P1226。
// 把 b 写成二进制,a^b 就是若干个 a^(2^k) 相乘;a^(2^k) 靠不断平方得到。
#include <bits/stdc++.h>
using namespace std;

long long power(long long a, long long b, long long p) {
    long long result = 1 % p;  // 写 1 % p 而不是 1:p = 1 时答案应该是 0
    a %= p;                    // 先取模,保证下面两个数相乘不超过 p*p
    while (b > 0) {
        if (b & 1) result = result * a % p;  // b 的这一个二进制位是 1:把当前的 a 乘进答案
        a = a * a % p;                       // a 平方,对应 b 的下一个二进制位
        b >>= 1;
    }
    return result;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    long long a, b, p;
    cin >> a >> b >> p;
    cout << a << '^' << b << " mod " << p << '=' << power(a, b, p) << '\n';
    return 0;
}

例:3¹³,13 的二进制是 1101,所以 3¹³ = 3⁸ × 3⁴ × 3¹。循环里 a 依次变成 3¹、3²、3⁴、3⁸,遇到 b 的二进制位是 1 就乘进答案。模数固定的题,把参数 p 换成常量 MOD,写成两个参数的 power(a, b),本章其余地方都是这个写法。

易错点

  • result、a 必须是 long long,否则 a * a 溢出。
  • 进函数先 a %= p。a 本身可能比 p 大。
  • 指数 b 用 long long,题目里的指数经常超过 int。
  • b = 0 时答案是 1(p = 1 时是 0)。循环一次都不执行,返回初值,所以初值写 1 % p。

练一练:洛谷 P1226 【模板】快速幂;本系统 E08 camp(最后一步要算一个幂)。

7.3 最大公约数与最小公倍数 【必会】

一句话:gcd(a, b) = gcd(b, a % b),一直除到 b 变成 0。 识别信号:分数约分、通分;“周期”“同时到达”;“a 和 b 互质”。 复杂度:O(log min(a, b))。

// 最大公约数、最小公倍数、扩展欧几里得。
// 输入 T,然后 T 行 a b(都是正整数);每行输出 gcd(a,b)、lcm(a,b)、a 在模 b 意义下的逆元(不存在输出 -1)。
#include <bits/stdc++.h>
using namespace std;

long long gcd(long long a, long long b) {  // 辗转相除:gcd(a, b) = gcd(b, a % b),直到 b = 0
    return b == 0 ? a : gcd(b, a % b);
}

// 求一组整数 x、y 使 a*x + b*y = gcd(a, b),返回值是 gcd(a, b)
long long exgcd(long long a, long long b, long long &x, long long &y) {
    if (b == 0) {
        x = 1, y = 0;  // a*1 + 0*0 = a
        return a;
    }
    long long g = exgcd(b, a % b, y, x);  // 注意这里 x、y 交换了位置
    y -= a / b * x;
    return g;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int T;
    cin >> T;
    while (T--) {
        long long a, b, x, y;
        cin >> a >> b;
        long long g = exgcd(a, b, x, y);
        // a*x + b*y = 1 两边对 b 取模就是 a*x ≡ 1,所以 x 就是逆元;gcd 不是 1 时没有逆元
        long long inverse = g == 1 ? (x % b + b) % b : -1;          // x 可能是负数,调整到 [0, b)
        cout << gcd(a, b) << ' ' << a / g * b << ' ' << inverse << '\n';  // lcm 先除后乘,防止溢出
    }
    return 0;
}
  • gcd(12, 18) = 6,lcm(12, 18) = 12 ÷ 6 × 18 = 36。
  • exgcd 求出一组 x、y 使 a·x + b·y = gcd(a, b)。用处是模数不是质数时求逆元:gcd(a, b) = 1 时,x 就是 a 在模 b 下的逆元。例如 3 在模 10 下的逆元是 7(3 × 7 = 21)。这一条属于【了解】:知道“模数不是质数时,逆元要用 exgcd 求,而且不一定存在”就够了。

易错点

  • 最小公倍数先除后乘:a / g * b。写成 a * b / g,a、b 到 10⁹ 时乘积就接近 long long 的上限,再大就溢出。
  • NOIP 用的是 C++14,标准库里没有 gcd(C++17 才有)。__gcd(a, b) 是 GCC 自带的,考场上能用,但在这台 Mac 上编译不过。自己写这三行,两边都没问题。
  • 分数相加、通分时,分母会越乘越大。NOIP2020 T1 排水系统就是分数加法,不少人没想到分母会超过 long long 而丢分。看到分数,先估一下分母最大能到多少。

练一练:洛谷 P1029 最大公约数和最小公倍数问题;P1082 [NOIP2012] 同余方程(exgcd)。

7.4 质数与筛法 【必会】

一句话:判断一个数是不是质数,试除到 √n;要一批质数,用筛法。 识别信号:“质数”“约数”“倍数”“互质”;n 到 10⁶~10⁷ 且要对每个数做和约数、倍数有关的事。 复杂度:试除 O(√n);埃氏筛 O(n log log n);线性筛 O(n)。

判断一个数是不是质数,以及筛法的基本想法(埃氏筛),都只有几行:

bool is_prime(long long n) {
    if (n < 2) return false;                        // 0 和 1 不是质数
    for (long long d = 2; d * d <= n; d++)          // 只需试到根号 n
        if (n % d == 0) return false;
    return true;
}

// 埃氏筛:从小到大,遇到质数就把它的倍数全部划掉。O(n log log n),n 到 1e7 没问题
for (int i = 2; i <= n; i++) {
    if (is_composite[i]) continue;                  // 被划掉过,是合数
    for (long long j = 1LL * i * i; j <= n; j += i) is_composite[j] = true;
}

“枚举倍数”这个写法本身就很有用:for (i = 1..n) for (j = i; j <= n; j += i) 总共只执行约 n·ln n 次(n = 10⁶ 时约 1.4×10⁷ 次),不是 n²。凡是“对每个数,处理它所有的倍数”都可以这么写。NOIP2021 T1 报数的正解用的就是这个写法。

线性筛更快,每个合数只被划一次:

// 线性筛(欧拉筛):O(n) 求出 n 以内的全部质数。洛谷 P3383:q 次询问第 k 小的质数。
// 每个合数只被它最小的质因子筛掉一次,所以是线性的。
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int n, q;
    cin >> n >> q;
    vector<bool> is_composite(n + 1, false);  // true 表示是合数;vector<bool> 每个元素只占 1 位,n = 1e8 也只要 12.5MB
    vector<int> primes;
    for (int i = 2; i <= n; i++) {
        if (!is_composite[i]) primes.push_back(i);  // 没被筛掉,是质数
        for (size_t j = 0; j < primes.size() && 1LL * i * primes[j] <= n; j++) {
            is_composite[i * primes[j]] = true;  // 筛掉 i * primes[j],它的最小质因子是 primes[j]
            if (i % primes[j] == 0) break;       // primes[j] 已经是 i 的最小质因子,再往后筛就不是“最小质因子”了
        }
    }
    while (q--) {
        int k;
        cin >> k;
        cout << primes[k - 1] << '\n';  // primes 从下标 0 开始存
    }
    return 0;
}

把一个数分解质因数,也是试除到 √n:

// 质因数分解:从 2 开始试除,只需试到 sqrt(n)。
// 输入 T,然后 T 行每行一个 n(n >= 2);每行输出“质因子 指数 质因子 指数 …”,质因子从小到大。
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int T;
    cin >> T;
    while (T--) {
        long long n;
        cin >> n;
        for (long long d = 2; d * d <= n; d++) {  // d 用 long long:d*d 会超过 int
            if (n % d != 0) continue;
            int exponent = 0;
            while (n % d == 0) {  // 把 d 除干净,所以之后能整除 n 的 d 一定是质数
                n /= d;
                exponent++;
            }
            cout << d << ' ' << exponent << ' ';
        }
        if (n > 1) cout << n << " 1";  // 剩下的 n 是一个大于 sqrt 的质因子,最多只有一个
        cout << '\n';
    }
    return 0;
}

易错点

  • d * d <= n 里 d 要用 long long。n 到 10¹² 时 d 到 10⁶,d * d 超过 int。
  • 分解质因数的循环结束后,如果 n 还大于 1,剩下的那个 n 是一个大质因子,别漏了。例如 14 = 2 × 7,循环在 d = 3 时就结束了,7 是靠最后那句输出的。
  • 1 不是质数,也没有质因子。
  • 线性筛里 if (i % primes[j] == 0) break; 要写在“划掉”那一句的后面。写在前面会漏掉 4、8、9 这样的数。
  • 筛的数组开到 n + 1。n = 10⁸ 时用 bool 数组要 100 MB,vector<bool> 或 bitset 只要 12.5 MB。

练一练:洛谷 P3383 【模板】线性筛素数;P1075 质因数分解;P7960 [NOIP2021] 报数。

7.5 逆元与组合数 【必会】

一句话:取模的世界里不能直接除,“除以 a”要换成“乘以 a 的逆元”;有了阶乘和阶乘的逆元,组合数 C(n, m) 就能 O(1) 算出来。 识别信号:“从 n 个里选 m 个”“走格子的路径数”“把 n 个东西分成几组”,而且 n 到 10⁵ 以上、要取模。 复杂度:预处理 O(n),每次查询 O(1)。

逆元:a 的逆元 inv(a) 是满足 a × inv(a) % p == 1 的那个数。模数 p 是质数时,inv(a) = a^(p-2) % p(费马小定理),用快速幂算。例如 p = 7:3 的逆元是 3⁵ % 7 = 243 % 7 = 5,验证 3 × 5 = 15,15 % 7 = 1。于是“除以 3”就是“乘以 5”。

组合数:C(n, m) = n! ÷ (m! × (n−m)!),在取模的世界里就是 fact[n] × inv_fact[m] × inv_fact[n-m]。

// 组合数取模:预处理阶乘和阶乘的逆元,之后 O(1) 回答 C(n, m) mod 1e9+7。
// 输入 T,然后 T 行 n m;每行输出 C(n, m)。m < 0 或 m > n 时答案是 0。
#include <bits/stdc++.h>
using namespace std;

const int MOD = 1000000007;              // 必须是质数,才能用费马小定理求逆元
const int N = 1000000;                   // 式子里会出现的最大的 n
long long fact[N + 1], inv_fact[N + 1];  // fact[i] = i!,inv_fact[i] = i! 的逆元

long long power(long long a, long long b) {
    long long result = 1;
    a %= MOD;
    while (b > 0) {
        if (b & 1) result = result * a % MOD;
        a = a * a % MOD;
        b >>= 1;
    }
    return result;
}

void init() {
    fact[0] = 1;
    for (int i = 1; i <= N; i++) fact[i] = fact[i - 1] * i % MOD;
    inv_fact[N] = power(fact[N], MOD - 2);  // 费马小定理:a 的逆元是 a^(MOD-2)。只做这一次快速幂
    for (int i = N; i >= 1; i--) inv_fact[i - 1] = inv_fact[i] * i % MOD;  // 1/(i-1)! = 1/i! * i
}

long long C(int n, int m) {
    if (m < 0 || m > n) return 0;  // 不合法的先挡掉,否则下标越界
    return fact[n] * inv_fact[m] % MOD * inv_fact[n - m] % MOD;  // n! / (m! * (n-m)!)
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    init();  // 别忘了调用
    int T;
    cin >> T;
    while (T--) {
        int n, m;
        cin >> n >> m;
        cout << C(n, m) << '\n';
    }
    return 0;
}

排列数同理:A(n, m) = n! ÷ (n−m)! = fact[n] * inv_fact[n - m] % MOD。只求一个数的逆元,直接 power(a, MOD - 2)。

n 不大(几千以内),或者模数不是质数时,用杨辉三角递推,只有加法,不需要逆元:

// 杨辉三角递推组合数:C(n, m) = C(n-1, m-1) + C(n-1, m)。只用加法,模数不是质数也能用。
// 输入第一行 q p(1 <= p <= 1e9+7),然后 q 行 n m(0 <= n, m <= 2000);每行输出 C(n, m) mod p。
#include <bits/stdc++.h>
using namespace std;

const int N = 2000;
int c[N + 1][N + 1];  // (N+1)^2 个 int:N = 2000 约 16MB;N = 5000 约 100MB,开之前先算

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int q, p;
    cin >> q >> p;
    for (int i = 0; i <= N; i++) {
        c[i][0] = 1 % p;  // 从 i 个里一个都不选:1 种
        for (int j = 1; j <= i; j++)
            c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % p;  // 第 i 个选 + 第 i 个不选;两个小于 p 的数相加不超过 int
    }
    while (q--) {
        int n, m;
        cin >> n >> m;
        cout << c[n][m] << '\n';  // m > n 的格子从没被赋值,全局数组默认是 0,正好是正确答案
    }
    return 0;
}

不取模的组合数涨得很快:C(60, 30) ≈ 1.2×10¹⁷,C(67, 33) 就超出 long long 了。阶乘也一样,20! ≈ 2.4×10¹⁸ 是 long long 能存下的最大的阶乘。所以题目不取模又要用组合数时,n 最多只能到 60 出头。

易错点

  • 阶乘数组要开到式子里出现的最大的 n。 它常常不是题目里的 n:走网格的路径数是 C(n+m−2, n−1),数组得开到 n + m(练习 E07)。开小了是越界,结果是 RE 或者一个莫名其妙的数。
  • C(n, m) 在 m < 0 或 m > n 时必须返回 0。推式子时这种情况经常出现,不挡住就是数组下标为负。
  • init() 忘了调用,所有组合数都是 0。
  • fact[n] * inv_fact[m] % MOD * inv_fact[n - m] % MOD:中间那个 % MOD 不能省,省了就是三个数连乘溢出。
  • 这套办法要求模数是质数,而且 n 小于模数。两个条件 NOIP 里通常都满足,但动手前看一眼题面。

练一练:洛谷 P3811 【模板】模意义下的乘法逆元;P2822 [NOIP2016] 组合数问题(杨辉三角 + 二维前缀和);本系统 E07 grid。

7.6 计数的基本方法 【必会】

计数题没有固定的模板,但常用的想法就下面几种。读题时逐条对一遍,看哪条用得上。

加法原理(分类):把所有方案分成互不重叠的几类,总数是各类之和。 例:从家到学校,公交有 3 条线路,地铁有 2 条,一共 3 + 2 = 5 种走法。 计数 DP 的转移就是加法原理:按“最后一步怎么走”分类,每一类的方案数加起来。分类必须不重、不漏。

乘法原理(分步):一件事分几步做,总数是每一步选法数的乘积。前提是每一步有几种选法,不受前面具体选了什么的影响。 例:n 个格子排成一排,每格涂 k 种颜色之一,共 kⁿ 种;要求相邻两格颜色不同,第一格 k 种,之后每格 k−1 种,共 k × (k−1)ⁿ⁻¹ 种。n = 3、k = 3 时是 12。

补集转化:“至少有一个……”不好数时,改数“一个都没有”,再用总数去减。 例:上面的涂色问题,至少有一对相邻格子同色的方案数 = 全部 − 相邻都不同 = kⁿ − k × (k−1)ⁿ⁻¹。n = 3、k = 3 时是 27 − 12 = 15。 看到“至少”“存在”“不全相同”就先想补集。

容斥:数“满足 A 或 B”时,A 的个数加 B 的个数,会把同时满足的算两遍,要减掉一遍。 例:1~100 里能被 2 或 3 整除的数:50 + 33 − 16 = 67(16 是能被 6 整除的个数)。 三个条件:单个的加,两两同时的减,三个同时的再加回来。1~100 里能被 2、3、5 之一整除的:50 + 33 + 20 − 16 − 10 − 6 + 3 = 74。

贡献法:题目问“所有方案的某个量加起来是多少”时,不去枚举方案,改成枚举每个元素,数它被算了多少次。 例:数组 1 2 3 的所有连续子区间的元素和,加起来是多少?a[i] 出现在 i × (n−i+1) 个子区间里(左端点有 i 种选法,右端点有 n−i+1 种),所以答案是 1×1×3 + 2×2×2 + 3×3×1 = 20。直接枚举验证:1 + 2 + 3 + 3 + 5 + 6 = 20。 “所有子区间的最大值之和”也是这个思路:对每个 a[i],数有多少个区间以它为最大值(5.3 节的单调栈,练习 E12)。 看到“求所有……的……之和”,而 n 大到不可能枚举,就想贡献法。

隔板法:把 n 个一样的球分给 k 个不同的盒子。每盒至少一个:在 n−1 个空隙里插 k−1 块板,C(n−1, k−1) 种。允许空盒:先借给每盒一个球,变成 n+k 个球每盒至少一个,C(n+k−1, k−1) 种。 例:x₁ + x₂ + x₃ = 10 的正整数解有 C(9, 2) = 36 组,非负整数解有 C(12, 2) = 66 组。

两个要认得的数列(暴力打表时看到它们,就知道公式了):

名字 前几项 递推 出现在哪
错排 D(n) D(1)..D(6) = 0, 1, 2, 9, 44, 265 D(n) = (n−1) × (D(n−1) + D(n−2)) n 个人每人都没拿到自己的东西
卡特兰数 第 0~6 项 = 1, 1, 2, 5, 14, 42, 132 第 n 项 = C(2n, n) − C(2n, n−1) n 对括号的合法序列数;n 个数的出栈顺序数;不越过对角线的走格子

练一练:洛谷 P1044 [NOIP2003] 栈(卡特兰数);P8865 [NOIP2022] 种花(前缀和 + 乘法原理);本系统 E07 grid、E12 watch。

7.7 计数题的部分分 【必会】

一句话:想不出式子也有分——n 小就真的把每一种方案都枚举出来,数一数有多少种合法。 识别信号:数据范围表的前两三行写着 n ≤ 8、n ≤ 10、n ≤ 20。

// 计数题的暴力:DFS 枚举所有方案,逐个检查是否合法,数一数。
// 例题:长度为 n 的序列,每个位置填 1..k,相邻两个数不能相等,有多少种填法?(n <= 10,k <= 5)
// 正解是公式 k * (k-1)^(n-1)。暴力的用处:拿小数据的分、打表找规律、给正解对拍。
#include <bits/stdc++.h>
using namespace std;

int n, k, seq[15];
long long total;

bool valid() {  // 检查一个填好的方案是否合法:照着题面的要求逐条写
    for (int i = 2; i <= n; i++)
        if (seq[i] == seq[i - 1]) return false;
    return true;
}

void dfs(int pos) {  // 正在决定第 pos 个位置填什么
    if (pos > n) {   // 全部填完:检查,合法就计数
        if (valid()) total++;
        return;
    }
    for (int value = 1; value <= k; value++) {
        seq[pos] = value;
        dfs(pos + 1);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> k;
    dfs(1);
    cout << total << '\n';
    return 0;
}

这个暴力故意写成“先把整个方案填完,再检查”。它慢,但 valid() 就是把题面的要求原样翻译一遍,最不容易写错。写对了的暴力有三个用处:

  1. 拿分:表里最前面的几档。
  2. 打表找规律:把 n = 1, 2, 3, … 的答案依次打出来。上面的程序取 k = 3,输出是 3, 6, 12, 24, 48——每次乘 2,于是猜出 3 × 2ⁿ⁻¹。再对照 7.6 节的两个数列和 2ⁿ、n!、斐波那契数、C(2n, n),认出来就直接写公式。
  3. 对拍:之后写的 DP 或公式,拿小数据和暴力比。计数题的样例通常很小,取模又让错误的答案看起来“像那么回事”,不对拍很难发现错。

其他几档怎么拿

  • n ≤ 几百、几千:写计数 DP(3.7 节),不追求优化。状态里多记一维、转移多一层循环都可以,O(n²)、O(n³) 能过就行。
  • 特殊性质档(9.3 节):把性质代进去,答案常常就是一个幂或者一个组合数。“所有 a[i] 都相等”“树是一条链”“没有任何限制(m = 0)”这几种最常见。
  • 暴力也要取模。 方案数很快就超过 long long,题目让取模,暴力里的累加也得 % MOD。

练一练:本系统 E07 grid(朴素 DP、特殊性质、正解三种档位都有)。模拟考二之后订正 NOIP2024 T2 遗失的赋值时,再回来对照 7.6 和这一节;那道题的提示写在 realp/模拟卷做法要点.md 里,考完再看。

7.8 期望的线性性 【了解】

一句话:“和的期望”等于“期望的和”,而且不要求各部分互相独立。 例:掷两个骰子,点数和的期望是 3.5 + 3.5 = 7。n 个人随机拿 n 顶帽子,“拿到自己帽子的人数”的期望:每个人拿对的概率是 1/n,n 个人加起来正好是 1。

NOIP2016 换教室是期望 DP。期望题的输出有两种:一种是小数,用 double 算,输出写 cout << fixed << setprecision(2) << ans(保留两位小数);另一种是“对质数取模”,这时分数 a/b 要写成 a × inv(b)(7.5 节)。

7.9 矩阵快速幂 【了解】

一句话:递推式固定、要算的项数大到 10¹⁸ 时,把“递推一步”写成乘一个矩阵,再对矩阵做快速幂。 识别信号:n ≤ 10¹⁸,而 n 小的时候是一个简单的递推(比如斐波那契)。 复杂度:O(k³ log n),k 是矩阵的边长。 例:斐波那契数列 F(1) = F(2) = 1。矩阵 [[1, 1], [1, 0]] 的 n 次方,右上角那个数就是 F(n)。

// 矩阵快速幂:求 n×n 矩阵 A 的 k 次方,每个元素对 1e9+7 取模。洛谷 P3390。
// 和整数快速幂逐行对应:乘法换成矩阵乘法,1 换成单位矩阵。
#include <bits/stdc++.h>
using namespace std;

const int MOD = 1000000007, N = 105;
int n;

struct Matrix {
    long long v[N][N];
    Matrix() { memset(v, 0, sizeof v); }  // 新建的矩阵全是 0
};

Matrix multiply(const Matrix &x, const Matrix &y) {
    Matrix z;
    for (int i = 1; i <= n; i++)
        for (int k = 1; k <= n; k++)
            for (int j = 1; j <= n; j++)
                z.v[i][j] = (z.v[i][j] + x.v[i][k] * y.v[k][j]) % MOD;  // 第 i 行乘第 j 列
    return z;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    long long k;
    cin >> n >> k;
    Matrix a, result;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++) {
            cin >> a.v[i][j];
            a.v[i][j] = (a.v[i][j] % MOD + MOD) % MOD;  // 输入可能是负数,先变成非负
        }
    for (int i = 1; i <= n; i++) result.v[i][i] = 1;  // 单位矩阵:相当于整数里的 1
    while (k > 0) {
        if (k & 1) result = multiply(result, a);
        a = multiply(a, a);
        k >>= 1;
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) cout << result.v[i][j] << ' ';
        cout << '\n';
    }
    return 0;
}

练一练:洛谷 P3390 【模板】矩阵快速幂;P1962 斐波那契数列。

7.10 本章易错点汇总 【必会】

计数题的错大多不是想错,而是下面这几样。出了问题先对着表查一遍。

现象 多半是 改法
小样例对,大样例错 两个 int 相乘溢出;或者三个数连乘 变量全用 long long;每乘一次就 % MOD
输出了负数 减法之后没加 MOD (a - b + MOD) % MOD
输出的数比 MOD 还大 某一步忘了取模 加法、乘法之后都跟一个 % MOD
涉及除法的答案不对 直接写了 / 乘以逆元 power(b, MOD - 2)
RE,或者组合数全是 0 阶乘数组开小了;init() 没调用;C(n, m) 的 m 越界 数组开到式子里最大的 n;C 开头挡住 m < 0 和 m > n
样例都对,评测全错 模数抄错了(10⁹+7 和 998244353 弄混) 模数从题面复制,全程只用常量 MOD
暴力和正解在 n 较大时对不上 暴力没取模,自己溢出了 暴力也取模

本章必会清单

  • 加、减、乘、除四种运算的取模写法,各默写一遍
  • 默写快速幂,交到洛谷 P1226 通过
  • 默写 gcd;说出 lcm 为什么要先除后乘
  • 默写“阶乘 + 阶乘逆元 + C(n, m)”这一套,包括 m 不合法时返回 0
  • 说出什么时候用杨辉三角而不是阶乘逆元
  • 判断一个数是不是质数、分解质因数,各写一遍(注意 d * d <= n 和最后剩下的大质因子)
  • 看到“至少”能想到补集,看到“所有……之和”能想到贡献法
  • 给一道计数题,20 分钟内写出“枚举所有方案再检查”的暴力,并用它打表
  • 背下错排和卡特兰数的前五项