3 条题解
-
0
太棒了!自己动手写这道题的数据生成器,你会发现一个非常有趣的现象:这道题根本无法用“数据量”卡掉暴力法,出题人卡的是选手的“精度陷阱(Bug)”!
核心设计思路(10 个测试点分布)
题目约定 ,最大的长度只有 8 位。即使是用最笨的循环去扒每一位数,计算机一秒钟处理 1 亿次,而 的数据量对于这种算法,总耗时连 毫秒都不到。
所以,这道题的测试数据设计核心在于:精准打击那些偷懒使用
<cmath>库中pow()函数而没有处理浮点精度误差的错解! 如果选手写了sum += pow(digit, N),由于浮点数的科学计数法存储机制,像pow(5, 3)可能会在底层算成124.9999999,强转整型后变成124,导致本来是自幂数的判定为F。为了抓住他们,我们必须在数据中埋入足够多、覆盖各个位数的“真实自幂数”。- Test 1 & Test 2:样例 1 和 样例 2。
- Test 3:一位数与两位数边界。 全是自幂数,但两位数中没有自幂数。
- Test 4:三位数与四位数靶场。包含 等所有的真实自幂数,夹杂临界值。
- Test 5:五位数与六位数靶场。包含 等所有的真实自幂数。
- Test 6:七位数靶场。包含 等所有的真实自幂数。
- Test 7:八位数(上限边界)靶场。包含 以及逼近 的边界合数。
- Test 8:压力测试 。全是大位数的伪装者(看似符合但实际不是)。
- Test 9:【精准精度杀手卷】 。我将 以内总共 个真实的自幂数全部塞进去,混杂大量的普通数字。只要选手的
pow精度有一丁点差池,就会在这里收获Wrong Answer! - 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 部署与评测反馈建议(给教练的后台秘籍)
- 防除 0 与爆栈机制: 这个生成器代码底层彻底屏蔽了除法运算,而是使用字符串拆解和纯正整数循环乘法,完全免疫了除以 或者除法浮点精度的异常,而且没有一处递归调用,不存在爆栈风险。
- “坑”人的艺术(考点区分):
如果你把这组数据放到 OJ 上评测,你会看到极度明显的区分度:
- 那些为了图方便使用了
<cmath>中的pow(d, n),但没有通过四舍五入直接转成 int 的人,极大概率会在 Test 9 吃到一发或多发Wrong Answer。因为真实的自幂数被错判成了F。这就是为什么我硬编码罗列了所有 27 个自幂数并将它们塞入高压测试集的原因。 - 那些使用了版本二标准代码的同学,将稳稳地拿到满屏漂亮的绿灯
100 Accepted!
- 那些为了图方便使用了
- 空间安全:单个
in文件不超过 ,对 OJ 没有任何评测硬盘负担。
-
0
你好!很高兴带你把草稿纸上的推导化为实实在在的代码。
在这道题中,由于待判断的数字规模极小(),我们并不需要在宏观的“时间复杂度”上去做降维打击。因此,版本的演进将聚焦于**“代码的优雅度、容错性与 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; }【复杂度分析与思考】
- 空间复杂度:。只用了几个
int变量。 - 时间复杂度:。求长度跑了 次,拆数字算次方又跑了 次,每次算次方循环内部又执行 次乘法。因为 ,单次处理最多只需要算 次循环。对现代计算机来说简直是光速。
- 优化思考:这段代码逻辑完美,但略显臃肿(用了两次
while循环)。有没有办法能一次性直接知道数字的长度,并且能像拿抽屉里的东西一样,直接把每一位数字拿出来?有,这就是接下来的版本二。
版本二:竞赛标答版 —— 字符串映射法(降维打击,极力推荐)
我们抛弃数学除法,直接把输入的数字当成**一串字符(
string)**读进来。 这样一来,数字的长度 就是字符串的长度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; }【复杂度分析与建议】
- 空间复杂度:(为数字长度)。虽然用到了
string对象占用了少量内存,但最大长度才 8 个字符,基本等同于 ,微乎其微。 - 时间复杂度:。与版本一相同,但因为省略了反复的除法和取模这种相对较慢的 CPU 指令,转而使用了数组级别的内存访问,其底层常数执行时间更短。
- 教练点拨:在信息学竞赛中,“把大整数当作字符串(或者字符数组)处理”是一项必备的高级技能。在以后的高精度加减乘除(大整数运算)题目中,所有的数字都会大到无法放进任何整型变量里,只能用
string来接收和处理。版本二正是培养这种“字符与数字随意转换”思维的最佳练手模板。
把这两种思路都在脑海中过一遍。建议实战中首选版本二的写法,清爽且不易出错。
- 空间复杂度:。只用了几个
-
0
你好!很高兴能以教练的身份指导你挑战这道题。这是一道非常经典的**“数字拆解与位运算模拟”**问题,也是训练大家细心程度的绝佳素材。
我们把键盘推开,拿出草稿纸,一步步来解剖这道题。
1. 教练的思路提示(不提供完整代码)
- 提示一:如何把数字“大卸八块”? 给你一个数
153,计算机怎么知道它的个位、十位、百位分别是多少?记住一个经典套路:“对 取模(% 10)”能剥出最后一位,“除以 (/ 10)”能砍掉最后一位。 - 提示二:指数 从何而来? 题目里说“各位数字的 次方之和”,这个 是数字的位数。所以,在你开始算乘方之前,是不是得先想个办法把这个数总共有“几位”数出来?
- 提示三:保护好“案发现场”。 我们数位数、剥离数字时,这个原数字
153会被砍得连渣都不剩(变成 0)。但是最后我们还需要拿“计算总和”与“原数字”去核对!这就暗示你:必须要找个替身(临时变量)来代替原数字去挨刀,保留好原数字。 - 提示四:多组数据的处理。 题目说第一行有 个数字,且“不需要等到所有输入结束再输出”。这就意味着你可以写一个大循环,来一个数字,处理一个,打印一个,干脆利落。
2. 预备知识点总结
攻克这道题,你需要掌握以下几个知识点:
- 多组数据循环结构:熟练使用
while (M--)或者for循环来处理多次独立输入。 - 整数拆解术:熟练掌握
while (temp > 0)配合temp % 10和temp /= 10的分离各位数字技巧。 - 计算乘方:可以自己写一个小循环算 的 次方,也可以包含
<cmath>库使用pow()函数(注意:pow返回的是浮点数,需要四舍五入或强转为int避免精度丢失,竞赛中为了稳妥,推荐自己写个小循环算乘方,或者用round())。 - 字符串与数字的转换(进阶思维):如果把数字当成
string读入,算位数是不是瞬间就变成str.length()了?这也是一种极佳的破题思路。
3. 启发式与图形式的草稿纸推导(教学模拟)
“来,拿出草稿纸。我们用样例
153来模拟一下计算机是怎么干活的。”第一步:数位数 (求 )
我们先弄个“替身”:
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。 - 不用开任何大数组,空间复杂度是最完美的 。
- 我们自始至终只需要保留输入数字
- 时间复杂度:
- 计算位数需要跑 次循环(其实就是数字的长度)。
- 求总和又要跑 次,每次算乘方最多乘 次。
- 处理一个数字的时间大约是长度的平方,即 。
- 因为数字小于 ,长度最大是 。 次运算。 最大是 。总运算次数只有区区 次!这对于一秒钟能算一亿次的计算机来说连塞牙缝都不够,时间复杂度极优,绝对不会超时!
4. 读题时的“题眼”(关键词)提取技巧
做这道题,在读题时必须立刻锁定以下几个关键词:
- “ 位数”和“ 次方” —— 题眼:动态次幂。 很多人听说过“水仙花数”(3位数的3次方),容易在写代码时直接写死成算 3 次方。题目特意强调了 ,意味着你必须先求出长度,再计算次幂。
- “小于 ” —— 题眼:防爆
int检查。- 最大位数是 8。
- 如果各位全是 9,最大总和是 (三亿多)。
- C++ 中标准的
int上限是 21 亿。这意味着使用int是绝对安全的,不会溢出,不需要开long long。
- “不需要等到所有输入结束在依次输出” —— 题眼:流式处理。 意味着直接写在
while(M--)里面边读边印就行了,不用专门搞个数组把所有结果存起来最后再一起打印,这能极大简化你的代码结构。
思路已经很清晰了,你现在面临的选择是:是用“纯数学除法”来计算位数和拆解数字,还是把输入当成“字符串(
string)”来巧妙绕过数位数的麻烦?去草稿纸上选一种你喜欢的方法,把核心的几步写成代码试试看吧!如果有遇到困难,随时来找教练。
- 提示一:如何把数字“大卸八块”? 给你一个数
- 1
信息
- ID
- 13924
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 3
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者