题单 · 习题解析

洛谷 P1217 [USACO1.5] 回文质数 Prime Palindromes

★★★ 关键一步是**把两件事的顺序调过来**:一亿个数里回文数只有 **19 998** 个 ⇒ 造回文再判质数,比一个一个审少看 **5001 倍**;★ 「偶数位回文必被 11 整除」(交错和恒为 0,9999 个全枚举、0 个例外、唯一的质数就是 11)**只值 2.00 倍** ⇒ **一个技巧好不好看和它值多少钱是两件事**;⚠⚠ 而这一页有两条草稿被实测打回来:**「一亿个数逐个审必超时」是错的**(本机 0.69 秒 / 时限 1 秒 —— 判回文排在短路的前面,八次乘除否掉 99.98%),⚠ 而**「那就筛」比不筛还慢 1.27 倍**(0.88 秒 + 95.4 MB)⇒ ★★★ 一句不用跑程序的判据:**「该不该筛」比的是「值域」和「你要判多少次质数」** —— 同章三道题给出三种结局(P1075 比值 5×10⁻¹⁰ 筛都筛不动 / 本题 2×10⁻⁴ 筛了更慢 / P1865 ≈1 必须筛);★★ 三个错法的触发条件**全长在 a 的最下端**(a ≤ 11 / a ≤ 5 / a ≤ 1)⇒ 顺手写的生成器 300 轮只抓到 2 次,**而官方样例 `5 500` 一测就死两个** —— [「样例比对拍还狠」](/sol/p2249/)第二次,原因这次说得清:**出题人照着边界挑样例,随机生成器不会**;★ 九个格子「触发 ≡ 抓获」一个不差

原题:洛谷 P1217出自 第 41 章 质数:试除 → 埃氏筛 → 线性筛 的题单题面本地存档:2026-09-02
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

因为 151 既是一个质数又是一个回文数(从左到右和从右到左是看一样的),所以 151 是回文质数。

写一个程序来找出范围 [a, b](5 ≤ a < b ≤ 100,000,000,一亿)间的所有回文质数。

输入格式

第一行输入两个正整数 a 和 b。

输出格式

输出一个回文质数的列表,一行一个。

提示

提示 1:找出所有的回文数再判断它们是不是质数(素数)。

提示 2:要产生正确的回文数,你可能需要几个像下面这样的循环。产生长度为 5 的回文数:

for (d1 = 1; d1 <= 9; d1+=2) {    // 只有奇数才会是素数
    for (d2 = 0; d2 <= 9; d2++) {
        for (d3 = 0; d3 <= 9; d3++) {
            palindrome = 10000*d1 + 1000*d2 + 100*d3 + 10*d2 + d1;//(处理回文数...)
        }
    }
}

题目翻译来自 NOCOW。USACO Training Section 1.5。

时限 1 秒,内存 125 MB。

输入输出样例

输入

5 500

输出

5
7
11
101
131
151
181
191
313
353
373
383

[5, 500] 里的回文质数,一行一个。⚠ 注意里面有 5,也有 11。

学生版:一页看懂这道题

这道题考什么:把 [a, b] 里的回文质数找齐(5 ≤ a < b ≤ 10⁸,时限 1 秒)。考的不是判质数,是调换两件事的顺序 —— 题面提示 1 说的「先找出回文数再判质数」:10⁸ 以内的回文数只有 19 998 个,比逐个数过去少看 5001 倍。判质数用的还是第 41 章的试除(第 ③ 步:按位数造回文,偶数位只留 11,再逐个试除)。

最容易错的三处

  1. 偶数位整段跳过(第 ④ 步):漏掉 11 自己 —— 9999 个偶数位回文数全枚举了一遍,不被 11 整除的是精确的 0 个,其中是质数的就 11 这一个。
  2. 「末位不能是 5」⇒ 连首位是 5 的也跳过(第 ④ 步):漏掉 5 自己。官方样例 5 500 的 a 正好是题面允许的最小值 5,把前两个错法一测就死。
  3. 把 1 也当成质数(第 ④ 步):题面写着 5 ≤ a,所以它在题面允许的输入上一次都不会错(精确的 0);只有把下界放开(a ∈ [1, 4])才现形 80 / 300。

⚠ 而这三个触发条件(a ≤ 11、a ≤ 5、a ≤ 1)全长在 a 的最下端,顺手写的随机区间几乎落不到那儿 —— 那一档三个 bug 一共只抓到 2 次。

能 AC 的:p1217.cpp(第 ③ 步,造回文再判质数)—— 本机 1.4 毫秒 / 时限 1 秒,快 500 倍。① 逐个审 0.68 秒(独占能过、12 个核占满就过不去,余量只有 1.47 倍),② 先筛再扫 0.88 秒 + 95.4 MB,同样险 —— 而筛法在这道题上是负收益。

下面每一步里灰色的「深入」块是折叠的:那是这些结论怎么实测出来的,第一遍可以跳过。

1第 ① 版:从 a 数到 b,一个一个审

p1217Brute.cpp第 ① 版:逐个判回文 + 判质数 —— 参照物,⚠ 而它比你以为的快得多
// 第 ① 版:从 a 数到 b,一个一个判「是不是回文」+「是不是质数」—— 参照物
//
// 这是所有人真实的第一反应,也是**唯一一条和「造回文数」完全无关的路**,
// 本页所有对拍拿它当标准答案。
// ⚠ 顶格 b = 10⁸:光外层就一亿次,每个数还要拆位 + 试除 ⇒ 想都不用想。
#include <bits/stdc++.h>
using namespace std;
static bool isPrime(int x) {
if (x < 2) return false;
for (long long i = 2; i * i <= x; i++) if (x % i == 0) return false;
return true;
}
static bool isPal(int x) {
int t = x, r = 0;
while (t) { r = r * 10 + t % 10; t /= 10; }
return r == x;
}
int main() {
int a, b;
if (scanf("%d %d", &a, &b) != 2) return 0;
for (int x = a; x <= b; x++)
if (isPal(x) && isPrime(x)) printf("%d\n", x);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
深入:一亿个数一个一个审到底多快(进阶)
⚠⚠ 「一亿个数一个一个审,肯定超时」—— 又是想当然,草稿第二次被打回
顶格 a = 5, b = 10⁸(3 次取中位数)
★ 真独占 ★ 0.68 秒 / 时限 1 秒(余量 1.47 倍)
⚠ 12 个核占满时 ⚠ 0.97 ~ 1.10 秒 ⇒ 过不去了

⚠⚠⚠ 这一格的历史本身就是一课,而且是订正过的: 2026-09-11 重测量到 1.16 秒,当时记成「机器换核了」(.wslconfig 的 processors 8 → 12); 2026-09-06 再测,独占又回到 0.68 秒 —— 和换核之前一模一样。 ⇒ ★★★ 换核根本没让它变慢,是量的时候机器不空。 (同一个误诊在 P2367 和 P1045 上各出现过一次,这是第三次。)

⇒ ★★★ 而订正之后这一格更值钱了:「它能不能过」不取决于你改了什么,取决于评测机当时忙不忙 —— 而那件事你控制不了。这就是硬规矩第 5 条那句 「优先写倍数,秒数只当量级」的字面意思: 一个只有 1.47 倍余量的结论,撑不过评测机上的一次调度。

为什么它不慢:isPal(x) && isPrime(x) 是短路的,而判回文排在前面 —— 八次乘除就把 99.98% 的数否掉了,真正走到试除那一步的只有 19 998 个。

⇒ 这是「复杂度不等于耗时,循环体里有什么才算」的第三次 (前两次是上一章的 P1372 和本章的 P1865)—— ⚠ 而这一次的结论要反过来说:它不是过不了,是余量太薄,赌不起。 (洛谷的评测机比本机慢,那道题的讨论区里满屏都是「3 个 TLE」。)

2⚠ 第 ② 版:「这一章刚学了筛,那就筛」—— 而它比不筛还慢

p1217Sieve.cpp⚠ 第 ② 版:埃氏筛到 b,再扫一遍挑回文 —— 纯亏
深入:筛法在这道题上是负收益(进阶)
★★★ 这一页最反常识的一格:筛法在这道题上是「负收益」
顶格 b = 10⁸(3 次取中位数) 真独占 ⚠ 12 个核占满 内存
第 ① 版(不筛,逐个试除) 0.68 秒 0.97 ~ 1.10 几乎为 0
⚠ 第 ② 版(先筛再扫) 0.88 秒 0.94 ~ 1.04 95.4 MB / 限制 125 MB(余量 1.31 倍)
倍数(筛法 ÷ 暴力) ★ 慢 1.24 倍 ⚠ 0.95(反过来快一点点)

⚠⚠⚠ 这一格 2026-09-11 记成「翻面了,因为换核」—— 2026-09-06 重测发现那是误诊。 真独占下「筛法更慢 1.24 倍」照旧成立;会翻面的是满载那一列 (两版一起变慢,而暴力慢得更多 ⇒ 比值滑到 0.95)。 ⇒ ★★★ 所以这一页真正稳的不是「谁快」(那取决于机器忙不忙),是下面这句机器无关的话:

筛法把「判一个数是不是质数」从试除降到查表 —— 可它得先为一亿个数各划 2.43 亿次,而这道题真正需要判质数的只有 19 998 个。 ⇒ 两版从头到尾在同一个量级上(独占 0.88 vs 0.68 秒,满载 1.0 vs 1.0), 而多出来的 95.4 MB 内存和 2.43 亿次划表一分钱都没换来。

⇒ ★★★ 一句不用跑程序的判据:

「该不该筛」比的是「值域」和「你要判多少次质数」。

同一章三道题,这个比值给出三种结局:

值域 要判几次 比值 该不该筛
P1075 2 × 10⁹ 1 次 5 × 10⁻¹⁰ ✗ 筛都筛不动(1907 MB)
★ 本题 10⁸ 19 998 次 2 × 10⁻⁴ ✗ 能筛,但筛了更慢
P1865 10⁶ 询问覆盖整段 ≈ 1 ★ 必须筛

⇒ 和第 38 章 P2367 那条(「所有修改是不是都在所有询问之前」) 是同一个形状:一句话就能判,而且判完连测都不用测。

3★★ 关键一步:把两件事的顺序调过来

★★★ 一亿个数里,回文数只有 19 998 个

题面提示 1 说的就是这件事,可它值多少倍要自己乘一遍:

深入:回文数逐位数的个数(进阶)
位数 1 2 3 4 5 6 7 8 合计
≤ 10⁸ 的回文数 9 9 90 90 900 900 9000 9000 ★ 19 998

⇒ 「枚举回文数」比「枚举所有数」少看 5001 倍。 造法就是题面提示 2 那几层循环:只枚举前一半,后一半镜像过去。

★ 再省一半:位数是偶数的回文数,一定被 11 整除

把一个回文数按位置交替加减(a₁ − a₂ + a₃ − …),能被 11 整除当且仅当这个交错和是 11 的倍数。

偶数位的回文数:第 i 位和第 2k+1−i 位相等,而它们在交错和里的符号正好相反 ⇒ 一一抵消 ⇒ 交错和 恒等于 0 ⇒ 必被 11 整除。

深入:这条性质的全枚举与称重(进阶)
量的是(不是抽样,是把 9999 个偶数位回文数全部枚举了一遍) 实测
不被 11 整除的 ★ 0 个
其中是质数的 ★ 1 个 —— 就是 11 自己

⚠ 而这条漂亮性质要称一次重:它把候选从 19 998 砍到 10 000, 只值 2.00 倍 —— 真正值 5001 倍的是上面那一步「把顺序调过来」。 ⇒ ★ 一个技巧好不好看,和它值多少钱,是两件事。

p1217.cpp★ 正解:按位数造回文数(偶数位只留 11),再试除判质数
// P1217 回文质数 —— 正解:**先造回文数,再判质数**
//
// ★★ 关键一步是把两件事的顺序调过来:
// [a, b] 里最多有 10⁸ 个数,可 **≤ 10⁸ 的回文数只有 19 998 个** ——
// 「枚举回文数」比「枚举所有数」少了四个数量级。
//
// ★★★ 再省一半:**位数是偶数的回文数一定能被 11 整除**(证明见页面第 3 步)
// ⇒ 偶数位里唯一的质数就是 11 本身。⇒ 真正要试的只有
// 1、3、5、7 位那 9 + 90 + 900 + 9000 = **9999 个**,外加 11。
//
// ⇒ 每个候选再试除到 √x(≤ 3163),总量小到微不足道。
#include <bits/stdc++.h>
using namespace std;
static bool isPrime(int x) {
if (x < 2) return false;
if (x % 2 == 0) return x == 2;
for (int i = 3; (long long)i * i <= x; i += 2) if (x % i == 0) return false;
return true;
}
/** 把前半段 h 镜像成一个 len 位的回文数 */
static int build(int h, int len) {
int x = h;
int rest = len / 2; // 要镜像回去的位数
int t = (len % 2) ? h / 10 : h; // 奇数位时中间那位不重复
for (int i = 0; i < rest; i++) { x = x * 10 + t % 10; t /= 10; }
return x;
}
int main() {
int a, b;
if (scanf("%d %d", &a, &b) != 2) return 0;
for (int len = 1; len <= 9; len++) {
if (len % 2 == 0) {
// ★ 偶数位的回文数全都被 11 整除 ⇒ 只有 11 自己是质数
if (len == 2 && a <= 11 && 11 <= b) printf("11\n");
continue;
}
int half = (len + 1) / 2; // 前半段(含中间那位)的位数
int lo = (half == 1) ? 1 : (int)pow(10, half - 1) + 0.5;
int hi = (half == 1) ? 9 : (int)pow(10, half) - 0.5;
for (int h = lo; h <= hi; h++) {
int x = build(h, len);
if (x < a) continue;
if (x > b) break;
if (isPrime(x)) printf("%d\n", x);
}
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
深入:三条路的秒表对照(进阶)
★ 三条路的对照(顶格 a = 5, b = 10⁸,答案都是 779 个数)
版本 本机耗时 内存 交上去
① 逐个审 0.68 秒(满载 1.0) ~0 ⚠ 独占能过、满载过不去
② 先筛再扫 0.88 秒(满载 1.0) 95.4 MB ⚠ 同一个量级,一样险
★ ③ 造回文再判质数 ★ 1.4 毫秒 ~0 ★ AC,快 500 倍

★ 三版输出逐字节相同(顶格 779 行),这也是本页对拍的形状。

4⚠ 三个错法 —— 而这一页是「样例比对拍还狠」的第二个例子

p1217NoEleven.cpp✗ 错法一:偶数位整段跳过 —— 漏掉了 11 自己
p1217Five.cpp✗ 错法二:「末位不能是 5」⇒ 跳过首位是 5 的 —— 漏掉了 5 自己
p1217One.cpp✗ 错法三:把 1 也当成了质数 —— 而题面 5 ≤ a 把它挡死了
p1217Gen.cpp生成器(四档:顺手写的 / 小区间 / 违反题面 / 顶格)
深入:三个错法的四档对拍表(进阶)
★★★ 三个触发条件全长在「a 的下端」,而顺手写的生成器永远够不着
档位(每档 300 轮) ✗ 漏掉 11 ✗ 漏掉 5 ✗ 1 当质数
0 顺手写的(a ∈ [5, 2000]、b ≤ 20000) ⚠ 2 ★ 精确的 0 ★ 精确的 0
1 小区间(a ≤ 20、b ≤ 200) 127 20 ★ 精确的 0
⚠⚠ 2 违反题面(a ∈ [1, 4]) 285 295 ★ 80
★ 满足触发条件的轮数 2 / 127 / 285 0 / 20 / 295 0 / 0 / 80

★★ 九个格子,「满足触发条件」和「真被抓」一个不差 —— 抓不到时别加轮数,去数一数。

★★★ 而第一行才是主角:顺手写的那一档,三个 bug 一共只抓到 2 次, 因为三个触发条件(a ≤ 11、a ≤ 5、a ≤ 1)全都长在 a 的最下端, 而「随机一个区间」几乎不会落到那儿。

⇒ ⚠⚠ 可官方样例 5 500 把前两个都一测就死 —— 它的 a 正好就是题面允许的最小值 5。 ⇒ ★★ 这是「样例可能比对拍还狠」的第二个例子, 而这一次能说清楚原因:出题人挑样例时是照着边界挑的,随机生成器不会。

深入:「精确的 0」的自检(进阶)
★ 第三个错法那个「精确的 0」,配了自检

「把 1 当质数」在题面允许的输入上一次都不会错 —— 因为题面写着 5 ≤ a,1 根本进不来。

⇒ 把那半句放开(档 2:a ∈ [1, 4]),它当场被抓 80 / 300 (★ 而 80 恰好 ≡ a == 1 的轮数)。

⇒ ★★ 同一个动作干两件事(和 P1439 那次同形): 给「精确的 0」做自检 + 称出「5 ≤ a」这半句是谁的命门 —— 它是这一版的命门,对正解一点用都没有(正解本来就写的 x < 2)。

5★ 哪一版就已经能过了

p1217Count.cpp本页所有数字的出处(含 9999 个偶数位回文的全枚举)
// P1217 的度量程序:./p1217Count csv (本页的数字都出自它)
//
// pal : ★★★ ≤ 10⁸ 的回文数一共几个 —— 这一个数就是「把顺序调过来」值多少倍。
// even : ★★ 「偶数位的回文数必被 11 整除」——把它们**全部**枚举一遍验证(不是抽样)。
// ⚠ 顺带给这条漂亮性质**称一次重**:它只值 2 倍,不是主角。
// ans : [5, 10⁸] 里回文质数一共几个。
// ms : 秒表:正解 / 筛法(真在 10⁸ 上筛一次)/ 暴力(在 10⁶ 上量再外推)。
// mem : 筛到 10⁸ 要多少内存,题面给多少。
//
// ⚠ 秒表用 steady_clock 在进程内量,正解跑 20 次取平均
// ([第 38 章那一跤](/sol/p1972/):亚毫秒的秒表不能只跑一次)。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
static const int TOP = 100000000; // 10⁸
static bool isPrime(int x) {
if (x < 2) return false;
if (x % 2 == 0) return x == 2;
for (int i = 3; (long long)i * i <= x; i += 2) if (x % i == 0) return false;
return true;
}
static bool isPal(int x) {
int t = x, r = 0;
while (t) { r = r * 10 + t % 10; t /= 10; }
return r == x;
}
static int build(int h, int len) {
int x = h, rest = len / 2, t = (len % 2) ? h / 10 : h;
for (int i = 0; i < rest; i++) { x = x * 10 + t % 10; t /= 10; }
return x;
}
int main(int argc, char** argv) {
bool csv = (argc > 1 && string(argv[1]) == "csv");
/* ① 把 ≤ 10⁸ 的回文数全部造出来,按位数奇偶分开数 */
long long palOdd = 0, palEven = 0, evenNot11 = 0, evenPrime = 0, ansCount = 0;
int evenPrimeWho = -1;
for (int len = 1; len <= 9; len++) {
int half = (len + 1) / 2;
long long lo = (half == 1) ? 1 : (long long)llround(pow(10, half - 1));
long long hi = (half == 1) ? 9 : (long long)llround(pow(10, half)) - 1;
for (long long h = lo; h <= hi; h++) {
long long x = build((int)h, len);
if (x > TOP) break;
if (len % 2) palOdd++;
else {
palEven++;
if (x % 11 != 0) evenNot11++;
if (isPrime((int)x)) { evenPrime++; evenPrimeWho = (int)x; }
}
if (x >= 5 && isPrime((int)x)) ansCount++;
}
}
/* ② 秒表 */
double usFast = 0;
{
auto t0 = steady_clock::now();
volatile long long sink = 0;
for (int rep = 0; rep < 20; rep++) {
long long c = 0;
for (int len = 1; len <= 9; len++) {
if (len % 2 == 0) { if (11 >= 5 && 11 <= TOP) c++; continue; }
int half = (len + 1) / 2;
long long lo = (half == 1) ? 1 : (long long)llround(pow(10, half - 1));
long long hi = (half == 1) ? 9 : (long long)llround(pow(10, half)) - 1;
for (long long h = lo; h <= hi; h++) {
long long x = build((int)h, len);
if (x > TOP) break;
if (x >= 5 && isPrime((int)x)) c++;
}
}
sink = sink + c;
}
usFast = duration<double, micro>(steady_clock::now() - t0).count() / 20.0;
}
double sv[3], br[3];
for (int rep = 0; rep < 3; rep++) {
{
auto t0 = steady_clock::now();
vector<char> comp((size_t)TOP + 1, 0);
for (long long i = 2; i * i <= TOP; i++)
if (!comp[i]) for (long long j = i * i; j <= TOP; j += i) comp[j] = 1;
volatile long long c = 0;
for (int x = 5; x <= TOP; x++) if (!comp[x] && isPal(x)) c = c + 1;
sv[rep] = duration<double, milli>(steady_clock::now() - t0).count();
}
{
auto t0 = steady_clock::now();
volatile long long c = 0;
for (int x = 5; x <= TOP; x++) if (isPal(x) && isPrime(x)) c = c + 1;
br[rep] = duration<double, milli>(steady_clock::now() - t0).count();
}
}
auto med3 = [](double a, double b, double c) { return max(min(a, b), min(max(a, b), c)); };
double msSieve = med3(sv[0], sv[1], sv[2]);
double msBrute = med3(br[0], br[1], br[2]);
double sieveMB = (double)(TOP + 1) / 1048576.0;
if (csv) {
printf("palOdd,%lld\n", palOdd);
printf("palEven,%lld\n", palEven);
printf("palAll,%lld\n", palOdd + palEven);
printf("evenNot11,%lld\n", evenNot11);
printf("evenPrime,%lld\n", evenPrime);
printf("evenPrimeWho,%d\n", evenPrimeWho);
printf("ansCount,%lld\n", ansCount);
printf("candFast,%lld\n", palOdd + 1);
printf("gainOrder,%.0f\n", (double)TOP / (double)(palOdd + palEven));
printf("gainEven,%.2f\n", (double)(palOdd + palEven) / (double)(palOdd + 1));
printf("usFast,%.0f\n", usFast);
printf("msSieve,%.0f\n", msSieve);
printf("msBrute,%.0f\n", msBrute);
printf("sieveOverBrute,%.2f\n", msSieve / msBrute);
printf("bruteOverFast,%.0f\n", msBrute * 1000.0 / usFast);
printf("sieveMB,%.1f\n", sieveMB);
printf("sieveHeadroom,%.2f\n", 125.0 / sieveMB);
return 0;
}
printf("① ≤ 10⁸ 的回文数:奇数位 %lld 个 + 偶数位 %lld 个 = **%lld 个**\n", palOdd, palEven, palOdd + palEven);
printf(" ⇒ 「先造回文再判质数」比「一个一个审」少看 %.0f 倍的数\n", (double)TOP / (double)(palOdd + palEven));
printf("\n② 偶数位的回文数**全部**被 11 整除:%lld 个里有 %lld 个例外\n", palEven, evenNot11);
printf(" 其中是质数的只有 %lld 个,就是 %d\n", evenPrime, evenPrimeWho);
printf(" ⚠ 而这条漂亮性质只把候选从 %lld 砍到 %lld —— **只值 %.2f 倍**,它不是主角\n",
palOdd + palEven, palOdd + 1, (double)(palOdd + palEven) / (double)(palOdd + 1));
printf("\n③ [5, 10⁸] 里的回文质数一共 %lld 个\n", ansCount);
printf("\n④ 顶格 a = 5、b = 10⁸ 的秒表\n");
printf(" 正解(造回文 + 试除,跑 20 次取平均):%.0f 微秒\n", usFast);
printf(" 筛法(真在 10⁸ 上筛一次再扫):%.0f 毫秒,内存 %.1f MB(题面给 125 MB,余量 %.2f 倍)\n",
msSieve, sieveMB, 125.0 / sieveMB);
printf(" 暴力(一个一个审,真跑 10⁸):%.0f 毫秒 / 时限 1 秒 ⇒ ⚠ **它没超时,余量只有 %.2f 倍**\n",
msBrute, 1000.0 / msBrute);
printf(" ⇒ ★★★ 筛法比暴力还**慢** %.2f 倍 —— 真正要判质数的只有 %lld 个数,而它为一亿个数各划了一遍\n",
msSieve / msBrute, palOdd + palEven);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
只有第 ③ 版是稳的 —— 而前两版「过不过得去」都在 1.5 倍以内
版本 顶格 交上去
① 逐个审 0.68 秒(满载 1.0)/ 1 秒 ⚠ 同一台机器上,独占能过、满载过不去
② 先筛再扫 0.88 秒(满载 1.0)+ 95.4 MB ⚠ 一样险
★ ③ 造回文再判质数 1.4 毫秒 ★ AC

⇒ 这道题的价值恰恰在这儿:它是本章第一道「筛法不是答案」的题 (第 41 章整章都在讲怎么把筛写快,而这道题的正解一次筛都不用)。

深入:余量 1.47 倍算不算过(进阶)

⚠ 而「余量 1.47 倍算不算过」这件事,本书已经吃过两次亏: 第 12 章 P1923 那条断言只赢 4%,在 8 路并行的闸门里被自己晃翻过; 而这一页自己是第二次 —— 同一台机器、同一份代码, 独占 0.68 秒、12 个核占满 1.0 秒,判决当场从「能过」变成「过不去」。 ⚠⚠ 更值钱的是那一次差点被归错因:2026-09-11 量到 1.16 秒时记成了「机器换核了」, 而 2026-09-06 独占重测又回到 0.68 —— 换核没让它变慢,是量的时候机器不空。 ⇒ 秒表的主语是余量,不是量级 —— 1.47 倍那种余量,连评测机上的一次调度都撑不过; ⇒ ★★ 而它为什么变慢这个问题,答案默认是「机器当时在忙」,不是「机器变了」 (第 6 章 P2367、第 42 章 P1045 各中过一次,这是第三次)。