第 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() 就是把题面的要求原样翻译一遍,最不容易写错。写对了的暴力有三个用处:
- 拿分:表里最前面的几档。
- 打表找规律:把 n = 1, 2, 3, … 的答案依次打出来。上面的程序取 k = 3,输出是 3, 6, 12, 24, 48——每次乘 2,于是猜出 3 × 2ⁿ⁻¹。再对照 7.6 节的两个数列和 2ⁿ、n!、斐波那契数、C(2n, n),认出来就直接写公式。
- 对拍:之后写的 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 分钟内写出“枚举所有方案再检查”的暴力,并用它打表
- 背下错排和卡特兰数的前五项