每年GESP三级考试,编程题里最稳的一类就是“模拟题”。2025年6月这套卷子第三部分第2题“分糖果”,正是这类题的典型代表:题目本身不难,算法就是纯模拟,但它特别考验选手能不能把题意读透、把边界想清楚。很多学生考完对答案,都说“思路一下就想到了,但代码写得磕磕绊绊”,或者“样例过了,但不确定自己漏了什么情况”。这篇文章我就以这道分糖果为例,把从读题到代码再到排错的全过程掰开揉碎讲一遍,希望能帮备考GESP C++三级的朋友少走弯路。
这道题适合所有准备GESP三级、四级,或者刚学完数组和循环、想练模拟题的新手。如果你正在刷题但总是卡在“不知道代码为什么不对”,这篇内容正好对症。我会先讲清楚题目在说什么、出题人想考什么,再给完整的C++参考代码,最后把最容易踩的坑逐个列出来。全程用我平时带学生的口吻讲,保证没有一句废话。
1. 题目理解与考点拆解
1.1 这道题到底在说什么
我先按最常见的题目版本来还原一下题面,如果你手里拿到的版本稍有差异,对照着调整即可。
n个小朋友围成一圈坐着,每人手里有一些糖果。老师发令后,每个小朋友同时把自己手中糖果的一半分给右边的小朋友。分之前,如果某个小朋友手里的糖果数是奇数,老师会先补给他1颗,让他凑成偶数再分。这个过程不断重复,直到所有小朋友手里的糖果数都相等为止。要求输出一共进行了多少轮。
这道题的题面信息量不大,但里面有三个点必须抓准。第一,“同时”两个字是灵魂,它意味着所有人的分配动作发生在同一时刻,不能一个接一个地分。第二,“奇数补一颗”发生在分糖之前,而不是分完再补。第三,终止条件是“所有小朋友糖果数相等”,不是“每个人都不变”,虽然这两个状态经常同步出现,但判断标准要以相等为准。
有的版本还会额外问“最后每人手里有多少颗糖”,那就再多输出一个数。我写这篇的时候先把基础版本讲透,扩展版本在代码里顺带提一下。
1.2 出题人想考什么
这道题挂在C++三级,核心考点其实很明确:数组的遍历与更新、循环控制和条件判断。具体拆开来看是这几层。
首先是数组模拟能力。n个小朋友围成一圈,你至少得用一个长度为n的数组存状态,而且因为“围成一圈”,访问第0个人的右边是第1个人,访问第n-1个人的右边又绕回第0个人,这里要用取模运算,考的是对循环下标的理解。
其次是状态同步更新的能力。“同时分糖”这个要求,听起来简单,写起来特别容易出问题。如果一边遍历一边改原数组,那么第i个人分完糖之后,第i+1个人分的时候拿到的是被改过的值,整个模拟就全错了。正确的做法是用一个临时数组存“每个人要分出去的一半”,全部算完之后再统一更新,这一点是本题最大的得分分水岭。
第三是循环终止条件的写法。你需要一个函数判断当前数组是否所有元素相同,这个判断每轮做一次,代码很简单,但很多人会把它写进while的条件里,导致代码又长又容易出错。
第四是边界意识。n=1的情况、初始全部相等的情况、糖果数很大的情况,这些极端输入能不能正确处理,直接决定你是在“过了样例”还是“真的会做这道题”。后文我会专门用一节讲这些边界。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路与算法设计
2.1 核心思路:老老实实模拟
这种题没有太高深的算法,核心策略就是一个字——“模”。按题目描述,一轮一轮地操作,每轮做四件事:
- 检查当前状态是否所有小朋友糖果数相等,若相等则停止;
- 对每个小朋友,如果糖果数是奇数,先加1(老师补糖);
- 计算每个小朋友分出去的一半(也就是当前糖果数的一半),存到临时数组;
- 每个小朋友更新自己的糖果数:自己留下的一半,加上左边小朋友分过来的那一半。
第4步需要特别说明一下。题目说“分给右边的小朋友”,那么第i个人会收到来自第i-1个人(也就是左边那个人)分出来的那一半。所以第i个人的新糖果数 = a[i] / 2 + a[(i-1+n) % n] / 2。这里加n再取模,是为了防止数组下标变成负数。
这个思路从读题到落地,理论上也就十行代码的事。但为什么考场上还是有不少人写不出来?我观察下来,问题几乎都出在“同时更新”这个要求上。很多人想不明白为什么要用临时数组,总觉得直接原地改也对。我们举一个最简单的例子:两个小朋友,A有2颗,B有2颗。如果原地改,A先分1颗给B,B变成3颗,B再分1颗给A(此时B有3颗,奇数,老师先补1颗变成4,分2颗给A),结果是A有3颗,B有2颗,直接错乱。同步分配的话,A给B 1颗,B给A 1颗,结果两人都还是2颗。你看,有没有临时数组,结果天差地别。
2.2 关键细节:同步分配、奇偶补糖、结束条件
同步分配的实现我上面已经说了,用临时数组b存每一轮的“半糖量”,等所有a[i]都算完了,再统一把a[i]更新成a[i]=b[i]+b[(i-1+n)%n]。
奇偶补糖的顺序也值得多说一句。正确的逻辑是:先看a[i]是不是奇数,是奇数就先a[i]++,然后b[i]=a[i]/2。如果把顺序反过来,先b[i]=a[i]/2再判断,就会漏算老师补的那颗糖。我曾见过一个学生写的是b[i] = a[i] / 2; if (a[i] % 2 == 1) b[i]++;,这相当于把奇数颗的一半算成了比正确答案多1,因为奇数n的一半向下取整是(n-1)/2,补1颗变成n+1后一半是(n+1)/2,比(n-1)/2正好多1。所以从数值上看他这种写法等于先除再加,其实是对的,但它改变了原数组a[i]的奇偶状态,后面的判断就乱了。最好是老实按“先补后分”写,逻辑上和题面一一对应,不容易乱。
结束条件建议用一个单独的函数check(),遍历数组判断所有元素是否相等。不要在while条件里写一堆a[0]==a[1] && a[1]==a[2]这样的东西,n一变就要改代码,可维护性太差。这个函数每轮调用一次,开销很小,但能让你主流程清爽很多。
2.3 复杂度与数据范围分析
这道题的数据范围一般不大,n通常在100以内,每人的初始糖果数也不会太离谱,所以暴力模拟完全够用。
但有一个隐藏问题值得注意:轮数可能并不小。虽然n和初始糖果数都有限,但理论上整个过程可能要很多轮才能收敛。我印象中这类分糖果问题在某些特殊初值下,轮数可以达到几千甚至上万,但每一轮都是O(n)的操作,就算n=100、轮数10000,总操作量也就是100万次,对C++来说眨眼就跑完了,完全不用担心超时。
我建议做题之前先估算一下最坏情况:n=100,每轮check是100次比较,每轮更新要拷贝数组,总体也就几百次操作。如果某道题把n加大到10000,那就要注意复杂度了,但三级考试不会这么考。所以这道题不需要优化,也不需要开什么特殊数据结构,老老实实写就能满分。
3. C++代码实现与逐段解读
3.1 完整参考代码
直接上代码,这段是我按考试要求写的标准解法,读入和输出都用scanf/printf,运行效率比cin/cout更稳,考试时也建议这么写。
cpp复制#include <bits/stdc++.h>
using namespace std;
int n;
int a[105];
bool check() {
for (int i = 1; i < n; i++) {
if (a[i] != a[0]) return false;
}
return true;
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i]);
}
int cnt = 0;
while (!check()) {
int b[105];
// 第一遍:补糖 + 计算每个人分出去的一半
for (int i = 0; i < n; i++) {
if (a[i] % 2 == 1) a[i]++;
b[i] = a[i] / 2;
}
// 第二遍:统一更新,i 收到的是 i-1 分出来的一半
for (int i = 0; i < n; i++) {
a[i] = b[i] + b[(i - 1 + n) % n];
}
cnt++;
// 防御:防止极端情况死循环
if (cnt > 1000000) {
printf("error\n");
return 0;
}
}
printf("%d\n", cnt);
return 0;
}
如果你拿到的题目还要求输出最终的糖果数,那就在while结束后再printf一个空格和a[0](此时所有a[i]都相等,输出谁都可以)。注意第一遍遍历里我直接改了a[i](补糖),这没问题,因为我们算的是“分出去的一半”,补糖后的a[i]刚好也是“自己原来那份加上补的糖”,逻辑上是完整且自洽的。
3.2 核心逻辑逐段讲解
这段代码看起来短,但每一段都有讲究,我按顺序拆开讲。
check()函数,遍历1到n-1,只要有一个和a[0]不同就返回false。这样写比在while里写条件清晰得多。不过这里有个小隐患:如果n=0,循环体不会执行,函数会返回true。但题目不可能给你0个小朋友,所以不用管。
主循环里第一遍for循环是“补糖+分半”的合并处理。这里注意,补糖是直接改a[i],因为补糖这个动作没有副作用——它不涉及其他小朋友,只是让当前这个人手里的糖变多。改完a[i]之后,立刻取一半存入b[i]。这里的b[i]就是“第i个人本轮要分出去的数量”。
第二遍for循环是同步更新。很多人会问,为什么不是a[i] = b[i] + b[(i+1) % n]?因为题目说的是“分给右边”,那第i个人收到的是“左边那个人分给他的”,也就是第i-1个人分出来的b[i-1]。写成b[(i-1+n)%n]就是为了处理i=0时下标变成-1的问题。这里非常建议你自己在草稿纸上画一个n=3的图,把第0个人、第1个人、第2个人之间的分糖方向标出来,代码自然就写对了。就我经验,凡是靠死记硬背公式的,过两天必忘,但自己在纸上推一遍的,再遇到类似题都能秒写。
cnt计数器每轮加1,表示完成了一轮分配。因为div操作后所有人的糖数都会变化,所以理论上一定有收敛,但数学上存在某些初始状态可能导致循环非常长,加一个防御性判断是好的编程习惯,考试时虽然用不到,但能防止自己调试时不小心写错逻辑导致程序卡死。
3.3 用样例验证代码
光说理论不行,我们拿一个具体样例来手跑一遍。
假设n=3,初始糖果数为[2, 5, 8]。
初始状态:2, 5, 8,不全相等,进入循环。
第1轮:
- 第0个人:2是偶数,不补,b[0] = 1。
- 第1个人:5是奇数,补1变成6,b[1] = 3。
- 第2个人:8是偶数,不补,b[2] = 4。
更新: - a[0] = b[0] + b[2] = 1 + 4 = 5。
- a[1] = b[1] + b[0] = 3 + 1 = 4。
- a[2] = b[2] + b[1] = 4 + 3 = 7。
状态变为 [5, 4, 7],cnt=1。
第2轮:
- 第0个人:5奇数补1变成6,b[0]=3。
- 第1个人:4偶数不补,b[1]=2。
- 第2个人:7奇数补1变成8,b[2]=4。
更新: - a[0] = 3 + 4 = 7。
- a[1] = 2 + 3 = 5。
- a[2] = 4 + 2 = 6。
状态变为 [7, 5, 6],cnt=2。
第3轮:
- 第0个人:7奇数补1变成8,b[0]=4。
- 第1个人:5奇数补1变成6,b[1]=3。
- 第2个人:6偶数不补,b[2]=3。
更新: - a[0] = 4 + 3 = 7。
- a[1] = 3 + 4 = 7。
- a[2] = 3 + 3 = 6。
状态变为 [7, 7, 6],cnt=3。
第4轮:
- 第0个人:7奇补1变8,b[0]=4。
- 第1个人:7奇补1变8,b[1]=4。
- 第2个人:6偶不补,b[2]=3。
更新: - a[0] = 4 + 3 = 7。
- a[1] = 4 + 4 = 8。
- a[2] = 3 + 4 = 7。
状态变为 [7, 8, 7],cnt=4。
第5轮:
- 第0个人:7奇补1变8,b[0]=4。
- 第1个人:8偶不补,b[1]=4。
- 第2个人:7奇补1变8,b[2]=4。
更新: - a[0] = 4 + 4 = 8。
- a[1] = 4 + 4 = 8。
- a[2] = 4 + 4 = 8。
全部相等,cnt=5。
所以结果是5轮。这个样例说明了一个有意思的现象:即使初始值相差很大,补糖机制会不断把数值“拉平”,最终收敛到所有人相等。你也可以把这个样例输入到自己的代码里跑一下,看输出是不是5,用来验证自己写的版本是否和我的逻辑一致。
4. 常见问题与调试技巧
4.1 最常见的问题速查表
我汇总一下这些年带学生刷这道题、以及在各平台答疑时遇到的典型错误,直接整理成一张表。
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 结果比预期大 | 把同步更新写成了原地更新,导致后面的人收到被改过的值 | 用临时数组b存“分出的量”,第二遍循环统一更新a |
| 结果比预期小或负数 | 计算下标时用了(i-1)%n,在i=0时得到-1 | 写成(i-1+n)%n |
| 补糖后结果不对 | 先算一半再判断奇数,或者补糖顺序和分配顺序颠倒 | 严格按“先判断奇数并加1,再取一半”的顺序写 |
| 死循环或超时 | 结束条件写错,或者更新逻辑有误导致状态反复横跳 | 用check()函数判断全等,加轮数上限做防御 |
| n=1时卡住 | n=1时没有左右邻居,但算法不会自动特殊处理 | 单独判断,n=1时直接输出0 |
第1条是我见过最多的错误,比例大概能占到所有错误的一半以上。很多人写第一遍for的时候顺手就把a[i]改了,然后第二遍再算a[i]=a[i]+b[(i-1+n)%n],结果每一轮里“右边那个人”收到的实际上是已经被更新过的a[i],完全违背了“同时”的语义。我建议大家在写代码之前,先在草稿纸上用箭头把“谁给谁”画清楚,再动键盘,这个习惯能帮你省下大量调试时间。
4.2 调试模拟题的独家技巧
模拟题调试其实很讲方法。我的习惯是先把n调小,人工手跑3到5轮,把结果写下来,再和程序的输出对比。如果对不上,就在循环里加一句中间状态输出,比如每轮结束后打印cnt和整个a数组。
cpp复制// 调试用,确认无误后删掉
for (int i = 0; i < n; i++) {
printf("%d ", a[i]);
}
printf("\n");
这个方法能帮你很快定位问题出在第几轮。比如你发现第3轮开始数值和手算不一致,那就重点检查第3轮之前的更新逻辑。还有一种技巧是“对拍”:如果题目给了多个样例,你本地跑一遍,再用一个最笨的写法再跑一遍,两个结果对照。考试时间有限,不太有条件做完整对拍,但你至少可以把自己脑补的几个边界样例跑一下。
这里分享一个我常用的边界测试集合:
- n=1,初始任意值,预期输出0;
- 初始所有人相等,比如[4,4,4],预期输出0;
- 初始所有人为0,预期输出0;
- 初始值部分相同,但轮数较多的数据,比如[1,99]这类极端差异。
把这些输入准备好,每次写完模拟题先跑一遍,基本能拦住90%的常见错误。
4.3 考试时的注意事项
GESP三级考试是上机操作,编译环境用的是常见的C++编译器,考试系统会自动编译运行你的代码并和标准输出比对,所以格式非常严格。有几个点我必须提醒。
第一,输出不要有多余的东西。有些人调试时加了一堆printf,最终提交前忘了删,结果一分没有。我的习惯是调试和正式代码分开,正式提交前把调试输出全部清掉。第二,注意用scanf/printf,不要用cin/cout,虽然现在考试环境一般不会因为这点卡时间,但养成习惯总没坏处。第三,数组开大一点,题目说n≤100,你开105就够了,但如果你习惯开1005,也完全没影响,留点余量总比越界好。
另外,考试时如果某道题卡住了,先跳过做后面的,不要死磕。编程题分值一般分布均匀,把能拿到的分拿到才是王道。分糖果这种题目正常来说15分钟内应该搞定,如果半小时还没调出来,大概率是思路哪里偏了,不妨冷静下来重新读一遍题面,很多时候是漏看了“同时”两个字。
5. 备考建议与同类题扩展
5.1 GESP三级模拟题的套路总结
从历年的GESP三级真题来看,模拟类题目的出现频率相当高。比如经典的“小猫分鱼”,分成鱼的思路和分糖果几乎一模一样,都是环形数组加同时分配;“打印数字”本质上是按照规则逐行生成输出,也属于模拟;“数组清零”则是对数组做一系列区间操作。可以说,在三级阶段,模拟是性价比最高的一类算法训练。
做模拟题的通用套路,我总结成四步:
- 阅读题意,画出状态变化图(尤其是环形结构);
- 明确每一步修改的顺序和同步性,找出“谁依赖谁”;
- 确定终止条件和边界情况;
- 用临时数组或备份数组管理同步更新。
这四步看起来简单,但每一步都能筛掉一批人。第2步尤其关键,因为很多模拟题都埋了“同时操作”的坑,不只是分糖果,像“围成一圈报数”“轮流传花”这类题也有类似陷阱。
5.2 相似题目与变形
如果这道分糖果你做完了,我建议你马上做两道变体题来巩固。
第一道是把“分给右边”改成“分给左边”,思路一样,只是下标从(i-1+n)%n变成(i+1)%n。别看只差一个符号,很多人改完就写错,练一次就能把下标取模搞扎实。
第二道是增加“每轮老师额外给某个小朋友指定数量的糖”这样的规则。这时候你需要在循环里加一个条件判断,整体模拟流程不变,但分支变多了,能有效锻炼你对复杂规则的理解能力。
另外,GESP官网上有一道经典的“小猫分鱼”真题,很多资料都把它和分糖果并列为三级模拟题的必练题目,建议找出来做一做。还有一道“春游”题,也是GESP三级真题,它考察的是根据条件筛选和计数,虽然不完全是环形模拟,但同样考逻辑严谨性。
5.3 分享一点我的教学体会
我带过不少准备GESP三级的学生,发现一个非常普遍的现象:很多孩子看到分糖果这种题,第一反应是“这题简单”,然后噼里啪啦写代码,最后调试一两个小时也过不了。问题不在于他们不会写代码,而在于没有把题面里的“同时”“奇数补糖”这些关键词翻译成代码逻辑。
所以我的建议是,考试前两三周,每天找一道模拟题练手,而且每一道都要做到“先手跑样例,再写代码,最后用样例验证”这个完整流程。练到后面你会发现,模拟题的正确率不仅取决于代码能力,更取决于你读题时有多细。把细节吃透了,考场上看到同类题,哪怕规则再绕,你也能有条不紊地拆解。
分糖果这道题本身不难,但它把GESP三级阶段最重要的几个能力点全串起来了:数组、循环、条件判断、取模运算,还有最重要的——按规则同步更新状态。把这题吃透,对你做后面的题绝对有好处。希望这篇文章能帮你在备考路上少踩几个坑,考试顺利。
