1. 这道“分糖果”到底在考什么
先说结论:GESP三级里出现“分糖果”这道题,乍一看是模拟题,实际上考的是“能不能把规则转化为数学表达式”的功底,而且卡时间、卡边界条件非常狠。
我自己带过几轮备考GESP三级的学员,三级整体难度在“能写循环、能用数组、能处理字符串”这个层面,但2025年6月这套卷子的第三部分编程题,第二题“分糖果”属于典型的“题目短、坑多、想全了很简单,想漏了全盘皆输”的题。
先还原一下这道题在我记忆中的描述:
有n个小朋友围成一圈,老师给每个小朋友若干糖果。现在按要求重新分配:每个小朋友把自己糖果的一半给右边相邻的小朋友,同时接收左边小朋友给的一半。重复若干轮后,如果某个小朋友手里的糖果数是奇数,老师会额外补给他1颗,使得他能继续参与分配。问经过指定轮数后,某个指定位置的小朋友手里有多少颗糖果(或者问补糖总数是多少)。输入包含小朋友数量n、初始糖果数组、轮数k,输出最终状态或补糖总数。
这类题在GESP三级的历年真题里出现过同族变体,比如“小猫分鱼”也是同一套逻辑。核心考的是三件事:数组的循环移位更新、奇数补1的边界判断、多轮模拟的轮次控制。如果扩展到四级难度,还会加上“多次询问”或者“要求取模输出”,但三级不考取模,所以难点集中在“怎么用代码准确地表达分糖规则”。
这道题适合谁来参考?如果你正在备考GESP三级、准备C++二级升三级、或者刚学完数组和循环想找点有区分度的练习题,这篇就是给你写的。如果你已经会做,也可以用这篇的思维方式自查一下,看自己的解法是不是最优的、边界是不是都处理到位了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 三种常见解题思路,我建议你直接选第二种
2.1 先看错误率最高的“想当然”解法
很多第一次做这道题的人,上来就会写这样的伪代码:
code复制for 每一轮:
for 每个小朋友i:
a[i] = a[i] / 2 + a[左边] / 2
这个写法的致命问题在于:你在同一个循环里同时更新了所有小朋友的糖果数,但后面的小朋友用的是已经被更新过的数据,而不是本轮开始时左边的原始数据。举一个最简单的例子:两个小朋友,糖果分别是1和3,按照规则各给一半,所有人都先把自己的一半给出去,再接收别人给的一半。如果用上面的写法,i=0更新完之后,i=1用的左边数据已经不是原始数据了,整轮结果全乱。
这不是粗心,是很多初学者第一次接触“同步更新”和“异步更新”的区别,思维上没转过弯来。处理这种问题的业界标准做法有两个:一是用临时数组保存本轮开始的状态,所有计算都基于临时数组;二是在单数组上从固定方向遍历,通过数学关系保证每个位置读取到的都是旧值。第二种写法在约瑟夫环、差分数组里很常见,但对这道题来说容易绕晕,不建议初学者用。
2.2 最稳妥的思路:临时数组做快照
我推荐的做法是这样的:
cpp复制int a[1005]; // 初始糖果
int b[1005]; // 本轮快照
int n, k;
for (int turn = 1; turn <= k; turn++) {
for (int i = 0; i < n; i++) {
b[i] = a[i]; // 先把本轮所有小朋友的糖果数复制到b
}
for (int i = 0; i < n; i++) {
int left = (i - 1 + n) % n;
int right = (i + 1) % n;
// 每个人给出自己的一半,接收左边给的一半(注意:接收的是本轮开始时的值)
a[i] = b[i] / 2 + b[left] / 2;
}
// 处理奇数补糖
for (int i = 0; i < n; i++) {
if (a[i] % 2 == 1) {
a[i]++;
supply++; // 记录补糖总数,视题目要求而定
}
}
}
这段代码的思路非常简单:先把上一轮的状态完整复制到b里,再统一用b的数据计算新一轮的a。这样既不污染数据,逻辑也清晰。你只需要在写三层循环的时候别把变量名搞混就行。
2.3 进阶思路:一次性合并补糖判断
如果你想要一个更紧凑的版本,可以把“算新值”和“判断补糖”合并到一个循环里:
cpp复制for (int turn = 1; turn <= k; turn++) {
for (int i = 0; i < n; i++) b[i] = a[i];
for (int i = 0; i < n; i++) {
int left = (i - 1 + n) % n;
int total = b[i] / 2 + b[left] / 2;
if (total % 2 == 1) {
total++;
supply++;
}
a[i] = total;
}
}
注意!我在学生作业里见过很多次这种写法引发的bug:如果题目要求“奇数补糖后再参与下一轮分配”,那补糖后的结果必须写回a[i],否则下一轮的b[i]快照就是错的。上面的写法没问题,但如果你在合并循环里先判断了old值就continue,那就错了。优先推荐2.2的分离写法,因为它的每一步意图都非常明确,不容易出错。
3. 为什么临时数组方案是三级考生的最优解
C++三级这个阶段,很多同学刚刚掌握一维数组、循环嵌套、函数的基本用法,还没有接触链表、结构体、vector等高级数据结构。这时候解题的第一原则是“用最简单的工具做对事,再考虑优化”。
临时数组方案有三个不可替代的优点:
第一,逻辑与手算过程完全一致。你拿纸笔手动模拟的时候,是不是也是先把当前每个人的糖果数写下来,然后从第一个小朋友开始算“给出去多少、收到多少、现在剩多少”?临时数组就是把这个手动过程原样翻译成代码,不容易出错。
第二,边界处理集中在一个公式里。围成一圈的“左右邻居”关系,用取模运算(i - 1 + n) % n和(i + 1) % n统一处理,不会出现i=0时左邻居是a[-1]的越界问题。取模运算是三级考纲里明确要求掌握的内容,这题正好用上。
第三,调试非常方便。每一轮结束之后,你可以顺手cout输出一下b数组和a数组的对比,肉眼就能看出是“数据没复制干净”还是“计算式子写错了”。相比之下,如果你硬要在原数组上做原地更新,一旦出错,你根本说不清这个数据到底是第几轮算出来的。
那有没有更好的方案呢?如果你的目标是拿满分,而且你确信自己和“原地更新”的边界能相处融洽,那原地更新确实省一个数组的空间。但GESP三级机考的内存限制通常是256MB,n最大也就1000,多开一个int数组才4KB,完全没必要省这个空间换风险。
4. 实操过程:从读题到AC的完整记录
下面我以一次完整的实操记录,带你把这道题走一遍。假设题目输入格式为:
code复制第一行两个整数n和k,表示小朋友数量和轮数
第二行n个整数,表示每个小朋友初始的糖果数
第三行一个整数q,表示q次询问
接下来q行,每行一个整数pos,表示询问第pos个小朋友最终的糖果数
输出要求:对每个询问输出第pos个小朋友在k轮结束后的糖果数(注意补糖已经包含在内)。
4.1 先写数据定义和输入
cpp复制#include <iostream>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
int a[1005] = {0};
int b[1005] = {0};
for (int i = 0; i < n; i++) {
cin >> a[i];
}
int q;
cin >> q;
// 因为要等k轮结束后才能回答询问,所以先把询问存下来
int query[1005] = {0};
for (int i = 0; i < q; i++) {
cin >> query[i];
}
这里有一个容易被忽视的细节:如果先读q再模拟,那就必须把询问位置都存起来,不能读一个答一个。原因是模拟发生在读入全部数据之后,如果边读边答,那回答的时候k轮还没开始模拟,给的就是初始值,全错。
4.2 模拟每一轮的分糖过程
cpp复制 int supply = 0; // 如果需要统计补糖总数,用这个变量
for (int turn = 1; turn <= k; turn++) {
// 快照
for (int i = 0; i < n; i++) {
b[i] = a[i];
}
// 按规则分糖
for (int i = 0; i < n; i++) {
int left = (i - 1 + n) % n;
int give = b[i] / 2;
int receive = b[left] / 2;
a[i] = give + receive;
}
// 奇数补糖
for (int i = 0; i < n; i++) {
if (a[i] % 2 == 1) {
a[i]++;
supply++;
}
}
}
有一件事需要特别强调:“给出自己的一半”和“接收左边给的一半”这两个操作里,用的都是本轮开始时的快照b。有的同学会写成a[i] = a[i] / 2 + a[left] / 2,当i增大之后,a[i]和a[left]可能已经被本轮修改过了,数据就会串味。这也是为什么快照数组必须放在最外层循环里每轮更新一次,而不是只初始化一次。
4.3 回答询问并输出
cpp复制 for (int i = 0; i < q; i++) {
int pos = query[i];
cout << a[pos] << endl;
}
return 0;
}
如果题目的小朋友编号从1开始,查询时记得pos--。这一步我在考场上也栽过,因为样例输入里经常混着0基和1基两种编号方式,必须仔细看题。
4.4 手动跑一组样例验证
输入:
code复制4 2
1 2 3 4
2
0
2
模拟过程:
初始:a = [1, 2, 3, 4]
第1轮:
- b = [1, 2, 3, 4]
- i=0:左边是3号,receive = b[3]/2 = 4/2 = 2,give = b[0]/2 = 0,a[0] = 2
- i=1:左边是0号,receive = b[0]/2 = 0,give = b[1]/2 = 1,a[1] = 1
- i=2:左边是1号,receive = b[1]/2 = 1,give = b[2]/2 = 1,a[2] = 2
- i=3:左边是2号,receive = b[2]/2 = 1,give = b[3]/2 = 2,a[3] = 3
- 补糖后:a = [2, 1, 2, 3](这里1和3是奇数,各补1颗,变为2和4)
等一下,重新验证:
- 第1轮补糖前 a = [2, 1, 2, 3],奇数有1和3,补糖后a = [2, 2, 2, 4],supply = 2
第2轮:
- b = [2, 2, 2, 4]
- i=0:receive = b[3]/2 = 2,give = b[0]/2 = 1,a[0] = 3
- i=1:receive = b[0]/2 = 1,give = b[1]/2 = 1,a[1] = 2
- i=2:receive = b[1]/2 = 1,give = b[2]/2 = 1,a[2] = 2
- i=3:receive = b[2]/2 = 1,give = b[3]/2 = 2,a[3] = 3
- 补糖前 a = [3, 2, 2, 3],奇数有0号和3号,补糖后a = [4, 2, 2, 4],supply = 2
最终:a = [4, 2, 2, 4],所以询问0号输出4,询问2号输出2。手动验证和代码逻辑一致。
5. 实战中常见的坑,每一个我都见人踩过
5.1 快照数组更新的位置
有些同学会把for (int i = 0; i < n; i++) b[i] = a[i];放在外层循环外面,以为b只需要复制一次。这是错的。每轮开始前,a的状态已经因为上一轮的分糖和补糖发生了变化,b必须每轮都重新同步,否则用的还是最原始的初始状态。
这个错误在逻辑上非常隐蔽,因为样例数据量小的时候,结果差距可能只有1或者2,但k大的时候会越差越远。我的建议是:在写代码的时候就在注释里写明// 第turn轮开始前,b保存本轮初始状态,强制自己确认位置。
5.2 取模的方向与负数
C++中的取模运算符%对负数不是我们数学意义上的“非负余数”。比如-1 % 4在C++里结果是-1,不是3。所以计算左邻居的时候,直接写(i - 1) % n,当i=0时你得到的是-1,数组下标就访问到a[-1],未定义行为,可能不报错但结果错误,也可能直接崩溃。
正确写法是(i - 1 + n) % n,加上n之后确保了非负。我记得GESP的阅卷系统对越界访问不会给明确报错,很多同学在本地Dev-C++上跑样例“看起来对了”,一提交就0分,十有八九就是这个原因。
5.3 补糖是否影响本轮后续计算
题目描述“如果是奇数,老师补1颗”这句话,一定要认真看它有没有说“补完后继续参与本轮分配”。如果它说“补完后进入下一轮”,那本轮后面的小朋友分糖时不能把刚补的糖算进去;如果它说“补完后立即参与本轮后续分配”,那你的快照数组就要在每次补糖后更新b。
从我掌握的真题信息来看,GESP这类题的规则通常是“每轮结束后统一补糖”,即本轮分糖过程中不考虑补糖,所有小朋友都分完后再补,补完进下一轮。这是最常规的设定,也是我上面代码遵循的规则。但考场上仍然要逐字读题,因为CSP-J和GESP都出现过“边分边补”和“分完统一补”两套规则并存的情况。
5.4 数组大小和边界
n的范围如果题目不说,尽量开大一点。三级考试常见数据范围是n <= 1000,k <= 100,开int a[1005]足够。有些同学图省事开int a[105],万一数据给到1000就数组越界了。多开一点空间不丢分,少开一点可能丢掉整个题的分数。
另外,如果你要统计补糖总数,注意supply要用long long。虽然单轮补糖数最多n颗,但k轮累加后可能超过int范围吗?n=1000,k=1000,每轮最多补1000颗,总计就是1000000,int够用。但如果题目特别地把轮数设置得很大(比如k=1e9),supply就一定要用long long。三级不考1e9轮这种极端场景,但养成这个习惯没坏处。
6. 如果题目稍微变一下,你能不能接住
GESP的出题人非常喜欢在经典模型上加“一层皮”,也就是换一个故事背景,但核心算法不变。糖果、鱼、苹果、玩具,本质上都是同一个数组同步更新模型。
6.1 变体一:输出每轮补糖总数
如果题目要求输出每一轮结束后老师补了多少颗糖,那就在我的代码里加一个数组ans[turn]记录第turn轮的补糖数,在第3个循环里累加后赋值:
cpp复制int perRound[1005] = {0};
// 在第3个循环里
if (a[i] % 2 == 1) {
a[i]++;
perRound[turn]++;
}
输出的时候遍历perRound数组即可。
6.2 变体二:反转方向分糖
如果规则改成“给左边接收右边”,只需要把left和right的取模方向对调:
cpp复制int right = (i + 1) % n;
int receive = b[right] / 2;
其他部分完全不用动。这也是为什么我强调用临时数组方案——方向改变只影响一行代码,而不是整个计算逻辑。
6.3 变体三:所有糖果同时翻倍再分
有的题会在每轮开头说“老师先给每个小朋友发x颗糖”,那就在快照之前先执行a[i] += x。顺序很重要:先加糖再快照,还是先快照再加糖,会导致完全不同的结果。必须按题目描述一个字一个字地理清楚。
7. 考场上的做题节奏与检查套路
GESP三级的考试时间一般是90到120分钟,编程题通常有两道到三道。我建议的策略是:第一道简单题15分钟写完,这道“分糖果”控制在30分钟以内,剩下的时间用来检查。
检查的时候按三步走。第一步,读题核对变量名:题目说输入是n m还是n k,输出是补糖总数还是某个小朋友的糖果数,别写反了。第二步,边界测试:手动构造三组数据,n=1(只有1个小朋友围成一圈,自己给自己糖果)、n=2(左右邻居是同一个人的特殊情况?不是,但要注意两个小朋友互相给)、k=0(不做任何操作,直接输出初始值)。第三步,大轮数测试:把k设成很大的数,看程序会不会卡死或者数组越界。
n=1这个边界很多人都想不到。当n=1时,left = (0 - 1 + 1) % 1 = 0,right = (0 + 1) % 1 = 0,也就是说自己既是自己的左邻居也是右邻居。这时候“给出一半”和“接收一半”就是同一个人的同一个值,算出来结果不变。每轮补糖规则也照常。如果题目明确只有一个小朋友,这个边界必须能跑通。
最后一个提醒:GESP的编译环境是标准C++14,不要用getline和整行输入去读数字,避免混用cin和scanf导致同步问题。就用cin,加ios::sync_with_stdio(false);就行,稳定又高效。
8. 从这道题延伸出去,三级到四级之间还差什么
“分糖果”这道题的核心是“模拟 + 数组同步更新”,这正好是GESP三级到四级过渡的一个分水岭。三级考的是能不能用数组和循环准确地描述一个过程,四级则在这个基础上加上了复杂度分析和算法优化。
如果你想在备考路上走得更稳,把这道题做完之后可以继续尝试这几个扩展方向:
- 把“分糖果”改成“n个人排队传纸条”,规则从“左右各一半”改为“每个人只传给右边第m个人”,本质还是数组循环移位,但要求对取模运算更熟练;
- 把“多轮模拟”改为“要求用数学方法直接算出第k轮结果”,这需要找出周期或规律,很多四级模拟题会这么出;
- 把“一维数组”改为“二维网格分糖果”,即每个格子把一半分给上下左右四个邻居,这时候要处理二维坐标的一维化映射,是另一个难点。
我个人实际做题的体会是,这类模拟题最忌讳的就是“想当然”。每次看到“把一半给右边”这样的描述,先问自己三个问题:是同步还是异步?给的是原来的半个还是给完之后的半个?奇数补糖算不算在本轮分配里?这三个问题只要有一个没想清楚,代码写出来就是错的。如果你在做题过程中发现自己的输出和样例差一点点,不要急着改计算逻辑,先从这三个问题入手排查,大概率能精准定位。
