0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1226,日期见页头。两边不一致时信原站。
题目描述
给你三个整数 a、b、p,求 a^b mod p。
输入格式
输入只有一行三个整数,分别代表 a、b、p。
输出格式
输出一行一个字符串 a^b mod p=s,其中 a、b、p 分别为题目给定的值,s 为运算结果。
说明 / 提示
样例解释:2¹⁰ = 1024,1024 mod 9 = 7。
数据规模与约定
对于 100% 的数据,保证 0 ≤ a, b < 2³¹,a + b > 0,2 ≤ p < 2³¹。
输入输出样例
输入
2 10 9
输出
2^10 mod 9=7
这道题考什么:快速幂求 a^b mod p。算法不难,难在输出格式、溢出和边界。
最容易错的三处
- 只输出结果那一个数。题目要的是
a^b mod p=s,格式一个字符都不能差。算得全对也是 0 分。判据:对拍时参照物也要照格式输出,比对也不能strip。 - 连乘
b次。样例里b = 10完全正确,但b顶格是2³¹ − 1,要约 11 秒,时限只有 1 秒。快速幂只要 31 轮。 - 乘法用 32 位。
p到2³¹ − 1,res * a最大能到4.6 × 10¹⁸,32 位装不下。随机数据就能抓到 262/300。用long long就够了。
能 AC 的:p1226,快速幂加 long long,输出按 a^b mod p=s 的格式。
下面每一步里灰色的「深入」块是折叠的:那是这些结论怎么实测出来的,第一遍可以跳过。
1这一页要做的事:拿本书的清单,去查这道题的范围
快速幂怎么写,第 42 章从头讲到尾了; 那一章第 ⑧ 步还给了一张六个错法各靠什么现形的清单。
这一页只做一件事:把那六条,逐个代进 P1226 的数据范围。
结论是:六条里有三条在这道题上是结构性地发生不了的 —— 而每一条「发生不了」的理由,都是一句能写下来的算术。 ⇒ 一句听起来有道理的提醒,是不是这道题的命门,只能算,不能感觉。
★ 还有一条最要命的和算法一点关系都没有:输出格式(第 ② 步)。
// P1226 【模板】快速幂 —— 能 AC 的那一版//// 算法本身第 42 章从头讲到尾了,这一页只做一件事:// **把第 42 章列的六个错法,逐个代进这道题的数据范围,看哪些还能发生。**//// 题面的范围:0 <= a, b < 2³¹,a + b > 0,2 <= p < 2³¹。// 三处直接决定了写法:// ① **a >= 0** ⇒ 第 42 章那句「负数要归一化」在这道题上一次都用不上;// ② **p >= 2** ⇒ `1 % p` 恒等于 1,那句「初值不是 1 是 1 % p」也用不上;// ③ ★ **p < 2³¹** ⇒ `(p-1)² < 2⁶²` ⇒ **long long 刚好够,不用 __int128**。// 余量正好是 √2:装得下的上限是 2³¹·⁵ ≈ 3.04×10⁹,而题面给到 2³¹ ≈ 2.15×10⁹。//// ⚠ 而这道题真正会挂人的一条和算法无关:**输出格式**是 `a^b mod p=s`,不是一个数。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
int main() { ll a, b, p; if (scanf("%lld %lld %lld", &a, &b, &p) != 3) return 0;
ll a0 = a, b0 = b; // 输出里要原样打出来,先存一份 ll res = 1 % p; // p >= 2 ⇒ 这里就是 1;写 1 % p 只是习惯 a %= p; while (b) { if (b & 1) res = res * a % p; // (p-1)² < 2⁶²,long long 装得下 a = a * a % p; b >>= 1; }
printf("%lld^%lld mod %lld=%lld\n", a0, b0, p, res); return 0;}点「运行 ▶」看结果
顶格数据(a = b = p = 2³¹ - 1 附近)本机实测:0.00 秒(循环只跑 31 次)。
2⚠ 先说那个和算法无关、却一定会挂人的
// P1226 ⚠ 错法零:算得完全对,只输出了一个数//// 这是把第 42 章的模板原样搬过来最容易出的事 ——// 那一章的 fast.cpp 输出的就是一个数,因为那一章讲的是算法。//// 而这道题要的是:// a^b mod p=s// 一个字符都不能差(`mod` 两边各一个空格、等号两边**没有**空格)。//// ⇒ 交上去 0 分,而你在本地怎么看都觉得答案是对的。// ★ 这类错对拍抓得到 —— 但前提是**参照物也照着格式输出**,而且比对**不许 strip**。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
int main() { ll a, b, p; if (scanf("%lld %lld %lld", &a, &b, &p) != 3) return 0; ll res = 1 % p; a %= p; while (b) { if (b & 1) res = res * a % p; a = a * a % p; b >>= 1; } printf("%lld\n", res); // ⚠ 就是这里 return 0;}点「运行 ▶」看结果
第 42 章的模板 fast.cpp 输出的是一个数 —— 因为那一章讲的是算法。
而这道题要的是:
a^b mod p=s
mod 两边各一个空格,等号两边没有空格,一个字符都不能差。
① 参照物也照着格式输出(拿第 42 章那份当参照物的话,两边一起错,对拍全绿);
② 比对不许 strip。
⇒ 第 10 章 P1271 那条踩过的坑: 自己搭的临时对拍最容易在「怎么比」上放水。 正式断言一律逐字节比。
3第 ① 版:老老实实连乘 b 次
// P1226 第 ① 版:老老实实连乘 b 次//// 它是对的,只是 b 能到 2³¹-1 ≈ 2.15×10⁹ ——// 一次乘法 + 一次取模按 1.5 纳秒算,光这一步就是 **3 秒以上**,时限 1 秒。//// ⚠ 而它在样例上完全正确(b = 10),本地随手试也看不出任何问题。// 这就是这道题的第一道坎:**样例的 b 和顶格的 b 差了两亿倍。**//// 为了让它在页面上点得动,这一版带一个上限:b 超过 3×10⁸ 就直接罢工报一行字。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
int main() { ll a, b, p; if (scanf("%lld %lld %lld", &a, &b, &p) != 3) return 0; ll a0 = a, b0 = b; if (b > 300000000LL) { printf("b = %lld,连乘要跑 %lld 次 —— 这一版就不跑了,见页面上的外推。\n", b, b); return 0; } ll res = 1 % p; a %= p; for (ll i = 0; i < b; i++) res = res * a % p; printf("%lld^%lld mod %lld=%lld\n", a0, b0, p, res); return 0;}点「运行 ▶」看结果
它是对的 —— level 3(b ≤ 30)那一档 300 轮,它和快速幂逐字节相同。
它唯一的毛病是 b 能到 2³¹ - 1:
| 循环次数 | 本机实测 | |
|---|---|---|
连乘(b = 3 × 10⁸) |
300 000 000 | 1.57 秒 |
连乘(b = 2³¹ - 1,外推) |
2 147 483 647 | ★ 约 11.2 秒 |
| 快速幂(同一组) | ★ 31 | 0.00 秒 |
次数差 6.9 × 10⁷ 倍,时限 1 秒。
样例的 b = 10。样例的 b 和顶格的 b 差了两亿倍 ——
这道题的第一道坎不在算法上,在「有没有把范围代进去算一下」。
4★★★ 把第 42 章那六条,逐个代进这道题的范围
// ★★★ 把第 42 章列的**六个错法**,逐个代进 P1226 的数据范围//// 用法:./p1226Six [轮数] 人话版(默认 300 轮)// ./p1226Six [轮数] csv 给 check:viz 用//// 第 42 章第 ⑧ 步给了一张「靠什么现形」的清单。这一页想问的是另一个问题://// > **那六个错法,在 P1226 这道题的数据范围里,有几个是根本发生不了的?**//// 题面:`0 <= a, b < 2³¹`,`a + b > 0`,`2 <= p < 2³¹`。三处硬约束:// ① `a >= 0` ⇒ 「负数要归一化」那条**永远用不上**;// ② `p >= 2` ⇒ `1 % p` 恒等于 1,「初值不是 1」那条**永远用不上**;// ③ `p < 2³¹` ⇒ `(p-1)² < 2⁶²`,**long long 刚好够** ——// 装得下的上限是 `2³¹·⁵ ≈ 3.04×10⁹`,题面给到 `2³¹ ≈ 2.15×10⁹`,// ★ **余量正好是 √2**(一个是 2 的 31 次方,一个是 31.5 次方)。//// 所以这份程序跑**三档**,同一批错法 —— 三档是为了把「0 次」的两种原因分开:// 档 A **照题面顺手随机**;// 档 A' **仍然照题面**,但专门把两个边界**配在一起**(b = 0 且 a ≡ 0 mod p);// 档 B **把题面那三处各放宽一点**(允许 a < 0 / p = 1 / p 到 2³²)。// ⇒ 在 A 是 0、在 A' 活了 ⇒ 题面**允许**,只是顺手生成器造不到;// 在 A 和 A' 都是 0、只有 B 活 ⇒ 题面**结构上挡死了**它。//// ⚠ 演示溢出的那一版用的是 unsigned 乘法(回绕是标准规定的),// 真写 `long long` 相乘溢出是 UB,-O2 下不可复现 —— 那样这一页的数就钉不住了。
#include <bits/stdc++.h>using namespace std;typedef long long ll;typedef __int128 lll;
/* 参照物:处处正确(__int128 乘法、负数归一化、1 % p、0⁰ = 1) */static ll okFull(ll a, ll b, ll p) { ll res = 1 % p; a %= p; if (a < 0) a += p; while (b) { if (b & 1) res = (ll)((lll)res * a % p); a = (ll)((lll)a * a % p); b >>= 1; } return res;}static ll wrongLoop(ll a, ll b, ll p) { // while (b > 1):最后一位没乘进去 ll res = 1 % p; a %= p; if (a < 0) a += p; while (b > 1) { if (b & 1) res = (ll)((lll)res * a % p); a = (ll)((lll)a * a % p); b >>= 1; } return res;}static ll wrongOrder(ll a, ll b, ll p) { // 先平方、再判这一位 —— 整整错一位 ll res = 1 % p; a %= p; if (a < 0) a += p; while (b) { a = (ll)((lll)a * a % p); if (b & 1) res = (ll)((lll)res * a % p); b >>= 1; } return res;}static ll wrongNeg(ll a, ll b, ll p) { // 不把负数归一化 ll res = 1 % p; a %= p; // ⚠ 少了 if (a < 0) a += p; while (b) { if (b & 1) res = (ll)((lll)res * a % p); a = (ll)((lll)a * a % p); b >>= 1; } return res;}/** 64 位乘法:`x * y` 超过 2⁶³ 就회绕成负数 —— 用 unsigned 算再当成有符号看,结果可复现。 */static ll mul64(ll x, ll y, ll p) { ll t = (ll)((unsigned long long)x * (unsigned long long)y); return t % p; // t 变负数时这里就跟着负了}static ll wrongOverflow(ll a, ll b, ll p) { // 乘法只用 64 位(第 42 章那一章要 __int128) ll res = 1 % p; a %= p; if (a < 0) a += p; while (b) { if (b & 1) res = mul64(res, a, p); // ★ p > 3.04×10⁹ 时这里会翻车 a = mul64(a, a, p); b >>= 1; } return res;}static ll wrongInt(ll a, ll b, ll p) { // ★ 这道题真正的溢出线:用 32 位 unsigned res = (unsigned)(1 % p), aa = (unsigned)(a % p), pp = (unsigned)p; while (b) { if (b & 1) res = res * aa % pp; aa = aa * aa % pp; b >>= 1; } return (ll)res;}static ll wrongOne(ll a, ll b, ll p) { // 初值写成 1(不是 1 % p) ll res = 1; a %= p; if (a < 0) a += p; while (b) { if (b & 1) res = (ll)((lll)res * a % p); a = (ll)((lll)a * a % p); b >>= 1; } return res;}static ll wrongZero(ll a, ll b, ll p) { // 「底数 ≡ 0 就返回 0」—— 把 0⁰ 也吞了 ll aa = a % p; if (aa < 0) aa += p; if (aa == 0) return 0; return okFull(a, b, p);}
static const char* NAMES[7] = { "wrongLoop", "wrongOrder", "wrongNeg", "wrongOverflow", "wrongOne", "wrongZero", "wrongInt"};typedef ll (*Fn)(ll, ll, ll);static Fn FNS[7] = { wrongLoop, wrongOrder, wrongNeg, wrongOverflow, wrongOne, wrongZero, wrongInt };
int main(int argc, char** argv) { int rounds = (argc > 1) ? atoi(argv[1]) : 300; bool csv = (argc > 2 && string(argv[2]) == "csv");
/* 装得下的 p 的上限:最大的 p 使 (p-1)² < 2⁶³ */ ll ovLimit = 0; { ll lo = 2, hi = 5000000000LL; while (lo < hi) { ll mid = lo + (hi - lo + 1) / 2; if ((lll)(mid - 1) * (mid - 1) < (lll)9223372036854775807LL) lo = mid; else hi = mid - 1; } ovLimit = lo; }
int caught[3][7] = {{0}}; int b0Rounds = 0, azeroRounds = 0; for (int lv = 0; lv < 3; lv++) { mt19937_64 rng(20260827ull + (unsigned)lv); for (int r = 0; r < rounds; r++) { ll a, b, p; if (lv == 0) { // 档 A:照题面顺手随机 p = 2 + (ll)(rng() % 2147483646ull); // [2, 2³¹) b = (ll)(rng() % 2147483648ull); // [0, 2³¹) a = (ll)(rng() % 2147483648ull); if (r % 7 == 0) b = 0; // 顺手也多造些 b = 0 if (a + b == 0) a = 1; if (b == 0) { b0Rounds++; if (a % p == 0) azeroRounds++; } } else if (lv == 1) { // 档 A':仍在题面内,两个边界配在一起 p = 2 + (ll)(rng() % 1000000ull); // p 小一点,好让 a = k·p 也塞得进 2³¹ a = p * (1 + (ll)(rng() % 3)); // ★ a ≡ 0 (mod p),且 a > 0 b = 0; // ★ 同时 b = 0 } else { // 档 B:三处各放宽一点 p = 1 + (ll)(rng() % 4294967295ull); // [1, 2³²) b = (ll)(rng() % 2147483648ull); a = (ll)(rng() % 4294967296ull) - 2147483648LL;// 允许负 if (r % 5 == 0) p = 1; if (r % 7 == 0) b = 0; if (a + b == 0) a = 1; } ll want = okFull(a, b, p); for (int i = 0; i < 7; i++) if (FNS[i](a, b, p) != want) caught[lv][i]++; } }
if (csv) { printf("rounds,%d\novLimit,%lld\nb0Rounds,%d\nazeroRounds,%d\n", rounds, ovLimit, b0Rounds, azeroRounds); for (int i = 0; i < 7; i++) printf("A_%s,%d\n", NAMES[i], caught[0][i]); for (int i = 0; i < 7; i++) printf("C_%s,%d\n", NAMES[i], caught[1][i]); for (int i = 0; i < 7; i++) printf("B_%s,%d\n", NAMES[i], caught[2][i]); return 0; }
printf("同一批错法(第 42 章第 ⑧ 步那张清单),三档数据各 %d 轮:\n\n", rounds); printf(" %-16s %12s %12s %12s %s\n", "错法", "A 顺手随机", "A' 配边界", "B 放宽题面", "结论"); printf(" %-16s %12s %12s %12s\n", "----------------", "------------", "------------", "------------"); for (int i = 0; i < 7; i++) { const char* verdict = ""; if (caught[0][i] == 0 && caught[1][i] == 0 && caught[2][i] > 0) verdict = "★ 题面结构上挡死了它"; else if (caught[0][i] == 0 && caught[1][i] > 0) verdict = "★ 题面允许,只是顺手造不到"; else if (caught[0][i] > 0) verdict = "白送"; printf(" %-16s %9d/%d %9d/%d %9d/%d %s\n", NAMES[i], caught[0][i], rounds, caught[1][i], rounds, caught[2][i], rounds, verdict); } printf("\n 档 A 里 b = 0 的有 %d 轮,其中 a ≡ 0 (mod p) 的有 %d 轮 —— wrongZero 靠的就是后面这个数。\n", b0Rounds, azeroRounds); printf("\n ★ 那条溢出线:64 位乘法装得下的最大 p 是 %lld(≈ %.2f×10⁹),\n", ovLimit, ovLimit / 1e9); printf(" 而题面给到 2³¹ - 1 = 2147483647(≈ 2.15×10⁹)—— 余量 %.3f 倍,正好是 √2。\n", (double)ovLimit / 2147483647.0); printf(" ⇒ **long long 刚好够,不用 __int128** —— 这是算出来的,不是猜的。\n"); return 0;}点「运行 ▶」看结果
三档数据,同一批错法,各 300 轮:
| 第 42 章列的错法 | 那一章说它靠什么现形 | A 顺手随机 | A’ 配边界 | B 放宽题面 | 结论 |
|---|---|---|---|---|---|
wrongLoop while (b > 1) |
b ≥ 1 | 257 | 0 | 206 | 白送 |
wrongOrder 先平方再判位 |
几乎白送 | 257 | 0 | 206 | 白送 |
wrongNeg 不归一化负数 |
a < 0 | 0 | 0 | 55 | ★ 题面挡死 |
wrongOverflow 乘法只用 64 位 |
p > 3.04×10⁹ | 0 | 0 | 51 | ★ 题面挡死 |
wrongOne 初值写成 1 |
p = 1 且 b = 0 | 0 | 0 | 9 | ★ 题面挡死 |
wrongZero 0⁰ 当成 0 |
a ≡ 0 且 b = 0 | 0 | ★ 300 | 0 | ★★ 题面允许,顺手造不到 |
wrongNeg:题面写着0 ≤ a⇒ 负数根本进不来。wrongOne:题面写着2 ≤ p⇒1 % p恒等于1,那一句改不改结果一样。 ⚠ 而第 42 章那一章说得很准:它要「p = 1且b = 0同时成立」—— 这道题把p = 1直接划掉了。- ★★★
wrongOverflow最值得算:64 位乘法装得下的最大p是 3 037 000 500((p-1)² < 2⁶³解出来的),而题面给到2³¹ - 1 = 2 147 483 647。 ⇒ 余量 1.414 倍 —— 正好是 √2(一个是 2 的 31 次方,一个是 31.5 次方)。 所以这道题long long刚好够,不用__int128。
⇒ 同一句提醒,换一道题就能从「命门」变成「用不上」。判据只有一个:把范围代进去算。
(第 10 章 P1068 那条的同一族:那道题查的是「别用 1.5 会掉精度」,
这道题查的是本书自己写的六条。)
深入:「0 次」的两种原因(进阶)
wrongZero 在顺手随机那一档也是 0 / 300,看起来和上面三条一样。但它不一样。
第 42 章说它要「a ≡ 0 且 b = 0」。题面确实把 a = 0 且 b = 0 划掉了(a + b > 0)——
可 a ≡ 0 (mod p) 不等于 a = 0:a = p、2p、3p 都行,而且它们都大于 0。
⇒ 造一档仍然在题面之内、但把两个边界配在一起的数据(b = 0 且 a = k·p),
它当场变成 ★ 300 / 300。
★★★ 这给了一个可以照做的判据 —— 对拍抓到 0 次时,造两档:
| 造哪一档 | 它活了说明 |
|---|---|
| 放宽题面的约束 | 题面结构上挡死了它 ⇒ 这条提醒对这道题用不上 |
| 仍在题面内,但把边界配到一起 | 题面允许它 ⇒ 只是你的生成器造不到,必须补这一档 |
⇒ 第 5 章 P1042 说过「抓不到分两种:概率低 vs 结构上不可能」; 这一页给的是怎么把这两种分开。
5★ 这道题真正的溢出线:32 位
// P1226 ⚠ 错法:把 res / a / p 都放进 32 位//// 这是这道题**唯一一个真的会因为溢出而挂**的写法:// p 到 2³¹-1 ⇒ `res * a` 最大 (p-1)² ≈ 4.6×10¹⁸,32 位装 4.29×10⁹ 都够呛。//// ★ 对照第 42 章那条「乘法要用 __int128」:那一章的 p 能到 10¹⁸,所以非 __int128 不可;// 这道题 p < 2³¹ ⇒ (p-1)² < 2⁶² ⇒ **long long 刚好够**(余量 √2,见 p1226Six.cpp)。// ⇒ **同一句提醒,换一道题就从「命门」变成「用不上」。判据只有一个:把范围代进去算。**//// ⚠ 这里写 unsigned 而不是 int:有符号溢出是 UB,-O2 下不可复现,页面上的数就钉不住了。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
int main() { ll a, b, p; if (scanf("%lld %lld %lld", &a, &b, &p) != 3) return 0; ll a0 = a, b0 = b;
unsigned res = (unsigned)(1 % p), aa = (unsigned)(a % p), pp = (unsigned)p; while (b) { if (b & 1) res = res * aa % pp; // ⚠ 32 位相乘,回绕 aa = aa * aa % pp; b >>= 1; } printf("%lld^%lld mod %lld=%u\n", a0, b0, p, res); return 0;}点「运行 ▶」看结果
p 到 2³¹ - 1 ⇒ res * a 最大 (p-1)² ≈ 4.6 × 10¹⁸,32 位连 4.29 × 10⁹ 都装不下。
⚠ 而它样例照过(p = 9,一切都很小)。四档 300 轮:
| 生成器 | 被抓 |
|---|---|
| level 0 顺手随机 | 262 / 300 |
level 1 配边界(b = 0) |
0 / 300 ← b = 0 时一次乘法都不做 |
| level 2 顶格 | ★ 300 / 300 |
level 3 b ≤ 30 |
289 / 300 |
深入:两道题的溢出线对照(进阶)
把这道题和第 42 章并排看:
| 第 42 章 | P1226 | |
|---|---|---|
p 的上限 |
10¹⁸ |
2³¹ - 1 |
| 32 位够吗 | ✗ | ✗ 262/300 被抓 |
| 64 位够吗 | ✗ 要 __int128 |
★ 够,余量 √2 |
⇒ 「乘法要用 __int128」这句话没有主语就是错的 ——
主语是 p 的上限,而那条线是 3 037 000 500。
6生成器:四档,第 ② 档是这一页的主角
// 数据生成器(P1226 对拍用):`./p1226Gen <seed> [level]`//// level 0(默认)★ **照题面顺手随机**:0 <= a,b < 2³¹,2 <= p < 2³¹// level 1 ★★ **在题面内把两个边界配在一起**:b = 0 **且** a 是 p 的倍数// —— 专门给 `wrongZero` 用的。题面允许,顺手随机造不出来。// level 2 顶格:p 和 b 都贴着 2³¹-1(量的是速度,不是正确性)// level 3 b 很小(<= 30)—— 这一档连乘版跑得动,用来验「连乘 ≡ 快速幂」//// ⚠ 题面保证 a + b > 0,生成器必须照办(a = b = 0 那一组不合法,答案也没定义)。
#include <bits/stdc++.h>using namespace std;static mt19937_64 rng;static long long ri(long long lo, long long hi) { return lo + (long long)(rng() % (unsigned long long)(hi - lo + 1));}
int main(int argc, char** argv) { rng.seed((argc > 1) ? (unsigned long long)atoll(argv[1]) : 1ull); int level = (argc > 2) ? atoi(argv[2]) : 0;
long long a, b, p; if (level == 1) { p = ri(2, 1000000); a = p * ri(1, 3); // ★ a ≡ 0 (mod p),而且 a > 0 b = 0; // ★ 同时 b = 0 } else if (level == 2) { p = ri(2147000000LL, 2147483647LL); b = ri(2147000000LL, 2147483647LL); a = ri(2147000000LL, 2147483647LL); } else if (level == 3) { p = ri(2, 2147483647LL); b = ri(0, 30); a = ri(0, 2147483647LL); if (a + b == 0) a = 1; } else { p = ri(2, 2147483647LL); b = ri(0, 2147483647LL); a = ri(0, 2147483647LL); if (rng() % 7 == 0) b = 0; if (a + b == 0) a = 1; } printf("%lld %lld %lld\n", a, b, p); return 0;}点「运行 ▶」看结果
// P1226 ⚠ 错法:顺手加了一句「底数是 0 就直接返回 0」//// 这是第 42 章清单里的 `wrongZero`,那一章说它要「**a ≡ 0 且 b = 0** 同时成立」才现形。//// ★ 这道题的题面把 `a + b > 0` 写死了 —— 于是 a = 0 且 b = 0 确实不可能。// 但**「a ≡ 0 (mod p)」不等于「a = 0」**:a = p、2p、3p… 都行,而且它们都 > 0。// ⇒ 这个错法在这道题上**是能发生的**,只是顺手写的生成器造不出来// (要 b = 0 和 a 是 p 的倍数**同时**成立)。//// ⚠ 它和 wrongNeg / wrongOne / wrongOverflow 那三个不是一回事 ——// 那三个是**题面结构上挡死的**,放宽题面才能活。// 分辨方法见 p1226Six.cpp 那张三档表。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
int main() { ll a, b, p; if (scanf("%lld %lld %lld", &a, &b, &p) != 3) return 0; ll a0 = a, b0 = b;
ll res; if (a % p == 0) { res = 0; // ⚠ 就是这一句:0⁰ 也被它吞了 } else { res = 1 % p; a %= p; while (b) { if (b & 1) res = res * a % p; a = a * a % p; b >>= 1; } } printf("%lld^%lld mod %lld=%lld\n", a0, b0, p, res); return 0;}点「运行 ▶」看结果
| 档 | 造什么 | 干什么用的 |
|---|---|---|
| level 0 | 照题面顺手随机 | 基线 —— 抓 Fmt 300、Int 262、Zero 0 |
| ★ level 1 | b = 0 且 a = k·p |
唯一能抓到 Zero 的一档:300 / 300 |
| level 2 | p、b 都贴着 2³¹-1 |
Int 在这儿是 300 / 300 |
| level 3 | b ≤ 30 |
这一档连乘版跑得动 ⇒ 验「连乘 ≡ 快速幂」0 次不一致 |
7一张总表
| 版本 | 做法 | 样例 | 顺手随机 300 轮 | 配边界 300 轮 | 顶格 | 结果 |
|---|---|---|---|---|---|---|
① p1226Fmt |
只输出一个数 | ✗ | ✗ 300/300 被抓 | ✗ 300/300 | — | ✗ 0 分 |
② p1226Mul |
连乘 O(b) |
✓ | ✓ | ✓ | 外推 11.2 秒 | ✗ TLE |
③ p1226Int |
快速幂,32 位 | ✓ | ✗ 262/300 被抓 | ✓(b=0) |
✗ 300/300 | ✗ WA |
④ p1226Zero |
快速幂 +「a≡0 返回 0」 |
✓ | ★ ✓ 0/300 | ✗ 300/300 | ✓ | ✗ WA |
⑤ p1226 |
快速幂 + long long + 格式 |
✓ | ✓ | ✓ | 0.00 秒 | ★ AC |
8★★ 第 46 章补的一节:那两行 b & 1 / b >>= 1,就是在读 b 的二进制
深入:为什么补这一节(进阶)
这道题同时挂在第 12 章、第 42 章和
第 46 章的题单上。前七步讲的是溢出、取模和输出格式(第 12 / 42 章的角度)——
拿第 46 章的关键词 grep 这一页:位运算 / 二进制 / 移位 / 异或,0 处命中。
⇒ 所以第 46 章要的那个角度,这一页一个字都还没写。
★★ 这是本书第六次「命中登记表 ≠ 不用写」(前五次:P1776 / P1177 / P2367 / P3374 / P1044)。
⚠⚠ 而它更值得记一笔:同一页在第 42 章那一轮是「命中且真的一个字都不用改」的第一个反例
(当时 grep 快速幂 命中 7 处,正解就是它,六个错法逐个算过)——
换到第 46 章就又要补了。
⇒ ★★★ 「不用写」是对某一章说的,不是对那一页说的。
13 = 1101₂ = 8 + 4 + 1
⇒ a¹³ = a⁸ × a⁴ × a¹而 a¹、a²、a⁴、a⁸ … 这一串,每一个都是上一个自乘一次。
于是快速幂的循环体就写出来了 —— 它只干两件事:
| 那一行 | 第 46 章第 8 步那张表里的哪个动作 | 它在问什么 |
|---|---|---|
if (b & 1) res = res * base % p; |
判奇偶(取最低位) | 「b 的当前最低位是 1 吗?是就把这个 a^(2^k) 乘进去」 |
b >>= 1; |
右移 | 「这一位看完了,丢掉,下一位挪到最低位来」 |
base = base * base % p; |
— | 「a^(2^k) 变成 a^(2^(k+1))」 |
⇒ ★★ 循环走几轮 = b 的二进制有几位。 而不是「b 有多大」——
这就是「O(log b)」这三个字的字面意思。
// P1226 补的这一节:★★★ 快速幂那两行 `b & 1` / `b >>= 1`,就是在**读 b 的二进制**// ./p1226Bits 人话版(拿 b = 13 走一遍,再把三笔账算完)// ./p1226Bits csv 给 check:viz 用//// ★ 立论一行:把 b 写成二进制 `b = 2^{k1} + 2^{k2} + …` ⇒ `a^b = a^{2^{k1}} × a^{2^{k2}} × …`// 循环里 base 依次是 a^1, a^2, a^4, a^8 …(每轮自乘一次);// `b & 1` 问的是「**当前最低位要不要**」,`b >>= 1` 是「**这一位看完了,丢掉**」。// ⇒ 所以轮数 = **b 的二进制位数**,乘法次数 = 位数 + popcount(b) − 1。// (最后一轮的自乘是白做的,所以减 1。)//// 三件事:// ① 对 b = 1..10⁶ 逐个验「乘法次数 ≡ 位数 + popcount − 1」;// ② ★★ 称一下题面那句 `0 ≤ b` 的分量:**b 允许为负的话,`b >>= 1` 永远停不下来**// (算术右移,−1 >> 1 还是 −1 —— [第 46 章第 9 步](/ch/46-bitwise/)证过);// ⚠ 演示死循环要先给它一个出口([第 30 章 B3625](/sol/b3625/) 那条)。// ③ 这道题的倍数:b 顶格 2³¹−1 ⇒ 连乘 21 亿轮 vs 快速幂 31 轮。#include <bits/stdc++.h>using namespace std;
/** 快速幂,顺手数一数它做了几次乘法、走了几轮 */static long long qpow(long long a, long long b, long long p, long long* muls, long long* rounds) { long long res = 1 % p, base = a % p, mu = 0, rd = 0; while (b) { rd++; if (b & 1) { res = res * base % p; mu++; } // ★ 这一位是 1 ⇒ 把 a^{2^k} 乘进去 b >>= 1; // ★ 这一位看完了 if (b) { base = base * base % p; mu++; } // ★ base 变成下一个 a^{2^{k+1}} } if (muls) *muls = mu; if (rounds) *rounds = rd; return res;}
int main(int argc, char** argv) { bool csv = (argc > 1 && argv[1][0] == 'c');
/* ---------- ① 乘法次数 ≡ 位数 + popcount − 1 ---------- */ long long bad = 0, checked = 0, badSelf = 0; // badSelf:拿一个**已知错**的公式(少减那个 1)验这段代码是活的 for (long long b = 1; b <= 1000000; b++) { long long mu, rd; qpow(3, b, 1000000007LL, &mu, &rd); long long bits = 64 - __builtin_clzll((unsigned long long)b); long long pc = __builtin_popcountll((unsigned long long)b); checked++; if (rd != bits || mu != bits + pc - 1) bad++; if (mu != bits + pc) badSelf++; // ⚠ 自检:这一行必须**不是** 0 }
/* ---------- ② 称「0 ≤ b」这句保证:b 为负时 b >>= 1 永远停不下来 ---------- */ /* ⚠ 给它一个出口,否则挂住的是自己的闸门 */ const long long CAP = 1000; long long negRounds = 0; { long long b = -13; while (b && negRounds < CAP) { b >>= 1; negRounds++; } } long long posRounds = 0; { long long b = 13; while (b && posRounds < CAP) { b >>= 1; posRounds++; } }
/* ---------- ③ 顶格的倍数 ---------- */ long long bMax = 2147483647LL; long long fastRounds = 64 - __builtin_clzll((unsigned long long)bMax); long long ratio = bMax / fastRounds;
if (csv) { printf("checked,%lld\nbad,%lld\nbadSelf,%lld\n", checked, bad, badSelf); printf("negRounds,%lld\nposRounds,%lld\ncap,%lld\n", negRounds, posRounds, CAP); printf("bMax,%lld\nfastRounds,%lld\nratio,%lld\n", bMax, fastRounds, ratio); return 0; }
printf("★ 拿 b = 13 = 1101₂ 走一遍(a = 3,p = 10⁹+7):\n\n"); { long long a = 3, b = 13, p = 1000000007LL; long long res = 1 % p, base = a % p; int k = 0; printf(" 轮 b 的二进制 最低位 base = a^(2^k) res\n"); while (b) { char bin[40]; int len = 0; long long tb = b; while (tb) { bin[len++] = char('0' + (tb & 1)); tb >>= 1; } for (int i = 0; i < len / 2; i++) { char c = bin[i]; bin[i] = bin[len - 1 - i]; bin[len - 1 - i] = c; } bin[len] = 0; printf(" %d %-12s %lld 3^%-3d = %-8lld", k, bin, b & 1, 1 << k, base); if (b & 1) { res = res * base % p; printf(" × -> %lld\n", res); } else printf(" (跳过) %lld\n", res); b >>= 1; if (b) base = base * base % p; k++; } printf("\n 13 = 8 + 4 + 1 ⇒ 3^13 = 3^8 × 3^4 × 3^1 = %lld\n\n", res); } printf(" ① b = 1..%lld 逐个验「轮数 = 位数」且「乘法次数 = 位数 + popcount − 1」:对不上 %lld 个\n", checked, bad); printf(" ⚠ 自检:同一段代码去验一个已知错的公式(少减那个 1)—— 对不上 %lld 个\n\n", badSelf); printf(" ② 题面那句 0 ≤ b 是命门:b = 13 时 `b >>= 1` 走 %lld 轮就停;\n", posRounds); printf(" b = −13 时走满 %lld 轮的上限还没停(算术右移,负数右移永远是 −1)\n\n", negRounds); printf(" ③ b 顶格 %lld:连乘要 %lld 轮,快速幂只要 %lld 轮 ⇒ 差 %lld 倍\n", bMax, bMax, fastRounds, ratio); return 0;}点「运行 ▶」看结果
深入:三笔账的计数细节(进阶)
-
★★ 轮数和乘法次数都能写成关于二进制的精确等式,
b = 1..10⁶逐个验、对不上 0 个:轮数 =
b的二进制位数;乘法次数 = 位数 +popcount(b)− 1。(每轮必有一次
base自乘(最后一轮那次是白做的,所以减 1), 而「这一位是 1」时才多一次res乘 —— 那正好是popcount(b)次。) ⚠ 这个 0 配了自检:同一段代码去验一个少减了 1 的公式,当场对不上 10⁶ 个。 ★ 而它和第 38 章那条「sum(r)的步数正好等于popcount(r)」是同一把尺子 —— 树状数组和快速幂,量的都是「二进制里有几个 1」。 -
★★★ 题面那句
0 ≤ b是命门,而挡住的正是>>那一行。 负数的右移是算术右移(左边补进来的是符号位)⇒−1 >> 1还是−1,b >>= 1永远停不下来。实测:b = 13走 4 轮就停,b = −13走满 1000 轮的上限还没停。 ⇒ ★ 这也是为什么这里写b & 1而不是b % 2 == 1: 后者对负奇数是false(−7 % 2 == −1),是一个更难查的 bug (第 46 章第 10 步量过)。 -
★ 这道题的倍数就是这么来的:
b顶格2³¹−1, 连乘要 2147483647 轮,快速幂只要 31 轮 ⇒ 差 6927 万倍。 ⇒ 第 ③ 步那个「连乘版外推 11.2 秒」的账,在这儿有了一个不依赖秒表的说法。
- ★★★ 一句听起来有道理的提醒,是不是这道题的命门,只能算,不能感觉。
第 42 章列的六个错法,代进这道题的范围之后三个是结构性地发生不了的:
a ≥ 0挡死负数、p ≥ 2挡死1 % p、 而p < 2³¹让long long刚好够 —— 那条线是 3 037 000 500,余量正好是 √2。 - ★★★ 对拍抓到 0 次的时候,造两档就能知道原因是哪一种。
放宽题面才活 ⇒ 题面挡死了它(这条提醒用不上);
仍在题面内、把边界配到一起就活 ⇒ 题面允许它(你的生成器缺一档)。
wrongZero就是后者:顺手随机0/300,配上b = 0且a = k·p立刻 300/300。 - ★★ 和算法无关的那一条,才是这道题最先挂人的:输出
a^b mod p=s,不是一个数。 而它对拍抓得到 —— 前提是参照物也照格式输出、比对不许strip。 - ★★★
b & 1和b >>= 1不是「写得花哨」,它们就是「读 b 的二进制」这句话本身(第 ⑧ 步)。 轮数 = 位数、乘法次数 = 位数 +popcount(b)− 1,两条都是能验的等式; 而题面那句0 ≤ b挡住的正是>>—— 负数右移永远停不下来。