华为OD机试C卷“抢7游戏”C++模拟题解与实战技巧 1. 项目概述从“抢7游戏”到华为OD机试的实战演练最近在准备华为OD机试C卷的朋友应该对“抢7游戏”这个题目不陌生。它不仅是C卷里一道经典的编程题更是一个绝佳的思维训练场能同时考察你的逻辑抽象、边界条件处理和代码实现能力。简单来说这道题模拟了一个两人轮流报数的游戏规则是从1开始报数遇到包含数字7或是7的倍数的数时不能直接报出而是要说“过”。题目会给定一个总轮数N要求你模拟这个过程并最终输出每个玩家说“过”的次数。听起来是不是有点像我们小时候玩的“逢7过”酒桌游戏没错其核心逻辑就是它。但在机试的语境下它远不止一个游戏。它考察的是你能否将一个生活化的、口头的游戏规则精准地转化为计算机可执行的、无歧义的逻辑判断。这恰恰是软件工程师日常工作中最核心的能力之一——需求分析与建模。很多同学栽在这道题上不是因为算法多高深而是因为漏掉了某些边界情况或者代码写得冗长易错。今天我就结合自己多年的开发经验和辅导经历用C带你从头到尾拆解这道题不仅给出能AC通过所有测试用例的代码更会分享如何构建健壮的逻辑、如何进行有效的测试以及那些考场上的实战技巧。2. 核心需求与逻辑模型拆解在动手写代码之前我们必须把模糊的自然语言描述提炼成一个精确的、可量化的逻辑模型。这是避免后期反复调试和遗漏用例的关键。2.1 规则的形式化定义题目描述通常比较精简我们需要从中提取出所有判断条件游戏进程两个玩家假设为A和B轮流进行从数字1开始递增报数。“过”的触发条件当前要报的数字满足以下两个条件之一条件一倍数该数字是7的倍数。即num % 7 0。条件二包含数字7该数字的十进制表示中任何一位上出现了数字‘7’。例如7, 17, 27, 70, 71, 177等。输出要求模拟完前N个数字的报数过程后分别输出玩家A和玩家B说“过”的次数。这里有一个非常重要的隐含条件当数字触发“过”时该数字本身被跳过不计入后续报数序列。也就是说报数的序列是自然数序列但遇到要“过”的数玩家不读出它游戏直接轮到下一个人报下一个数。这一点在理解“轮流”机制时至关重要。玩家A报1B报2...直到某个数需要“过”那么说出“过”的玩家消耗了他的回合下一个数字由对手开始报。2.2 逻辑建模与算法选择这道题本质上是一个模拟题。我们不需要复杂的数据结构或算法核心就是按照规则循环N次并正确分配回合和统计次数。算法思路如下初始化设置两个计数器countA和countB分别记录A和B说“过”的次数。设置一个变量currentNumber从1开始代表当前要报的数字。设置一个变量currentPlayer来表示当前回合的玩家可以用0表示A1表示B或者布尔值。循环N次我们需要处理前N个“报数动作”。注意是“动作”的数量为N而不是处理到数字N。因为有些数字被“过”掉了但每个“过”也消耗一个动作。判断当前数字对于currentNumber判断是否满足“过”的条件。更新计数与回合如果满足条件则当前玩家计数器加1。然后无论是否满足条件当前回合结束currentPlayer切换为另一玩家。关键点数字currentNumber在每次判断后都自增1。因为无论这个数是被报出来还是被“过”掉它都已经在游戏中处理过了下一个要考察的数字就是currentNumber 1。输出结果循环结束后输出countA和countB。判断“包含数字7”的高效实现这是代码中的一个细节亮点。我们可以写一个独立的函数containsSeven(int num)。方法一循环取余在一个while循环中不断对num取余10num % 10判断余数是否为7然后将num除以10num / 10直到num为0。这是最通用和清晰的方法。bool containsSeven(int num) { while (num 0) { if (num % 10 7) { return true; } num / 10; } return false; }方法二转换为字符串将整数转换为std::string然后使用find(7)方法。代码更简洁但涉及字符串创建和操作对于大量数据虽然本题N通常不大效率稍低于数学方法。在机试中两种方法通常都可接受。实操心得在机试这种时间有限的场合我推荐使用循环取余法。原因有三1) 它不依赖STL的字符串转换是最基础的编程思维体现2) 效率足够高3) 能向考官展示你对数字运算的基本功。将核心逻辑封装成函数也让主循环更加清晰。3. C代码实现与逐行解析接下来我们实现一个完整、健壮且注释清晰的C解决方案。我们会考虑输入处理、核心逻辑和输出。#include iostream using namespace std; /** * 判断一个正整数是否包含数字7 * param num 待判断的正整数 * return 如果包含数字7则返回true否则返回false */ bool containsSeven(int num) { while (num 0) { if (num % 10 7) { // 检查当前个位数 return true; } num / 10; // 去掉个位数继续检查下一位 } return false; } /** * 模拟抢7游戏 * param N 游戏的总回合数报数动作次数 * param countA 玩家A说“过”的次数输出参数 * param countB 玩家B说“过”的次数输出参数 */ void playGame(int N, int countA, int countB) { // 初始化 countA 0; countB 0; int currentNumber 1; // 当前要报的数字从1开始 bool isPlayerATurn true; // true表示当前是玩家A的回合 for (int i 0; i N; i) { // 判断当前数字是否需要“过” bool shouldPass (currentNumber % 7 0) || containsSeven(currentNumber); if (shouldPass) { // 根据当前回合玩家更新对应计数器 if (isPlayerATurn) { countA; } else { countB; } // 注意这里没有else分支。无论是否“过”都算处理完一个动作。 } // 当前回合结束切换玩家 isPlayerATurn !isPlayerATurn; // 处理下一个数字 currentNumber; } } int main() { int N; // 处理输入题目通常保证输入是有效的正整数 cin N; int countA 0, countB 0; // 调用游戏模拟函数 playGame(N, countA, countB); // 输出结果通常空格隔开末尾换行 cout countA endl countB endl; // 或者 cout countA countB endl; 根据题目要求格式调整 return 0; }代码关键点解析函数封装将containsSeven和playGame逻辑分离。playGame函数接收N和两个引用参数用于返回结果这使得主函数main非常干净只负责输入输出和调用符合模块化设计思想。在机试中清晰的模块划分是加分项。循环控制for (int i 0; i N; i)这里循环的是“动作次数”i而不是数字currentNumber。这是模拟题的核心必须明确循环变量代表的意义。玩家切换使用布尔变量isPlayerATurn来标识当前回合玩家。每次循环结束后通过isPlayerATurn !isPlayerATurn进行切换。逻辑简单直观。数字递增currentNumber放在循环末尾。这意味着每个数字无论是否被“过”在本次循环中被检查且只被检查一次然后我们就永远不再考虑它了。这正确模拟了游戏进程。输入输出机试环境通常是标准输入输出。注意输出格式有时要求换行输出两个数有时要求空格隔开。务必仔细阅读题目示例。上述代码提供了两种常见输出方式的注释。4. 边界条件与测试用例设计再优雅的代码如果没有经过充分测试也是不可靠的。对于模拟题设计全面的测试用例是满分的关键。4.1 必须考虑的边界情况N1最小输入。只报一个数数字1。1既不是7的倍数也不含7所以A报“1”无人说“过”。输出应为“0 0”。N很小但包含触发点例如N7。过程A报1B报2A报3B报4A报5B报6A遇到7是7的倍数A说“过”。循环结束。输出应为“1 0”。连续触发“过”例如数字6,7,8。7需要过8包含7吗不包含注意是数字8不是18。但假设从数字67开始67含7过68不含69不含70是7的倍数过。需要测试玩家在连续“过”时的切换是否正确。较大的N验证程序效率。我们的算法时间复杂度是O(N * L)其中L是数字的平均位数对于containsSeven函数在N最大为10000的常见限制下完全无压力。但要注意int类型是否足够currentNumber可能会增长到N的数量级通常int足够。包含7的多种情况个位是717、十位是770、多位含7177。确保containsSeven函数都能正确识别。4.2 测试用例速查表你可以用下面的表格快速验证你的程序逻辑。在机试中如果时间允许在本地运行这些用例是很好的习惯。输入N模拟过程数字/动作预期输出 (A, B)验证要点1A: 1(0, 0)最小边界无触发7A:1, B:2, A:3, B:4, A:5, B:6, A:过(7)(1, 0)首次触发倍数8...同上... B:8(1, 0)触发后正常报数14... A:过(7), B:8, A:9, B:10, A:11, B:12, A:13, B:过(14)(1, 1)倍数触发玩家B触发17... B:过(14), A:15, B:16, A:过(17)(2, 1)包含数字7触发20... A:过(17), B:18, A:19, B:过(20)(2, 2)混合触发连续计数21... B:过(20), A:过(21)(3, 2)倍数触发且21也含数字‘2’和‘1’但判断正确避坑技巧在机试环境里我习惯在代码编写完成后立即用几个简单用例在脑中或草纸上模拟一遍比如N1, 7, 8。这能快速发现循环初始值、玩家切换逻辑上的重大错误比直接提交等待判题要快得多。5. 性能优化与代码风格探讨虽然这道题对性能不敏感但写出高质量的代码能体现你的专业素养。5.1 潜在优化点containsSeven函数的调用在循环中我们对每个currentNumber都调用了这个函数。对于非7倍数的数字这个调用是必要的。对于已是7倍数的数字其实可以跳过这个函数调用因为已经满足“过”的条件。这就是逻辑运算符||的短路求值特性。当(currentNumber % 7 0)为真时containsSeven(currentNumber)将不会被计算。我们的代码已经利用了这一点无需改动。空间复杂度我们只使用了几个整型变量空间复杂度是O(1)已经是最优。5.2 代码风格与可读性对于机试和日常工程清晰的代码远比炫技的代码更重要。有意义的变量名countA,countB,currentNumber,isPlayerATurn都比简单的a,b,i,flag要好得多。函数注释即使时间紧为函数写一行简单的注释说明其功能和参数能让代码脉络更清晰。避免全局变量像本例一样通过函数参数和返回值传递数据是更好的实践。一致性保持缩进、括号风格一致。6. 常见错误分析与排查根据过往经验同学们在实现这道题时容易踩以下几个坑混淆“数字”和“动作”最常见的错误。写成了for (int num 1; num N; num)然后内部判断num。这完全错了因为N是动作次数不是最大数字。当有“过”的时候数字的增长会超过N。必须用单独的变量i或动作计数器来循环N次。玩家切换逻辑错误错误1在“过”的时候切换了玩家在“不过”的时候忘了切换。这会导致回合混乱。错误2切换逻辑放错了位置。应该放在每次判断之后作为回合的结束。我们的代码将其放在循环末尾无论是否“过”都执行切换是正确的。纠正记住一个原则——每处理一个数字一个动作无论结果如何都必须切换一次玩家。containsSeven函数实现错误处理负数本题不需要但函数通用性考虑。对于数字0函数应返回false。我们的实现while (num 0)正确处理了这一点。可以用num ! 0作为循环条件但针对正整数num 0更清晰。输入输出格式不符这是“非战斗减员”。一定要用题目给的示例输入输出验证你的程序。是输出countA和countB空格隔开还是换行末尾是否需要换行仔细核对。初始化错误忘记将countA,countB初始化为0或者currentNumber没有从1开始。调试建议如果提交后不能全部通过不要慌张。首先检查几个边界用例N1, 7, 8, 14在自己的环境中是否正确。如果本地正确但线上不对极有可能是输入输出格式问题或者线上环境有特殊的编译要求比如某些OJ需要int main()必须返回0。对于模拟题最有效的调试方法就是在关键步骤打印中间变量。例如在循环内打印i, currentNumber, isPlayerATurn, shouldPass, countA, countB然后与手算过程对比立刻就能定位问题所在。7. 从解题到思维提升这道题还考察了什么“抢7游戏”不仅仅是一道编程题。它在华为OD机试中出现至少考察了候选人的以下能力需求理解与转化能力将一段文字游戏规则无歧义地转化为逻辑判断语句%70和containsSeven。这是软件工程师对接产品需求的第一步。模拟与流程控制能力准确无误地控制循环、条件判断和状态当前玩家切换是编程的基本功。边界条件与测试思维能否考虑到N1、连续触发等特殊情况体现了思维的严密性。自己设计测试用例是保障代码质量的重要习惯。代码整洁与模块化意识将独立功能判断含7封装成函数使主逻辑清晰。即使是在机试中这种习惯也能让代码更易写、易读、易调试。所以练习这道题收获的不仅仅是一个AC代码更是对基础编程思维的一次巩固。在平时的练习中不妨多思考一下“如果规则变了怎么办”例如如果是“抢3游戏”或者遇到特定数字要拍手又该如何快速修改代码这种举一反三的能力才是学习的关键。最后在考场上遇到这道题或类似模拟题时我的建议是先花2-3分钟在草稿纸上理清状态和流程画一个简单的状态转移图或列出前10个数字的模拟过程。磨刀不误砍柴工清晰的思路能让你在编码时一气呵成避免反复修改从而为后面更复杂的题目节省宝贵时间。记住在机试中一次通过率远比运行速度更重要。