题目信息
- 平台:牛客 (Niuke)
- 题目:25_12-4 质数统计
- 题目链接:d832b0c1a0bd4394a3229f06c6f0b50b
题目描述
\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。
初步思路
由于存在多组区间查询,每次都遍历区间进行质数判断会导致时间超限。因此,最有效的方案是预处理出一定范围内的所有质数,并利用前缀和(或类似结构)来快速回答区间质数统计查询。
算法分析
本题是经典的质数筛法结合前缀和的应用。
-
质数筛法(Euler Sieve / 欧拉筛):
- 核心思想 :欧拉筛(也称线性筛)能够在线性时间复杂度
O(N)内找出所有小于等于N的质数。其关键在于保证每个合数只被其最小质因数筛除一次。 - 步骤简述 :
- 初始化一个布尔数组
isPrime,标记所有数为质数(true)。 - 遍历从 2 到
N的所有数i:- 如果
isPrime[i]为true,则i是质数,将其加入质数列表primes。 - 遍历
primes列表中的每个质数p:- 标记
i * p为合数(isPrime[i * p] = false)。 - 如果
i % p == 0,则p是i的最小质因数,此时停止对当前i用更大的质数进行筛除,以确保i * p只被其最小质因数筛到。
- 标记
- 如果
- 初始化一个布尔数组
- 时间复杂度:O(N)
- 空间复杂度 :O(N),用于存储
isPrime数组和primes列表。
- 核心思想 :欧拉筛(也称线性筛)能够在线性时间复杂度
-
前缀和:
- 在完成质数筛后,我们可以构建一个前缀和数组
s。s[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;
}
总结与反思
- 欧拉筛的高效性 :对于需要处理较大范围内的质数相关问题,欧拉筛是优于埃拉托斯特尼筛的首选方法,因为它能达到理论上的线性时间复杂度
O(N)。 - 前缀和的妙用 :结合前缀和技术,可以将多次区间查询的复杂度从
O(Q * N)优化到O(Q)(算上预处理就是O(N+Q)),极大地提高了效率。 - C++ 数组与
vector的选择 :在 C++ 中,对于固定大小且较大的数组,可以使用原始数组或std::vector。std::vector提供了更好的内存管理和安全性,而原始数组在某些极限性能场景下可能略有优势,但需要手动管理大小。 - 常数优化 :在
eulerSieve中,primes.reserve(N)可以预分配vector的内存,减少动态扩容的开销,这是一种常见的常数时间优化技巧。 - 变量含义 :在使用原始数组的实现中,
isPrime[i]的含义是i是否为合数,而std::vector<bool> isPrime(N+1,true)中的isPrime[i]含义是i是否为质数。这两种定义都是可以的,但需要保持一致性。