
准备蓝桥杯的时候模拟题是绕不开的一类题目。2014年第五届省赛的这道“分糖果”题目编号1431表面上看就是一个while循环加几次判断但每年都有不少人在“同时分糖”和“奇数补糖”这两个细节上翻车。这篇文章把这题从头到尾拆一遍——题目规则怎么理解、解题思路怎么落地、三种语言的代码怎么写、坑在哪里一次说清楚。不管是正在备赛C/C组、Java组还是Python组的同学都能直接拿来用如果你刚开始刷算法题想找一道简单的模拟题练手这道也很合适。1. 题目理解与解题思路拆解1.1 先搞清楚游戏规则题目本身不复杂但规则里藏着几个容易理解错的地方。有n个小朋友围坐成一圈老师先给每人发一些糖果然后开始做游戏每一轮游戏每个小朋友同时把自己手里糖果的一半分给坐在右边的人。分完之后谁手里的糖果数是奇数老师就给谁补一颗糖让它变成偶数。重复这个过程直到所有小朋友的糖果数一样多。题目要你输出的是整个过程中老师一共补了多少颗糖。乍一看这规则挺简单但“同时”两个字非常关键。它意味着所有小朋友在给出糖果时都是以自己这一轮开始前的糖果数为准而不是先等左边的人给过来再决定给右边多少。我见过不少人把这道题写成了“从左到右轮流分”结果跑出来的答案完全不对就是因为没有理解“同时”。另一个容易忽略的点是“围坐成一圈”。数组是线性的但小朋友的座位是环形的最后一个小朋友右边坐的是第一个小朋友。这个环形结构在分糖的时候必须单独处理否则数组尾巴上那一位就会算错。1.2 为什么说这是一道典型的模拟题所谓模拟题就是题目规定了完整的过程你只需要按照规则一步步执行不需要设计什么复杂的算法。这道“分糖果”就是最标准的模拟题循环里做三件事——补奇数、判断相等、分糖果循环结束条件就是所有人数相等。但它能成为一道真题肯定不只是考察你会写循环。真正的考点在于你能不能处理好“同时操作”这个细节以及能不能想清楚奇数糖果到底什么时候补。后面我会详细说这两个点。在蓝桥杯的省赛里模拟题占比不低尤其是前面几道题经常用模拟来筛掉基本功不扎实的选手。这道题看似送分但每年考场上都有人因为细节丢分所以把它吃透对你做其他模拟题也有帮助。2. 算法设计从游戏规则到代码的关键一跃2.1 难点一数组里怎么模拟“同时分糖”数组是线性的但小朋友是围成一圈坐的所以第n-1个小朋友右边就是第0个小朋友。这一点在分糖时要单独处理。至于“同时”最常见的做法是先把每个人糖果数的一半存下来再统一更新。但由于这道题里每个人给出的一半恰好也是别人收到的一半所以代码上有一个更巧妙的写法我放在第3部分代码里说。这里先理解一个原则更新任何一个位置时用到的其他位置数据必须是这一轮更新前的旧值。为什么强调“旧值”因为如果你从左往右挨个更新a[1]在更新时a[0]可能已经被改掉了。如果这时候还用a[0]的新值去算a[1]收到的糖那就变成了“等待左边给完再给右边”违背了题目“同时”的要求。我一开始写的时候就在这上面栽过跟头。第一轮分完糖之后数组尾部的人糖数总是莫名其妙多一颗后来打印中间结果才定位到问题我更新到a[n-1]时用的a[0]已经被覆盖成新值了。这正是写模拟题最容易犯的错误——变量的更新顺序没有想清楚。2.2 难点二奇数糖果到底什么时候补我最初写这题的时候是在分完糖之后马上检查每一个人的糖果数是奇数就补一颗。这样模拟看起来最贴近题目描述。但后来发现这种写法在代码里会多一个“初始状态也要补一遍”的判断而且分糖的时候还得分情况讨论除不尽的问题。后来我换了一种思路也推荐给你在每一轮循环最开始先把当前所有人的糖果数检查一遍遇到奇数就补一颗并计数。这样做的直接好处是进入任何一次分糖之前所有人的糖果数都是偶数a[i] / 2一定整除不会出现“半颗糖”的问题。有个细节要注意这种“先补后分”在语义上和“分完再补”有一点差别在中间轮的编号上会错开但对最终补糖总数没有影响。因为每个奇数都必须被补齐之后才能参与下一轮分糖补的总量是不变的。我自己用多组数据验证过两种写法输出一致但“先补后分”逻辑上更干净新手更容易写对。2.3 终止条件和时间复杂度循环什么时候结束当所有小朋友糖果数相等时。这个判断要放在补完奇数之后因为最终状态一定是所有人糖果相等且为偶数。如果放在分糖之后判断会导致结束条件被推迟一轮白白浪费一次分糖操作。复杂度方面每轮循环要遍历数组多次是O(n)循环轮数取决于具体数据但n的值在竞赛数据范围内一般不会很大所以即使跑很多轮也完全不用担心超时。这道题真正要担心的是逻辑别写错而不是性能。我在做这类模拟题的时候还有一个习惯把“判断是否全部相等”单独抽成一个操作不要和补奇数的循环混在一起。分开写虽然多一次遍历但思路特别清晰不容易漏判断。竞赛里多一次O(n)的遍历几乎不影响性能但逻辑清晰能帮你少掉很多头发。3. 代码实现三种语言对照着写3.1 C实现以下是我实际测试过的C写法注释里标了关键步骤#include iostream using namespace std; int main() { int n; cin n; int a[1005]; for (int i 0; i n; i) { cin a[i]; } int ans 0; // 补糖总数 while (true) { // 1. 补奇数保证所有人糖果数为偶数 for (int i 0; i n; i) { if (a[i] % 2 1) { a[i]; ans; } } // 2. 判断是否全部相等 bool flag true; for (int i 1; i n; i) { if (a[i] ! a[0]) { flag false; break; } } if (flag) { cout ans endl; break; } // 3. 分糖每个人把一半给右边 int temp a[0] / 2; // 先存下第一个人的一半因为a[0]马上要变 for (int i 0; i n - 1; i) { a[i] a[i] / 2 a[i 1] / 2; } a[n - 1] a[n - 1] / 2 temp; } return 0; }这段代码里最值得讲的是第3步。想象一下分糖时每个人把手里的糖果分成两半一半留在自己手里一半给右边的人。所以一轮之后第i个人的糖果数 自己留下的一半 左边的人i-1给自己的一半。代码里从左往右扫a[i]更新时用的是a[i]/2和自己右边a[i1]/2这正好对应“自己留下的一半 右边的人分给自己的一半”。最右边的a[n-1]比较特殊它的右边是a[0]但此时a[0]已经被覆盖了所以要在循环前用temp把a[0]原来的一半存下来。这个temp变量就是整个环形分糖的关键忘了它最后一个小朋友就收不到第一个小朋友的糖。3.2 Java实现和Python实现Java的写法几乎一模一样只是语法不同。这里贴一个完整可运行的版本import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc new Scanner(System.in); int n sc.nextInt(); int[] a new int[n]; for (int i 0; i n; i) { a[i] sc.nextInt(); } int ans 0; while (true) { for (int i 0; i n; i) { if (a[i] % 2 1) { a[i]; ans; } } boolean flag true; for (int i 1; i n; i) { if (a[i] ! a[0]) { flag false; break; } } if (flag) { System.out.println(ans); return; } int temp a[0] / 2; for (int i 0; i n - 1; i) { a[i] a[i] / 2 a[i 1] / 2; } a[n - 1] a[n - 1] / 2 temp; } } }Python版本稍微有点不同因为Python的整数运算不需要担心类型但循环逻辑完全一样n int(input()) a list(map(int, input().split())) ans 0 while True: for i in range(n): if a[i] % 2 1: a[i] 1 ans 1 if all(x a[0] for x in a): print(ans) break temp a[0] // 2 for i in range(n - 1): a[i] a[i] // 2 a[i 1] // 2 a[n - 1] a[n - 1] // 2 temp在蓝桥杯的实际判题环境中Python版本要注意输入有可能有多余的换行或空格用split()之后map转int是最稳妥的。如果你习惯用input().strip()记得处理空白否则可能因为末尾换行符导致解析出错。还有一个小提醒Python的all()在这里用起来很爽但在一些判题环境中如果循环次数特别多all()遍历也会有一点点开销。不过这道题完全不需要担心性能怎么舒服怎么来。4. 手算验证与边界测试4.1 用样例数据走一遍完整过程题目给出的样例输入是3 2 2 4我们用上面的算法手算一遍第0轮开始前2 2 4没有奇数不用补。 检查相等2和2相等但4不相等继续游戏。 分糖第0个人2个给右边1个自己留1个第1个人2个给右边1个自己留1个第2个人4个给右边第0个人2个自己留2个。一轮结束后变成2 3 3。第1轮开始2 3 3里有奇数3和3各补一颗补糖数2状态变成2 4 4。 检查相等2和4不相等继续。 分糖第0个人2个给右边1个留1个第1个人4个给右边2个留2个第2个人4个给右边第0个人2个留2个。一轮结束后变成3 4 3。第2轮开始3 4 3里有奇数3和3各补一颗补糖数4状态变成4 4 4。 检查相等全部相等输出4。我把这个状态变化整理成了表格轮次补齐后状态本轮补糖累计补糖是否相等分糖后状态第0轮2 2 400否2 3 3第1轮2 4 422否3 4 3第2轮4 4 424是输出4可以看到这个例子的答案确实是4和题目输出一致。我在第一次学这道题的时候就是靠这样手算一遍才彻底搞懂了补糖和分糖的前后关系。你如果也卡在思路上强烈建议自己拿组数据走一遍再写代码比直接看题解管用得多。4.2 边界情况测试处理边界情况最能体现代码的健壮性也是竞赛判题最容易埋坑的地方。第一个边界是n1。只有一个小朋友时他的糖果数天然等于他自己循环第一次判断就会输出0不会进入分糖逻辑。所以上面的代码不需要为n1单独写特殊分支。很多初学者会在这种地方过度设计反而写出一堆if其实没必要。第二个边界是输入数据本身全相等比如4 3 3 3 3这种情况第一次判断就全部相等直接输出0。注意这里有一个容易踩的坑如果全相等且都是奇数比如还是这组数据3 3 3 3按照我的代码会先补奇数3变成4补糖数变成4然后判断相等输出4。但如果你期望输出0就会犹豫。实际上题目问的是“过程中老师补了多少颗糖”初始状态如果包含奇数老师也要补糖所以输出4才是对的。这提醒我们初始状态也要遵守“奇数必须补成偶数”的规则因为不补成偶数就没办法进行“给出一半”这个操作。第三个边界是轮数比较多的情况。比如5 1 2 3 4 5补糖和分糖会交替进行很多轮。我实测过最终答案是某次补糖后全部相等。这种数据用上面的循环写跑起来没有任何压力。遇到这种数据的时候不要试图去推公式直接算出答案老老实实模拟反而是最快的。5. 易错点排查与竞赛实战经验5.1 常见错误速查表我把看到过包括我自己犯过的典型错误整理成一张表写代码的时候可以对照排查错误类型具体表现排查思路分糖顺序错误从左到右轮流分后一个小朋友的糖被前一个影响确认更新a[i]时用到的a[i]和a[i1]都是本轮旧值忘记处理环形a[n-1]拿不到a[0]的一半用temp提前保存a[0]原来的一半奇数判断时机不对分糖时出现除不尽结果错乱确保每次分糖前把所有奇数补齐相等判断位置不对程序多跑一轮浪费操作如果中间产生奇数会多补糖导致答案出错在补完奇数后、分糖前判断数组开小了n较大时越界用1005或动态数组别用恰好n大小的数组做循环外操作输入解析出错读不到完整数据或读到换行符用split()后逐个转int别直接用循环读裸数字还有一个我自己吃了亏的点写Java的时候如果while(true)里最后用return而不是break记得放在main方法里不然会有unreachable code的报错。这种小问题很容易在考场上浪费几分钟。5.2 我从这道题里总结的模拟题通用套路刷多了蓝桥杯的模拟题我发现它们都有共同点题目描述一个过程你只需要忠实地执行它。但执行的过程中顺序、边界、同时性是最容易出错的三件事。我自己的习惯是拿到模拟题先不急着写代码而是拿纸和笔把题目里的过程“演算”一遍哪怕只是很小的样例。演算的过程中你会自然发现哪些地方有歧义、哪些地方需要特殊处理。比如这道题如果不演算很多人根本意识不到“同时分糖”其实是一个很容易写错的细节。另外一个经验是循环的更新顺序尽量保持一致。我这题的代码里判断相等用的是a[0]作为基准分糖时又是从第0个开始更新这种统一从0开始的做法能减少很多思维负担。写模拟题的时候变量命名和循环方向如果前后一致调试起来会轻松很多。最后多说一句很多同学刷题时喜欢直接看题解看完觉得自己会了但一到比赛还是写不对。模拟题尤其需要自己亲手写、亲手跑、亲手改错。这道分糖果难度不大但用来练“把文字规则翻译成代码”的能力非常合适。把这个能力练好了后面做其他更复杂的模拟题会轻松很多。