0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1313,日期见页头。两边不一致时信原站。
题目描述
给定一个多项式 (by + ax)ᵏ,请求出多项式展开后 xⁿ × yᵐ 项的系数。
输入格式
输入共一行,包含 5 个整数,分别为 a, b, k, n, m,每两个整数之间用一个空格隔开。
输出格式
输出共一行,包含一个整数,表示所求的系数。
这个系数可能很大,输出对 10007 取模后的结果。
数据范围
- 对于 30% 的数据,有
0 ≤ k ≤ 10; - 对于 50% 的数据,有
a = 1,b = 1; - 对于 100% 的数据,有
0 ≤ k ≤ 1000,0 ≤ n, m ≤ k,n + m = k,0 ≤ a, b ≤ 10⁶。
noip2011 提高组 day2 第 1 题。时限 1 秒,内存 125 MB(128000 KB)。
输入输出样例
输入
1 1 3 1 2
输出
3
a = 1、b = 1、k = 3、n = 1、m = 2 ⇒ (y + x)³ 里 x¹y² 的系数是 C(3,1) = 3。
1一行纸上推导:二项式定理
(by + ax)ᵏ 展开,每一项是「从 k 个括号里挑 n 个出 ax、剩下 m 个出 by」:
(by + ax)ᵏ = Σᵢ₌₀..ₖ C(k, i) · (ax)ⁱ · (by)ᵏ⁻ⁱ
⇒ xⁿyᵐ 那一项的系数 = C(k, n) · aⁿ · bᵐ。
剩下的全是这一章的活:C(k, n) mod 10007 怎么算、aⁿ mod 10007 怎么算
(后者就是第 42 章那五行)。
★ 两件白送的好事都写在题面里:
10007 是质数,而 k ≤ 1000 < 10007 ⇒ 本章第 6 步
那个「p > n」的前提在这里是满足的,逆元那一套可以放心用。
(⚠ 下一道题 P3807 就不满足了 —— 那才是它失效的场。)
2第 ① 版:照定义算 —— 而它稳拿 30 分
// ✗ 第 ① 版:所有人真实的第一反应 —— 组合数照定义算 C(k,n) = k! / (n! · m!)//// 幂那两下大家都会记得取模(题面明写了要模 10007),栽的是**阶乘**这一头:// long long 装得下 **20!**(2.43×10¹⁸ < 9.22×10¹⁸),**21! 就是第一个装不下的**。//// ★ 所以它不是「一交就零分」—— 题面 30% 那一档写着 `0 ≤ k ≤ 10`,// 那一整档它算得**一个字不差**。⇒ **这一版稳拿 30 分。**// ⚠ 到 100% 档 k ≤ 1000 时,k! 有 2568 位,long long 早在第 21 步就绕回去了;// 而绕回去之后那句整数除法连「除得尽」都不成立 —— 输出的是个看着很正常的垃圾数。#include <bits/stdc++.h>using namespace std;typedef long long ll;const ll P = 10007;
static ll qpow(ll a, ll b) { ll res = 1; a %= P; while (b) { if (b & 1) res = res * a % P; a = a * a % P; b >>= 1; } return res;}
int main() { ll a, b, k, n, m; if (scanf("%lld %lld %lld %lld %lld", &a, &b, &k, &n, &m) != 5) return 0;
ll fk = 1, fn = 1, fm = 1; for (ll i = 1; i <= k; i++) fk *= i; // ⚠ k ≥ 21 就溢出了 for (ll i = 1; i <= n; i++) fn *= i; for (ll i = 1; i <= m; i++) fm *= i; ll C = fk / (fn * fm); // ⚠ 溢出之后这句连整除都不成立
printf("%lld\n", (((C % P) + P) % P) * qpow(a, n) % P * qpow(b, m) % P); return 0;}点「运行 ▶」看结果
long long 装得下 20! = 2 432 902 008 176 640 000(< 9.22×10¹⁸),
21! 是第一个装不下的。度量程序把 k = 0 … 40 每个 n 都试了一遍:
第一个算错的 k 就是 21。
⇒ 题面 30% 那一档写着 0 ≤ k ≤ 10 —— 它在那一整档一个字都不差。
「暴力值多少分」是「暴力 × 那道题分档」的属性 的又一次。
⚠ 而溢出之后不只是「数大了」:fk / (fn * fm) 是整数除法,
fk 一绕回去,连「除得尽」都不成立了 —— 输出的是个看着完全正常的垃圾数。
3第 ② 版:知道要取模了,可最后那一下还是除法
fac[k] % p 之后,它和 fac[n]·fac[m] 之间的整除关系整个没了 ——
「先取模再整除」和「先整除再取模」不是一回事,甚至连商是不是整数都不一定。
⇒ 正确的做法是把「除以 x」换成「乘以 x 的逆元」,而 10007 是质数
⇒ 逆元 = x^(p−2) mod p(费马小定理)。
⚠ 而它样例照过:样例的 k = 3,三个阶乘都还没碰到模数,那句除法碰巧还是真除法。
4★ 第 ③ 版:杨辉三角 —— 这一版就已经能 AC 了
// ★ 第 ③ 版:杨辉三角递推 —— **这一版就已经能 AC 了**//// 本章第 3 步那份 brute.cpp 原样搬过来:C(i,j) = C(i−1,j−1) + C(i−1,j),// 全程只有加法 ⇒ **不用逆元、不用快速幂、连「p 是不是质数」都不用问**。//// k ≤ 1000 ⇒ 三角一共 k(k+1)/2 ≈ 50 万个格子,本机不到 1 毫秒(时限 1 秒)。// ⇒ ★ 这道题只问**一次**,所以那套 O(k) 预处理 + O(1) 回答的模板在这儿一分钱都不值 ——// 它是给下一道题(多组询问的 P3807)准备的。//// ⚠ 但 a^n、b^m 那两下还是要快速幂(n、m 到 1000,硬乘 1000 次也行,只是没必要)。#include <bits/stdc++.h>using namespace std;typedef long long ll;const ll P = 10007;
static ll qpow(ll a, ll b) { ll res = 1; a %= P; while (b) { if (b & 1) res = res * a % P; a = a * a % P; b >>= 1; } return res;}
static ll C[1005][1005];
int main() { ll a, b, k, n, m; if (scanf("%lld %lld %lld %lld %lld", &a, &b, &k, &n, &m) != 5) return 0;
for (ll i = 0; i <= k; i++) { C[i][0] = 1; for (ll j = 1; j <= i; j++) C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % P; } printf("%lld\n", C[k][n] * qpow(a, n) % P * qpow(b, m) % P); return 0;}点「运行 ▶」看结果
| 写法 | 基本运算 | 本机 | 余量 |
|---|---|---|---|
| ★ 杨辉三角(只有加法) | 500 500 次加法 | 0.47 ms | 2143 倍 |
| 阶乘 + 倒推逆元 | 2014 次乘法 | 0.006 ms | 十六万倍 |
次数少 248.5 倍、秒表快 75.7 倍 —— 而两版都在时限的千分之一以内。
⇒ ★★ 这道题该交的是杨辉三角:它只有加法,不问「p 是不是质数」、不用逆元、
不用快速幂,k ≤ 1000 的三角只有 50 万个格子。
那套「O(k) 预处理 + 每次询问 O(1)」的模板是给多组询问准备的,
而这道题只问一次 ⇒ 它在这儿一分钱都不值。
⇒ 「这个技巧在这道题上成立」和「这道题该用它」是两句话 的又一次。
★ 那套模板真正派上用场是在下一道题 P3807:T 组询问、n+m 到 2×10⁵。
5⚠ 三个错法 —— 而官方样例一个都没挡住
| 版本 | 样例输出 | 挡住了吗 |
|---|---|---|
| 正解 | 3 | |
| ① long long 阶乘 | 3 | ✗ 放过(k = 3,还没溢出) |
| ② 取模之下做除法 | 3 | ✗ 放过(那句除法碰巧还是真除法) |
✗ a、b 弄反 |
3 | ✗ 放过 —— ★ 样例的 a 和 b 都是 1 |
✗ int + 忘了 a %= p |
3 | ✗ 放过(a = 1,平方永远不溢出) |
⚠ 用 C(k,m) |
3 | ✗ 放过(C(3,1) = C(3,2)) |
⇒ 「样例是个『一测就死』的过滤器」这条规律的极端情形:五个全放过。 ★ 而这一次五个「放过」的原因能一个个说清楚,而且都指向样例里那两个 1。
6★★★ 那半行「50% 的数据 a = 1,b = 1」,正是「弄反」的盲区
x 配的是 a、y 配的是 b,所以系数是 C(k,n) · aⁿ · bᵐ。
可题面把 by 写在前面 ⇒ 顺手一读就成了「第一个数配第一个指数」,写出 aᵐ · bⁿ。
★★ 而题面自己给的第二档是 「对于 50% 的数据,有 a = 1,b = 1」 ——
a = b 的时候 aᵐbⁿ 和 aⁿbᵐ 恒等。
⇒ ★★★ 这个错法交上去稳拿 50 分,而且从输出上完全看不出哪里错了: 出题人那一档不是给暴力留的分,它同时是这个错法的结构性盲区。
| 档位(每档 300 轮) | a ≠ b 的轮数 |
✗ 弄反真被抓 |
|---|---|---|
0 顺手写的(a, b ≤ 100、k ≤ 30) |
297 | 277 |
1 照题面随机(a, b ≤ 10⁶) |
300 | 299 |
⚠⚠ 2 a = b = 1(题面 50% 那一档) |
0 | ★ 精确的 0 |
★ 档 2 那个 0 是能证的,证明就是那一档的定义;它同时是这个错法的自检
——同一个动作干两件事:造一档「题面允许而顺手不会造」的数据,
既把 0 证清楚,又称出「a = b = 1」那一档到底护着谁。
7★★ 一个「看着像下标写反、其实恒等」的版本,和它的自检
C(k, m) = C(k, k−n) = C(k, n)。所以「把 C(k,n) 写成 C(k,m)」这个看着典型的下标手滑,
在这道题上一次都不会错:四个档里三个是精确的 0。
★★ 而「一个反例都没有」和「这段代码根本没在跑」在输出上长得一模一样
(第 157 条)⇒ 必须配自检。这一页的自检只要放开题面那句 n + m = k:
| 档位(每档 300 轮) | ⚠ 用 C(k,m) 被抓 |
|---|---|
0 / 1 / 2(题面保证 n + m = k) |
★ 0 / 0 / 0 |
⚠ 3 违反题面:n、m 各自随机 |
296 / 300 |
⇒ ★★★ 同一个动作干了两件事:① 证明这段代码是活的;
② 称出 n + m = k 是这一版的命门 —— 去掉它,「用哪个下标」立刻是生死之别。
⚠ 顺带说清楚这个 0 的边界:它只保证「组合数那一半」不受影响。
aⁿ · bᵐ 那一半照样是分得清 n 和 m 的 —— 上一节那个 Swap 就是证据。
8★ 生成器:四个档,每个档专治一个版本
| 档位(每档 300 轮) | ① 阶乘溢出 | ② 模下除法 | ✗ 弄反 | ✗ int+没取模 | ⚠ 用 C(k,m) |
|---|---|---|---|---|---|
0 顺手写的(a,b ≤ 100、k ≤ 30) |
80(触发 87) | 186 | 277(a≠b 297) |
★ 0(触发 0) | ★ 0 |
1 照题面(a,b ≤ 10⁶、k ≤ 1000) |
293(触发 295) | 292 | 299(a≠b 300) |
298(触发 299) | ★ 0 |
2 a = b = 1(题面 50% 档) |
294(触发 294) | 294 | ★ 0(a≠b 0) |
★ 0(触发 0) | ★ 0 |
⚠ 3 违反题面 n+m=k |
299 | 298 | 298 | 298 | 296 |
★★★ 三条读得出来的结论:
int那个错法的触发条件是「两件各自无害的事凑在一起」 —— 只是「忘了a %= p」,用long long存完全没事(a·a = 10¹²,离 9.2×10¹⁸ 还远); 只是「全用int」,先取过模也完全没事(10006² ≈ 1.0×10⁸ < 2.1×10⁹)。 两件合在一起才炸,而线精确在a ≥ 46341(46341² > 2³¹)—— 和第 18 章 P1516、第 34 章 P2872 一字不差,那是int的性质,不是这道题的性质。 ⇒ 顺手写的小a、小b是精确的 0,只有照题面顶到 10⁶ 才抓得到。- 「触发」和「被抓」差多少,只能量 —— 第 ① 版档 0 是 87 触发 / 80 被抓 (另外 7 轮它溢出了却恰好蒙对),档 2 是 294 ≡ 294(一个不差)。 ⇒ 「它算的是另一个量」推不出「它一定和正解不同」 的又一次。
- 档 2 那一列有三个精确的 0 —— 而它是题面自己给的一档(50% 的数据
a=b=1)。 ⇒ 「顺手写的档位能同时把好几个 bug 打成 0」 在这里换了个来源: 这一次不是生成器偷懒,是出题人给的档本身就窄。
9★ 哪一版就已经能过了
// P1313 的度量程序:./p1313Count csv (本页的数字都出自它)//// line : ★ 第 ① 版那条溢出线到底在哪 —— 从 k = 0 一路试上去,第一个算错的 k。// trig : ★ 三个错法各自的**触发条件**在四个档里出现几轮(对照「真被抓」看两层差多少)。// cost : ★ 杨辉三角 vs 阶乘+逆元:次数差多少倍、秒表差多少(顶格 k = 1000)。//// ⚠ 这里**复刻**了 p1313Gen.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;const ll P = 10007;
static ll qpow(ll a, ll b) { ll res = 1; a %= P; while (b) { if (b & 1) res = res * a % P; a = a * a % P; b >>= 1; } return res;}static double med3(double a, double b, double c) { return max(min(a, b), min(max(a, b), c)); }
/** 复刻 p1313Gen.cpp */static void gen(unsigned seed, int mode, ll& a, ll& b, ll& k, ll& n, ll& m) { mt19937 rng(seed * 1000003u + 20260903u); if (mode == 0) { a = (ll)(rng() % 101u); b = (ll)(rng() % 101u); k = (ll)(rng() % 31u); } else if (mode == 2) { a = 1; b = 1; k = (ll)(rng() % 1001u); } else { a = (ll)(rng() % 1000001u); b = (ll)(rng() % 1000001u); k = (ll)(rng() % 1001u); } if (mode == 3) { n = (ll)(rng() % (unsigned)(k + 1)); m = (ll)(rng() % (unsigned)(k + 1)); } else { n = (ll)(rng() % (unsigned)(k + 1)); m = k - n; }}
/** int 版那个快速幂会不会溢出:把它的每一步都拿 long long 走一遍 */static bool intPowOverflows(ll a, ll e) { const ll TOP = 2147483647LL; ll res = 1; while (e) { if (e & 1) { if (res * a > TOP) return true; res = res * a % P; } if (a * a > TOP) return true; a = a * a % P; e >>= 1; } return false;}
static ll fac[2005], inv[2005];static void prep(ll k) { fac[0] = 1; for (ll i = 1; i <= k; i++) fac[i] = fac[i - 1] * i % P; inv[k] = qpow(fac[k], P - 2); for (ll i = k; i >= 1; i--) inv[i - 1] = inv[i] * i % P;}static ll okAns(ll a, ll b, ll k, ll n, ll m) { prep(k); ll C = fac[k] * inv[n] % P * inv[k - n] % P; return C * qpow(a, n) % P * qpow(b, m) % P;}static ll naiveAns(ll a, ll b, ll k, ll n, ll m) { ll fk = 1, fn = 1, fm = 1; for (ll i = 1; i <= k; i++) fk *= i; for (ll i = 1; i <= n; i++) fn *= i; for (ll i = 1; i <= m; i++) fm *= i; ll C = fk / (fn * fm); return (((C % P) + P) % P) * qpow(a, n) % P * qpow(b, m) % P;}
int main(int argc, char** argv) { bool csv = (argc > 1 && string(argv[1]) == "csv");
/* ① 第 ① 版那条线:k 从 0 往上,第一个让它算错的 k(每个 k 把 n 全试一遍) */ int firstBad = -1; for (ll k = 0; k <= 40 && firstBad < 0; k++) for (ll n = 0; n <= k; n++) if (naiveAns(7, 11, k, n, k - n) != okAns(7, 11, k, n, k - n)) { firstBad = (int)k; break; }
/* ② 四个档 × 三个触发条件 */ int trigK[4] = {0, 0, 0, 0}, trigOvf[4] = {0, 0, 0, 0}, trigAB[4] = {0, 0, 0, 0}; for (int mode = 0; mode < 4; mode++) for (int s = 1; s <= 300; s++) { ll a, b, k, n, m; gen((unsigned)s, mode, a, b, k, n, m); if (k >= 21) trigK[mode]++; // Naive 溢出 if (intPowOverflows(a, n) || intPowOverflows(b, m)) trigOvf[mode]++; // NoModA 溢出 if (a != b) trigAB[mode]++; // Swap 有机会现形 }
/* ③ 顶格 k = 1000:杨辉三角 vs 阶乘 + 逆元(次数 + 秒表) */ const ll K = 1000; ll addPascal = K * (K + 1) / 2; // 三角里真正做加法的格子数 ll mulFast = K + K + 14; // 阶乘 K 次 + 倒推 K 次 + 一次快速幂约 14 步 static vector<vector<int>> C; double msPascal, msFast; { double t[3]; for (int r = 0; r < 3; r++) { auto t0 = steady_clock::now(); C.assign(K + 1, vector<int>(K + 1, 0)); for (ll i = 0; i <= K; i++) { C[i][0] = 1; for (ll j = 1; j <= i; j++) C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % (int)P; } t[r] = duration<double, milli>(steady_clock::now() - t0).count(); } msPascal = med3(t[0], t[1], t[2]); } { double t[3]; for (int r = 0; r < 3; r++) { auto t0 = steady_clock::now(); volatile ll sink = 0; for (int rep = 0; rep < 100; rep++) { prep(K); sink += fac[K] + inv[K]; } t[r] = duration<double, milli>(steady_clock::now() - t0).count() / 100.0; } msFast = med3(t[0], t[1], t[2]); }
if (csv) { printf("firstBad,%d\n", firstBad); for (int i = 0; i < 4; i++) printf("trigK%d,%d\n", i, trigK[i]); for (int i = 0; i < 4; i++) printf("trigOvf%d,%d\n", i, trigOvf[i]); for (int i = 0; i < 4; i++) printf("trigAB%d,%d\n", i, trigAB[i]); printf("addPascal,%lld\n", addPascal); printf("mulFast,%lld\n", mulFast); printf("opsRatio,%.1f\n", (double)addPascal / (double)mulFast); printf("msPascal,%.3f\n", msPascal); printf("msFast,%.3f\n", msFast); printf("msRatio,%.1f\n", msPascal / msFast); printf("pascalUnderLimit,%.0f\n", 1000.0 / msPascal); return 0; } printf("① 第 ① 版(long long 阶乘)那条溢出线\n"); printf(" k = 0 .. %d 每个 n 都试过 ⇒ 第一个算错的 k = **%d**(因为 21! 装不下 long long)\n", 40, firstBad); printf(" ⇒ ★ 题面 30%% 那一档是 k ≤ 10 ⇒ **这一版稳拿 30 分**\n");
const char* NAME[4] = {"顺手写的(a,b ≤ 100、k ≤ 30)", "照题面(a,b ≤ 10⁶、k ≤ 1000)", "a = b = 1(题面 50% 那一档)", "⚠ 违反题面:n + m ≠ k"}; printf("\n② 四个档各 300 轮,三个错法的**触发条件**各出现几轮\n"); for (int i = 0; i < 4; i++) printf(" 档 %d %-30s k ≥ 21 的 %3d 轮 / int 会溢出的 %3d 轮 / a ≠ b 的 %3d 轮\n", i, NAME[i], trigK[i], trigOvf[i], trigAB[i]); printf(" ⇒ ★ 档 2 那两个 0 就是「a = b = 1 ⇒ 弄反了也一样」的证明(题面 50%% 档的形状)\n");
printf("\n③ 顶格 k = 1000:两种正确写法(独占实测,3 次取中位数;时限 1000 ms)\n"); printf(" 杨辉三角 %8lld 次加法 %.3f ms ⇒ 余量 %.0f 倍\n", addPascal, msPascal, 1000.0 / msPascal); printf(" 阶乘 + 逆元 %8lld 次乘法 %.3f ms ⇒ 次数少 %.1f 倍、秒表快 %.1f 倍\n", mulFast, msFast, (double)addPascal / (double)mulFast, msPascal / msFast); printf(" ⇒ ★ 两版都远在时限之内 ⇒ **这道题该交的是杨辉三角**,逆元那套是给下一道题练手的\n"); return 0;}点「运行 ▶」看结果
| 版本 | 交上去 | 为什么 |
|---|---|---|
① long long 阶乘 |
30 分 | 30% 档是 k ≤ 10,而它的线在 k = 21 |
| ② 模下做除法 | 0 分 | 除不尽的那一刻就全塌了 |
✗ a、b 弄反 |
★ 50 分 | 50% 档写着 a = b = 1 |
| ★ ③ 杨辉三角 | ★ AC | 50 万次加法,0.47 ms |
| ④ 阶乘 + 逆元 | AC | 快 76 倍,而这道题一分钱用不上 |
⇒ 这道题真正要学的两件事都不在算法上: 一行二项式定理,以及读懂数据范围那三行在说什么 —— 它们既是「哪个暴力值多少分」的答案,也是「哪个错法能骗过多少分」的答案。