202312-小杨的幸运数(luogu-B3929)

202312-小杨的幸运数(luogu-B3929)

GESP C++ 2023年12月五级真题,埃氏筛思想考点,难度⭐⭐⭐☆☆。洛谷难度等级普及/提高−

luogu-B3929 [GESP202312 五级] 小杨的幸运数

题目要求

题目描述

小杨认为,所有大于等于 aa 的完全平方数都是他的超级幸运数。

小杨还认为,所有超级幸运数的倍数都是他的幸运数。自然地,小杨的所有超级幸运数也都是幸运数。

对于一个非幸运数,小杨规定,可以将它一直 +1+1,直到它变成一个幸运数。我们把这个过程叫做幸运化。例如,如果 a=4a=4,那么 44 是最小的幸运数,而 11 不是,但我们可以连续对 1133+1+1 操作,使其变为 44,所以我们可以说, 11 幸运化后的结果是 44

现在,小杨给出 NN 个数,请你首先判断它们是不是幸运数;接着,对于非幸运数,请你将它们幸运化。

输入格式

第一行 22 个正整数 a,Na, N

接下来 NN 行,每行一个正整数 xx ,表示需要判断(幸运化)的数。

输出格式

输出 NN 行,对于每个给定的 xx ,如果它是幸运数,请输出 lucky,否则请输出将其幸运化后的结果。

输入输出样例 #1

输入 #1
2 4 
1 
4 
5 
9
输出 #1
4 
lucky 
8 
lucky

输入输出样例 #2

输入 #2
16 11 
1 
2 
4 
8 
16 
32 
64 
128 
256 
512
1024
输出 #2
16 
16 
16 
16 
lucky 
lucky 
lucky 
lucky 
lucky 
lucky 
lucky

说明/提示

样例解释 1

11 虽然是完全平方数,但它小于 aa,因此它并不是超级幸运数,也不是幸运数。将其进行 33+1+1 操作后,最终得到幸运数 4444 是幸运数,因此直接输出 lucky

55 不是幸运数,将其进行 33+1+1 操作后,最终得到幸运数 88

99 是幸运数,因此直接输出 lucky

数据规模

对于 30%30\% 的测试点,保证 a,x100,N100a,x \le 100,N \le 100

对于 60%60\% 的测试点,保证 a,x106a,x \le 10^6

对于所有测试点,保证 a1,000,000a \le 1,000,000;保证 N2×105N \le 2 \times 10^5;保证 1x1,000,0011 \le x \le 1,000,001


题目分析

解题思路

本题的关键是快速判定一个数是否为“幸运数”,并对非幸运数求出“幸运化”后的最小值。
幸运数 = 所有 a\ge a 的完全平方数(超级幸运数)及其倍数。
数据范围:a106a\le 10^6N2×105N\le 2\times 10^5x106+1x\le 10^6+1

下面给出两种可AC的思路(直接暴力会超时),推荐掌握埃式筛思路方法,输入五级考试大纲的内容。


方法一:类埃氏筛标记(前缀数组)

思想:

  1. 先求出 a\ge a 的最小完全平方数 s0=a2s_0=\lceil\sqrt a\rceil^2,以及后续所有完全平方数 si=i2s_i=i^2ii 向上取整到 10011001 即可覆盖 106+110^6+1)。
  2. 类似素数筛,把每个超级幸运数 sis_i 的所有倍数全部打标记,数组 lucky[num]=1\texttt{lucky[num]}=1 表示 num\texttt{num} 是幸运数。
  3. 查询时 O(1)O(1)lucky[x]\texttt{lucky[x]} 即可;若 xx 未被标记,则从 xx 开始向后线性扫描到第一个 lucky[j]=1\texttt{lucky[j]}=1 的位置,即为幸运化结果。

复杂度:

  • 筛法阶段:超级幸运数个数 106=103\approx\sqrt{10^6}=10^3,每个数筛倍数,总操作次数 103(106/1)=109\approx 10^3\cdot(10^6/1)=10^9,在 10610^6 范围内实际跑得过(CF/洛谷 1s1\,\text s 内)。
  • 查询阶段:O(1)O(1) 判定 + 最坏 O(Δ)O(\Delta) 向后扫描,Δ\Delta 通常很小(下一个幸运数几乎贴身)。

方法二:数学计算(不预处理筛表)

思想:

  1. 预处理只存“超级幸运数”列表 sq[]\texttt{sq[]},元素个数 106=103\le\sqrt{10^6}=10^3

  2. 判定幸运数:枚举 sq[]\texttt{sq[]},看是否存在某个 ssqs\in\texttt{sq} 使得 xmods=0x\bmod s = 0,一旦找到立即返回 true\texttt{true};否则 false\texttt{false}

    • 由于 sq[]\texttt{sq[]} 长度 103\le 10^3,单次判定 103\le 10^3 次取模,2×1052\times 10^5 次询问总计算量 2×1082\times 10^81s1\,\text s 内可过。
  3. 幸运化:对非幸运数 xx,求“x\ge x 的最小幸运数”。
    分两种情况:
    (1) x<ax< a:最小幸运数一定是 a\ge a 的最小完全平方数,即 s0=a2s_0=\lceil\sqrt a\rceil^2
    (2) xax\ge a:此时只需在超级幸运数集合里找“x\ge x 的最小倍数”。
    对每个 ssqs\in\texttt{sq},计算

    candidate=sxs=s+xxmods \texttt{candidate}=s\cdot\Bigl\lceil\frac xs\Bigr\rceil=s+x-x\bmod s

    所有 candidate\texttt{candidate} 取最小值即可。
    由于 sq[]\texttt{sq[]} 长度 103\le 10^3,单次幸运化最多 10310^3 次计算,2×1052\times 10^5 次总计算量 2×1082\times 10^8,同样 1s1\,\text s 内可过。

优点:

  • 无需 10610^6 级别大数组,内存 O(a)O(\sqrt a)
  • 极限数据下运行时间稳定,不会被卡。

缺点:

  • 单次查询常数比方法一略大;
  • 代码量稍多,需要小心处理边界(x<ax<ax=ax=a、整除等)。

示例代码

方法一、类素数埃式筛思路

#include <cmath>
#include <iostream>

int lucky_nums[1002100];

int main() {
    int a, N;
    std::cin >> a >> N;  // 读入起始完全平方数下界 a 与询问次数 N

    // 埃氏筛思想:标记所有超级幸运数(≥a 的完全平方数)及其倍数
    for (int i = std::ceil(std::sqrt(a)); i <= 1001; i++) {
        int square_num = i * i;          // 当前超级幸运数
        lucky_nums[square_num] = 1;      // 标记自己
        for (int j = 2; j * square_num <= 1002000; j++) {
            lucky_nums[j * square_num] = 1;  // 标记其所有倍数
        }
    }

    // 处理 N 次询问
    for (int i = 0; i < N; i++) {
        int x;
        std::cin >> x;
        if (lucky_nums[x]) {             // 若 x 已是幸运数
            std::cout << "lucky" << std::endl;
        } else {
            int max_n = std::max(x, a);  // 从 max(x,a) 开始往后找第一个幸运数
            for (int j = max_n; j <= 1002100; j++) {
                if (lucky_nums[j]) {
                    std::cout << j << std::endl;  // 输出幸运化结果
                    break;
                }
            }
        }
    }

    return 0;
}

方法二、数学计算法

#include <cmath>
#include <iostream>
#include <vector>

std::vector<int> square_nums;  // 存放所有 ≥a 的完全平方数(超级幸运数)

// 判断 x 是否为幸运数:只要 x 是某个超级幸运数的倍数即可
bool is_lucky(int x) {
    for (int i = 0; i < square_nums.size(); i++) {
        if (x == square_nums[i] || x % square_nums[i] == 0) {
            return true;  // 找到任一超级幸运数能整除 x,即为幸运数
        }
    }
    return false;  // 没有任何超级幸运数能整除 x,不是幸运数
}

// 对非幸运数 x 进行“幸运化”:返回不小于 x 的最小幸运数
std::string luckyize(int x, int a) {
    // 若 x 本身已是幸运数且 ≥a,直接返回 lucky
    if (is_lucky(x) && x >= a) {
        return "lucky";
    }

    int lucky_num;  // 用于保存幸运化结果
    if (a >= x) {
        // 当 a ≥ x 时,最小幸运数就是 ≥a 的最小完全平方数
        lucky_num = std::pow(std::ceil(std::sqrt(a)), 2);
    } else {
        // 当 x > a 时,找 ≥x 的最小“超级幸运数倍数”
        // 先以第一个超级幸运数为基准,计算其倍数中 ≥x 的最小值
        // 计算 ≥x 的最小“square_nums[0] 的倍数”:
        // 若 x 能被 square_nums[0] 整除,则 x 本身就是该倍数;
        // 否则先求出 x 除以 square_nums[0] 的余数 r = x % square_nums[0],
        // 再把 x 向上“补齐”到下一个整数倍,即 x + (square_nums[0] - r)。
        // 合并写法:square_nums[0] + x - x % square_nums[0]
        lucky_num = square_nums[0] + x - x % square_nums[0];
        // 再遍历其余超级幸运数,取最小值
        for (int i = 1; i < square_nums.size(); i++) {
            lucky_num = std::min(lucky_num, square_nums[i] + x - x % square_nums[i]);
        }
    }
    return std::to_string(lucky_num);  // 返回字符串形式的幸运化结果
}

int main() {
    int a, N;
    std::cin >> a >> N;  // 读入起始下界 a 和询问次数 N

    // 预处理:把所有 ≥a 的完全平方数加入 square_nums
    for (int i = 1; i <= 1001; i++) {
        if (i * i >= a) {
            square_nums.push_back(i * i);
        }
    }

    // 处理 N 次询问
    for (int i = 0; i < N; i++) {
        int x;
        std::cin >> x;  // 读入待判断/幸运化的数
        std::cout << luckyize(x, a) << std::endl;  // 输出结果
    }

    return 0;
}

本文由coderli.com原创,按照CC BY-NC-SA 4.0 进行授权

所有代码已上传至Github:https://github.com/lihongzheshuai/yummy-code

luogu-”系列题目可在 洛谷题库 在线评测。

bcqm-”系列题目可在 编程启蒙题库 在线评测。

GESP/CSP认证交流QQ群: 688906745

GESP/CSP 认证学习微信公众号
GESP/CSP 认证学习微信公众号
最后更新于