题单 · 习题解析

洛谷 P3383 【模板】线性筛素数

★★★ 这一页第二次订正了**本书自己的正文**:第 41 章第 10 步那两条结论**主语都是 n = 10⁷**,到这道题的 10⁸ 上**一起塌了** —— ① 线性筛从「略输」变成**赢 2.32 倍**;② 「同样的次数、代价差一倍」(埃氏筛 0.04 vs 忘了 break 0.09)变成**只差 4%**(2.32 vs 2.41)。原因是同一个:95 MB 的表装不进任何一级缓存,「扫得连续」和「划得少」两个优势**一起被内存往返淹掉**;★★ 它和同章 [P3912](/sol/p3912/) 是一对而结论相反:那道题只要「有几个」⇒ 素数一个都不用存、表能压成位;这道题要「第 k 小」⇒ **22.0 MB 的 int 表躲不掉**(⚠ 而内存限制正好也从 125 MB 变成 512 MB);★★★ I/O 那张表把题面自己那句提醒拆开称重:写这一侧 **endl → '\n' 值 8.8 倍**(255 → 29 ms,`endl` 的问题是每行 flush 一次),再关同步只值 1.16 倍;读这一侧关同步值 6.2 倍 ⇒ 并排的两条建议**分量差一个数量级**;⚠⚠ 而它仍要打折:最差一套 428 ms、最好 17 ms,**可时限有 3000 ms** ⇒ 默认 cin/cout/endl 那版端到端 0.94 秒,**它能过**;★★ 六个待测版本里对拍只抓得到一个(`pr[k]` 少减 1),其余五个答案全对 ⇒ 关卡不在答案上

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

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

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

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

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

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

题目背景

本题已更新,从判断素数改为了查询第 k 小的素数。

提示:本题输入输出、运算数据量较大。

  • 对于 C++ 语言,如果你使用 cin 来输入输出,建议使用 std::ios::sync_with_stdio(0) 来加速,同时使用 '\n' 换行输出。
  • 对于 Java 语言,使用线性筛并且优化输入输出,也可以在规定时限内通过本题,但是时限可能较紧张。
  • 对于 Python 语言,语言性能差异较大,需要使用到 numpy 库的数组以替代列表,且使用埃氏筛法,依然可以在合适的时间和内存消耗下通过本题。

题目描述

如题,给定一个范围 n,有 q 个询问,每次输出第 k 小的素数。

输入格式

第一行包含两个正整数 nq,分别表示查询的范围和查询的个数。

接下来 q 行每行一个正整数 k,表示查询第 k 小的素数。

输出格式

输出 q 行,每行一个正整数表示答案。

数据范围

对于 100% 的数据,n = 10⁸1 ≤ q ≤ 10⁶保证查询的素数不大于 n

时限 3 秒,内存 512 MB。(Data by NaCly_Fish)

输入输出样例

输入

100 5
1
2
3
4
5

输出

2
3
5
7
11

n = 100,问第 1 ~ 5 小的素数。

1第 ① 版:不筛,每个询问从 2 开始数

p3383Brute.cpp第 ① 版:每问都从头试除数过去 —— 参照物
// 第 ① 版:不筛 —— 每个询问都从 2 开始一个一个试除,数到第 k 个为止
//
// 这是「还没想到筛」的人真实的第一反应,也是本页的参照物。
// ⚠ 顶格 n = 10⁸、q = 10⁶:光第一问就可能要数到第 5 761 455 个素数 ⇒ 想都不用想。
#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;
}
int main() {
int n, q;
if (scanf("%d %d", &n, &q) != 2) return 0;
for (int i = 0; i < q; i++) {
int k;
if (scanf("%d", &k) != 1) break;
int c = 0, x = 1;
while (c < k) { x++; if (isPrime(x)) c++; }
printf("%d\n", x);
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

顶格 n = 10⁸光第一问就可能要数到第 5 761 455 个素数,而这样的询问有 10⁶ 个。 ⇒ 这道题的题名已经把答案写出来了:把素数筛出来、存成一张表,第 k 小就是 pr[k−1]

2★ 正解:线性筛 —— 而这道题非存那张表不可

★★ 它和同章的 P3912 是一对,结论正好相反
要回答的是 素数表 表能不能压成位
P3912 「一共有几个 一个都不用存 ★ 能(6.0 MB)
★ 本题 k 小是谁 躲不掉(π(10⁸) = 5 761 455 个 int = 22.0 MB 位表仍可,但表躲不掉

⚠ 而这道题的内存限制是 512 MB(那道题只有 125)—— ⇒ 同一章的两道模板题,一道要为内存精打细算,一道根本不用想它。 ★ 读数据范围的时候,那两行数字(n 和内存限制)是一起读的

p3383.cpp★ 正解:线性筛 + 素数表 + 快读快写
// P3383【模板】线性筛素数 —— 正解:线性筛把素数表存下来,第 k 小 O(1) 回答
//
// ★★ 这道题和同章的 [P3912](/sol/p3912/) 是一对,而**结论相反**:
// 那道题只要「有几个」⇒ 表可以压成位、素数一个都不用存;
// 这道题要「第 k 小是谁」⇒ **必须把素数按序存下来** ⇒ 那个 int 数组躲不掉。
// ⚠ 而这道题的内存限制是 **512 MB**(那道题只有 125)—— 所以这里根本不用为内存操心。
//
// ★ 那句 break 保证的是「每个合数只被它的**最小**质因子划掉一次」([本章第 6 步](/ch/41-primes/))。
//
// ⚠⚠ 真正会挂人的是 I/O:q 可以到 10⁶ ⇒ 读 8 MB、写 10 MB。
// 题面自己写着「建议 sync_with_stdio(0),并且用 '\n' 换行」——
// 这一页把那句话量了一遍(见 p3383Io.cpp)。
#include <bits/stdc++.h>
using namespace std;
static const int MAXN = 100000000;
static bool comp[MAXN + 1];
static int pr[5761460];
static int cnt = 0;
static char ibuf[1 << 22];
static size_t ilen = 0, ipos = 0;
static inline int gc() {
if (ipos == ilen) { ilen = fread(ibuf, 1, sizeof(ibuf), stdin); ipos = 0; if (!ilen) return -1; }
return ibuf[ipos++];
}
static inline int readInt() {
int c = gc();
while (c != -1 && (c < '0' || c > '9')) c = gc();
int v = 0;
while (c >= '0' && c <= '9') { v = v * 10 + (c - '0'); c = gc(); }
return v;
}
static char obuf[1 << 22];
static size_t opos = 0;
/** ⚠ 缓冲区写满就先吐出去 —— 顶格输出约 10 MB,一次性攒在数组里会溢出
* ([第 12 章那一跤](/sol/p1226/)的输出侧版本:缓冲区开小了会一声不吭地出事)。 */
static inline void flushOut() { fwrite(obuf, 1, opos, stdout); opos = 0; }
static inline void writeInt(int x) {
if (opos + 16 > sizeof(obuf)) flushOut();
char t[12]; int m = 0;
if (!x) t[m++] = '0';
while (x) { t[m++] = char('0' + x % 10); x /= 10; }
while (m) obuf[opos++] = t[--m];
obuf[opos++] = '\n';
}
int main() {
int n = readInt(), q = readInt();
for (int i = 2; i <= n; i++) {
if (!comp[i]) pr[cnt++] = i;
for (int j = 0; j < cnt && (long long)pr[j] * i <= n; j++) {
comp[pr[j] * i] = true;
if (i % pr[j] == 0) break; // ★ 这一句就是「线性」的全部
}
}
for (int i = 0; i < q; i++) {
int k = readInt();
writeInt(pr[k - 1]); // ⚠ 第 k 小 ⇒ 下标 k−1
}
flushOut();
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 那句 break 保证了什么 —— 这道题正好把它量在 n = 10⁸ 上
n = 10⁸(都要顺手收集素数表) 划的次数
★ 线性筛 94 238 544
⚠ 忘了那句 break 242 570 204
埃氏筛(i·i 起) 242 570 204 —— 和上一行一模一样

★★ 最后两行相等不是巧合,本章第 5、6 步证过: 忘了 break 之后,每个合数会被它所有质因子各划一次,而那正是埃氏筛在做的事。

⇒ 那句 break 换来的是 2.57 倍的次数。⚠ 它换来多少时间?见下一节 —— 答案不是 2.57 倍。

3★★★ 而本章第 10 步那两条结论,在 n = 10⁸ 上一起塌了

p3383Erat.cpp⚠ 埃氏筛 + 收集素数表 —— 答案一模一样
p3383NoBreak.cpp⚠ 忘了那句 break —— 答案也一模一样
★★★ 「同样的次数,代价可以差一倍」的主语是 n = 10⁷

第 41 章第 10 步n = 10⁷ 上量出两句话,这道题的 n10⁸

n = 10⁷(本章第 10 步) n = 10⁸(这一页)
线性筛 ÷ 埃氏筛 0.045 / 0.04 ⇒ 线性筛略输 ★ 埃氏筛是线性筛的 2.32 倍线性筛赢
埃氏筛 ↔ 忘了 break(次数相同) 0.04 vs 0.09 ⇒ 差一倍 ★ 2.32 vs 2.41 ⇒ 只差 4%

★★★ 两条结论同时翻面,而原因是同一个n = 10⁸ 的那张 bool 表有 95 MB装不进任何一级缓存。 于是「埃氏筛扫得连续」这个优势、和「线性筛划得少」这个优势, 一起被内存往返淹掉,剩下的只有「你一共碰了多少次内存」。

⇒ ★★ 每一句「谁比谁快」都要带上规模这个主语。 (本章第 10 步已按这一页的实测补了一整段。)

p3383Bit.cpp★ 位压缩筛 + 提取素数表 —— 比线性筛再快 1.90 倍
// ★ 再快一点:先用「只存奇数 + 一位一个数」的埃氏筛,再把素数提取成表
//
// ★★ 这是 [P3912](/sol/p3912/) 那条结论在这道题上的延伸:
// 筛那一步的主角是**表能不能待在缓存里** —— 6.0 MB 的位表比 95.4 MB 的 bool 表快得多。
// ⚠ 但这道题**躲不掉那个 int 素数表**(要回答「第 k 小」)⇒ 内存还是 22 MB 起步,
// 只是题面给了 512 MB,一点不紧。
#include <bits/stdc++.h>
using namespace std;
static const int MAXN = 100000000;
static uint64_t bs[MAXN / 128 + 2];
static int pr[5761460];
static inline bool bget(int k) { return (bs[k >> 6] >> (k & 63)) & 1ULL; }
static inline void bset(int k) { bs[k >> 6] |= 1ULL << (k & 63); }
static char ibuf[1 << 22];
static size_t ilen = 0, ipos = 0;
static inline int gc() {
if (ipos == ilen) { ilen = fread(ibuf, 1, sizeof(ibuf), stdin); ipos = 0; if (!ilen) return -1; }
return ibuf[ipos++];
}
static inline int readInt() {
int c = gc();
while (c != -1 && (c < '0' || c > '9')) c = gc();
int v = 0;
while (c >= '0' && c <= '9') { v = v * 10 + (c - '0'); c = gc(); }
return v;
}
static char obuf[1 << 22];
static size_t opos = 0;
/** ⚠ 缓冲区写满就先吐出去 —— 顶格输出约 10 MB,一次性攒在数组里会溢出
* ([第 12 章那一跤](/sol/p1226/)的输出侧版本:缓冲区开小了会一声不吭地出事)。 */
static inline void flushOut() { fwrite(obuf, 1, opos, stdout); opos = 0; }
static inline void writeInt(int x) {
if (opos + 16 > sizeof(obuf)) flushOut();
char t[12]; int m = 0;
if (!x) t[m++] = '0';
while (x) { t[m++] = char('0' + x % 10); x /= 10; }
while (m) obuf[opos++] = t[--m];
obuf[opos++] = '\n';
}
int main() {
int n = readInt(), q = readInt();
int cnt = 0;
if (n >= 2) {
int half = (n - 1) / 2;
for (long long k = 1; (2 * k + 1) * (2 * k + 1) <= n; k++) {
if (bget((int)k)) continue;
long long p = 2 * k + 1;
for (long long j = p * p; j <= n; j += 2 * p) bset((int)((j - 1) / 2));
}
pr[cnt++] = 2;
for (int k = 1; k <= half; k++) if (!bget(k)) pr[cnt++] = 2 * k + 1;
}
for (int i = 0; i < q; i++) { int k = readInt(); writeInt(pr[k - 1]); }
flushOut();
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 把表缩小仍然是最赚的一步 —— 即使这道题躲不掉那张 22 MB 的素数表
n = 10⁸(独占实测,端到端含 I/O) 耗时 maxRSS
埃氏筛 + 收集 0.95 秒 132 MB
忘了 break 0.93 秒 132 MB
★ 线性筛 0.44 秒 132 MB
★ 位压缩筛 + 提取 0.27 秒 41 MB

⇒ 和 P3912 那一页是同一句话:筛那一步的主角是「表能不能待在缓存里」。 ★ 这道题的时限是 3 秒 ⇒ 四种都过得去;位压缩赢的是余量,不是及格线。

4⚠⚠ 题面自己点名的那件事:I/O —— 量完之后要打个折

p3383Cin.cpp⚠ 默认 cin / cout + endl —— 题面点名的那一版
p3383Io.cpp四种读法 × 五种写法(各 3 次取中位数)
// P3383 的 I/O 账:./p3383Io [csv]
//
// ⚠⚠ **题面自己点名了这件事**:「本题输入输出、运算数据量较大 …… 如果你使用 cin 来输入输出,
// 建议使用 std::ios::sync_with_stdio(0) 来加速,同时使用 '\n' 换行输出。」
// ⇒ 而[流传很广的提醒量完之后要打个折](/sol/p1972/) —— 这一份就是那把秤。
//
// 顶格形状:q = 10⁶ 个询问 ⇒ **读约 7.8 MB、写约 10 MB**(答案最大 9 位数)。
//
// 四种读法 × 五种写法,各跑 3 次取中位数。
// ⚠ 顺序不能换:① 「同步开着」的两种写法和「默认 cin」必须排在关同步**之前**
// (关掉同步之后 cin/cout 会自己缓冲,之后的测量就不是那回事了);
// ② 关同步之后 cin 会预读一大块,freopen 换文件时缓冲里是上一份的残渣
// (第 45 章 read.cpp、第 6 章 p2367Read.cpp、第 35 章 p5788Io.cpp、第 37 章 p3378Io.cpp 都踩过)。
//
// ★ 报的是**绝对毫秒数**(时限 3000 ms)—— [倍数跨题几乎不变,而「够不够」比的是绝对时间](/sol/p1972/)。
#include <bits/stdc++.h>
#include <chrono>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
using namespace std::chrono;
static const int Q = 1000000;
static char inPath[64], outPath[64];
static vector<int> ks, vals;
static void makeData() {
snprintf(inPath, sizeof(inPath), "/tmp/p3383-in-%d.txt", (int)getpid());
snprintf(outPath, sizeof(outPath), "/tmp/p3383-out-%d.txt", (int)getpid());
mt19937 rng(20260902u);
ks.resize(Q); vals.resize(Q);
FILE* fp = fopen(inPath, "w");
if (!fp) { fprintf(stderr, "写不出临时文件\n"); exit(1); }
fprintf(fp, "%d %d\n", 100000000, Q);
for (int i = 0; i < Q; i++) {
unsigned k = 1u + (unsigned)(rng() % 5761455u); // ⚠ 先落到具名变量再传参
ks[i] = (int)k;
unsigned v = 2u + (unsigned)(rng() % 99999998u); // 冒充「第 k 小的素数」,位数一致
vals[i] = (int)v;
fprintf(fp, "%u\n", k);
}
fclose(fp);
}
static void reopenIn() { if (!freopen(inPath, "r", stdin)) { fprintf(stderr, "读不回\n"); exit(1); } }
/* ⚠ 输出重定向用 dup2 而不是 freopen:freopen 回不来
* (这台机器上没有 /dev/tty,第一版就把本程序自己的报告写丢了)。 */
static int savedOut = -1;
static void reopenOut() {
fflush(stdout);
int fd = open(outPath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { fprintf(stderr, "写不出\n"); exit(1); }
if (dup2(fd, 1) < 0) { fprintf(stderr, "dup2 失败\n"); exit(1); }
close(fd);
}
static void backOut() { fflush(stdout); if (dup2(savedOut, 1) < 0) exit(1); }
/* ---------------- 读 ---------------- */
static long long readCin(long long& sum) {
reopenIn();
auto t0 = steady_clock::now();
int n, q; cin >> n >> q; sum = n;
for (int i = 0; i < q; i++) { int k; cin >> k; sum += k; }
return duration_cast<milliseconds>(steady_clock::now() - t0).count();
}
static long long readNosync(long long& sum) {
reopenIn();
ios::sync_with_stdio(false); cin.tie(nullptr);
auto t0 = steady_clock::now();
int n, q; cin >> n >> q; sum = n;
for (int i = 0; i < q; i++) { int k; cin >> k; sum += k; }
return duration_cast<milliseconds>(steady_clock::now() - t0).count();
}
static long long readScanf(long long& sum) {
reopenIn();
auto t0 = steady_clock::now();
int n, q;
if (scanf("%d %d", &n, &q) != 2) exit(1);
sum = n;
for (int i = 0; i < q; i++) { int k; if (scanf("%d", &k) != 1) exit(1); sum += k; }
return duration_cast<milliseconds>(steady_clock::now() - t0).count();
}
static char ibuf[1 << 22];
static size_t ilen = 0, ipos = 0;
static inline int gc() {
if (ipos == ilen) { ilen = fread(ibuf, 1, sizeof(ibuf), stdin); ipos = 0; if (!ilen) return -1; }
return ibuf[ipos++];
}
static inline int readInt() {
int c = gc();
while (c != -1 && (c < '0' || c > '9')) c = gc();
int v = 0;
while (c >= '0' && c <= '9') { v = v * 10 + (c - '0'); c = gc(); }
return v;
}
static long long readFast(long long& sum) {
reopenIn(); ilen = ipos = 0;
auto t0 = steady_clock::now();
int n = readInt(), q = readInt(); sum = n;
for (int i = 0; i < q; i++) sum += readInt();
return duration_cast<milliseconds>(steady_clock::now() - t0).count();
}
/* ---------------- 写 ---------------- */
static long long writeEndl() {
reopenOut();
auto t0 = steady_clock::now();
for (int i = 0; i < Q; i++) cout << vals[i] << endl;
cout.flush();
long long ms = duration_cast<milliseconds>(steady_clock::now() - t0).count();
backOut(); return ms;
}
static long long writeNl() {
reopenOut();
auto t0 = steady_clock::now();
for (int i = 0; i < Q; i++) cout << vals[i] << '\n';
cout.flush();
long long ms = duration_cast<milliseconds>(steady_clock::now() - t0).count();
backOut(); return ms;
}
static long long writeNlNosync() {
reopenOut();
auto t0 = steady_clock::now();
for (int i = 0; i < Q; i++) cout << vals[i] << '\n';
cout.flush();
long long ms = duration_cast<milliseconds>(steady_clock::now() - t0).count();
backOut(); return ms;
}
static long long writePrintf() {
reopenOut();
auto t0 = steady_clock::now();
for (int i = 0; i < Q; i++) printf("%d\n", vals[i]);
fflush(stdout);
long long ms = duration_cast<milliseconds>(steady_clock::now() - t0).count();
backOut(); return ms;
}
static char obuf[1 << 22];
static size_t opos = 0;
static inline void flushOut() { fwrite(obuf, 1, opos, stdout); opos = 0; }
static inline void putInt(int x) {
if (opos + 16 > sizeof(obuf)) flushOut();
char t[12]; int m = 0;
if (!x) t[m++] = '0';
while (x) { t[m++] = char('0' + x % 10); x /= 10; }
while (m) obuf[opos++] = t[--m];
obuf[opos++] = '\n';
}
static long long writeFwrite() {
reopenOut(); opos = 0;
auto t0 = steady_clock::now();
for (int i = 0; i < Q; i++) putInt(vals[i]);
flushOut(); fflush(stdout);
long long ms = duration_cast<milliseconds>(steady_clock::now() - t0).count();
backOut(); return ms;
}
static long long med3(long long a, long long b, long long c) { return max(min(a, b), min(max(a, b), c)); }
int main(int argc, char** argv) {
bool csv = argc > 1 && string(argv[1]) == "csv";
savedOut = dup(1);
if (savedOut < 0) return 1;
makeData();
long long inBytes = 0, outBytes = 0;
{ FILE* f = fopen(inPath, "rb"); fseek(f, 0, SEEK_END); inBytes = ftell(f); fclose(f); }
/* ⚠ 顺序:同步开着的两种写法 + 默认 cin 必须排在关同步之前 */
long long wEndl = med3(writeEndl(), writeEndl(), writeEndl());
{ FILE* f = fopen(outPath, "rb"); fseek(f, 0, SEEK_END); outBytes = ftell(f); fclose(f); }
long long wNl = med3(writeNl(), writeNl(), writeNl());
long long s1, s2, s3, s4;
long long rCin = med3(readCin(s1), readCin(s1), readCin(s1));
long long rNos = med3(readNosync(s2), readNosync(s2), readNosync(s2));
long long wNlNos = med3(writeNlNosync(), writeNlNosync(), writeNlNosync());
long long rScan = med3(readScanf(s3), readScanf(s3), readScanf(s3));
long long wPrt = med3(writePrintf(), writePrintf(), writePrintf());
long long rFast = med3(readFast(s4), readFast(s4), readFast(s4));
long long wFwr = med3(writeFwrite(), writeFwrite(), writeFwrite());
int same = (s1 == s2 && s2 == s3 && s3 == s4);
remove(inPath); remove(outPath);
if (csv) {
printf("inBytes,%lld\n", inBytes);
printf("outBytes,%lld\n", outBytes);
printf("rCin,%lld\n", rCin);
printf("rNosync,%lld\n", rNos);
printf("rScanf,%lld\n", rScan);
printf("rFast,%lld\n", rFast);
printf("wEndl,%lld\n", wEndl);
printf("wNl,%lld\n", wNl);
printf("wNlNosync,%lld\n", wNlNos);
printf("wPrintf,%lld\n", wPrt);
printf("wFwrite,%lld\n", wFwr);
printf("same,%d\n", same);
printf("worstIo,%lld\n", rCin + wEndl);
printf("bestIo,%lld\n", rFast + wFwr);
return 0;
}
printf("顶格 q = 10⁶:输入 %lld 字节(%.1f MB),输出 %lld 字节(%.1f MB)\n",
inBytes, inBytes / 1048576.0, outBytes, outBytes / 1048576.0);
printf("\n读(3 次取中位数,时限 3000 ms)\n");
printf(" 默认 cin %lld ms / 关同步 cin %lld ms / scanf %lld ms / fread 快读 %lld ms\n",
rCin, rNos, rScan, rFast);
printf(" 四种读法校验和一致:%s\n", same ? "是" : "否");
printf("\n写(3 次取中位数)\n");
printf(" cout << endl %lld ms / cout << '\\n' %lld ms / 关同步 + '\\n' %lld ms / printf %lld ms / fwrite %lld ms\n",
wEndl, wNl, wNlNos, wPrt, wFwr);
printf("\n⇒ 最差的一套(默认 cin + endl)合计 %lld ms,最好的一套(快读 + fwrite)合计 %lld ms\n",
rCin + wEndl, rFast + wFwr);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★★★ 题面把两件事并排说,而它们的分量差一个数量级

顶格 q = 10⁶:输入 7.4 MB、输出 8.5 MB。独占实测,每格 3 次取中位数(时限 3000 ms):

毫秒 毫秒
默认 cin 173 cout << endl 255
★ 关同步 cin 28 cout << '\n'(同步仍开着) 29
scanf 42 关同步 + '\n' 25
fread 快读 6 printf 32
fwrite 手拼 11

★★★ 题面那句话里并排放着两条建议,分量完全不一样

  • 写这一侧:endl'\n' 值 ★ 8.8 倍(255 → 29),而再关同步只值 1.16 倍
  • 读这一侧:关同步值 ★ 6.2 倍(173 → 28)。

⇒ ★★ endl 的问题从来不是「换行」,是它每一行都 flush 一次 —— 10⁶ 行就是 10⁶ 次系统调用。

⚠⚠ 而这句流传很广的提醒,量完之后仍然要打折P1972 那条的第二次): 最差的一套(默认 cin + endl)合计 428 毫秒,最好的一套(快读 + fwrite)合计 17 毫秒 —— 差 25 倍,可时限有 3000 毫秒,而筛只占 370 毫秒。 ⇒ 端到端实测:默认 I/O 那一版 0.94 秒 / 3 秒,它能过。 ⇒ ★★ 题面那句提醒是对的(省了 411 毫秒),但它不是「必须」—— 「够不够」比的是绝对时间和那条时限,而这道题给了 3 秒。

5⚠ 唯一一个真会答错的:第 k 小写成了 pr[k]

p3383Off.cpp✗ 下标少减了一个 1
p3383Gen.cpp生成器(照题面缩小 / 边界 / 顶格记号)
★★ 六个待测版本,对拍只抓得到一个 —— 又一次
档位(每档 300 轮) pr[k] ⚠ 埃氏筛 ⚠ 忘了 break ★ 位压缩 ⚠ 默认 I/O
0 照题面缩小(n ≤ 20000 300 / 300 0 0 0 0
1 边界(k = 1k = π(n) 300 / 300 0 0 0 0

⇒ 和同章 P3912 一模一样的形状:这一章题单的后两道题,关卡都不在答案上。 对拍和官方样例只筛得掉那个 off-by-one(它每一问都错,样例第一行就死), 剩下的四个差别全在次数、缓存和 I/O 上只能数次数 + 上秒表

★ 顺带一条生成器上的:题面写着「保证查询的素数不大于 n」—— ⇒ 生成器必须亲手做到:先小筛一遍数出 π(n),再在 [1, π(n)] 里挑 k「生成器要亲手做到题面那几句保证」)。

6★ 哪一版就已经能过了

p3383Count.cpp筛那一半数字的出处(四种筛 × 次数 × 内存)
// P3383 的度量程序:./p3383Count csv (本页筛那一半的数字出自它)
//
// marks : 四种筛在 n = 10⁸ 上各划了多少次(机器无关的那把尺子)。
// ms : 同样四种,2 次取较小值(★ 只报倍数,绝对秒数在页面上注明独占实测)。
// mem : ★ 这道题**躲不掉那个 int 素数表**(要回答「第 k 小」)—— 算一遍它多大。
//
// ⚠ 和 [P3912](/sol/p3912/) 那一页的对照:那道题只要「有几个」⇒ 素数一个都不用存、
// 表还能压成位;这道题要「第 k 小是谁」⇒ 表躲不掉。**同一章,两个相反的结论。**
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
static const int TOP = 100000000;
static bool comp[TOP + 1];
static int prs[5761460];
static uint64_t bs[TOP / 128 + 2];
static inline bool bget(int k) { return (bs[k >> 6] >> (k & 63)) & 1ULL; }
static inline void bset(int k) { bs[k >> 6] |= 1ULL << (k & 63); }
/** ① 线性筛 + 素数表 */
static int linear(int n, long long& mk) {
memset(comp, 0, (size_t)n + 1); mk = 0;
int cnt = 0;
for (int i = 2; i <= n; i++) {
if (!comp[i]) prs[cnt++] = i;
for (int j = 0; j < cnt && (long long)prs[j] * i <= n; j++) {
comp[prs[j] * i] = true; mk++;
if (i % prs[j] == 0) break;
}
}
return cnt;
}
/** ② 忘了那句 break */
static int noBreak(int n, long long& mk) {
memset(comp, 0, (size_t)n + 1); mk = 0;
int cnt = 0;
for (int i = 2; i <= n; i++) {
if (!comp[i]) prs[cnt++] = i;
for (int j = 0; j < cnt && (long long)prs[j] * i <= n; j++) { comp[prs[j] * i] = true; mk++; }
}
return cnt;
}
/** ③ 埃氏筛(i·i 起)+ 收集 */
static int eratCollect(int n, long long& mk) {
memset(comp, 0, (size_t)n + 1); mk = 0;
for (long long i = 2; i * i <= n; i++)
if (!comp[i]) for (long long j = i * i; j <= n; j += i) { comp[j] = true; mk++; }
int cnt = 0;
for (int i = 2; i <= n; i++) if (!comp[i]) prs[cnt++] = i;
return cnt;
}
/** ④ 只存奇数 + 位压缩,再提取素数表 */
static int bitCollect(int n, long long& mk) {
memset(bs, 0, sizeof(uint64_t) * ((size_t)n / 128 + 2)); mk = 0;
int half = (n - 1) / 2, cnt = 0;
for (long long k = 1; (2 * k + 1) * (2 * k + 1) <= n; k++) {
if (bget((int)k)) continue;
long long p = 2 * k + 1;
for (long long j = p * p; j <= n; j += 2 * p) { bset((int)((j - 1) / 2)); mk++; }
}
prs[cnt++] = 2;
for (int k = 1; k <= half; k++) if (!bget(k)) prs[cnt++] = 2 * k + 1;
return cnt;
}
int main(int argc, char** argv) {
bool csv = (argc > 1 && string(argv[1]) == "csv");
const char* NAME[4] = {"线性筛", "忘了 break", "埃氏筛(i·i)+收集", "奇数位压缩+提取"};
long long mk[4] = {0, 0, 0, 0};
int cnt[4] = {0, 0, 0, 0};
double ms[4] = {0, 0, 0, 0};
for (int rep = 0; rep < 2; rep++)
for (int k = 0; k < 4; k++) {
long long m = 0;
auto t0 = steady_clock::now();
int c = (k == 0) ? linear(TOP, m) : (k == 1) ? noBreak(TOP, m)
: (k == 2) ? eratCollect(TOP, m) : bitCollect(TOP, m);
double t = duration<double, milli>(steady_clock::now() - t0).count();
if (rep == 0 || t < ms[k]) ms[k] = t;
mk[k] = m; cnt[k] = c;
}
bool same = (cnt[0] == cnt[1] && cnt[1] == cnt[2] && cnt[2] == cnt[3]);
double memTable = (double)cnt[0] * 4.0 / 1048576.0; // int 素数表
double memBool = (double)(TOP + 1) / 1048576.0;
double memBit = (double)(TOP / 128 + 2) * 8.0 / 1048576.0;
if (csv) {
printf("pi,%d\n", cnt[0]);
printf("same,%d\n", same ? 1 : 0);
for (int k = 0; k < 4; k++) printf("marks%d,%lld\n", k, mk[k]);
printf("ratNoBreak,%.2f\n", ms[1] / ms[0]);
printf("ratErat,%.2f\n", ms[2] / ms[0]);
printf("ratBit,%.2f\n", ms[0] / ms[3]);
printf("memTable,%.1f\n", memTable);
printf("memLinear,%.1f\n", memBool + memTable);
printf("memBit,%.1f\n", memBit + memTable);
printf("headLinear,%.1f\n", 512.0 / (memBool + memTable));
return 0;
}
printf("π(10⁸) = %d,四种筛数出来的个数一致:%s\n", cnt[0], same ? "是" : "否");
printf("\n① 划的次数(机器无关)\n");
for (int k = 0; k < 4; k++) printf(" %-20s %14lld\n", NAME[k], mk[k]);
printf("\n② 秒表比值(2 次取较小值)\n");
printf(" 忘了 break ÷ 线性筛 = %.2f\n", ms[1] / ms[0]);
printf(" 埃氏筛+收集 ÷ 线性筛 = %.2f\n", ms[2] / ms[0]);
printf(" 线性筛 ÷ 位压缩+提取 = %.2f\n", ms[0] / ms[3]);
printf("\n③ 内存(算出来的字节数),题面给 **512 MB**\n");
printf(" 素数表本身(%d 个 int) %6.1f MB ← ⚠ 这道题躲不掉它\n", cnt[0], memTable);
printf(" 线性筛(bool 表 + 素数表) %6.1f MB(余量 %.1f 倍)\n", memBool + memTable, 512.0 / (memBool + memTable));
printf(" 位压缩(位表 + 素数表) %6.1f MB\n", memBit + memTable);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★ 只要「筛出来存成表」这一步做对了,四种写法都能过
版本 顶格端到端 交上去
① 每问从头数 天文数字 0 分
⚠ 埃氏筛 + 收集 + 快读快写 0.95 秒 / 3 秒 AC
⚠ 线性筛但忘了 break 0.93 秒 AC(⚠ 而它已经不是线性的了)
⚠ 线性筛 + 默认 cin/cout/endl 0.94 秒 AC
★ 线性筛 + 快读快写 0.44 秒 AC
★ 位压缩 + 提取 + 快读快写 0.27 秒 AC

这道题的时限(3 秒)和内存(512 MB)都很宽 —— 它考的是「你会不会写线性筛」, 不是「你能不能压到极限」。 ⚠ 但正因为宽,它是个很好的量场:同一道题上,那句 break、那张表的形状、那套 I/O 各值多少,全都能干净地量出来 —— 而这一页量出来的三个答案, 有两个和本书自己原来写的不一样(见第 3 步)。