xtuoj 素数个数

题目

思路

一、问题限制与挑战

  • 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. 欧拉筛必须顺序遍历1到R

    要筛[L, R],必须知道每个数的因子分解

    需要遍历i=1,2,3,...,R → 10亿次,不可能

  2. 埃式筛可以直接定位区间内倍数

    对于素数p,直接计算:

    第一个≥L的p倍数 = ceil(L/p) * p

    然后每次+p → 只操作区间内的数

  3. 内存限制

    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

这需要:

  1. 顺序访问所有数 i(1到R)

  2. 知道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万区间 ≈ 几毫秒

八、最终答案

  1. 小素数筛选:可以用埃式筛或欧拉筛,差异不大

  2. 区间筛必须用埃式筛思想(倍数标记法)

  3. 不能完全用线性筛:因为线性筛需要全局顺序访问,不符合区间筛的局部性需求

本质原因 :区间筛是"局部问题",线性筛是"全局算法"。

局部问题只能用局部方法解决,而倍数标记法(埃式筛思想)正好满足这种局部性需求。

代码

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;
}
相关推荐
jyyyx的算法博客1 小时前
LeetCode 面试题 16.18. 模式匹配
算法·leetcode
uuuuuuu1 小时前
数组中的排序问题
算法
Stream1 小时前
加密与签名技术之密钥派生与密码学随机数
后端·算法
Stream1 小时前
加密与签名技术之哈希算法
后端·算法
少许极端2 小时前
算法奇妙屋(十五)-BFS解决边权为1的最短路径问题
数据结构·算法·bfs·宽度优先·队列·图解算法·边权为1的最短路径问题
c骑着乌龟追兔子2 小时前
Day 27 常见的降维算法
人工智能·算法·机器学习
hetao17338372 小时前
2025-12-02~03 hetao1733837的刷题记录
c++·算法
田里的水稻2 小时前
math_旋转变换
算法·几何学
ada7_2 小时前
LeetCode(python)——94.二叉
python·算法·leetcode·链表·职场和发展