2025-12-04-牛客刷题笔记-25_12-4-质数统计

题目信息


题目描述

\hspace{15pt} 给定 n n n 次询问,每次询问一个闭区间 [ l , r ] [l,r] [l,r],请你输出该区间内质数(素数)的数量。

输入描述:

\hspace{15pt} 第一行输入一个整数 n ( 1 ≦ n ≦ 1 0 4 ) n\left(1\leqq n\leqq 10^4\right) n(1≦n≦104) 表示询问次数。
\hspace{15pt} 此后 n n n 行,第 i i i 行输入两个整数 l i , r i ( 1 ≦ l i ≦ r i ≦ 1 0 6 ) l_i,r_i\left(1\leqq l_i\leqq r_i\leqq 10^6\right) li,ri(1≦li≦ri≦106) 表示第 i i i 次查询的区间。

输出描述:

\hspace{15pt} 对于每一次查询,在一行上输出一个整数,表示区间 [ l i , r i ] [l_i,r_i] [li,ri] 内质数的数量。

示例1

输入:

复制代码
1
1 5

输出:

复制代码
3

示例2

输入:

复制代码
3
2 10
11 11
100 120

输出:

复制代码
4
1
5

说明:
∙   \hspace{23pt}\bullet\, ∙ 区间 [ 2 , 10 ] [2,10] [2,10] 内的质数为 2 , 3 , 5 , 7 2,3,5,7 2,3,5,7,数量为 4 4 4;
∙   \hspace{23pt}\bullet\, ∙ 区间 [ 11 , 11 ] [11,11] [11,11] 仅包含质数 11 11 11,数量为 1 1 1;
∙   \hspace{23pt}\bullet\, ∙ 区间 [ 100 , 120 ] [100,120] [100,120] 内的质数为 101 , 103 , 107 , 109 , 113 101,103,107,109,113 101,103,107,109,113,数量为 5 5 5。


初步思路

由于存在多组区间查询,每次都遍历区间进行质数判断会导致时间超限。因此,最有效的方案是预处理出一定范围内的所有质数,并利用前缀和(或类似结构)来快速回答区间质数统计查询。


算法分析

本题是经典的质数筛法结合前缀和的应用。

  1. 质数筛法(Euler Sieve / 欧拉筛)

    • 核心思想 :欧拉筛(也称线性筛)能够在线性时间复杂度 O(N) 内找出所有小于等于 N 的质数。其关键在于保证每个合数只被其最小质因数筛除一次。
    • 步骤简述
      1. 初始化一个布尔数组 isPrime,标记所有数为质数(true)。
      2. 遍历从 2 到 N 的所有数 i
        • 如果 isPrime[i]true,则 i 是质数,将其加入质数列表 primes
        • 遍历 primes 列表中的每个质数 p
          • 标记 i * p 为合数(isPrime[i * p] = false)。
          • 如果 i % p == 0,则 pi 的最小质因数,此时停止对当前 i 用更大的质数进行筛除,以确保 i * p 只被其最小质因数筛到。
    • 时间复杂度:O(N)
    • 空间复杂度 :O(N),用于存储 isPrime 数组和 primes 列表。
  2. 前缀和

    • 在完成质数筛后,我们可以构建一个前缀和数组 ss[i] 表示从 1 到 i(含 i)的质数总个数。
    • 构建前缀和数组的过程也是 O(N) 时间复杂度。
    • 对于每次查询区间 [l, r],其质数个数为 s[r] - s[l-1],查询时间复杂度为 O(1)。

总时间复杂度 :预处理 O(N) + 多次查询 O(1) = O(N + Q),其中 Q 是查询次数。

(关于欧拉筛和埃拉托斯特尼筛的更详细对比,可参考 质数筛法详解.md


代码实现

方案一:使用原始数组和计数器 (C++)

此方案使用原始 bool 数组和 int 数组来存储质数列表及质数数量的前缀和,索引从 1 开始。

cpp 复制代码
#include<iostream>
#define il inline
using namespace std;

#define fastio \
  ios::sync_with_stdio(false); \
  cin.tie(0);

typedef long long ll;

const ll N = 10000006;
int primes[N],j,s[N+1];  // primes: 存储质数列表; j: 质数数量; s: 质数前缀和
bool isPrime[N+1];        // isPrime[i]: i是否为合数 (0表示质数, 1表示合数)

void eulerSieve()
{
    // 初始化,通常isPrime默认为false (表示质数), 但这里用0表示质数,1表示合数
    // 习惯性将0和1标记为合数或非质数,这里代码里 isPrime[1] = 1; 
    // isPrime数组默认初始化为0 (false)即可,表示所有数暂时都是质数

    for(ll i = 2;i<=N;++i)
    {
        if(!isPrime[i]) // 如果i是质数
        {
            primes[++j] = i; // 加入质数列表
            s[i]++;           // 当前i是质数,所以前缀和在i处加1
        }
        // 遍历已找到的质数去筛除合数
        for(ll q = 1;q<=j && primes[q] * i <= N;++q) 
        {
            isPrime[primes[q]*i] = 1; // 标记为合数
            if(!(i%primes[q])) // 如果i能被primes[q]整除,说明primes[q]是i的最小质因数
            {                  // 此时停止,避免重复筛除,确保线性复杂度
                break;
            }
        }
    }
    isPrime[1] = 1; // 1不是质数
    // 构建前缀和数组,s[i]存储1到i的质数总数
    for(ll i = 2;i<=N;++i)
    {
        s[i] += s[i-1];
    }
}


il void solve(){
    int l,r;
    cin >> l >> r;
    cout << s[r] - s[l-1] << "\n"; // 查询区间 [l, r] 的质数个数
}

int main()
{
    fastio
    
    eulerSieve();  // 调用筛法初始化
    
    int t = 1;
    cin >> t;
    while(t--)
    {
        solve();
    }
    
    return 0;
}

方案二:使用 std::vector (C++)

此方案使用 std::vector 动态数组来存储质数列表和布尔标记,更符合现代 C++ 风格。

cpp 复制代码
#include<iostream>
#include<vector>
#define il inline
using namespace std;

#define fastio \
  ios::sync_with_stdio(false); \
  cin.tie(0);

typedef long long ll;

const ll N = 10000006;
vector<int> primes; // 存储质数列表
vector<int> s(N+1); // s[i]: 1到i的质数总数 (前缀和)
vector<bool> isPrime(N+1,true); // isPrime[i]: i是否为质数 (true表示质数)

void eulerSieve()
{
    primes.reserve(N);        // 预分配空间,提高效率(可选优化,避免多次扩容)
    isPrime[0] = isPrime[1] = false; // 0和1不是质数
    for(ll i = 2;i<=N;++i)
    {
        if(isPrime[i]) // 如果i是质数
        {
            primes.push_back(i); // 加入质数列表
            s[i]++;               // 当前i是质数,所以前缀和在i处加1
        }
        // 遍历已找到的质数去筛除合数
        for(ll q = 0;q<primes.size() && (ll)primes[q] * i <= N;++q)  // 从0开始,因为vector索引从0开始 
        {
            isPrime[primes[q]*i] = false; // 标记为合数
            if(i%primes[q] == 0) // 如果i能被primes[q]整除,说明primes[q]是i的最小质因数
            {                    // 此时停止,避免重复筛除,确保线性复杂度
                break;
            }
        }
    }
    // 构建前缀和数组,s[i]存储1到i的质数总数
    for(ll i = 2;i<=N;++i)
    {
        s[i] += s[i-1];
    }
}


il void solve(){
    int l,r;
    cin >> l >> r;
    cout << s[r] - s[l-1] << "\n"; // 查询区间 [l, r] 的质数个数
}

int main()
{
    fastio
    
    eulerSieve();  // 调用筛法初始化
    
    int t = 1;
    cin >> t;
    while(t--)
    {
        solve();
    }
    
    return 0;
}

总结与反思

  1. 欧拉筛的高效性 :对于需要处理较大范围内的质数相关问题,欧拉筛是优于埃拉托斯特尼筛的首选方法,因为它能达到理论上的线性时间复杂度 O(N)
  2. 前缀和的妙用 :结合前缀和技术,可以将多次区间查询的复杂度从 O(Q * N) 优化到 O(Q)(算上预处理就是 O(N+Q)),极大地提高了效率。
  3. C++ 数组与 vector 的选择 :在 C++ 中,对于固定大小且较大的数组,可以使用原始数组或 std::vectorstd::vector 提供了更好的内存管理和安全性,而原始数组在某些极限性能场景下可能略有优势,但需要手动管理大小。
  4. 常数优化 :在 eulerSieve 中,primes.reserve(N) 可以预分配 vector 的内存,减少动态扩容的开销,这是一种常见的常数时间优化技巧。
  5. 变量含义 :在使用原始数组的实现中,isPrime[i] 的含义是 i 是否为合数,而 std::vector<bool> isPrime(N+1,true) 中的 isPrime[i] 含义是 i 是否为质数。这两种定义都是可以的,但需要保持一致性。
相关推荐
小O的算法实验室1 小时前
2024年IEEE IOTJ SCI2区TOP,基于混合算法的水下物联网多AUV未知环境全覆盖搜索方法,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
齐生11 小时前
iOS 知识点 - 一篇文章弄清「输入事件系统」(【事件传递机制、响应链机制】以及相关知识点)
笔记·面试
Slaughter信仰1 小时前
图解大模型_生成式AI原理与实战学习笔记(前三章综合问答)
人工智能·笔记·学习
洲星河ZXH1 小时前
Java,比较器
java·开发语言·算法
CoderYanger1 小时前
递归、搜索与回溯-FloodFill:33.太平洋大西洋水流问题
java·算法·leetcode·1024程序员节
CodeByV1 小时前
【算法题】双指针(二)
数据结构·算法
koo3642 小时前
pytorch深度学习笔记5
pytorch·笔记·深度学习
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [fs]binfmt_script
linux·笔记·学习