题单 · 习题解析

洛谷 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,一亿)间的所有回文质数。

输入格式

第一行输入两个正整数 ab

输出格式

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

提示

提示 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

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.69 秒 / 时限 1 秒
余量 只有 1.46 倍

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

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

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

p1217Sieve.cpp⚠ 第 ② 版:埃氏筛到 b,再扫一遍挑回文 —— 纯亏
★★★ 这一页最反常识的一格:筛法在这道题上是「负收益」
顶格 b = 10⁸(独占实测,3 次取中位数) 耗时 内存
第 ① 版(不筛,逐个试除) 0.69 秒 几乎为 0
⚠ 第 ② 版(先筛再扫) 0.88 秒 95.4 MB / 限制 125 MB(余量 1.31 倍)
倍数 慢 1.27 倍

为什么会亏:筛法把「判一个数是不是质数」从试除降到查表 —— 可它得先为一亿个数各划一遍,而这道题真正需要判质数的只有 19 998 个。

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

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

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

值域 要判几次 比值 该不该筛
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.69 秒 ~0 ⚠ 本机过、余量 1.46 倍 —— 赌不起
② 先筛再扫 0.88 秒 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 ≤ 20b ≤ 200 127 20 精确的 0
⚠⚠ 2 违反题面a ∈ [1, 4] 285 295 80
★ 满足触发条件的轮数 2 / 127 / 285 0 / 20 / 295 0 / 0 / 80

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

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

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

★ 第三个错法那个「精确的 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.69 秒 / 1 秒 ⚠ 本机过,余量 1.46 倍 —— 换台评测机就是 TLE
② 先筛再扫 0.88 秒 + 95.4 MB ⚠ 比 ① 还险
★ ③ 造回文再判质数 1.4 毫秒 AC

⇒ 这道题的价值恰恰在这儿:它是本章第一道「筛法不是答案」的题第 41 章整章都在讲怎么把筛写快,而这道题的正解一次筛都不用)。 ⚠ 而「余量 1.46 倍算不算过」这件事,本书已经吃过一次亏: 第 12 章 P1923 那条断言只赢 4%,在 8 路并行的闸门里被自己晃翻过。 ⇒ 秒表的主语是余量,不是量级。