3 条题解

  • 0
    @ 2026-6-5 11:01:40

    太棒了!自己动手写数据生成器是吃透算法边界情况的最佳途径。

    正如我们在之前讨论的,原题规定的 B1000B \le 1000 实在太小了,哪怕是最最暴力的 O(N2)O(N^2) 解法也能在 1 毫秒内通过。作为出题人和教练,为了真正区分出暴力解法和优化解法(让暴力法拿不到满分),我们必须在后台数据中“暗度陈仓”,埋入几个 10510^5 级别的大数据测试点。

    下面为你精心设计的 10 个测试点覆盖策略以及 C++ 数据生成器代码。

    核心设计思路(10 个测试点分布)

    1. Test 1 (2 10):样例 1。基本功能测试。
    2. Test 2 (98 100):样例 2。区间内没有素数的特殊情况。
    3. Test 3 (2 2)最小值极小区间A=BA = B,且本身是素数。考察边界是否处理包含 AABB 本身。
    4. Test 4 (4 4)合数极小区间A=BA = B,且本身是合数。
    5. Test 5 (2 100):常规小区间测试。
    6. Test 6 (990 1000):约定范围的最大区间边缘。
    7. Test 7 (2 1000):约定范围的最大遍历跨度
    8. Test 8 (800 900):常规随机取值区间。
    9. Test 9 (99990 100000)【区分度杀手数据 1】。故意超纲,测试高位大数字时,试除法是否会超时或出现除零错误。
    10. Test 10 (2 100000)【区分度杀手数据 2】。终极卡时数据!这个数据要求对十万以内的所有数做判断。O(N2)O(N^2) 暴力法在此处需要约 101010^{10} 次运算,必得 TLE(超时);而 O(NN)O(N\sqrt{N}) 优化法只需 3×1073\times 10^7 次运算,完美 AC

    数据生成器完整 C++ 代码 (generator.cpp)

    请将以下代码保存为 generator.cpp,本地编译运行即可在当前目录生成极其轻量的 20 个测试数据文件。

    #include <iostream>
    #include <fstream>
    #include <string>
    #include <vector>
    
    using namespace std;
    
    // 标程函数:使用 O(N * sqrt(N)) 的最优解来生成标准答案
    // 生成器必须比普通程序更稳健,所以我们用已经充分优化的解法
    int solve(int A, int B) {
        int ans = 0;
        for (int x = A; x <= B; ++x) {
            // 题目约定 A >= 2,但为了标程的绝对鲁棒性,加一句防范
            if (x < 2) continue; 
            
            bool is_prime = true;
            // 剪枝:试除到平方根即可
            for (int i = 2; i * i <= x; ++i) {
                if (x % i == 0) {
                    is_prime = false;
                    break;
                }
            }
            if (is_prime) {
                ans++;
            }
        }
        return ans;
    }
    
    // 定义一个结构体用来存取区间的起点和终点
    struct TestCase {
        int A, B;
    };
    
    int main() {
        // 优化系统 IO 速度
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
    
        // 精选的 10 个测试用例
        vector<TestCase> test_cases = {
            {2, 10},            // Test 1: 样例 1
            {98, 100},          // Test 2: 样例 2 (0 个素数)
            {2, 2},             // Test 3: 最小边界,单点素数
            {4, 4},             // Test 4: 最小边界,单点合数
            {2, 100},           // Test 5: 常规小范围
            {990, 1000},        // Test 6: 原题范围的大数端
            {2, 1000},          // Test 7: 原题范围的全局遍历
            {800, 900},         // Test 8: 随机中位区间
            {99990, 100000},    // Test 9: 卡常数数据,大值小区间
            {2, 100000}         // Test 10: 终极杀手数据,卡掉 O(N^2) 暴力解法
        };
    
        // 循环生成 10 组测试数据
        for (int i = 0; i < test_cases.size(); ++i) {
            int A = test_cases[i].A;
            int B = test_cases[i].B;
            
            // --- 1. 生成 .in 文件 ---
            string in_filename = to_string(i + 1) + ".in";
            ofstream fout_in(in_filename);
            if (!fout_in.is_open()) {
                cerr << "无法创建输入文件: " << in_filename << "\n";
                continue;
            }
            // 写入 A 和 B,用空格隔开
            fout_in << A << " " << B << "\n";
            fout_in.close();
    
            // --- 2. 使用标程计算正确答案 ---
            int ans = solve(A, B);
    
            // --- 3. 生成 .out 文件 ---
            string out_filename = to_string(i + 1) + ".out";
            ofstream fout_out(out_filename);
            if (!fout_out.is_open()) {
                cerr << "无法创建输出文件: " << out_filename << "\n";
                continue;
            }
            fout_out << ans << "\n";
            fout_out.close();
    
            // 终端输出生成日志,方便核对
            cout << "成功生成 Test " << (i + 1) << "\t"
                 << "| 区间: [" << A << ", " << B << "]\t"
                 << "| 素数个数: " << ans << "\n";
        }
    
        cout << "\n所有测试数据生成完毕!单文件仅数个字节,总和远小于 2MB。" << endl;
        return 0;
    }
    

    OJ 部署与评测反馈(教练的后台视角)

    1. “背刺”暴力解法:当你把这套数据传到你的 OJ 平台后,设置时间限制(Time Limit)为 1000ms(1秒)。那些没有写 i * i <= x 而是写了 i < x 的同学,会在 Test 10 长时间卡住,最后无奈收获一个鲜红的 TLE(Time Limit Exceeded)。这样他们才能刻骨铭心地记住“平方根降维”的威力。
    2. 防除 0 异常:内层循环强制从 i = 2 开始试除,从根本上杜绝了对 00 取模的 Exception 崩溃问题。
    3. 安全与轻量:没有使用递归调用(防爆栈),也没有动态开辟巨大的数组内存。10 个输入输出文件加起来只有几十个字节,对 OJ 硬盘空间的消耗几乎为零!
    • 0
      @ 2026-6-5 10:58:01

      你好!很高兴带你把草稿纸上的逻辑转化为真正的 C++ 代码。

      在信息学奥赛中,关于“素数”的考察极其频繁。对于这道题(B1000B \le 1000),虽然最笨的方法也能过,但我们完全可以借此机会,把素数判定的**“初级 -> 进阶 -> 终极”**三种形态都掌握。这将是你日后冲击省一等奖的宝贵财富。

      下面为你提供三个完全符合 NOIP/CSP C++14 标准的独立可运行代码。


      版本一:新手直觉版 —— 纯暴力试除法

      这个版本完全按照定义来:判断 xx 是不是素数,就拿 22x1x-1 之间的所有数去试着除它。

      #include <iostream>
      
      using namespace std;
      
      int main() {
          // 优化输入输出速度
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int A, B;
          cin >> A >> B;
      
          int ans = 0; // 记录素数的个数
      
          // 外层循环:遍历 A 到 B 之间的每一个数字 x
          for (int x = A; x <= B; ++x) {
              
              // 关键点1:假设法(Flag标记)
              // 遇到一个数字,我们先“无罪推定”,假定它是素数
              bool is_prime = true;
              
              // 内层循环:从 2 试除到 x-1
              // 易错点1:不能从 1 开始,也不能除到 x,否则任何数都会被认为能整除
              for (int i = 2; i < x; ++i) {
                  // 如果 x 能够被 i 整除,说明找到了其他因子
                  if (x % i == 0) {
                      is_prime = false; // 撕下伪装,它不是素数!
                      break;            // 关键点2:一票否决,直接跳出内层循环,不再做无用功
                  }
              }
              
              // 结算:如果经历了一整轮的试除,is_prime 依然坚挺为 true,那它就是真素数
              if (is_prime) {
                  ans++;
              }
          }
      
          cout << ans << "\n";
      
          return 0;
      }
      

      【复杂度分析与优化思考】

      • 空间复杂度O(1)O(1)。只用了几个基本变量。
      • 时间复杂度O(B2)O(B^2)。外层循环执行 BB 次,内层最坏情况下也要执行 BB 次。
      • 优化思考:如果 B=1000B=1000,循环大约需要执行几十万次,瞬间出结果。但如果考场上 B=107B = 10^7(一千万),这个代码就要计算接近 101410^{14} 次,会直接超时(TLE)!我们需要把内层循环砍掉一半以上。

      版本二:竞赛标答版 —— 平方根降维试除法(强烈推荐肌肉记忆)

      利用我们在草稿纸上推导出的数学定理:如果一个数 xx 有因子,那么较小的那个因子一定 x\le \sqrt{x}

      #include <iostream>
      
      using namespace std;
      
      int main() {
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int A, B;
          cin >> A >> B;
      
          int ans = 0;
      
          for (int x = A; x <= B; ++x) {
              bool is_prime = true;
              
              // 关键点优化(时间复杂度降维的核心):
              // 我们不需要一直试除到 x-1。只需要试除到 x 的平方根即可!
              // 竞赛高级写法:不写 i <= sqrt(x)(因为开方是浮点运算,速度慢且容易有精度误差)
              // 而是写成 i * i <= x,完美转化为纯整数乘法比较!
              for (int i = 2; i * i <= x; ++i) {
                  if (x % i == 0) {
                      is_prime = false;
                      break;
                  }
              }
              
              if (is_prime) {
                  ans++;
              }
          }
      
          cout << ans << "\n";
      
          return 0;
      }
      

      【复杂度分析与优化建议】

      • 时间复杂度:极优的 O(BB)O(B \sqrt{B})。此时就算 BB 达到 10710^7(一千万),由于内层循环最多只执行 31623162 次,总计算量在千万级别,依然能在 0.5 秒内稳稳跑完!
      • 空间复杂度:最优的 O(1)O(1)
      • 教练点拨for (int i = 2; i * i <= x; ++i) 这个 for 循环的写法,是你信息学奥赛生涯中必须印在脑子里的代码。 以后遇到任何需要单点判断素数的题目,默写这段代码就是满分标准答案。

      版本三:极客/省选入门版 —— 埃拉托斯特尼筛法(Sieve of Eratosthenes)

      版本一和版本二是**“单点判断”(判断某个具体的数字是不是素数)。但如果题目是问“一个巨大区间内有多少个素数”**,我们有一种用“空间换时间”的终极魔法——筛法。 思路:素数的倍数绝对不是素数!我们拿一个大本子(数组)记下所有数,从 22 开始,把 22 的所有倍数划掉;接着把 33 的倍数划掉……剩下的没被划掉的,全都是素数!

      #include <iostream>
      #include <vector>
      
      using namespace std;
      
      int main() {
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int A, B;
          cin >> A >> B;
      
          // 关键点1:开辟一个大小为 B+1 的布尔数组,初始全部假设为素数(true)
          // 数组下标就代表数字本身
          vector<bool> is_prime(B + 1, true);
          
          // 0 和 1 不是素数,先强行划掉
          is_prime[0] = false;
          is_prime[1] = false;
      
          // 核心算法:埃氏筛
          // 从 2 开始,遍历到 B 的平方根即可
          for (int i = 2; i * i <= B; ++i) {
              if (is_prime[i]) { // 如果 i 是素数,那么把它的倍数全划掉
                  // 优化:可以直接从 i 的平方(i*i)开始划,因为比它小的倍数之前肯定被其他小素数划过了
                  for (int j = i * i; j <= B; j += i) {
                      is_prime[j] = false; // 划掉!你是个合数!
                  }
              }
          }
      
          // 统计 A 到 B 之间幸存下来的素数
          int ans = 0;
          for (int x = A; x <= B; ++x) {
              if (is_prime[x]) {
                  ans++;
              }
          }
      
          cout << ans << "\n";
      
          return 0;
      }
      

      【复杂度分析与进阶展望】

      • 时间复杂度:恐怖的 O(BloglogB)O(B \log \log B)。这个复杂度在数学上极其接近 O(B)O(B),也就是线性时间!如果题目让你求 1110710^7 的所有素数,这是唯一能拿到满分的算法类型。
      • 空间复杂度O(B)O(B)。我们牺牲了一点点内存(开了一个长度为 BB 的数组)来换取了极速的运算。在竞赛中,10710^7 的布尔数组也就占用不到 10MB10\text{MB} 内存(现在的比赛内存限制通常是 256MB256\text{MB}512MB512\text{MB}),完全值得!
      • 教练忠告:在这个阶段,你必须熟练默写版本二(平方根降维法)。对于版本三(埃氏筛法),你可以作为拓展眼界来理解它的“排除法思维”,在之后的拔高训练中,我们会详细专门攻克各种“筛法”。
      • 0
        @ 2026-6-5 10:56:20

        你好!很高兴能继续陪你打怪升级。这次你遇到的是信息学奥赛里最最经典、也是未来很多复杂数论算法(比如筛法)的基石问题——“素数(质数)判定与区间统计”

        来,深呼吸,准备好草稿纸和笔,我们先理清思路,不急着敲代码。


        1. 教练的思路提示(不提供完整代码)

        • 提示一:把大问题拆成两个小任务。
          • 任务 1:我们需要一个大循环,把从 AABB 之间的每一个数都“拎”出来。
          • 任务 2:对于被拎出来的这个数 xx,我们要写一段独立的逻辑去判断它到底是不是素数。如果是,计数器就加一。
        • 提示二:如何让计算机判断素数? 题目说除了 11 和它自身,不能被其他数整除。那我们就暴力一点,对于数字 xx,我们让一个变量 ii22 开始,一直循环尝试除到 x1x-1。如果中间哪怕有一个 ii 能把 xx 整除(即 x % i == 0),那 xx 就绝对不是素数!
        • 提示三:“一票否决制”与布尔标记。 在判断一个数是不是素数时,我们可以先假设它是素数(立个 Flag:bool is_prime = true)。在尝试除的过程中,一旦发现能整除的数,就把 Flag 变成 false,并且立刻停止尝试(break,因为只要找到一个因子,就已经宣判它不是素数了,没必要往下找。

        2. 预备知识点总结

        要优雅地解出这道题,你需要掌握以下编程武器:

        1. 嵌套循环:外层循环遍历区间 [A,B][A, B],内层循环用于试除判断。
        2. 布尔型变量(bool:作为状态标记(Flag),记录当前的数是否“保持着素数的纯洁性”。
        3. 取模运算(%:判断整除的核心。
        4. break 语句:用于在内层循环中一旦发现不是素数,立刻“刹车”跳出,节约计算时间。
        5. 数学函数的初步应用:通过平方根缩小试除范围(进阶优化必需)。

        3. 启发式与图形式的草稿纸推导(教学模拟)

        “来,拿出草稿纸。我们用样例 1 的数据:221010 之间找素数。”

        第一步:外层框架(把数字拎出来)

        [草稿纸推导] 区间遍历
        待检查的数字: [ 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
        素数计数器: cnt = 0
        

        第二步:模拟内层判断(找出破绽)

        教练启发:“假设现在轮到判断数字 99。按照笨办法,我们要拿哪些数去试着除它?”

        • 试除 229 % 2 != 0 (安全)
        • 试除 339 % 3 == 0 (危险!发现因子 3!不是素数!立刻 break!)

        第三步:寻找试除的极限点(时间复杂度优化核心)

        教练提问:“假设我们要判断数字 1616。我们需要拿 2,3,4,5,,152, 3, 4, 5, \dots, 15 全去试一遍吗?” 我们在草稿纸上把 1616 的所有乘法因子列出来:

        • 1×16=161 \times 16 = 16
        • 2×8=162 \times 8 = 16
        • 4×4=164 \times 4 = 16
        • 8×2=168 \times 2 = 16 (注意,这里开始反转了!)

        教练总结规律:“你发现了吗?因子总是成对出现的!一个大,一个小。如果 1616 有一个大于 44 的因子(比如 88),那么它必然对应一个小于 44 的因子(即 22)。这意味着什么?意味着我们只要在较小的那半边找不到因子,较大的那半边就绝对不可能有因子!” 这个分界点,就是数字的平方根(x\sqrt{x}

        第四步:复杂度的思考

        • 空间复杂度
          • 只需要几个变量 A,B,i,j,cntA, B, i, j, cnt,不管测多大的数,内存不增加。空间复杂度为优秀的 O(1)O(1)
        • 时间复杂度(不优化)
          • 外层循环 BB 次,内层循环最多试除 BB 次。最坏时间复杂度为 O(B2)O(B^2)
          • B=1000B = 1000 时,计算次数 1,000,000\approx 1,000,000 次(一百万),对计算机 1 秒 1 亿次的性能来说,直接跑也能满分通过(AC)。
        • 时间复杂度(优化后)
          • 利用平方根法则,内层循环最多只试除 B\sqrt{B} 次。时间复杂度骤降为 O(BB)O(B\sqrt{B})
          • B=1000B = 1000 时,100031\sqrt{1000} \approx 31。总计算次数最多只有 1000×31=31,0001000 \times 31 = 31,000 次!
          • 为什么一定要学这个优化? 如果哪天题目把范围放大到 B=1,000,000B = 1,000,000(一百万),不优化要循环 101210^{12} 次(直接超时),而优化后只需循环 106×103=10910^6 \times 10^3 = 10^9 次(勉强能过)。如果用更高级的“埃氏筛”或“欧拉筛”,还能更快。

        4. 读题时的“题眼”(关键词)提取技巧

        下次遇到关于“素数/质数”的题目,请在题干中圈出这些词:

        1. “大于 1 的正整数” —— 题眼:边界条件。这非常重要!数字 1 既不是素数也不是合数。如果题目给的 AA11,你在写代码时如果不特判掉 1,就会出错!本题约定了 A2A \ge 2,所以题目仁慈地帮你避开了这个陷阱。
        2. “包括 AABB —— 题眼:外层循环的边界。你的 for 循环必须要写成 <= B 而不是 < B
        3. 2AB10002 \le A \le B \le 1000 —— 题眼:数据范围决定算法
          • 10001000 这个量级暗示你可以用最暴力的 O(N2)O(N^2) 写法。
          • 但作为竞赛生,看到素数判定,不管数据多小,肌肉记忆都应该直接写出 O(N)O(\sqrt{N}) 的试除法优化代码

        好了,现在思路已经非常清晰了。你可以尝试把“外层遍历区间”加上“内层用标记位(Flag)加 break 进行素数判定”的代码在草稿纸上写出来。如果写顺了,再尝试把内层循环的上限改成 x\sqrt{x}。遇到卡壳随时叫我!

        • 1

        信息

        ID
        13923
        时间
        1000ms
        内存
        128MiB
        难度
        3
        标签
        递交数
        17
        已通过
        4
        上传者