3 条题解

  • 0
    @ 2026-6-5 11:29:53

    太棒了!自己动手写这道题的数据生成器,你会发现一个非常有趣的现象:这道题根本无法用“数据量”卡掉暴力法,出题人卡的是选手的“精度陷阱(Bug)”!

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

    题目约定 X<108X < 10^8,最大的长度只有 8 位。即使是用最笨的循环去扒每一位数,计算机一秒钟处理 1 亿次,而 M=100M=100 的数据量对于这种算法,总耗时连 0.0010.001 毫秒都不到。

    所以,这道题的测试数据设计核心在于:精准打击那些偷懒使用 <cmath> 库中 pow() 函数而没有处理浮点精度误差的错解! 如果选手写了 sum += pow(digit, N),由于浮点数的科学计数法存储机制,像 pow(5, 3) 可能会在底层算成 124.9999999,强转整型后变成 124,导致本来是自幂数的判定为 F。为了抓住他们,我们必须在数据中埋入足够多、覆盖各个位数的“真实自幂数”

    1. Test 1 & Test 2:样例 1 和 样例 2。
    2. Test 3一位数与两位数边界191 \sim 9 全是自幂数,但两位数中没有自幂数。
    3. Test 4三位数与四位数靶场。包含 153,370,371,407,1634,8208,9474153, 370, 371, 407, 1634, 8208, 9474 等所有的真实自幂数,夹杂临界值。
    4. Test 5五位数与六位数靶场。包含 54748,54883454748, 548834 等所有的真实自幂数。
    5. Test 6七位数靶场。包含 1741725,42108181741725, 4210818 等所有的真实自幂数。
    6. Test 7八位数(上限边界)靶场。包含 24678050,8859347724678050, 88593477 以及逼近 10810^8 的边界合数。
    7. Test 8:压力测试 M=100M=100。全是大位数的伪装者(看似符合但实际不是)。
    8. Test 9【精准精度杀手卷】 M=100M=100。我将 10810^8 以内总共 2727 个真实的自幂数全部塞进去,混杂大量的普通数字。只要选手的 pow 精度有一丁点差池,就会在这里收获 Wrong Answer
    9. Test 10:纯随机混合的大杂烩测试点,考察程序整体鲁棒性。

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

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

    #include <iostream>
    #include <fstream>
    #include <string>
    #include <vector>
    #include <random>
    #include <algorithm>
    
    using namespace std;
    
    // 标程:极度严谨的字符串处理 + 纯整数幂运算(绝对不会有精度误差)
    bool isArmstrong(int x) {
        string s = to_string(x);
        int n = s.length();
        int sum = 0;
        for (char c : s) {
            int d = c - '0';
            int p = 1;
            for (int i = 0; i < n; ++i) {
                p *= d;
            }
            sum += p;
        }
        return sum == x;
    }
    
    int main() {
        // 优化系统 IO 速度
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
    
        // 预先罗列出 10^8 以内的所有自幂数(水仙花数、四叶玫瑰数、五角星数等)共 27 个
        vector<int> real_armstrong = {
            1, 2, 3, 4, 5, 6, 7, 8, 9,           // 1位 (9个)
            153, 370, 371, 407,                  // 3位 (4个)
            1634, 8208, 9474,                    // 4位 (3个)
            54748, 92727, 93084,                 // 5位 (3个)
            548834,                              // 6位 (1个)
            1741725, 4210818, 9800817, 9926315,  // 7位 (4个)
            24678050, 24678051, 88593477         // 8位 (3个)
        };
    
        // 随机数发生器
        mt19937 rng(1337); 
    
        // 手动构造精心设计的 10 组测试集
        vector<vector<int>> test_cases(10);
    
        // Test 1: 样例 1
        test_cases[0] = {152, 111, 153};
    
        // Test 2: 样例 2
        test_cases[1] = {8208, 548834, 88593477, 12345, 5432};
    
        // Test 3: 一位数与两位数边界
        test_cases[2] = {1, 5, 9, 10, 11, 55, 99};
    
        // Test 4: 三位数与四位数靶场
        test_cases[3] = {153, 370, 371, 407, 100, 999, 1634, 8208, 9474, 1000, 9999};
    
        // Test 5: 五位数与六位数靶场
        test_cases[4] = {54748, 92727, 93084, 548834, 10000, 99999, 100000, 999999};
    
        // Test 6: 七位数靶场
        test_cases[5] = {1741725, 4210818, 9800817, 9926315, 1000000, 9999999, 1234567};
    
        // Test 7: 八位数边界
        test_cases[6] = {24678050, 24678051, 88593477, 10000000, 99999999, 87654321};
    
        // Test 8: 高压测试 - 100个逼近边界的伪装数
        for(int i=0; i<100; ++i) {
            test_cases[7].push_back(rng() % 90000000 + 10000000); // 生成10^7 ~ 10^8的数
        }
    
        // Test 9: 【精度杀手】加入全部 27 个自幂数,再用随机数填满 100 个
        for(int num : real_armstrong) {
            test_cases[8].push_back(num);
        }
        while(test_cases[8].size() < 100) {
            test_cases[8].push_back(rng() % 100000000); 
        }
        // 打乱顺序,掩人耳目
        shuffle(test_cases[8].begin(), test_cases[8].end(), rng);
    
        // Test 10: 纯随机的 100 个数
        for(int i=0; i<100; ++i) {
            test_cases[9].push_back(rng() % 100000000);
        }
    
        // 执行数据文件生成流程
        for (int i = 0; i < 10; ++i) {
            // --- 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;
            }
            
            int M = test_cases[i].size();
            fout_in << M << "\n"; // 第一行输出 M
            for (int x : test_cases[i]) {
                fout_in << x << "\n"; // 依次输出每个正整数
            }
            fout_in.close();
    
            // --- 2. 生成 .out 文件 ---
            string out_filename = to_string(i + 1) + ".out";
            ofstream fout_out(out_filename);
            if (!fout_out.is_open()) {
                cerr << "无法创建输出文件: " << out_filename << "\n";
                continue;
            }
            
            for (int x : test_cases[i]) {
                if (isArmstrong(x)) {
                    fout_out << "T\n";
                } else {
                    fout_out << "F\n";
                }
            }
            fout_out.close();
    
            // 终端输出生成日志,方便教练核对
            cout << "成功生成 Test " << (i + 1) << "\t | M = " << M << " | 已写入文件\n";
        }
    
        cout << "\n所有测试数据生成完毕!文件极小,完美符合 < 2MB 要求。" << endl;
        return 0;
    }
    

    OJ 部署与评测反馈建议(给教练的后台秘籍)

    1. 防除 0 与爆栈机制: 这个生成器代码底层彻底屏蔽了除法运算,而是使用字符串拆解和纯正整数循环乘法,完全免疫了除以 00 或者除法浮点精度的异常,而且没有一处递归调用,不存在爆栈风险。
    2. “坑”人的艺术(考点区分): 如果你把这组数据放到 OJ 上评测,你会看到极度明显的区分度:
      • 那些为了图方便使用了 <cmath> 中的 pow(d, n) ,但没有通过四舍五入直接转成 int 的人,极大概率会在 Test 9 吃到一发或多发 Wrong Answer。因为真实的自幂数被错判成了 F。这就是为什么我硬编码罗列了所有 27 个自幂数并将它们塞入高压测试集的原因。
      • 那些使用了版本二标准代码的同学,将稳稳地拿到满屏漂亮的绿灯 100 Accepted
    3. 空间安全:单个 in 文件不超过 1 KB1 \text{ KB},对 OJ 没有任何评测硬盘负担。
    • 0
      @ 2026-6-5 11:28:19

      你好!很高兴带你把草稿纸上的推导化为实实在在的代码。

      在这道题中,由于待判断的数字规模极小(X<108X < 10^8),我们并不需要在宏观的“时间复杂度”上去做降维打击。因此,版本的演进将聚焦于**“代码的优雅度、容错性与 C++ 特性的运用”**。我们会从最纯粹的“纯数学数学模拟法”过渡到更具极客精神的“字符串映射法”。

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


      版本一:新手基本功版 —— 纯数学拆分法(必须掌握)

      这个版本完全依靠除法 / 10 和取模 % 10 来完成数字的拆解。它是所有 OIer 必须烂熟于心的基本功,不管未来学多少高级语法,这种处理整数的底层逻辑绝对不能丢。

      #include <iostream>
      
      using namespace std;
      
      // 辅助函数:自己写一个计算次方的函数。
      // 为什么不用 <cmath> 里的 pow?因为内置的 pow 返回 double 浮点数,
      // 浮点数在底层是用科学计数法存储的,直接转 int 时可能会因为精度误差导致 26.99999 被截断成 26。
      // 纯整数乘法绝对精确,防范于未然。
      int my_pow(int base, int exp) {
          int res = 1;
          for (int i = 0; i < exp; ++i) {
              res *= base;
          }
          return res;
      }
      
      int main() {
          // 优化输入输出速度
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int M;
          cin >> M;
      
          while (M--) { // 循环 M 次,来一个处理一个
              int X;
              cin >> X;
      
              // 特判边界:0 也是自幂数 (0^1 = 0)
              // 虽然本题给的是正整数,但加上防御性编程是好习惯
              if (X == 0) {
                  cout << "T\n";
                  continue;
              }
      
              // 步骤 1:利用“替身1”来求数字的长度 N
              int temp1 = X;
              int N = 0;
              while (temp1 > 0) {
                  N++;
                  temp1 /= 10; // 每次砍掉最后一位
              }
      
              // 步骤 2:利用“替身2”来逐个扒取每一位,计算 N 次方之和
              int temp2 = X;
              int sum = 0;
              while (temp2 > 0) {
                  int digit = temp2 % 10;       // 取最后一位
                  sum += my_pow(digit, N);      // 累加它的 N 次方
                  temp2 /= 10;                  // 砍掉最后一位
              }
      
              // 步骤 3:对簿公堂,核对总和与原数
              if (sum == X) {
                  cout << "T\n";
              } else {
                  cout << "F\n";
              }
          }
      
          return 0;
      }
      

      【复杂度分析与思考】

      • 空间复杂度O(1)O(1)。只用了几个 int 变量。
      • 时间复杂度O(Mlog102(X))O(M \cdot \log_{10}^2(X))。求长度跑了 log10(X)\approx \log_{10}(X) 次,拆数字算次方又跑了 log10(X)\approx \log_{10}(X) 次,每次算次方循环内部又执行 log10(X)\approx \log_{10}(X) 次乘法。因为 X<108X < 10^8,单次处理最多只需要算 8+8×8=728 + 8 \times 8 = 72 次循环。对现代计算机来说简直是光速。
      • 优化思考:这段代码逻辑完美,但略显臃肿(用了两次 while 循环)。有没有办法能一次性直接知道数字的长度,并且能像拿抽屉里的东西一样,直接把每一位数字拿出来?有,这就是接下来的版本二。

      版本二:竞赛标答版 —— 字符串映射法(降维打击,极力推荐)

      我们抛弃数学除法,直接把输入的数字当成**一串字符(string)**读进来。 这样一来,数字的长度 NN 就是字符串的长度 s.length();提取每一位数字,直接用数组下标 s[i] - '0' 即可瞬间拿到!代码量锐减,逻辑极其清晰。

      #include <iostream>
      #include <string>
      
      using namespace std;
      
      // 纯整数次方计算函数保持不变
      int my_pow(int base, int exp) {
          int res = 1;
          for (int i = 0; i < exp; ++i) {
              res *= base;
          }
          return res;
      }
      
      int main() {
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          int M;
          if (!(cin >> M)) return 0; // 良好的鲁棒性
      
          while (M--) {
              string s;
              cin >> s; // 关键点1:把数字当做字符串读入!
      
              int N = s.length(); // 关键点2:瞬间获取数字位数 N!
              
              int sum = 0;
              // 关键点3:遍历字符串的每一个字符
              for (int i = 0; i < N; ++i) {
                  // 将字符 '0'~'9' 转换为真实的整数 0~9
                  // 底层逻辑:ASCII 码相减 ('5'的ASCII码减去'0'的ASCII码,刚好等于整数 5)
                  int digit = s[i] - '0'; 
                  sum += my_pow(digit, N);
              }
      
              // 关键点4:对簿公堂。注意,由于此时原数是字符串,我们需要用到 stoi 函数把它转回整数。
              // 或者不转,自己手动通过数学乘法把原字符串还原为真实大小去比,
              // 这里为了代码极简,直接用标准库函数 stoi (String TO Integer)。
              int original_number = stoi(s);
              
              if (sum == original_number) {
                  cout << "T\n";
              } else {
                  cout << "F\n";
              }
          }
      
          return 0;
      }
      

      【复杂度分析与建议】

      • 空间复杂度O(N)O(N)NN为数字长度)。虽然用到了 string 对象占用了少量内存,但最大长度才 8 个字符,基本等同于 O(1)O(1),微乎其微。
      • 时间复杂度O(Mlog102(X))O(M \cdot \log_{10}^2(X))。与版本一相同,但因为省略了反复的除法和取模这种相对较慢的 CPU 指令,转而使用了数组级别的内存访问,其底层常数执行时间更短
      • 教练点拨:在信息学竞赛中,“把大整数当作字符串(或者字符数组)处理”是一项必备的高级技能。在以后的高精度加减乘除(大整数运算)题目中,所有的数字都会大到无法放进任何整型变量里,只能用 string 来接收和处理。版本二正是培养这种“字符与数字随意转换”思维的最佳练手模板。

      把这两种思路都在脑海中过一遍。建议实战中首选版本二的写法,清爽且不易出错。

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

        你好!很高兴能以教练的身份指导你挑战这道题。这是一道非常经典的**“数字拆解与位运算模拟”**问题,也是训练大家细心程度的绝佳素材。

        我们把键盘推开,拿出草稿纸,一步步来解剖这道题。


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

        • 提示一:如何把数字“大卸八块”? 给你一个数 153,计算机怎么知道它的个位、十位、百位分别是多少?记住一个经典套路:“对 1010 取模(% 10)”能剥出最后一位,“除以 1010/ 10)”能砍掉最后一位。
        • 提示二:指数 NN 从何而来? 题目里说“各位数字的 NN 次方之和”,这个 NN 是数字的位数。所以,在你开始算乘方之前,是不是得先想个办法把这个数总共有“几位”数出来?
        • 提示三:保护好“案发现场”。 我们数位数、剥离数字时,这个原数字 153 会被砍得连渣都不剩(变成 0)。但是最后我们还需要拿“计算总和”与“原数字”去核对!这就暗示你:必须要找个替身(临时变量)来代替原数字去挨刀,保留好原数字。
        • 提示四:多组数据的处理。 题目说第一行有 MM 个数字,且“不需要等到所有输入结束再输出”。这就意味着你可以写一个大循环,来一个数字,处理一个,打印一个,干脆利落。

        2. 预备知识点总结

        攻克这道题,你需要掌握以下几个知识点:

        1. 多组数据循环结构:熟练使用 while (M--) 或者 for 循环来处理多次独立输入。
        2. 整数拆解术:熟练掌握 while (temp > 0) 配合 temp % 10temp /= 10 的分离各位数字技巧。
        3. 计算乘方:可以自己写一个小循环算 ddNN 次方,也可以包含 <cmath> 库使用 pow() 函数(注意:pow 返回的是浮点数,需要四舍五入或强转为 int 避免精度丢失,竞赛中为了稳妥,推荐自己写个小循环算乘方,或者用 round())。
        4. 字符串与数字的转换(进阶思维):如果把数字当成 string 读入,算位数是不是瞬间就变成 str.length() 了?这也是一种极佳的破题思路。

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

        “来,拿出草稿纸。我们用样例 153 来模拟一下计算机是怎么干活的。”

        第一步:数位数 (求 NN)

        我们先弄个“替身”:temp = 153

        [草稿纸推导] 数位数 (N = 0)
        temp = 153  | 大于 0 吗?是 -> 砍掉最后一位: temp = 15 | N 变成 1
        temp = 15   | 大于 0 吗?是 -> 砍掉最后一位: temp = 1  | N 变成 2
        temp = 1    | 大于 0 吗?是 -> 砍掉最后一位: temp = 0  | N 变成 3
        temp = 0    | 大于 0 吗?否 -> 结束!得知 N = 3
        

        第二步:扒皮抽筋算次方之和 (求 Sum)

        重新拿个新“替身”:temp2 = 153,总和 Sum = 0

        [草稿纸推导] 计算过程 (已知 N = 3)
        temp2 = 153 | 取最后一位: 153 % 10 = 3 | 算 3 的 3 次方 = 27  | Sum = 0 + 27 = 27   | 砍掉尾巴: temp2 = 15
        temp2 = 15  | 取最后一位: 15 % 10 = 5  | 算 5 的 3 次方 = 125 | Sum = 27 + 125 = 152| 砍掉尾巴: temp2 = 1
        temp2 = 1   | 取最后一位: 1 % 10 = 1   | 算 1 的 3 次方 = 1   | Sum = 152 + 1 = 153 | 砍掉尾巴: temp2 = 0
        temp2 = 0   | 结束!得到最终 Sum = 153
        

        第三步:对薄公堂

        原数字 153 == 算出来的 Sum 153 吗? 相等!输出 T

        第四步:复杂度的思考

        • 空间复杂度
          • 我们自始至终只需要保留输入数字 X, 替身 temp, 位数 N, 总和 Sum
          • 不用开任何大数组,空间复杂度是最完美的 O(1)O(1)
        • 时间复杂度
          • 计算位数需要跑 log10(X)\log_{10}(X) 次循环(其实就是数字的长度)。
          • 求总和又要跑 log10(X)\log_{10}(X) 次,每次算乘方最多乘 log10(X)\log_{10}(X) 次。
          • 处理一个数字的时间大约是长度的平方,即 O(log102(X))O(\log_{10}^2(X))
          • 因为数字小于 10810^8,长度最大是 888×8=648 \times 8 = 64 次运算。MM 最大是 100100。总运算次数只有区区 64006400 次!这对于一秒钟能算一亿次的计算机来说连塞牙缝都不够,时间复杂度极优,绝对不会超时!

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

        做这道题,在读题时必须立刻锁定以下几个关键词:

        1. NN 位数”和“NN 次方” —— 题眼:动态次幂。 很多人听说过“水仙花数”(3位数的3次方),容易在写代码时直接写死成算 3 次方。题目特意强调了 NN,意味着你必须先求出长度,再计算次幂
        2. “小于 10810^8 —— 题眼:防爆 int 检查。
          • 最大位数是 8。
          • 如果各位全是 9,最大总和是 8×98=8×43,046,721=344,373,7688 \times 9^8 = 8 \times 43,046,721 = 344,373,768(三亿多)。
          • C++ 中标准的 int 上限是 21 亿。这意味着使用 int 是绝对安全的,不会溢出,不需要开 long long
        3. “不需要等到所有输入结束在依次输出” —— 题眼:流式处理。 意味着直接写在 while(M--) 里面边读边印就行了,不用专门搞个数组把所有结果存起来最后再一起打印,这能极大简化你的代码结构。

        思路已经很清晰了,你现在面临的选择是:是用“纯数学除法”来计算位数和拆解数字,还是把输入当成“字符串(string)”来巧妙绕过数位数的麻烦?

        去草稿纸上选一种你喜欢的方法,把核心的几步写成代码试试看吧!如果有遇到困难,随时来找教练。

        • 1

        信息

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