
1. 项目概述从一道字符串处理题看编程基本功的锤炼“整理药名”这道题乍一看题目很多刚接触信息学奥赛NOI或类似编程竞赛的同学可能会觉得平平无奇。不就是处理一些字符串把大小写格式统一一下吗这能有什么难度如果你也这么想那可能就错过了这道题背后所蕴含的、对于编程初学者而言至关重要的训练价值。我在带学生备赛和自身刷题的过程中无数次遇到类似“1139整理药名”这样的题目它们往往被归类为“字符串基础”或“模拟”但恰恰是这些题目成为了区分“能写代码”和“能写好代码”的第一道分水岭。这道题的核心任务非常明确给定一系列药名这些药名的书写格式可能杂乱无章例如全大写、全小写、或者首字母大写其余小写等多种形式混杂。我们需要编写一个程序将它们统一规范化为一种标准格式每个药名的第一个字符如果是字母则变为大写其余字符如果是字母则变为小写。非字母字符如数字、连字符等则保持原样。这听起来就是一次toupper()和tolower()函数的简单调用但实际动手时新手常会陷入各种陷阱比如如何准确判断“第一个字符”和“其余字符”如何处理输入中的空格和换行当药名包含非字母时逻辑是否会出错以及如何高效地处理可能多达上万个的字符串输入。这正是“信息学奥赛一本通”这类题库的经典之处。它不追求炫酷的算法而是扎扎实实地考察你对编程语言基础库的熟悉程度、对输入输出流程的精确控制、对边界条件的缜密思考以及代码的鲁棒性。通过解决这道题你锻炼的不仅仅是字符串处理能力更是严谨的编程思维和将自然语言描述的需求无歧义地转化为计算机指令的能力。接下来我将带你深入拆解这道题从思路分析到代码实现再到优化和常见错误让你真正吃透这类基础题为后续更复杂的算法学习打下坚实的基础。2. 核心需求解析与解题思路确立在动手写任何一行代码之前我们必须像解数学题一样仔细审题将模糊的自然语言需求转化为清晰、无二义性的技术规格。这是避免后期反复调试和逻辑错误的关键。2.1 题目需求的技术化翻译首先我们重新精确描述“整理药名”的需求输入程序会接收若干行输入。每一行是一个待整理的药名字符串。输入的行数并未在题目中明确给出这是一个非常重要的信号意味着我们需要持续读取直到文件结束EOF。这是竞赛题中常见的输入方式。处理规则对于每一个输入的字符串我们需要遍历其中的每一个字符并根据其位置应用不同的规则位置判定规则依赖于字符是“第一个字符”还是“其余字符”。这里的“第一个”指的是字符串的第一个字符索引为0而不是第一个字母。即使第一个字符是数字它也属于“第一个字符”的范畴。规则应用对“第一个字符”如果该字符是一个英文字母‘a’-‘z’ 或 ‘A’-‘Z’则将其转换为大写形式如果不是字母则保持原样。对“其余字符”即字符串中第一个字符之后的所有字符如果该字符是一个英文字母则将其转换为小写形式如果不是字母则保持原样。输出将处理后的每个药名字符串按行输出即输入一行输出一行。2.2 思路设计与方案选型基于以上需求我们可以设计几种实现思路思路一逐字符遍历与条件判断这是最直观的方法。创建一个循环遍历字符串的每个字符。在循环内部使用索引i来判断当前字符的位置。如果i 0则应用“第一个字符”的规则否则应用“其余字符”的规则。在各自的条件分支内再使用isalpha()函数判断是否为字母然后决定使用toupper()还是tolower()进行转换。优点逻辑直白与控制流if-else完美对应易于理解和调试。缺点每次循环都需要进行两次条件判断位置判断和字母判断对于超长字符串或海量数据可能存在微小的性能开销但对于本题规模完全可以忽略。思路二分步处理先将整个字符串转换为全小写然后再单独处理第一个字符将其转换为大写如果是字母。或者先处理第一个字符再将剩余子串转换为小写。优点代码可能更简洁利用了字符串的批量操作。缺点需要注意边界条件。例如如果先转全小写那么第一个字符如果是字母确实会变成小写但如果不是字母呢我们后续“转大写”的操作必须只在它是字母时才进行否则可能会出错例如将数字‘1’传入toupper结果可能不是预期的。这增加了额外的判断。另外创建子串可能会有额外的内存开销。思路三使用标准库算法在C中可以结合std::transform算法和自定义函数对象或Lambda表达式来实现。这属于更“现代”的C风格。优点代码风格简洁、高效体现了对STL的熟练运用。缺点对于初学者理解门槛稍高Lambda表达式和迭代器的概念需要额外学习。注意对于竞赛入门和巩固基础而言思路一逐字符遍历是最推荐的。它强迫你思考每个字符的处理逻辑清晰地映射了题目要求是训练基本功的最佳方式。过早追求“简洁”可能会掩盖对底层逻辑的理解。因此本文将主要围绕思路一展开并在后续探讨思路三作为拓展。2.3 关键技术与函数准备无论采用哪种思路我们都需要熟悉以下几个核心的C/C标准库函数位于cctype头文件中isalpha(int c)检查字符c是否为英文字母。如果是返回非零值真否则返回0假。toupper(int c)如果c是小写字母则返回其对应的大写字母否则返回c本身。tolower(int c)如果c是大写字母则返回其对应的小写字母否则返回c本身。重要提示这些函数参数和返回值类型都是int但通常我们传入char类型是安全的因为char可以自动提升为int。但为了处理EOF等特殊情况标准将其定义为int。在我们的场景下直接传入字符即可。3. 代码实现与逐行精讲我们将采用C语言进行实现因为它兼具C的底层控制能力和STL的高级抽象是信息学奥赛的主流语言。这里给出两个版本的代码基础版和STL算法版。3.1 基础实现逐字符遍历版#include iostream #include string #include cctype // 包含 isalpha, toupper, tolower using namespace std; int main() { string drugName; // 使用 while (getline(cin, drugName)) 循环读取整行直到输入结束 while (getline(cin, drugName)) { // 遍历字符串中的每一个字符 for (size_t i 0; i drugName.length(); i) { char ch drugName[i]; // 使用引用方便直接修改原字符串 if (i 0) { // 第一个字符 if (isalpha(ch)) { ch toupper(ch); // 是字母则转大写 } // 不是字母则什么都不做ch保持不变 } else { // 其余字符 if (isalpha(ch)) { ch tolower(ch); // 是字母则转小写 } // 不是字母则什么都不做ch保持不变 } } // 处理完当前药名输出结果 cout drugName endl; } return 0; }代码精讲与避坑指南输入循环while (getline(cin, drugName))getline函数读取整行包括空格直到遇到换行符并将内容存入drugName同时丢弃换行符。这完美符合“每行一个药名”的输入要求。while循环的条件是getline的返回值。当getline成功读取一行时返回输入流cin可转换为true循环继续当遇到文件结束符EOF在评测系统中即输入数据已读完时getline失败返回false循环结束。这是处理未知行数输入的标准范式。遍历与索引for (size_t i 0; ...)使用size_t类型作为索引这是string::length()返回的类型可以避免有符号/无符号比较时的编译器警告。循环从i0开始到i drugName.length()结束确保遍历每个字符。字符引用char ch drugName[i]这是一个关键技巧。ch是drugName[i]的引用意味着对ch的修改直接作用于原字符串drugName上。这比每次都写drugName[i] toupper(drugName[i])更清晰、更高效。核心逻辑if (i 0) ... else ...清晰地分开了“第一个字符”和“其余字符”的处理逻辑。在每个分支内都先使用isalpha(ch)判断当前字符是否为字母。这是一个好习惯。虽然toupper和tolower对非字母字符会原样返回但显式判断使意图更明确代码更健壮。想象一下如果未来规则变成“第一个非字母字符要特殊处理”有isalpha的判断就更容易修改。输出cout drugName endl在循环体内输出实现一行输入对应一行输出。3.2 STL算法实现拓展思路对于想挑战一下自己的同学这里提供一个使用std::transform和Lambda表达式的版本#include iostream #include string #include cctype #include algorithm // 包含 std::transform using namespace std; int main() { string drugName; while (getline(cin, drugName)) { // 使用 std::transform 算法配合 Lambda 表达式 // 第一个版本处理第一个字符 if (!drugName.empty()) { char firstChar drugName[0]; if (isalpha(firstChar)) { firstChar toupper(firstChar); } } // 第二个版本处理从第二个字符开始的所有字符 if (drugName.length() 1) { transform(drugName.begin() 1, drugName.end(), drugName.begin() 1, [](unsigned char c) - unsigned char { // 使用 unsigned char 避免符号扩展问题 if (isalpha(c)) { return tolower(c); } return c; }); } cout drugName endl; } return 0; }版本解析这个版本将处理分成了两步。第一步单独处理首字符逻辑与基础版相同。第二步使用std::transform算法处理剩余部分。transform的前两个参数是输入范围的迭代器begin()1到end()第三个参数是输出起始位置的迭代器我们写回原位置begin()1第四个参数是一个Lambda表达式它定义了转换规则如果是字母就转小写否则原样返回。注意Lambda中的unsigned char这是一个细节。isalpha、toupper等函数期望参数是int类型且值在unsigned char范围或EOF内。如果直接传入char而char在某些编译器上默认为signed char当字符值大于127时转换为int会产生负值可能导致这些函数行为未定义。使用unsigned char可以安全地转换。在基础版中因为char被自动提升为int且通常药名是ASCII字符问题不大但这是一个良好的编程习惯。两种版本的对比与选择可读性与教学价值基础版完胜。逻辑一目了然非常适合初学者理解和调试。性能两者在时间复杂度上都是O(n)差别极小。STL版本可能因为函数调用和迭代器开销有极其微弱的劣势但完全可忽略。代码风格STL版本更“C”体现了对标准库的运用能力。在更复杂的变换中transform结合Lambda会非常强大。我的建议是作为入门学习和巩固基础务必掌握并熟练使用基础版。它是你构建更复杂程序的基石。STL版本可以作为知识拓展了解C提供了哪些工具但不必强求在初学阶段使用。4. 测试用例设计与边界情况分析写完代码不代表万事大吉。设计全面的测试用例是编程中至关重要的一环能帮你发现逻辑漏洞。下面我们设计一组测试用例输入样例预期输出测试目的AspirinAspirin测试已经是标准格式首字母大写其余小写的情况。ASPIRINAspirin测试全大写输入。aspirinAspirin测试全小写输入。aSPIRINAspirin测试首字母小写其余大写的情况。7-day7-day测试首字符为非字母数字的情况。vitamin-b12Vitamin-b12测试中间包含非字母字符连字符的情况。aA测试单字符字母输入。11测试单字符非字母输入。(empty line)(empty line)测试空行输入。程序应能处理并输出空行。aspirin(开头有空格)aspirin**重要**测试开头有空格的情况。根据题意第一个字符是空格不是字母所以规则不适用。空格应保留。重点分析最后一个用例aspirin前面有两个空格。很多初学者会在这里栽跟头。我们的规则是“第一个字符”这里的第一个字符是空格。isalpha(‘ ‘)为假所以无论是转大写还是转小写分支都不会执行空格被保留。紧接着的字符a此时它的索引i是2属于“其余字符”因此它会被转换为小写a如果原本是大写的话。最终输出开头空格保留药名部分为小写。这是符合题目字面要求的但你是否需要思考一下实际场景中药名开头会有空格吗题目是否隐含了需要去除首尾空白的假设这就是审题的深度。从“一本通”题库的常规风格来看它通常不会在字符串前后添加无关空格来增加难度除非题目明确说明。所以我们的程序按当前逻辑处理是合理的也体现了我们代码的鲁棒性——它能处理这种意外输入而不崩溃。你可以将上述测试用例保存到一个文本文件如test.in然后使用命令行重定向进行测试./your_program test.in查看输出是否与预期一致。5. 常见错误与深度排查在实际编写和调试这道题时以下是几个高频出现的错误点5.1 输入处理错误错误1使用cin drugName// 错误示例 string drugName; while (cin drugName) { // 问题所在 // ...处理逻辑 }问题cin 操作符以空白字符空格、制表符、换行为分隔符。如果药名中间有空格如Vitamin B12会被拆成Vitamin和B12两个字符串或者你想用空格分隔多个药名在一行输入这种方法会完全错误。题目要求“每行一个”所以必须用getline读取整行。修正坚持使用while (getline(cin, drugName))。错误2混合使用cin 和getlineint n; cin n; // 读取一个整数 string s; getline(cin, s); // 意图读取接下来的行但可能读到空行问题cin n读取整数后换行符\n留在了输入缓冲区。紧接着的getline会立刻读到这个换行符得到一个空字符串然后返回这通常不是我们想要的。修正在cin n后使用cin.ignore()忽略掉缓冲区中残留的换行符。或者如果题目明确第一行是数字n后面n行是数据也可以先用getline读掉第一行再用stoi转换。5.2 核心逻辑错误错误3错误理解“第一个字符”// 部分错误逻辑示例 if (isalpha(drugName[0])) { drugName[0] toupper(drugName[0]); } // 然后不管三七二十一把整个字符串转小写 for (int i 1; i drugName.length(); i) { drugName[i] tolower(drugName[i]); // 如果 drugName[i] 不是字母呢 }问题第二个循环里对每个字符都执行tolower。如果字符是数字或符号tolower虽然会返回原值但逻辑上不清晰。更严重的是如果题目规则变化比如数字要保持原样但符号要变这里就需要大改。我们推荐的显式isalpha判断更具扩展性。错误4忽略非字母字符的处理虽然toupper和tolower对非字母字符是安全的但思想上不能忽略它们。我们的代码逻辑必须清晰地表明“只有字母才进行大小写转换”。5.3 性能与习惯问题错误5在循环内频繁调用drugName.length()for (int i 0; i drugName.length(); i) { // 每次循环都调用 length()问题对于非常长的字符串length()是常数时间操作但理论上每次比较都调用一次不如将其值存入一个变量。不过现代编译器的优化通常能处理好这一点。对于初学者更应关注逻辑正确性。养成好习惯的话可以size_t len drugName.length(); for (size_t i 0; i len; i) { ... }错误6使用char数组和C风格字符串char drugName[100]; while (cin.getline(drugName, 100)) { ... }问题需要预先分配固定大小的数组存在缓冲区溢出的风险如果药名超过99个字符。std::string动态管理内存更安全、更方便。在C竞赛编程中除非有极端性能要求否则优先使用string。6. 举一反三与能力拓展成功解决“整理药名”后你的字符串处理能力已经上了一个台阶。但这只是一个开始。你可以尝试用类似的思维去解决“一本通”或其它OJ上的更多题目实现能力的迁移和拓展。相关题目推荐大小写转换尝试解决只进行全大写转全小写或全小写转全大写的题目。这更简单但可以练习transform算法的使用。单词首字母大写题目要求将一段英文句子中每个单词的首字母大写。这需要你能够识别“单词”的边界通常是空格其核心逻辑和“整理药名”非常相似只是判断“第一个字符”的规则从“整个字符串的首字符”变成了“每个单词的首字符”。密码强度检测要求判断一个密码字符串是否包含大写字母、小写字母、数字、特殊符号等。这需要你遍历字符串并用isupper、islower、isdigit等函数进行多种判断是cctype库函数的综合练习。字符串标准化处理例如去除字符串首尾空白trim、将多个连续空格替换为一个等。这类题目需要你仔细处理字符的删除和移动可能涉及到string的erase、find等成员函数难度稍高。思维拓展函数封装将“规范化一个药名”的逻辑写成一个独立的函数例如string normalizeDrugName(const string input)。这样主函数会更清晰并且这个函数可以在其他地方复用。这是良好的工程实践。单元测试编写一个简单的测试函数传入各种边界情况的字符串验证你的normalizeDrugName函数是否正确。这能极大地提升你代码的可靠性。探索C字符串视图string_view如果你的函数不需要修改字符串只是读取那么使用string_view作为参数可以避免不必要的拷贝提升性能。解决“1139整理药名”这样的题目真正的收获不在于ACAccept通过那一刻而在于整个分析、设计、编码、测试和反思的过程中你所巩固的基础知识和培养的严谨思维。字符串处理是编程的基石几乎无处不在。把这部分基本功打扎实后续学习数据结构、算法时你才能更专注于逻辑本身而不是被语法和细节绊倒。希望这篇详细的拆解能帮助你不仅通过这道题更理解这一类题从而在编程学习的道路上走得更稳、更远。