题目

思路
一、问题限制与挑战
-
R ≤ 1e9(10亿):太大,不能筛整个1到R
-
R-L ≤ 1e6(100万):区间长度有限,可以存下
-
多次查询:需要高效处理
二、核心数学原理
关键定理 :任何一个合数 n 必有一个质因数 p ≤ √n
推论:区间 [L, R] 中的合数,必有一个质因数 ≤ √R
例子 :R=1000000000(10亿)
√R ≈ 31623
只需要用到 ≤31623 的素数去判断区间内的数
三、两步算法
第1步:筛小素数(≤√R)
cpp
// 埃式筛法(简单高效)
void sieveSmallPrimes(int limit) {
memset(isPrime, true, sizeof(isPrime));
isPrime[0] = isPrime[1] = false;
for (int i = 2; i * i <= limit; i++) {
if (isPrime[i]) {
for (int j = i * i; j <= limit; j += i) {
isPrime[j] = false;
}
}
}
// 收集素数到primes数组
primeCount = 0;
for (int i = 2; i <= limit; i++) {
if (isPrime[i]) {
primes[primeCount++] = i;
}
}
}
复杂度:O(√R log log √R) ≈ O(31623) 很小
第2步:区间筛
cpp
// 标记区间[L, R]
void sieveSegment(long long L, long long R) {
// 1. 初始化区间数组
memset(isPrimeSeg, true, sizeof(isPrimeSeg));
if (L == 1) isPrimeSeg[0] = false; // 1不是素数
// 2. 用每个小素数筛区间
for (int idx = 0; idx < primeCount; idx++) {
long long p = primes[idx];
// 找第一个≥L的p的倍数
long long start = (L / p) * p;
if (start < L) start += p;
if (start == p) start += p; // 跳过素数本身
// 标记p的所有倍数
for (long long j = start; j <= R; j += p) {
isPrimeSeg[j - L] = false;
}
}
}
关键技巧:
-
j - L:将[L, R]映射到数组[0, R-L] -
start = (L/p)*p:找到第一个≤L的p倍数 -
跳过p本身:避免把区间内的素数标记为合数
四、为什么只能用埃式筛思想?
对比两种筛法:
| 特性 | 埃式筛 | 欧拉筛(线性筛) |
|---|---|---|
| 访问模式 | 跳跃式(p的倍数) | 顺序生成(1到n) |
| 内存需求 | 可局部访问 | 需要全局数组 |
| 适合场景 | 任意区间片段 | 连续区间从1开始 |
关键区别:
-
欧拉筛必须顺序遍历1到R
要筛[L, R],必须知道每个数的因子分解
需要遍历i=1,2,3,...,R → 10亿次,不可能
-
埃式筛可以直接定位区间内倍数
对于素数p,直接计算:
第一个≥L的p倍数 =
ceil(L/p) * p然后每次+p → 只操作区间内的数
-
内存限制
R=1e9需要1GB内存(如果每个bool用1字节)
区间筛只需1e6字节 ≈ 1MB
五、能否用线性筛优化?
小素数部分:可以
cpp
// 欧拉筛筛小素数(更高效)
void eulerSieve(int n) {
primeCount = 0;
for (int i = 2; i <= n; i++) {
if (!isComposite[i]) {
primes[primeCount++] = i;
}
for (int j = 0; j < primeCount && i * primes[j] <= n; j++) {
isComposite[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
}
优点 :比埃式筛稍快(O(n) vs O(n log log n))
但对本题:√R ≈ 31623很小,两者差异不大
区间筛部分:不能
根本原因:线性筛的核心是:
cpp
for i = 2 to R:
for 每个素数p ≤ i:
if p 整除 i: break
这需要:
-
顺序访问所有数 i(1到R)
-
知道i的质因数分解来决定break
在区间筛中:
-
我们只有区间[L, R],不知道区间外数的分解
-
无法决定何时break
-
无法保证每个合数只被最小质因数筛一次
六、完整算法流程
cpp
输入:L, R(R ≤ 1e9, R-L ≤ 1e6)
1. 计算 limit = √R
2. 筛出所有 ≤ limit 的素数(小素数)
方法:埃式筛或欧拉筛都可以
3. 创建数组 seg[0..R-L] 初始化为true
表示[L, R]的数都是素数候选
4. 对每个小素数 p:
a. 计算 start = 第一个≥L的p倍数
b. 如果 start == p: start += p(跳过p本身)
c. 标记 seg[start-L], seg[start+p-L], ... 为false
5. 统计 seg[] 中true的个数
七、复杂度分析
| 步骤 | 操作 | 复杂度 |
|---|---|---|
| 筛小素数 | 筛到√R ≈ 31623 | O(√R log log √R) 很小 |
| 区间筛 | 对每个小素数p标记区间内倍数 | O((R-L) log log R) |
| 总计 | 对100万区间 | ≈ 几毫秒 |
八、最终答案
-
小素数筛选:可以用埃式筛或欧拉筛,差异不大
-
区间筛 :必须用埃式筛思想(倍数标记法)
-
不能完全用线性筛:因为线性筛需要全局顺序访问,不符合区间筛的局部性需求
本质原因 :区间筛是"局部问题",线性筛是"全局算法"。
局部问题只能用局部方法解决,而倍数标记法(埃式筛思想)正好满足这种局部性需求。
代码
cpp
#include <stdio.h>
#include <stdbool.h>
#include <math.h>
#include <string.h>
#define MAX_R 1000000000
#define MAX_SEGMENT 1000000
bool isPrimeSmall[1000000];
bool isPrimeSegment[MAX_SEGMENT + 1];
void segmentSieve(long long L, long long R, int* count) {
if (L > R) return;
// 筛小素数用于测试
int sqrtR = sqrt(R);
memset(isPrimeSmall, true, sizeof(isPrimeSmall));
isPrimeSmall[0] = isPrimeSmall[1] = false;
for (int i = 2; i <= sqrtR; i++) {
if (isPrimeSmall[i]) {
for (int j = i * i; j <= sqrtR; j += i) {
isPrimeSmall[j] = false;
}
}
}
// 筛区间
memset(isPrimeSegment, true, sizeof(isPrimeSegment));
if (L == 1) isPrimeSegment[0] = false;
for (int i = 2; i <= sqrtR; i++) {
if (isPrimeSmall[i]) {
long long start = (L / i) * i;
if (start < L) start += i;
if (start == i) start += i; // 避免标记素数本身
for (long long j = start; j <= R; j += i) {
isPrimeSegment[j - L] = false;
}
}
}
*count = 0;
for (long long i = L; i <= R; i++) {
if (isPrimeSegment[i - L]) {
(*count)++;
}
}
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
long long L, R;
scanf("%lld %lld", &L, &R);
int count;
segmentSieve(L, R, &count);
printf("%d\n", count);
}
return 0;
}