题单 · 习题解析

洛谷 P3807 【模板】卢卡斯定理 / Lucas 定理

★★★ 这道题就是[第 43 章第 6 步](/ch/43-combinatorics/)那句警告的**考场现场**:那套「阶乘 + 逆元」要求 **p > n**,而这里 p ≤ 10⁵、n+m 可以到 2×10⁵ ⇒ 模板搬过来**恒输出 0**(不崩、不超时、格式完全正常,只是一直在答 0);★★ Lucas 干的事就是**把前提修回来** —— 按 p 进制拆开,每一位都 < p ⇒ 每一层都退回「p > n」那个成立的情形(阶乘表也从 200 001 项缩到 10⁵ 项,10 组顶格 10.3 ms、余量 97 倍);★★★ 而这一页最值钱的是**验算走了一条完全无关的路**:**Kummer 定理**(C(n+m,n) ≡ 0 (mod p) ⟺ n 和 m 按 p 进制相加有进位)—— 10 086 组全枚举,「答案为 0」5474 组 ≡「有进位」5474 组,**0 组对不上**,而它同时**解释了「忘了返回 0」那个错法的抓获率**;★★★ 四档 × 三个错法**十二格「触发 ≡ 抓获」一个不差,而且三条都能证**;★★★ 最刺眼的一格:**顺手写的生成器让所有组共用一个 p** ⇒ 「阶乘表只预处理一次」是**精确的 0** —— 触发条件藏在**输入的结构**里,不在数值范围里;⚠ 而官方样例 `2 / 1 2 5 / 2 1 5` 四个待测版本**全放过**,四个原因都指向同一件事:样例既只有一位、又只有一个 p

原题:洛谷 P3807出自 第 43 章 组合数与递推 的题单题面本地存档:2026-09-03
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P3807,日期见页头。两边不一致时信原站。

题目背景

这是一道模板题。

题目描述

给定整数 n, m, p 的值,求出 C(n+m, n) mod p 的值。

输入数据保证 p 为质数

注:C 表示组合数。

输入格式

本题有多组数据。

第一行一个整数 T,表示数据组数。

对于每组数据:一行,三个整数 n, m, p

输出格式

对于每组数据,输出一行,一个整数,表示所求的值。

数据范围

对于 100% 的数据,1 ≤ n, m, p ≤ 10⁵1 ≤ T ≤ 10

时限 1 秒,内存 125 MB(128000 KB)。

输入输出样例

输入

2
1 2 5
2 1 5

输出

3
3

两组数据的 p 都是 5:C(3,1) = 3C(3,2) = 3

1★★★ 这道题就是本章第 6 步那句警告的考场现场

★★★ 「p > n」这个前提,在这道题上是不成立的

本章第 6 步那套「阶乘 + 逆元」的模板,警告框里写着一句话: 它要求 p > n。理由一行:n ≥ pn! 里含着因子 pn! ≡ 0 (mod p), 而 0 在模 p 下没有逆元

这道题把那个前提明明白白地拆了p ≤ 10⁵,而 n + m 可以到 2×10⁵

⇒ 那套模板搬过来,只要 p ≤ n+m恒输出 0 —— 不是「偶尔错」,是一直在答 0。 而它能跑、不崩、输出格式完全正常

★ 所以这道题不是「换个更快的写法」,是换一个在这里还成立的定理

p3807Fac.cpp✗ 第 ① 版:把本章第 6 步的模板原样搬来 —— p ≤ n+m 时恒输出 0
// ✗ 第 ② 版:把[本章第 6 步](/ch/43-combinatorics/)那套模板原样搬过来 —— 而它在这道题上**恒输出 0**
//
// ★★★ 这道题就是本章第 6 步那句警告的**考场现场**:
// 那套「阶乘 + 逆元」要求 **p > n**。而这里 p ≤ 10⁵、n+m 可以到 2×10⁵
// ⇒ 只要 **p ≤ n+m**,(n+m)! 里就含着因子 p ⇒ fac[n+m] ≡ 0 (mod p)
// ⇒ 答案 = 0 · 逆元 · 逆元 = **0**,而且是**恒等于 0**,不是「有时错」。
//
// ⚠ 更糟的是逆元那一步:fac[n] 也可能 ≡ 0,而 **0 在模 p 下没有逆元** ——
// qpow(0, p−2) 老老实实算出来是 0,程序一声不吭,答案却早就没有意义了。
//
// ⇒ 这一版**能跑、不崩、输出格式完全正常**,它只是**一直在答 0**。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
static ll P;
static ll fac[200005], inv[200005];
static ll qpow(ll a, ll b) {
ll res = 1 % P; a %= P;
while (b) { if (b & 1) res = res * a % P; a = a * a % P; b >>= 1; }
return res;
}
int main() {
int T;
if (scanf("%d", &T) != 1) return 0;
while (T--) {
ll n, m;
if (scanf("%lld %lld %lld", &n, &m, &P) != 3) return 0;
ll N = n + m;
fac[0] = 1 % P;
for (ll i = 1; i <= N; i++) fac[i] = fac[i - 1] * i % P;
inv[N] = qpow(fac[N], P - 2);
for (ll i = N; i >= 1; i--) inv[i - 1] = inv[i] * i % P;
printf("%lld\n", fac[N] * inv[n] % P * inv[m] % P);
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p3807Pascal.cpp参照物:杨辉三角(不问 p 是不是质数、不用逆元)—— 顶格 4×10¹⁰ 次加法,只能当参照物

2★ 关键一步:把 n 和 m 按 p 进制拆开

★★ Lucas 定理 —— 一行式子,而它干的事就是「把前提修回来」

C(n, m) ≡ C(n mod p, m mod p) · C(⌊n/p⌋, ⌊m/p⌋) (mod p)

把它反复用下去,就是「nm 写成 p 进制,逐位求组合数再乘起来」。

⇒ ★★ 关键不在式子本身,在于每一位都 < p: 那一层的两个下标都落回了 p > n 那个成立的情形 ⇒ 阶乘 + 逆元在那儿是对的。

★ 三笔顺带的账,全都变好了:

本章模板 ★ Lucas
阶乘表要开多长 n + m200 001 只到 p − 110⁵
递归多深 log_p(n+m)p = 2 时最深 18
顶格(10 组)本机 恒输出 0 10.3 ms ⇒ 余量 97 倍
p3807.cpp★ 正解:Lucas —— 每组重算阶乘表,逐位求组合数
// P3807 【模板】卢卡斯定理 —— 正解
//
// 要算 C(n+m, n) mod p,题面保证 p 是质数,而 **p ≤ 10⁵、n+m 可以到 2×10⁵**
// ⇒ [本章第 6 步](/ch/43-combinatorics/)那套「阶乘 + 逆元」的前提(**p > n**)**在这里不成立**:
// (n+m)! 里含着因子 p ⇒ 它 ≡ 0 (mod p) ⇒ 那一版会恒输出 0(见 p3807Fac.cpp)。
//
// ★ Lucas 定理:把 n 和 m 按 **p 进制**拆开,逐位求组合数再乘起来:
// C(n, m) ≡ C(n mod p, m mod p) · C(n / p, m / p) (mod p)
// 每一位都 < p ⇒ **每一位都退回到了「p > n」那个成立的情形**,可以用阶乘 + 逆元。
//
// ⚠ 三处容易漏:
// ① 每组数据的 p 不一样 ⇒ **阶乘表要重算**(见 p3807Once.cpp);
// ② 递归那一半不能少(见 p3807OneDigit.cpp);
// ③ 单位那一层里 m 可能比 n 大 ⇒ **必须返回 0**(见 p3807NoGuard.cpp)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
static ll P;
static ll fac[100005], inv[100005];
static ll qpow(ll a, ll b) {
ll res = 1 % P; a %= P;
while (b) { if (b & 1) res = res * a % P; a = a * a % P; b >>= 1; }
return res;
}
/** 只到 p−1 就够了:Lucas 每一层的两个下标都 < p */
static void prep() {
fac[0] = 1 % P;
for (ll i = 1; i < P; i++) fac[i] = fac[i - 1] * i % P;
inv[P - 1] = qpow(fac[P - 1], P - 2);
for (ll i = P - 1; i >= 1; i--) inv[i - 1] = inv[i] * i % P;
}
/** 单位组合数:这里 n、m 都 < p,前提成立 */
static ll C(ll n, ll m) {
if (m > n) return 0; // ★ ③ 这一行少不得
return fac[n] * inv[m] % P * inv[n - m] % P;
}
static ll lucas(ll n, ll m) {
if (m == 0) return 1 % P;
return C(n % P, m % P) * lucas(n / P, m / P) % P; // ★ ② 递归那一半
}
int main() {
int T;
if (scanf("%d", &T) != 1) return 0;
while (T--) {
ll n, m;
if (scanf("%lld %lld %lld", &n, &m, &P) != 3) return 0;
prep(); // ★ ① 每组都要重算
printf("%lld\n", lucas(n + m, n));
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3★★★ 验算走一条完全无关的路:Kummer 定理

★★★ 「答案什么时候是 0」有一个不用 Lucas 就能回答的判据

Kummer 定理C(n+m, n) 里因子 p 的次数,等于把 nmp 进制相加进位的次数

C(n+m, n) ≡ 0 (mod p) ⟺ 按 p 进制加 n + m 时有进位。

这条路和 Lucas 一行代码都不共享(它只做除法和比较,不碰阶乘、不碰逆元)。 拿它把正解验一遍:六个质数 × n, m ∈ [0, 40] 全枚举,共 10 086 组 ——

  • 正解算出来答案为 0 的:5474 组;
  • 按 Kummer 数出来有进位的:5474 组;
  • 两边对不上的:0 组(顺带杨辉三角和 Lucas 也是 0 组不一致)。

「验算要走一条和算法完全无关的路」 的又一次; ★ 而这一次它还有第二个用途 —— 下面那个「忘了返回 0」的错法, 它的抓获率恰好就是这个判据

4⚠ 三个错法 —— 而官方样例三个都挡不住,原因各不相同

p3807OneDigit.cpp✗ 错法一:Lucas 只记住了一半 —— 忘了递归那一乘
p3807Once.cpp✗ 错法二:阶乘表只预处理一次 —— 而每组数据的 p 都不一样
p3807NoGuard.cpp✗ 错法三:单位那一层忘了「m 比 n 大就返回 0」
★★★ 样例 `2 / 1 2 5 / 2 1 5` —— 四个待测版本全放过,而四个原因能一个个说清
版本 样例输出 为什么放过
正解 3 3
① 本章模板 3 3 ★ 样例的 p = 5 大于 n+m = 3 ⇒ 那个前提在样例上是成立
✗ 只算一位 3 3 同上:p > n+mp 进制下只有一位,漏掉的那一乘本来就是 1
✗ 阶乘表只算一次 3 3 ★★ 样例两组数据的 p 都是 5 ⇒ 「重不重算」根本问不出来
✗ 忘了返回 0 3 3 只有一位,而那一位上 m ≤ n ⇒ 那一行本来就用不上

⇒ ★★ 这不是「样例太弱」,是「这组样例在结构上问不出这个问题」这道题的四个坑全长在「多位」和「多个 p」上,而样例既只有一位、又只有一个 p

5★ 生成器:四个档,而顺手写的那一档有一整块盲区

p3807Gen.cpp(四档)生成器:顺手写的 / 主力档 / ⚠ p > n+m / 顶格
★★★ 四个档 × 三个错法,十二格「触发 ≡ 抓获」一个不差
档位(每档 300 轮) ① 模板恒答 0 ✗ 只算一位 ✗ 表只算一次 ✗ 忘了返回 0
0 顺手写的(n,m ≤ 20全组共用一个 p 89 ≡ 89 65 ≡ 65 精确的 0(触发 0) 112 ≡ 112
1 主力档(n,m ≤ 1500、每组一个 p 263 ≡ 263 264 ≡ 264 225(触发 259) 267 ≡ 267
⚠ 2 p > n+m 0 ≡ 0 0 ≡ 0 260 ≡ 260 0 ≡ 0
3 顶格(n,m ≤ 10⁵ 252 ≡ 252 199 ≡ 199 235(触发 260) 259 ≡ 259

★★★ 三条读得出来的结论:

  1. 那十二个「≡」不是运气,三条都能证 —— ① 模板版恒输出 0 ⇒ 它错 ⟺ 存在一组 p ≤ n+m 而且真答案不是 0 (⚠ 注意第二个条件:p ≤ n+m 有 120 轮,可其中 31 轮真答案本来就是 0, 两边一起打 0 ⇒ 验的是零); ③ 「忘了返回 0」错 ⟺ 真答案是 0 而它给了个非零数有进位(上一节那条 Kummer)。 ⇒ 「≡」是量出来的运气,不是规律 —— 而这一页四个「≡」全是能证的
  2. ★★★ 顺手写的那一档,把「阶乘表只算一次」打成了精确的 0 —— 而原因不在数据规模上,在一个谁都不会多想的写法: 「T 组数据嘛,多打几行就完了」⇒ 生成器给所有组用了同一个 p。 ⇒ 那个错法的触发条件是「各组的 p 不全一样」,在那一档是 0 / 300。 ⇒ 「生成器最自然的默认值往往正是某个 bug 的藏身处」 的又一次, ★ 而这一次它藏在输入的结构里,不在数值范围里。
  3. 档 2(p > n+m)是一个对照档同时给三个 0 做交代 —— 把题面那个「前提不成立」的场反过来造一遍,模板版、只算一位、忘了返回 0 同时变回正确。三个 0 各有一行不用跑程序的证明。 ⇒ 和第 33 章 P1266 同一个动作

6★ 哪一版就已经能过了

p3807Count.cpp本页所有数字的出处(含 Kummer 校验)
// P3807 的度量程序:./p3807Count csv (本页的数字都出自它)
//
// kummer: ★★★ 一条**和 Lucas 一行代码都不共享**的路 —— Kummer 定理:
// C(n+m, n) ≡ 0 (mod p) ⟺ n 和 m 按 **p 进制**相加时**有进位**。
// 拿它把正解验一遍(小范围全枚举),再用它说清楚 NoGuard 那个错法的抓获率。
// trig : ★ 四个错法各自的**触发条件**在各档里出现几轮。
// cost : ★ 三种写法的规模账(杨辉三角的格子数、两种阶乘表的长度、顶格秒表)。
//
// ⚠ 这里**复刻**了 p3807Gen.cpp 的档位逻辑(同一个 mt19937、同一个种子公式)。
// ⚠ 秒表用 steady_clock 在进程内量,每档 3 次取中位数
// ([第 12 章 P1923 那一跤](/sol/p1923/):秒表断言的主语是余量,不是量级)。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
typedef long long ll;
static double med3(double a, double b, double c) { return max(min(a, b), min(max(a, b), c)); }
static vector<int> primesUpTo(int n) {
vector<char> isc(n + 1, 0);
vector<int> ps;
for (int i = 2; i <= n; i++) {
if (!isc[i]) ps.push_back(i);
for (ll j = (ll)i * i; j <= n; j += i) isc[j] = 1;
}
return ps;
}
/** 复刻 p3807Gen.cpp:把一轮的 T 组数据吐出来 */
static void gen(unsigned seed, int mode, vector<array<ll, 3>>& out) {
mt19937 rng(seed * 1000003u + 20260903u);
int nmMax = mode == 0 ? 20 : (mode == 3 ? 100000 : 1500);
int pMax = mode == 0 ? 100 : (mode == 3 ? 100000 : 200);
vector<int> ps = primesUpTo(pMax);
int T = 1 + (int)(rng() % 10u);
unsigned pick = rng() % (unsigned)ps.size();
int shared = ps[pick];
out.clear();
for (int t = 0; t < T; t++) {
ll n, m, p;
if (mode == 2) {
unsigned lo = (unsigned)(ps.size() / 2);
unsigned q = lo + rng() % (unsigned)(ps.size() - lo);
p = ps[q];
unsigned r = rng() % (unsigned)p;
ll N = (ll)r;
unsigned r2 = rng() % (unsigned)(N + 1);
n = (ll)r2; m = N - n;
} else {
unsigned r1 = rng() % (unsigned)nmMax, r2 = rng() % (unsigned)nmMax;
n = 1 + (ll)r1; m = 1 + (ll)r2;
if (mode == 0) p = shared;
else { unsigned r3 = rng() % (unsigned)ps.size(); p = ps[r3]; }
}
out.push_back({n, m, p});
}
}
/** Kummer:n 和 m 按 p 进制相加,有没有进位 */
static bool hasCarry(ll n, ll m, ll p) {
ll carry = 0;
while (n || m) {
ll d = n % p + m % p + carry;
carry = d >= p ? 1 : 0;
if (carry) return true;
n /= p; m /= p;
}
return false;
}
static ll P;
static vector<ll> fac, inv;
static ll qpow(ll a, ll b) {
ll res = 1 % P; a %= P;
while (b) { if (b & 1) res = res * a % P; a = a * a % P; b >>= 1; }
return res;
}
static void prep() {
fac.assign((size_t)P, 0); inv.assign((size_t)P, 0);
fac[0] = 1 % P;
for (ll i = 1; i < P; i++) fac[i] = fac[i - 1] * i % P;
inv[P - 1] = qpow(fac[P - 1], P - 2);
for (ll i = P - 1; i >= 1; i--) inv[i - 1] = inv[i] * i % P;
}
static ll Cc(ll n, ll m) {
if (m > n) return 0;
return fac[n] * inv[m] % P * inv[n - m] % P;
}
static ll lucas(ll n, ll m) {
if (m == 0) return 1 % P;
return Cc(n % P, m % P) * lucas(n / P, m / P) % P;
}
/** 杨辉三角(滚动两行),只在小 N 上用 */
static ll pascalC(ll N, ll k, ll p) {
vector<ll> prev(N + 1, 0), cur(N + 1, 0);
prev[0] = 1 % p;
for (ll i = 1; i <= N; i++) {
cur[0] = 1 % p;
for (ll j = 1; j <= i; j++) cur[j] = (prev[j - 1] + prev[j]) % p;
swap(prev, cur);
}
return prev[k];
}
int main(int argc, char** argv) {
bool csv = (argc > 1 && string(argv[1]) == "csv");
/* ① Kummer 校验:小范围全枚举,「答案 ≡ 0」和「有进位」对不对得上 */
int kumCases = 0, kumZero = 0, kumCarry = 0, kumBad = 0, pascalBad = 0;
for (int p : {2, 3, 5, 7, 11, 13})
for (ll n = 0; n <= 40; n++)
for (ll m = 0; m <= 40; m++) {
P = p; prep();
ll a = lucas(n + m, n);
ll b = pascalC(n + m, n, p);
bool carry = hasCarry(n, m, (ll)p);
kumCases++;
if (a == 0) kumZero++;
if (carry) kumCarry++;
if ((a == 0) != carry) kumBad++;
if (a != b) pascalBad++;
}
/* ② 四个档 × 四个触发条件 */
int trigFac[4] = {0, 0, 0, 0}, trigOnce[4] = {0, 0, 0, 0}, trigCarry[4] = {0, 0, 0, 0};
int facBad[4] = {0, 0, 0, 0}, oneBad[4] = {0, 0, 0, 0};
for (int mode = 0; mode < 4; mode++) {
vector<array<ll, 3>> rows;
for (int s = 1; s <= 300; s++) {
gen((unsigned)s, mode, rows);
bool fac_ = false, once_ = false, carry_ = false, facB = false, oneB = false;
for (auto& r : rows) if (r[2] <= r[0] + r[1]) fac_ = true;
for (size_t i = 1; i < rows.size(); i++) if (rows[i][2] != rows[0][2]) once_ = true;
for (auto& r : rows) if (hasCarry(r[0], r[1], r[2])) carry_ = true;
for (auto& r : rows) {
ll n = r[0], m = r[1];
P = r[2]; prep();
ll N = n + m;
ll truth = lucas(N, n);
// 模板版:p ≤ N 时 fac[N] ≡ 0 ⇒ 它恒输出 0
ll facAns = (P <= N) ? 0 : truth;
if (facAns != truth) facB = true;
// 只算一位那版:漏掉了 lucas(N / p, n / p) 那一乘
ll oneAns = Cc(N % P, n % P);
if (oneAns != truth) oneB = true;
}
if (fac_) trigFac[mode]++;
if (once_) trigOnce[mode]++;
if (carry_) trigCarry[mode]++;
if (facB) facBad[mode]++;
if (oneB) oneBad[mode]++;
}
}
/* ③ 规模账 */
const ll NM = 200000; // n + m 顶格
double gbPascalOps = (double)NM * (double)NM;
ll facLenTemplate = NM + 1; // 模板版的阶乘表要开到 n+m
ll facLenLucas = 100000; // Lucas 只要开到 p
double msLucas;
{
double t[3];
vector<int> ps = primesUpTo(100000);
for (int r = 0; r < 3; r++) {
auto t0 = steady_clock::now();
volatile ll sink = 0;
for (int g = 0; g < 10; g++) { // 题面顶格:10 组
P = ps[ps.size() - 1 - g]; // 接近 10⁵ 的质数
prep();
sink += lucas(200000, 100000);
}
t[r] = duration<double, milli>(steady_clock::now() - t0).count();
}
msLucas = med3(t[0], t[1], t[2]);
}
if (csv) {
printf("kumCases,%d\n", kumCases);
printf("kumZero,%d\n", kumZero);
printf("kumCarry,%d\n", kumCarry);
printf("kumBad,%d\n", kumBad);
printf("pascalBad,%d\n", pascalBad);
for (int i = 0; i < 4; i++) printf("trigFac%d,%d\n", i, trigFac[i]);
for (int i = 0; i < 4; i++) printf("trigOnce%d,%d\n", i, trigOnce[i]);
for (int i = 0; i < 4; i++) printf("trigCarry%d,%d\n", i, trigCarry[i]);
for (int i = 0; i < 4; i++) printf("facBad%d,%d\n", i, facBad[i]);
for (int i = 0; i < 4; i++) printf("oneBad%d,%d\n", i, oneBad[i]);
printf("pascalOps,%.3g\n", gbPascalOps);
printf("facLenTemplate,%lld\n", facLenTemplate);
printf("facLenLucas,%lld\n", facLenLucas);
printf("msLucas,%.1f\n", msLucas);
printf("lucasUnderLimit,%.0f\n", 1000.0 / msLucas);
return 0;
}
printf("① Kummer 定理:C(n+m, n) ≡ 0 (mod p) ⟺ n 和 m 按 p 进制相加有进位\n");
printf(" 六个质数 × n, m ∈ [0, 40] 全枚举,共 %d 组:答案为 0 的 %d 组、有进位的 %d 组\n",
kumCases, kumZero, kumCarry);
printf(" ⇒ 两边对不上的:**%d 组**(Lucas 和杨辉三角对不上的:%d 组)\n", kumBad, pascalBad);
printf(" ⇒ ★★ 这条路和 Lucas 一行代码都不共享 —— 它同时验了正解,也解释了 NoGuard 的抓获率\n");
const char* NAME[4] = {"顺手写的(n,m ≤ 20、全组共用一个 p)", "主力档(n,m ≤ 1500、每组一个 p)",
"⚠ p > n + m", "顶格(n,m ≤ 10⁵)"};
printf("\n② 四个档各 300 轮,四个触发条件各出现几轮\n");
for (int i = 0; i < 4; i++)
printf(" 档 %d %-38s p ≤ n+m 的 %3d 轮(其中真会答错 %3d)/ 各组 p 不全同的 %3d 轮 / 有进位的 %3d 轮(只算一位真会错 %3d)\n",
i, NAME[i], trigFac[i], facBad[i], trigOnce[i], trigCarry[i], oneBad[i]);
printf(" ⇒ ★ 档 0 那个 0(各组 p 全一样)就是「阶乘表只算一次」一次都抓不到的原因\n");
printf(" ⇒ ★ 档 2 那三个 0 是**一个对照档同时给三个错法做的自检**\n");
printf("\n③ 规模账(题面顶格:n + m = 2×10⁵、p ≤ 10⁵、T = 10)\n");
printf(" 杨辉三角 要做 %.3g 次加法(一组!题面最多 10 组) ⇒ 只能当参照物\n", gbPascalOps);
printf(" 本章模板 阶乘表开到 %lld 项 —— 跑得动,可 p ≤ n+m 时**恒输出 0**\n", facLenTemplate);
printf(" ★ Lucas 阶乘表只开到 %lld 项(p 那么大就够),10 组顶格 %.1f ms ⇒ 余量 %.0f 倍\n",
facLenLucas, msLucas, 1000.0 / msLucas);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★ 只有 Lucas 那一版能过 —— 而另外两条路各挂在不同的地方
写法 顶格代价 交上去
杨辉三角 4×10¹⁰ 次加法(一组,而题面最多 10 组) ✗ 一分不给
本章第 6 步的模板 阶乘表 200 001 项,跑得飞快 恒输出 0
★ Lucas 阶乘表只到 p,10 组共 10.3 ms AC,余量 97 倍

⚠ 注意中间那一行的形状:它不是慢,是错;而它错得一点征兆都没有 —— 不崩、不超时、格式完全正常,只是一直在答 0。 ⇒ 「恒输出一个常量」是一整类 bug 的形状 的又一次, ★ 而这一次它的来源是照搬了一个前提已经不成立的模板

⇒ 这道题真正要学的是那句话反过来说: 记一个模板的时候,把它的前提一起记下来 —— 前提失效的那一天,模板不会报错。