素数密度

GitHub跳转原题关系图返回列表

先筛出不超过 sqrt(R) 的素数,再在长度不超过一百万的区间内做分段筛。

OJ: luogu

题目 ID: P1835

难度:普及+/提高

标签:数论分段筛素数bytearraypython

日期: 2026-07-16 19:20

题意

统计 [L,R] 中素数个数。R 接近 2^31,但区间长度不超过 10^6

思路

不能筛到 R。任何区间合数都至少有一个不超过 sqrt(R) 的质因子:

  1. 普通埃氏筛得到 2..sqrt(R) 的素数;
  2. 建立长度 R-L+1 的区间标记;
  3. 对每个基础素数 p,从 max(p*p,ceil(L/p)*p) 开始标记倍数;
  4. L=1,单独把 1 标为非素数。

p*p 开始可避免把区间中的质数 p 自己误删。

Python 知识

  • bytearray 适合百万长度 0/1 标记。
  • 扩展切片赋值一次标记同一质数的全部倍数。
  • (left+p-1)//p*p 是不小于 left 的第一个 p 倍数。
  • sum(segment) 直接统计仍为 1 的位置。
  • /home/rainboy/mycode/hugo-blog/content/program_language/python/cpp_to_python_pitfalls.md:紧凑标记和切片性能。
  • /home/rainboy/mycode/hugo-blog/content/program_language/python/generator_expression.md:区间按需处理思路。

代码

python
import sys


def main():
    left, right = map(int, sys.stdin.buffer.read().split())
    limit = int(right ** 0.5)
    base_prime = bytearray(b"\x01") * (limit + 1)
    if limit >= 0:
        base_prime[0] = 0
    if limit >= 1:
        base_prime[1] = 0
    for prime in range(2, int(limit ** 0.5) + 1):
        if base_prime[prime]:
            start = prime * prime
            base_prime[start::prime] = b"\x00" * ((limit - start) // prime + 1)

    segment = bytearray(b"\x01") * (right - left + 1)
    for prime in range(2, limit + 1):
        if not base_prime[prime]:
            continue
        start = max(prime * prime, (left + prime - 1) // prime * prime)
        if start <= right:
            segment[start - left::prime] = b"\x00" * ((right - start) // prime + 1)
    if left == 1:
        segment[0] = 0
    print(sum(segment))


if __name__ == "__main__":
    main()
cpp
/**
 * P1835 素数密度
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-27 00:00
 * update_at: 2026-07-27 00:00
 */

#include <bits/stdc++.h>
using namespace std;

const int MAXV = 50000; // sqrt(2^31) ≈ 46340
const int MAXL = 1000005;

bool is_prime[MAXV + 5]; // 小范围内筛素数
int primes[MAXV], pcnt;
bool seg[MAXL]; // 区间 [l,r] 的素数标记

int main() {
    int l, r;
    scanf("%d%d", &l, &r);
    // 小范围素数筛
    memset(is_prime, 1, sizeof(is_prime));
    is_prime[0] = is_prime[1] = false;
    for (int i = 2; i <= MAXV; ++i) {
        if (is_prime[i]) {
            primes[++pcnt] = i;
            if ((long long)i * i <= MAXV) {
                for (int j = i * i; j <= MAXV; j += i)
                    is_prime[j] = false;
            }
        }
    }
    // 大区间筛
    for (int i = 1; i <= pcnt; ++i) {
        int p = primes[i];
        long long start = max(1LL * p * p, (l + p - 1LL) / p * p);
        for (long long j = start; j <= r; j += p)
            seg[j - l] = true;
    }
    if (l == 1) seg[0] = true;
    int ans = 0;
    for (int i = 0; i <= r - l; ++i)
        if (!seg[i]) ++ans;
    printf("%d\n", ans);
    return 0;
}

复杂度

基础筛和区间标记约 O(RloglogR+(RL+1)loglogR)O(\sqrt R\log\log R+(R-L+1)\log\log R),空间 O(R+RL+1)O(\sqrt R+R-L+1)

总结

数值上界大但查询区间短,是分段筛的典型信号:只为当前区间分配标记。