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。
1第 ① 版:从 a 数到 b,一个一个审
// 第 ① 版:从 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;}点「运行 ▶」看结果
顶格 a = 5, b = 10⁸(独占实测,3 次取中位数) |
|
|---|---|
| 本机耗时 | ★ 0.69 秒 / 时限 1 秒 |
| 余量 | ⚠ 只有 1.46 倍 |
为什么它不慢:isPal(x) && isPrime(x) 是短路的,而判回文排在前面 ——
八次乘除就把 99.98% 的数否掉了,真正走到试除那一步的只有 19 998 个。
⇒ 这是「复杂度不等于耗时,循环体里有什么才算」的第三次 (前两次是上一章的 P1372 和本章的 P1865)—— ⚠ 而这一次的结论要反过来说:它不是过不了,是余量太薄,赌不起。 (洛谷的评测机比本机慢,那道题的讨论区里满屏都是「3 个 TLE」。)
2⚠ 第 ② 版:「这一章刚学了筛,那就筛」—— 而它比不筛还慢
顶格 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★★ 关键一步:把两件事的顺序调过来
题面提示 1 说的就是这件事,可它值多少倍要自己乘一遍:
| 位数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 合计 |
|---|---|---|---|---|---|---|---|---|---|
| ≤ 10⁸ 的回文数 | 9 | 9 | 90 | 90 | 900 | 900 | 9000 | 9000 | ★ 19 998 |
⇒ 「枚举回文数」比「枚举所有数」少看 5001 倍。 造法就是题面提示 2 那几层循环:只枚举前一半,后一半镜像过去。
把一个回文数按位置交替加减(a₁ − a₂ + a₃ − …),能被 11 整除当且仅当这个交错和是 11 的倍数。
偶数位的回文数:第 i 位和第 2k+1−i 位相等,而它们在交错和里的符号正好相反
⇒ 一一抵消 ⇒ 交错和 恒等于 0 ⇒ 必被 11 整除。
| 量的是(不是抽样,是把 9999 个偶数位回文数全部枚举了一遍) | 实测 |
|---|---|
| 不被 11 整除的 | ★ 0 个 |
| 其中是质数的 | ★ 1 个 —— 就是 11 自己 |
⚠ 而这条漂亮性质要称一次重:它把候选从 19 998 砍到 10 000, 只值 2.00 倍 —— 真正值 5001 倍的是上面那一步「把顺序调过来」。 ⇒ ★ 一个技巧好不好看,和它值多少钱,是两件事。
// 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;}点「运行 ▶」看结果
| 版本 | 本机耗时 | 内存 | 交上去 |
|---|---|---|---|
| ① 逐个审 | 0.69 秒 | ~0 | ⚠ 本机过、余量 1.46 倍 —— 赌不起 |
| ② 先筛再扫 | 0.88 秒 | 95.4 MB | ⚠ 更险 |
| ★ ③ 造回文再判质数 | ★ 1.4 毫秒 | ~0 | ★ AC,快 500 倍 |
★ 三版输出逐字节相同(顶格 779 行),这也是本页对拍的形状。
4⚠ 三个错法 —— 而这一页是「样例比对拍还狠」的第二个例子
| 档位(每档 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。
⇒ ★★ 这是「样例可能比对拍还狠」的第二个例子,
而这一次能说清楚原因:出题人挑样例时是照着边界挑的,随机生成器不会。
「把 1 当质数」在题面允许的输入上一次都不会错 —— 因为题面写着 5 ≤ a,1 根本进不来。
⇒ 把那半句放开(档 2:a ∈ [1, 4]),它当场被抓 80 / 300
(★ 而 80 恰好 ≡ a == 1 的轮数)。
⇒ ★★ 同一个动作干两件事(和 P1439 那次同形):
给「精确的 0」做自检 + 称出「5 ≤ a」这半句是谁的命门 ——
它是这一版的命门,对正解一点用都没有(正解本来就写的 x < 2)。
5★ 哪一版就已经能过了
// 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;}点「运行 ▶」看结果
| 版本 | 顶格 | 交上去 |
|---|---|---|
| ① 逐个审 | 0.69 秒 / 1 秒 | ⚠ 本机过,余量 1.46 倍 —— 换台评测机就是 TLE |
| ② 先筛再扫 | 0.88 秒 + 95.4 MB | ⚠ 比 ① 还险 |
| ★ ③ 造回文再判质数 | 1.4 毫秒 | ★ AC |
⇒ 这道题的价值恰恰在这儿:它是本章第一道「筛法不是答案」的题 (第 41 章整章都在讲怎么把筛写快,而这道题的正解一次筛都不用)。 ⚠ 而「余量 1.46 倍算不算过」这件事,本书已经吃过一次亏: 第 12 章 P1923 那条断言只赢 4%,在 8 路并行的闸门里被自己晃翻过。 ⇒ 秒表的主语是余量,不是量级。