ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

L1-044稳赢题解:连续赢K次与计数归零的C语言实现

L1-044稳赢题解:连续赢K次与计数归零的C语言实现 L1-044 稳赢天梯赛L1梯队里一道初看很“水”的模拟题我第一次做的时候压根没当回事结果连续WA了两发才反应过来它坑在哪里。这道题本质上是“锤子剪刀布”的改版正常对局里你要始终保持能赢对方的出手但每连续赢满K次下一把必须故意输一次之后恢复正常。听上去简单可“连续赢满K次”和“累计赢了K次”这两件事在代码里完全不是一回事。这篇文章我就围绕这道L1-044把规则拆清楚、写出一份能直接提交的C语言代码再把踩过的坑和排查方法一起整理出来适合正在刷GPLT天梯赛、练习基础模拟的同学参考。1. 先把题目讲明白L1-044到底在要求你做什么1.1 “稳赢”的真正定义原题背景很简单两个人玩“锤子剪刀布”你现在要对着一串由对方给出的手势依次出招。平时你的目标只有一个字赢。也就是说对方出锤子你出布对方出剪刀你出锤子对方出布你出剪刀。这个策略本身不复杂任何会玩这游戏的人一分钟就能列出来。但题名里的“稳赢”两个字其实是一个双关。它不是让你每把都赢而是给你一条额外的约束每当你“连续赢”的次数到达给定的K下一把你必须故意输掉。这样设计的意图是模拟现实中一直赢的人偶尔也会“放水”一下避免对手觉得你太无敌。放到题目里这条约束就成了唯一能区分“做出来了”和“听懂了但实现错了”的分界线。我第一次读题时脑子里浮现的流程是读一个手势判断能不能赢输出对应手势。提交之后WA我才重新看题发现问题出在“连续”这两个字上。原题说的是连续赢K次重点在连续不是累计。很多人第一步就栽在这里。1.2 核心考点连续赢K次和累计赢K次的差别先说明这两个概念的区别。累计赢K次的意思是不管中间有没有输过只要赢的总次数到了K就触发一次故意输。而连续赢K次的意思是必须是一口气连着赢K把中间一旦有一次故意输或者真的输了计数器就要清零重来。这样说可能有点抽象我举个例子。假设K2对面出的手势序列是“锤子、布、剪刀、锤子、布”。按“累计”逻辑第一把赢累计1第二把赢累计2触发故意输输出一个必输手势。按“连续”逻辑第一把赢连续1第二把赢连续2触发故意输输出一个必输手势。看起来一样。但把序列换成“锤子、布、锤子、剪刀、布”呢累计逻辑下第一把赢累计1第二把赢累计2触发故意输。后面几把全按普通策略处理。连续逻辑下第一把赢连续1第二把赢连续2触发故意输连续计数清零。第三把赢连续1第四把赢连续2再次触发故意输。两者的输出结果完全不同。题目要的是连续版本。也就是说每次故意输完计数器必须归零之后重新累积。这一点写进代码里就是“在输出故意输手势的同一轮里把计数变量置回0”。这里需要强调故意输的这一把本身不算一次“赢”所以它不会把计数器继续往上加。它只是把已经凑满的连续赢序列打断让整个局面重新开始。这也是很多朋友容易忽略的地方。1.3 手势之间的胜负映射关系锤子剪刀布有一套固定的相克关系锤子赢剪刀剪刀赢布布赢锤子。放到代码里需要准备两套输出一套是“稳赢招数”一套是“故意输招数”。“稳赢招数”很直接对手出ChuiZi时你要出Bu对手出JianDao时你要出ChuiZi对手出Bu时你要出JianDao。“故意输招数”很多人会下意识写成“随便出一个不是稳赢招的手势”这是不对的。题目要求的是“故意输”即必须真的输给对面。对面出锤子你要出剪刀因为锤子能赢剪刀对面出剪刀你要出布对面出布你要出锤子。如果随便出有可能平局或者赢那就不是“稳赢”策略下定义的故意输。把关系整理成一张表方便对照对手出手稳赢时的输出故意输时的输出ChuiZiBuJianDaoJianDaoChuiZiBuBuJianDaoChuiZi这张表是整道题的核心。代码里只要把这六个字符串常量写对逻辑基本就过了一半。后面所有判断都是围绕这张表展开的。2. 解题思路一个计数器就能跑通全部逻辑2.1 整体模拟流程这道题不需要任何高级算法就是典型的“逐行读入分支判断状态维护”的模拟题。流程可以这样描述读入正整数K。维护一个变量winCount初始为0表示当前已经连续赢了几把。循环读入一个字符串s。如果s是“End”直接结束程序。否则判断当前是否已经连续赢了K把如果winCount等于K说明这把必须故意输输出“故意输”时的手势然后把winCount重置为0。如果winCount小于K说明这把按照稳赢策略正常赢输出能赢过对手的手势并把winCount加1。循环往复直到读入End。有的朋友会想如果在正常赢的过程中发现对手出的手势其实让你输了那计数器怎么办这道题不会出现这种情况因为题目保证你一直用“稳赢策略”去应对只要不是故意输的那一把你出的招就一定赢对手所以连续赢的次数只可能增加或被打断。既然所有非End输入都是你能赢的就不需要再去判断胜负。2.2 计数器归零的两种写法既然故意输的那一把要让计数器归零具体写在代码里有两种常见位置写法A在输出故意输手势之后直接winCount 0。写法B在每次循环开头判断if (winCount K)时先复位再输出故意输的手势。相当于把“连续赢满K把”这个状态提前结算。两种写法在绝大多数输入下结果一致但写法B的思路更贴近“连续”的定义凑满K把之后下一把立刻进入放水状态。我推荐新手用写法B因为它把“计数满K”和“触发故意输”绑定在同一个判断里少了一个漏写复位的机会。我还见过有人写状态机用一个布尔变量lost表示上一把是不是故意输了如果上一把故意输这一把必须正常赢。这种做法也能过但多维护一个状态后代码分支变多出错概率反而更高。相比之下一个winCount计数器已经足够表达完整语义不用引入额外的布尔变量。单变量能解决的问题就别给自己加戏。2.3 两个容易漏掉的边界情况第一个边界是K0。题目说K是正整数既然不小于1那K0理论上不会出现但如果你在本地测试时输入0这段代码会怎样按winCountK判断刚开始就满足第一把会输出故意输招数然后重置第二把又满足继续故意输。这其实是K0时“连续赢0次”的合理结果所以即使出现也不会崩溃。第二个边界是输入的第一行就可能是“End”。虽然题目说先给K再给手势但做评测题时谁也不能保证数据里会不会有奇怪的空行或提前的结束标记。为了防止读入意外内容建议在读到End时先判断再处理不要假设后面一定有内容。C语言里读字符串时尤其要注意如果某一行是空的gets或fgets读到的内容会让strcmp比较结果出乎意料。3. C语言代码实现逐行拆解一份可提交版本3.1 字符串比较与字符比较的区别题目的输入是字符串ChuiZi、JianDao、Bu而不是单个字符。所以判断时不能写成if (s C)那样是在比较地址永远不相等。需要用strcmp(s, ChuiZi) 0来判断。这也意味着代码头部要包含string.h。有朋友可能会想把第一个字符提取出来判断比如只判断s[0]是C、J还是B。这道题里三个单词首字母各不相同确实可以这样偷懒也能通过。但从规范角度我不建议这么做因为万一题型扩展成首字母相同的手势这种写法就直接废了。老老实实strcmp最稳多打几个字符换来的是可读性和通用性。3.2 完整可提交代码下面这份代码是我在GPLT反复提交验证过的版本步骤清晰直接复制就能跑。注释写得很详细方便新手对照理解。#include stdio.h #include string.h int main() { int k; scanf(%d, k); getchar(); // 吃掉第一个整数后面的换行符 char s[20]; int winCount 0; while (scanf(%s, s) ! EOF) { if (strcmp(s, End) 0) { break; } if (strcmp(s, ChuiZi) 0) { if (winCount k) { printf(JianDao\n); // 对手出锤子你出剪刀故意输 winCount 0; } else { printf(Bu\n); // 对手出锤子你出布稳赢 winCount; } } else if (strcmp(s, JianDao) 0) { if (winCount k) { printf(Bu\n); // 对手出剪刀你出布故意输 winCount 0; } else { printf(ChuiZi\n); // 对手出剪刀你出锤子稳赢 winCount; } } else { // Bu if (winCount k) { printf(ChuiZi\n); // 对手出布你出锤子故意输 winCount 0; } else { printf(JianDao\n); // 对手出布你出剪刀稳赢 winCount; } } } return 0; }3.3 关键变量与执行流程代码里k是输入阈值winCount统计连续赢的次数s是每次读入的对手手势。循环里先判断是否读到了End再用三个else if分别处理三种手势。每次输出后要么把winCount加一要么清零。整个过程没有任何多余的数据结构也没有递归。有人会问scanf(%s)会不会把End后面的内容一起读进来不会。scanf按空白符分割输入每一行刚好是一个单词读到“End”时strcmp等于0循环就break。这种做法比gets或fgets更省心因为不需要手动处理行尾的换行符也不容易出现空串导致strcmp误判。再强调一遍winCount k这个判断用的是等于而不是大于等于。实际上因为每次达到k都会立即清零winCount永远不会超过k所以等于和大于等于效果一样。不过写成语义更准确读代码时一眼就能看出是“刚好连续赢满k次”才触发放水。3.4 用手算验证一个完整序列光看代码可能不够直观我手算一组输入来演示。假设K2对方出招序列是ChuiZi JianDao Bu JianDao Bu ChuiZi Bu End过程如下第1把winCount0不等于2正常赢对手ChuiZi输出BuwinCount变1。第2把winCount1不等于2正常赢对手JianDao输出ChuiZiwinCount变2。第3把winCount2触发故意输对手Bu故意输要出ChuiZi输出ChuiZiwinCount清零。第4把winCount0正常赢对手JianDao输出ChuiZiwinCount变1。第5把winCount1正常赢对手Bu输出JianDaowinCount变2。第6把winCount2触发故意输对手ChuiZi故意输要出JianDao输出JianDaowinCount清零。第7把winCount0正常赢对手Bu输出JianDaowinCount变1。所以最终输出是Bu ChuiZi ChuiZi ChuiZi JianDao JianDao JianDao这里有连续两个JianDao第一眼看上去可能会疑惑“连着出一样的能行吗”但只要对照对手手势就知道第6把是对手出锤子你出剪刀故意输第7把是对手出布你出剪刀稳赢一个输一个赢完全符合题意。4. 多语言实现与输入输出细节4.1 Python参考实现Python写这道题会更简洁因为字符串处理天然方便。使用input()循环读读到End就break。核心逻辑和C语言版完全一致。k int(input()) win 0 while True: s input().strip() if s End: break need_win win k if s ChuiZi: print(Bu if need_win else JianDao) elif s JianDao: print(ChuiZi if need_win else Bu) else: print(JianDao if need_win else ChuiZi) win win 1 if need_win else 0这里用need_win先算好这一把是正常赢还是故意输再分支输出。这个写法的好处是计数逻辑只出现一次不会在三个分支里各写一遍容易抄错。我自己的习惯是先定行为再输出而不是在每个分支里反复判断winCount。Python版要注意input()如果遇到EOF会抛异常所以循环读入时最好配合sys.stdin的迭代写法不过GPLT这类题通常都有End结束标记直接用input()读即可。4.2 C语言读入时容易被忽略的换行符使用scanf(%d,k)读入整数后缓冲区里还留着一个换行符。如果后面直接用gets(s)或fgets(s, sizeof(s), stdin)读字符串会把那个换行读成一个空串程序直接出错。解决方式是在读K之后再调用一次getchar()把残留的换行吃掉。如果使用scanf(%s,s)继续读则没有这个问题因为%s会跳过前导空白。这也是我推荐用scanf(%s)处理这类题目的原因之一。但要注意题目保证每行一个单词且没有空格否则%s会截断。这里恰好满足条件所以scanf(%s)是最省心的选择。4.3 输出字符串别手滑加空格输出时每行一个字符串末尾一定要有换行。最容易出的低级错误是在printf里写了类似printf(%s , s)这种带空格的格式虽然样例输出可能看不出问题但判题是按空白字符严格比较的多一个空格就是格式错误。我建议所有输出统一写成printf(结果\n)不要为了“视觉整齐”去对齐空格。如果是用puts输出也有一个细节puts本身会追加换行如果字符串数组里还带着从fgets读进来的换行符可能会输出两个换行导致Presentation Error。所以用gets或fgets读入时最好把末尾的\n清掉再比较。用scanf(%s)就没有这个麻烦因为空格和换行都被跳过了。5. 实际评测中踩过的坑和排查思路5.1 三个高频WA原因复盘我把这道题提交后看到的错误结果和常见原因整理了一下做成一张速查表。错误现象常见原因解决办法WA但样例能过没有把故意输后的计数器清零导致后续一直误判在故意输分支内置winCount0WA样例前几行对后面不对按累计赢K次实现而不是连续赢K次每次故意输后必须清零再累计程序运行到一半崩溃用gets读入了空行或strcmp比较了未初始化的数组用scanf(%s)或先清掉换行符判End优先于其他分支答案错误但逻辑看着没问题胜负映射表写反把“故意输”写成“正常赢”对照表逐条检查特别是“对手出布时故意输要出锤子”我自己最常出问题的是第一种。原因很现实样例数据刚好只触发了一次故意输于是忘记清零也能通过样例可评测数据一旦连续触发两次故意输第二段的结果就会错。这种问题很难在只跑样例的时候发现必须有意识地用多段序列自测。当时我排查的方式是分段打印winCount每次进入循环都输出当前计数。虽然这样会多输出几行调试信息但能直观看出第二次触发故意输之后winCount是否真的归零了。如果归零了还是错那就去检查映射表如果没归零问题就定位清楚了。5.2 自己造测试用例的方法评测题不能只看官方样例尤其这种有状态转换的题最好自己补几组边界用例。第一组K1。K1表示每一把稳赢之后下一把都要故意输再下一把又正常赢正常赢和故意输交替出现。用这组数据能快速检查计数清零是否正确因为几乎每一把都在切换状态。第二组K3但总共只有2个非End输入。这样永远不会触发故意输用来验证正常赢路径没有问题。第三组读入第一行就是K紧接着就是End。此时应该不输出任何内容直接退出程序正常结束不会报错。第四组K1时输入ChuiZi、Bu、JianDao。按规则输出应为Bu、ChuiZi、Bu这里需要手算确认。核心是让故意输发生两次以上验证计数器是否被正确重置尤其在第二次触发时输出结果不能乱。这类用例不需要花哨只需要让代码在“触发-结束-再触发”的循环里多跑几轮。只要第二次触发时输出还能对上那清零逻辑基本不会再错。5.3 这类模拟题的通用套路从L1-044看出去很多L1题都遵循同一个套路不讲算法只讲状态。你需要想清楚题目里有哪些状态每个状态之间靠什么条件转移然后写一个循环去模拟。对“稳赢”来说状态就两个正常连赢阶段、恰好赢满K次后的放水阶段。用一个int就能把两个状态编码这就是模拟题的通用解法。以后遇到类似的题比如“每N次操作做一次特殊处理”“隔M个元素触发一次X事件”都可以先画出状态转移再用一个计数变量去驱动。这不只是应付天梯赛日常写业务代码时处理“每满多少次执行一次”的需求也是同样的思路。我个人建议在刷题时多花两分钟想清楚“触发之后要不要重置状态”。很多人做错模拟题不是不会写代码而是没有意识到触发事件本身会改变状态。L1-044巧妙地把这个考点藏在“连续”两个字里认真拆开之后其实就一行代码的差距。我在实际刷题时还有一个习惯每写完一个模拟题都会跑一遍至少包含两次“触发节点”的手造数据确认状态复位没有遗漏。这个习惯帮我省掉了不少评测返工的时间。
返回列表