L,R 可达 2^31 而区间长度只有 10^6,先筛出 [2,√R] 的小质数,再用它们对长度 10^6 的区间做分段筛,一次线性扫描取相邻质数的最小/最大差。
OJ: roj
题目 ID: 1619
难度:提高+/省选-
标签:数学数论质数埃氏筛分段筛python
创建: 2026-09-30 22:41
更新: 2026-10-07 13:50
形式化题目
给定整数
要求找出相邻两项之差最小的一对,以及差最大的一对,即求
当极值被多对同时取得时,输出下标
约束:
正解
思路
1. 朴素做法:逐个试除
最直接的写法是枚举
for n in range(L, R + 1):
if all(n % d for d in range(2, int(n ** 0.5) + 1)): # 不可行
primes.append(n)它的代价是
2. 为什么不能直接筛到
换一个思路:用埃氏筛一次性筛出
这就是本题的核心矛盾:数值上界极大(
3. 关键观察:合数的小质因子一定很小
设
也就是说:
于是算法分成两层:
| 层 | 作用域 | 数据量 | 内存 |
|---|---|---|---|
| 预处理:埃氏筛 | 46342 个候选 | 46 KB | |
| 主体:分段筛 | 最多 |
1 MB |
内存由"区间长度"而非"数值上界"决定,矛盾就此化解。
4. 分段筛的划除细节
开一个长度为 is_prime,下标
- 若
,说明 已经大到无法在区间内构成合数(最小的候选是 本身),后面的 只会更大,直接 break。 - 找出第一个要划掉的倍数位置:
两个下界各有道理:start 起,每 is_prime[start-L::p] 整段置 0。
最后单独处理 is_prime 中为 1 的位置对应的数字就是
5. 样例推演
以样例 2 17 为例,
| 小质数 |
start |
被划掉的数字 | 剩余候选 | |
|---|---|---|---|---|
| 2 | 否 | 4, 6, 8, 10, 12, 14, 16 | 2,3,5,7,9,11,13,15,17 | |
| 3 | 否 | 9, 15 | 2,3,5,7,11,13,17 | |
| 5 | 是( |
— | 循环在此 break | 2,3,5,7,11,13,17 |
得到质数序列
最小差
再看第二个样例 14 17:区间内质数只有 There are no adjacent primes.,同样吻合。
6. 取最值对:并列取靠前
primes 已经升序,相邻差就是 primes[i+1] - primes[i]。Python 的 max / min 在遇到多个极值时返回第一个达到极值的元素,这恰好就是题面要求的"输出靠前的素数对",不需要额外写判等逻辑:
choose = max if want_max else min
return choose(zip(primes, primes[1:]), key=lambda pair: pair[1] - pair[0])以 2 17 为例,zip(primes, primes[1:]) 给出 max 返回第一个键为 4 的 min 返回第一个键为 1 的
最后按 a,b are closest, c,d are most distant. 的格式输出即可。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 22:41
# update_at: 2026-09-30 22:41
import sys
LIMIT = 46341 # floor(sqrt(2**31 - 1)),筛出这段小数就能覆盖 [L,R] 的全部合数
CLOSEST = "are closest, "
DISTANT = "are most distant."
NONE = "There are no adjacent primes."
def small_primes(n: int) -> list[int]:
"""返回 [2, n] 内的所有质数,作为区间筛的“试探除数”集合。"""
sieve = bytearray([1]) * (n + 1) # 1 表示暂时认为它是质数
sieve[0:2] = b"\x00\x00"
for i in range(2, int(n ** 0.5) + 1):
if sieve[i]:
sieve[i * i :: i] = bytearray(len(sieve[i * i :: i])) # 质数 i 的倍数全是合数
return [i for i in range(2, n + 1) if sieve[i]]
def segment_primes(L: int, R: int, base: list[int]) -> list[int]:
"""返回闭区间 [L, R] 内的所有质数,按升序排列。
下标 i 对应数字 L+i,只筛这一段,内存取决于区间长度而不是 R 的大小。
"""
width = R - L + 1
is_prime = bytearray([1]) * width # 先全部假定为质数,再被小质数划掉
for p in base:
if p * p > R:
break
# 从 >= L 的第一个 p 的倍数开始划;L 较小时下界不能低于 p 本身
start = max(p * p, (L + p - 1) // p * p)
is_prime[start - L :: p] = bytearray(len(range(start, R + 1, p)))
if L == 1: # 1 既不是质数也不是合数,单独处理
is_prime[0] = 0
return [L + i for i in range(width) if is_prime[i]]
def best_pair(primes: list[int], want_max: bool) -> tuple[int, int]:
"""取相邻质数差最大(want_max=True)或最小的那一对,并列时取更靠前的。
max/min 返回第一个达到极值的元素,正好满足“输出靠前的素数对”。
"""
choose = max if want_max else min
return choose(zip(primes, primes[1:]), key=lambda pair: pair[1] - pair[0])
def solve() -> None:
data = list(map(int, sys.stdin.buffer.read().split()))
base = small_primes(LIMIT)
out: list[str] = []
for i in range(0, len(data), 2):
L, R = data[i], data[i + 1]
primes = segment_primes(L, R, base)
if len(primes) < 2: # 少于两个质数就没有“相邻质数对”
out.append(NONE)
continue
lo_a, lo_b = best_pair(primes, want_max=False)
hi_a, hi_b = best_pair(primes, want_max=True)
out.append(f"{lo_a},{lo_b} {CLOSEST}{hi_a},{hi_b} {DISTANT}")
print('\n'.join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 23:02
* update_at: 2026-10-05 23:02
*/
// 分段筛(区间筛)模板题:R 可达 2^31,但 R-L <= 10^6。
// 先筛出 [2, sqrt(R)] 内的小质数,再用它们划掉 [L,R] 中的合数,
// 剩下的就是区间内的全部质数,线性扫描相邻差取最小/最大即可。
#include <cstdio>
#include <cstring>
typedef long long ll;
const int BASE = 46341; // sqrt(2^31) < 46341,小质数筛到这个上界就够
const int MAXN = 1000005; // 区间长度 R-L+1 最大为 10^6+1
bool is_comp[BASE + 1]; // 埃氏筛的标记数组:is_comp[i]=1 表示 i 是合数
ll base_primes[5000]; // [2, 46341] 内的小质数约 4800 个(π(46341)=4792)
ll cnt_base; // 小质数个数
bool seg_comp[MAXN]; // seg_comp[i]=1 表示数字 L+i 是合数
// 埃氏筛预处理 [2, BASE] 内的小质数
void sieve_base() {
memset(is_comp, 0, sizeof(is_comp));
cnt_base = 0;
for (ll i = 2; i <= BASE; i++) {
if (!is_comp[i]) {
base_primes[++cnt_base] = i; // 下标从 1 开始存
for (ll j = i * i; j <= BASE; j += i) is_comp[j] = 1;
}
}
}
// 用小质数表对 [L, R] 做分段筛,划掉区间内所有合数
void segment_sieve(ll L, ll R) {
// 初始时假定区间内每个数都是质数候选(下标 i 对应数字 L+i)
memset(seg_comp, 0, sizeof(seg_comp));
for (ll k = 1; k <= cnt_base; k++) {
ll p = base_primes[k];
if (p * p > R) break; // p^2 > R 时 p 无法在区间内构成任何合数,后面只会更大
// 第一个要划掉的倍数:不小于 L 的最小 p 倍数,但不能低于 p^2
// (p^2 以下的倍数已被更小的质数划过;p >= L 时取 p^2 可避免误划质数 p 自己)
ll start = (L + p - 1) / p * p;
if (start < p * p) start = p * p;
for (ll j = start; j <= R; j += p) seg_comp[j - L] = 1;
}
// 数字 1 不是质数,但它不会被任何小质数划掉,需要单独排除
if (L == 1) seg_comp[0] = 1;
}
// 对区间内剩下的质数线性扫描,输出相邻差最小/最大的数对
void solve(ll L, ll R) {
segment_sieve(L, R);
// prev 记录上一个质数;diff 用 -1 区分"还没有质数对"
ll prev = 0, diff_min = -1, diff_max = -1;
ll ans_a1 = 0, ans_b1 = 0, ans_a2 = 0, ans_b2 = 0;
for (ll i = L; i <= R; i++) {
if (seg_comp[i - L]) continue; // 合数跳过
if (prev == 0) { prev = i; continue; }
ll diff = i - prev;
if (diff_min == -1 || diff < diff_min) {
// 并列时取更靠前的:只在严格更小时更新
diff_min = diff;
ans_a1 = prev; ans_b1 = i;
}
if (diff_max == -1 || diff > diff_max) {
// 并列时取更靠前的:只在严格更大时更新
diff_max = diff;
ans_a2 = prev; ans_b2 = i;
}
prev = i;
}
if (diff_min == -1)
printf("There are no adjacent primes.\n");
else
printf("%lld,%lld are closest, %lld,%lld are most distant.\n",
ans_a1, ans_b1, ans_a2, ans_b2);
}
int main() {
sieve_base();
ll L, R;
while (scanf("%lld %lld", &L, &R) == 2) {
solve(L, R);
}
return 0;
}复杂度
- 时间复杂度:预处理部分
,只做一次;之后每组数据的分段筛为 。代入 、 ,远优于逐个试除的 ,二者在 时相差约 倍。 - 空间复杂度:
,即 46 KB 的小质数表加上最大 1 MB 的区间标记数组,与直接筛到 所需的 相比是决定性的差异。
图示解析
下图给出
flowchart LR
A["预处理:埃氏筛<br/>[2, 46341]"] --> B["小质数表 base<br/>2,3,5,7,11,..."]
B --> C{"对每个 p:<br/>p*p > R ?"}
C -- 是 --> F["收集 is_prime 中<br/>仍为 1 的下标"]
C -- 否 --> D["start = max(p*p, ceil(L/p)*p)"]
D --> E["is_prime[start-L::p] = 0<br/>按步长 p 整段划除"]
E --> C
F --> G["primes = [L+i ...]<br/>升序质数序列"]
G --> H{"len(primes) >= 2 ?"}
H -- 否 --> I["There are no adjacent primes."]
H -- 是 --> J["min(zip(p, p[1:]), key=差)<br/>max(zip(p, p[1:]), key=差)"]
J --> K["a,b are closest,<br/>c,d are most distant."]
下表展示这一段的筛除过程(· 表示仍被标记为质数候选,× 表示已被划掉),可以清楚看到区间长度只有 21,全部工作都在这一小段内完成:
| 数字 | 1000000000 | … | 1000000007 | 1000000008 | 1000000009 | 1000000010 | … | 1000000020 |
|---|---|---|---|---|---|---|---|---|
| 被 |
× | × | · | × | · | × | × | × |
| 被 |
× | × | · | × | · | × | × | × |
最终 is_prime |
0 | … | 1 | 0 | 1 | 0 | … | 0 |
划除结束后只剩 1000000000 1001000000 中——该点的答案
总结
本题是"分段筛(区间筛)"的模板题,它的全部内容可以浓缩成一句观察:
若
是合数,则 必有不超过 的质因子。
由此把一次不可能的"筛到
实现上还有两个 Python 细节值得记住:用 bytearray 的扩展切片赋值(is_prime[start-L::p] = 0)完成整段划除,循环体在 C 层执行,比手写 for 快两个数量级;取"并列最靠前"的一对时直接依赖 min/max 返回首个极值的语义,不必额外比较下标。