题解:学而思编程 幸运数字

【题目来源】

学而思编程:幸运数字

【题目描述】

小猴对数字非常敏感,他非常钟爱质数,因为他的幸运数字也与质数有关。

小猴的幸运数字有两种,假设 \(x\) 是小猴的幸运数字:

(1)\(x\) 是质数;

(2)\(x\) 不是质数,但是 \(x\) 可以是两个质数的积。

现在小猴想要知道区间 \([L,R]\) 中有多少个幸运数字。

【输入】

第一行一个整数 \(T\),表示共有 \(T\) 测试数据;

每组测试数据,一行两个整数 \(L\)\(R\),表示区间 \([L,R]\)

【输出】

每组测试数据一行,每行一个整数,第 \(i\) 行表示第 \(i\) 组数据中区间 \([L,R]\) 中有多少个幸运数字。

【输入样例】

2
1 6
150 200

【输出样例】

5
23

【核心思想】

  1. 问题分析:给定区间 \([L, R]\),统计其中"幸运数字"的个数。幸运数字定义为:质数,或两个质数的乘积(包括相同质数的平方)。这是一个埃氏筛 + 双质数积标记 + 前缀和问题,关键在于离线预处理所有幸运数字,将每次查询优化到 \(O(1)\)

  2. 算法选择

    • 埃拉托斯特尼筛法:预处理 \([2, N]\) 范围内所有质数
    • 双质数积枚举:枚举所有质数对 \((p_i, p_j)\)\(i \leq j\)),标记其乘积为幸运数字
    • 前缀和sum[i] 表示 \([1, i]\) 中幸运数字的个数,区间查询 \(O(1)\)
  3. 关键步骤

    • 质数筛预处理
      • isPrime[x] = true\(2 \leq x \leq N\)),isPrime[0] = isPrime[1] = false
      • 遍历 \(i\)\(2\)\(N\):若 isPrime[i] 为真,则标记所有 \(i \times j\)\(j \geq 2\))为合数
    • 收集质数表
      • 遍历 \(i\)\(2\)\(N\):若 isPrime[i] 为真,存入 prime[++cnt]
    • 标记双质数积
      • 遍历 \(i\)\(1\)cnt(第一个质数 \(p_i\)
        • 遍历 \(j\)\(i\)cnt(第二个质数 \(p_j\),允许 \(p_i = p_j\)
          • prime[i] * prime[j] > Nbreak(后续更大,提前退出)
          • product[prime[i] * prime[j]] = true:标记该乘积为幸运数字
    • 构建前缀和
      • 遍历 \(i\)\(1\)\(N\)
        • isPrime[i] || product[i]sum[i] = sum[i-1] + 1
        • 否则:sum[i] = sum[i-1]
    • 处理 \(T\) 次查询
      • 读取 \(L, R\)
      • ans = sum[R] - sum[L-1]
      • 输出 ans
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \log \log N + \frac{N}{\ln N} \times \frac{N}{\ln N} + T)\),埃氏筛 \(O(N \log \log N)\),双质数积枚举约 \(O(\frac{N^2}{(\ln N)^2})\)(实际因提前退出远小于此),每次查询 \(O(1)\)
    • 空间复杂度:\(O(N)\)isPrimeproductsum 三个布尔/整型数组
  5. 离线预处理的核心思想

    • 值域固定性\(N = 10^7\) 固定,与查询无关,所有幸运数字可预先计算
    • 质数积枚举优化:内层循环从 \(j = i\) 开始(避免重复),且当乘积超限时 break,利用质数密度递减的特性减少无效计算
    • 两类幸运数字统一:质数本身和双质数积通过 || 合并,前缀和一次统计
    • 查询与计算分离:预处理阶段完成所有 heavy 计算,查询阶段仅 \(O(1)\) 差分,适合多组测试数据
    • 适用于"固定值域 + 多区间查询 + 数论性质判定"类问题,离线筛法配合前缀和可将每次查询降至常数时间

【解题思路】

【算法标签】

质数

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 10000000;  // 上限10^7
int T;
bool isPrime[N + 5], product[N + 5];  // isPrime标记素数,product标记两素数乘积
int prime[N + 5], cnt, sum[N + 5];  // prime存储素数,cnt素数个数,sum前缀和

// 埃拉托色尼筛法
void prime_table()
{
    memset(isPrime, true, sizeof(isPrime));  // 初始化所有数为素数
    isPrime[0] = isPrime[1] = false;  // 0和1不是素数

    for (int i = 2; i <= N; i++)  // 遍历每个数
    {
        if (isPrime[i] == true)  // 如果i是素数
        {
            for (int j = 2; j <= N / i; j++)  // 标记i的所有倍数
            {
                isPrime[i * j] = false;
            }
        }
    }
}

int main()
{
    // 1. 筛出所有素数
    prime_table();

    // 2. 将素数存入prime数组
    for (int i = 2; i <= N; i++)
    {
        if (isPrime[i] == true)
        {
            prime[++cnt] = i;
        }
    }

    // 3. 标记所有两个素数(可相同)的乘积
    for (int i = 1; i <= cnt; i++)  // 第一个素数
    {
        for (int j = i; j <= cnt; j++)  // 第二个素数,从i开始(包括相同素数)
        {
            if (1ll * prime[i] * prime[j] > N)  // 超过范围,提前退出
            {
                break;
            }
            product[prime[i] * prime[j]] = true;  // 标记乘积
        }
    }

    // 4. 计算前缀和:统计每个数之前满足条件的数的个数
    for (int i = 1; i <= N; i++)
    {
        if (product[i] || isPrime[i])  // 如果是素数或两素数乘积
        {
            sum[i] = sum[i - 1] + 1;
        }
        else
        {
            sum[i] = sum[i - 1];
        }
    }

    // 5. 处理查询
    cin >> T;  // 查询次数
    while (T--)
    {
        int L, R;
        cin >> L >> R;  // 查询区间[L,R]
        cout << sum[R] - sum[L - 1] << endl;  // 前缀和计算区间和
    }
    return 0;
}

【运行结果】

2
1 6
5
150 200
23
posted @ 2026-07-04 14:28  团爸讲算法  阅读(5)  评论(0)    收藏  举报