C++质数判断与字符统计:从洛谷P1125题解看算法选型与细节优化
1. 项目概述与核心思路最近在洛谷上刷题碰到了P1125这道“笨小猴的幸运密码”题目挺有意思把字符串处理、质数判断和简单模拟结合在了一起。题目大意是给你一个单词你需要统计出单词中出现次数最多和最少的字母然后计算它们的出现次数之差最后判断这个差是不是质数。如果是质数输出“Lucky Word”并输出这个质数否则输出“Unlucky”并输出这个差。这题本身难度不大但非常适合用来巩固C的基础语法、字符串操作和质数判断算法尤其是对刚接触算法竞赛的新手来说是一个很好的练手题。我之所以想专门写一篇关于这道题的解析是因为在实际教学和带新人的过程中发现很多朋友虽然能写出代码但对其中一些关键细节的理解不够深入比如质数判断的边界条件、大小写字母的处理、以及如何高效地统计字符频率。这道题就像一面镜子能清晰地反映出你对基础知识的掌握程度。接下来我会从题目分析、核心算法选择、代码实现细节到常见踩坑点完整地拆解一遍希望能帮你不仅“做对”这道题更能“吃透”它背后的逻辑。2. 题目需求深度解析与方案设计2.1 问题拆解与输入输出分析首先我们得把题目要求翻译成计算机能理解的任务清单。题目输入是一个仅由小写字母组成的单词长度在100以内。输出则有两种情况取决于一个计算出来的“幸运数”是否为质数。我们需要完成的步骤可以分解如下字符频率统计遍历输入的单词统计每个小写字母‘a’到‘z’出现的次数。找出极值从26个字母的统计结果中找出出现次数的最大值maxn和最小值minn。这里有个关键点最小值只考虑那些出现过的字母。如果一个字母根本没在单词里出现它的次数是0这个0不能参与最小值的比较。这是第一个容易出错的地方。计算幸运数计算幸运数 maxn - minn。质数判断判断这个幸运数是否是质数。输出结果根据判断结果按格式输出。输入输出格式很简单但陷阱往往藏在细节里。比如题目说单词由小写字母组成那我们就不需要处理大写字母这简化了问题。再比如当最大值和最小值相等时比如单词“aa”幸运数为0而0和1都不是质数。2.2 核心算法选型为什么用试除法而不是筛法质数判断是本题的核心算法点。一提到质数很多人会想到著名的埃拉托斯特尼筛法埃氏筛或者欧拉线性筛。但对于这道题我们需要做一个重要的技术选型决策。筛法埃氏筛/线性筛适用于一种场景需要一次性预处理出一个较大范围内比如1到N的所有质数然后可以O(1)时间查询任意一个数是否为质数。它的时间复杂度在预处理时是O(N log log N)或O(N)之后查询很快。试除法则适用于另一种场景只需要判断单个或少数几个数字是否为质数。对于一个数n我们只需要用2到√n之间的所有整数去试除如果都除不尽那n就是质数。现在让我们分析本题的需求判断目标只需要判断一个数幸运数是否为质数。数据范围单词长度最多100所以字母最多出现100次。maxn最大为100minn最小为1如果单词中所有字母都相同则minn也等于maxn此时差为0。因此幸运数的绝对值最大不会超过99100-1。判断次数整个程序运行期间只进行一次质数判断。结论非常明显幸运数n的最大值不超过99。对于这么小的范围使用筛法完全是“杀鸡用牛刀”。筛法需要预处理一个至少100大小的数组而试除法只需要一个最多循环10次√99 ≈ 9.95的循环。无论是代码简洁度还是实际运行效率试除法都完胜。注意这里有一个常见的思维误区。很多初学者学会了筛法后就想在所有质数问题上都用它忽略了具体场景。算法工具没有绝对的好坏只有是否适合当前的问题。在这道题里试除法是更简单、更直接、更高效的选择。2.3 整体代码框架设计基于以上分析我们可以勾勒出代码的主体结构#include iostream #include string #include cmath // 用于sqrt函数 using namespace std; // 函数声明 bool isPrime(int n); int main() { string word; int count[26] {0}; // 用于统计26个字母出现次数的数组 int maxn 0, minn 100; // 初始化最大值和最小值 // 1. 读入单词 cin word; // 2. 统计字母频率 for (char c : word) { count[c - a]; // 将字符映射到数组下标0-25 } // 3. 找出最大和最小出现次数忽略未出现的字母即次数为0的 for (int i 0; i 26; i) { if (count[i] 0) { // 关键只处理出现过的字母 if (count[i] maxn) maxn count[i]; if (count[i] minn) minn count[i]; } } // 4. 计算幸运数 int luckyNumber maxn - minn; // 5. 判断并输出 if (luckyNumber 1 isPrime(luckyNumber)) { // 注意质数定义要求大于1 cout Lucky Word endl; cout luckyNumber endl; } else { cout Unlucky Word endl; cout luckyNumber endl; } return 0; } // 质数判断函数试除法 bool isPrime(int n) { // 质数判断逻辑将在这里实现 }这个框架清晰地反映了我们的解题步骤。接下来我们需要填充最关键的isPrime函数并处理一些边界情况。3. 核心算法实现与细节打磨3.1 质数判断函数 isPrime 的完整实现质数的定义是在大于1的自然数中除了1和它本身以外不再有其他因数的数。根据这个定义数字0和1都不是质数。负数也不是质数。这是我们函数首先要处理的特殊情况。对于大于1的数n最经典的判断方法是试除法检查从2到√n之间的所有整数看是否能整除n。如果能被其中任何一个整除则n不是质数否则n是质数。为什么只需要检查到√n呢这是一个重要的数学优化。如果n有一个大于√n的因子a那么它必然对应一个小于√n的因子b因为a * b n。所以如果在2到√n之间都找不到因子那么大于√n的部分也肯定找不到。这能将循环次数从n次减少到√n次效率提升巨大。下面是isPrime函数的一个健壮实现bool isPrime(int n) { // 处理特殊情况小于2的数都不是质数 if (n 2) { return false; } // 单独处理2它是唯一的偶质数也是后续循环的起点 if (n 2) { return true; } // 排除所有偶数除了2 if (n % 2 0) { return false; } // 试除法从3开始每次加2只检查奇数直到sqrt(n) int limit sqrt(n); // 计算循环上限 for (int i 3; i limit; i 2) { if (n % i 0) { return false; // 发现因子不是质数 } } return true; // 通过所有检查是质数 }代码细节解读if (n 2) return false;严格遵循质数定义排除了0和1。if (n 2) return true;2是质数但它也是偶数。为了后续循环逻辑清晰我们把它单独处理。if (n % 2 0) return false;在排除了2之后所有其他偶数都不可能是质数因为它们至少有因子2。这一步能立即筛掉一半的数字。int limit sqrt(n);使用sqrt函数计算平方根。这里有一个重要技巧sqrt函数的参数和返回值是double类型我们将其赋值给int变量limit会发生隐式转换直接截断小数部分。例如sqrt(10)约等于3.162赋值给int后变成3。在循环条件i limit中i3是满足条件的这确保了检查的完整性。有些人喜欢写成i * i n来避免使用sqrt和浮点数运算这对于小整数也是可以的但要警惕i*i可能溢出的风险本题n很小不存在此问题。for (int i 3; i limit; i 2)从3开始只遍历奇数。因为偶数已经在前面被排除了所以不需要再检查它们是否能整除n一个奇数不可能被偶数整除。这又将需要检查的数字减少了一半。经过这两步优化排除偶数、只检查到√n对于n97这样的数我们只需要检查3, 5, 7, 9这4个数sqrt(97)≈9.8而不是从2到96的95个数效率提升非常显著。3.2 字符统计与极值查找的陷阱规避回到主函数统计字符和查找极值部分也有几个细节需要注意// 统计部分 for (char c : word) { count[c - a]; // 核心映射 }这里c - a是一个经典技巧。小写字母a的ASCII码是97b是98依此类推。c - a就将字符’a’映射到下标0’b’映射到1…’z’映射到25。这比用switch或if-else链要简洁高效得多。// 查找极值部分 int maxn 0, minn 100; // 初始化 for (int i 0; i 26; i) { if (count[i] 0) { // 关键过滤条件 if (count[i] maxn) maxn count[i]; if (count[i] minn) minn count[i]; } }初始化maxn初始化为0是合理的因为任何正数都会比0大。minn初始化为100单词最大长度也是合理的因为任何实际的次数至少为1都会比100小。你也可以初始化为一个很大的数如INT_MAX。if (count[i] 0)这是整个逻辑的灵魂。题目要求“出现次数最少的字母”指的是在单词中实际出现过的字母。如果不加这个判断那些count[i] 0的字母就会参与最小值比较导致minn永远为0除非某个字母出现负数次这不可能。那么maxn - minn就永远等于maxn结果就完全错了。一个极端情况如果单词中只有一个字母比如“aaaa”那么循环结束后maxn和minn都等于这个字母的出现次数比如4。此时幸运数luckyNumber 4 - 4 0。0不是质数所以输出“Unlucky Word”和0。这是符合题目逻辑的。3.3 整合与最终代码将以上所有部分整合就得到了本题的完整AC代码#include iostream #include string #include cmath #include climits // 如果需要使用INT_MAX using namespace std; bool isPrime(int n) { if (n 2) return false; if (n 2) return true; if (n % 2 0) return false; int limit sqrt(n); for (int i 3; i limit; i 2) { if (n % i 0) return false; } return true; } int main() { string word; cin word; int count[26] {0}; // 初始化所有元素为0 // 统计频率 for (char c : word) { count[c - a]; } // 找出最大和最小出现次数忽略0次 int maxn 0, minn 100; // minn初始化为一个比可能最大值大的数 for (int i 0; i 26; i) { if (count[i] 0) { if (count[i] maxn) maxn count[i]; if (count[i] minn) minn count[i]; } } int diff maxn - minn; if (diff 1 isPrime(diff)) { // 注意diff必须大于1才是质数的候选 cout Lucky Word endl diff endl; } else { cout Unlucky Word endl diff endl; } return 0; }4. 常见问题排查与性能优化探讨即使代码逻辑正确在提交时也可能遇到各种问题。下面是我总结的几个常见坑点及其解决方案。4.1 编译与运行时错误错误类型可能原因解决方案编译错误sqrt函数未声明包含头文件cmath或math.h。数组越界访问count[c - a]确保输入单词确实只包含小写字母。如果输入包含其他字符c - a可能为负数或大于25导致访问非法内存。题目已保证输入合法但自己测试时要注意。运行时错误/答案错误最小值minn初始化不当或未过滤0这是最常见的错误。务必在查找最小值的循环中加上if (count[i] 0)条件。质数判断函数对0和1处理不当在isPrime函数开头必须判断if (n 2) return false;。因为幸运数可能为0或1。输出格式错误严格按照题目要求输出“Lucky Word”或“Unlucky Word”后面换行再输出数字。注意大小写和空格。时间超限使用了低效的质数判断对于本题数据范围几乎不可能超时。但如果你的isPrime函数写成了从2循环到n-1对于n10^8级别的数虽然本题不会出现就会超时。务必使用优化后的试除法。4.2 关于输入加速的讨论在洛谷的很多题目尤其是P3383线性筛素数那种大数据量题的提示中会建议C用户使用std::ios::sync_with_stdio(0)和cin.tie(0)来关闭C输入输出流与C标准输入输出的同步以提升读取速度。对于本题P1125单词长度最多100数据量极小完全不需要这些优化。加上它们也不会错但属于“过度优化”。作为初学者理解这些优化的适用场景比盲目添加更重要。它们主要用于需要读取大量数据比如数十万、上百万个整数的题目。在本题中简单的cin word已经足够快。4.3 算法扩展思考如果单词长度极大虽然本题数据范围很小但我们可以思考一个扩展问题如果单词长度允许很长比如10^6甚至字母范围扩大到整个ASCII字符集我们的算法需要如何调整字符统计使用int count[26]数组仍然是O(1)空间遍历字符串是O(n)时间这已经是理论最优无法再优化。查找极值在26个元素的数组中查找非零的最大最小值是O(26)的常数时间可以忽略。质数判断幸运数diff的最大值变成了单词长度n。如果n很大比如10^6那么diff也可能达到10^6级别。此时我们优化后的试除法循环到√n复杂度是O(√n)对于n10^6循环大约1000次仍然非常快。即使n10^9循环约31622次在单次判断的语境下也是可以接受的。所以即使数据范围扩大我们当前的算法框架依然非常健壮。这体现了基础算法设计的通用性。5. 测试用例设计与验证编写完代码一定要用多种情况的测试用例来验证其正确性。下面是我设计的几组测试数据输入单词字母频率分析maxnminndiff质数判断预期输出errore:1, r:2, o:12111不是质数Unlucky Word1olympico:1, l:1, y:1, m:1, p:1, i:1, c:11100不是质数Unlucky Word0aa:11100不是质数Unlucky Word0aaa:22200不是质数Unlucky Word0abca:1, b:1, c:11100不是质数Unlucky Word0abcca:1, b:1, c:22111不是质数Unlucky Word1abccca:1, b:1, c:33122是质数Lucky Word2mississippim:1, i:4, s:4, p:24133是质数Lucky Word3bananab:1, a:3, n:23122是质数Lucky Word2你可以将上述输入复制到你的程序中逐一验证输出是否与“预期输出”一致。特别是前几个用例覆盖了差为0和1的边界情况这是最容易出错的地方。6. 从这道题延伸出的学习路径“笨小猴的幸运密码”虽然简单但它像一颗种子可以延伸出很多重要的编程和算法概念基础语法巩固数组操作、字符串遍历、ASCII码运用、函数定义与调用。算法思维训练理解质数的定义掌握试除法及其优化平方根边界、排除偶数学会根据数据范围选择最合适的算法筛法 vs 试除。细节处理能力理解“最小值排除0次”这一关键需求并能在代码中正确实现。培养处理边界条件如0、1、2的严谨习惯。问题分解能力将一个复杂问题判断幸运词分解为统计、找极值、计算、判断质数等清晰的子任务。如果你想继续深入可以尝试刷一些相关的洛谷题目质数判断P5736 【深基7.例2】质数筛练习筛法P3383 【模板】线性筛素数大数据量筛法。字符串处理P1308 [NOIP2011 普及组] 统计单词数更复杂的字符串匹配P1598 垂直柱状图字符统计的可视化。模拟题很多普及组的题目都是这种多步骤的模拟多做可以提升将自然语言描述转化为代码逻辑的能力。编程学习就像搭积木这种基础题就是最坚实的那几块积木。把它们理解透彻、搭得牢固后面学习更复杂的数据结构哈希表、树、图和算法动态规划、搜索时才会事半功倍。