备战GESP三级,手上正好刷到题库里这道“4126:分糖果”。老实说,我第一次看到题目名字时没太当回事,觉得“分糖果”这种生活化场景在小学奥数里都见多了,三级能考出什么花来?等真正把题读完、又在草稿纸上画了好几轮分配过程,才发现它没那么简单。这题不是让你真的拿for循环一颗一颗去发糖,而是考察你能不能把一个“循环过程”抽象成数学表达式。很多学生在考场上用了最直接的模拟写法,样例能过,但数据一大就超时,最后只能对着时间限制干瞪眼。
这道题来自GESP 2025年6月的三级场次,也就是大家常说的“2506”场。对准备9月场次的同学来说,这题几乎是最好的风向标:它不考偏门语法,不考复杂数据结构,只考两件事——你读题的时候能不能抓住“围成一圈”这个循环条件,以及你能不能写出不依赖循环的取模写法。下面我把这题的完整思路、推导过程、代码实现和考场上的各种坑一个一个拆开说,最后再顺带聊聊怎么把这道题的思维迁移到同场次的“小猫分鱼”和类似的模拟题上。
1. 先说结论:这道“分糖果”究竟考什么
1.1 真题长什么样
网上关于这道题的回忆版有好几个,表述上略有出入,但去掉生活化包装后,核心模型基本一致:有n个小朋友围成一圈,按顺时针方向编号为1到n,老师手里有m颗糖,从1号小朋友开始,按照编号顺序轮流发糖,每次给当前这位小朋友一颗,问第m颗糖最后被谁拿到。
这个就是题目的骨架。有的版本会说成“老师绕着圈发糖,发完一圈接着下一圈”,也有版本把它包装成“每个小朋友每次拿一颗,拿完一轮继续下一轮”,不管外面套了什么故事,你只要在纸上画一个圈、标上编号,就会发现它其实就是一个“循环访问数组下标”的问题。
为什么说这题适合拿来备考?因为它在GESP三级大纲里正好卡在一个非常关键的位置:你已经学过for循环、while循环,但还没系统接触算法复杂度优化。命题人不会直接考你“时间复杂度”这个概念,而是通过数据范围让你自己体会到“循环次数过多会挂”,这种考察方式比直接背概念更能筛出真正理解程序执行效率的学生。
1.2 题目包装下的三个核心考点
第一,循环下标的处理。围成一圈意味着1号后面跟着2号,n号后面又回到1号,下标不是简单递增,而是需要“绕回”。这是编程里面非常经典的取模场景,也是这题最大的考点。
第二,数据规模的敏感度。m可能是很大的数,如果你老老实实模拟发m颗糖,时间复杂度是O(m)。当m达到10^18级别,任何编译器都救不了你,程序会在循环里跑到天荒地老。把自己从“模拟每一步”的惯性思维中拉出来,是三级考试要考察的分水岭。
第三,边界条件的严谨性。题目是从1号开始发,第1颗给1号,那第m颗给谁,公式写出来以后还要不要考虑整除的情况?如果你用的是0基编号还是1基编号,最终输出要不要加1?这些细节不处理好,公式就算背下来也照样丢分。
1.3 谁适合看这篇解析
如果你正准备GESP三级,或者刚学完循环和数组,想找一些“有点思维量但又不超纲”的模拟题练手,这篇解析很合适。我会从最笨的暴力模拟开始推,再一步步化简到公式,这样就算你之前没接触过取模运算,也能跟着理解。已经有基础的同学可以直接跳到第三部分的代码实现和第四部分的坑点总结。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从暴力模拟到数学建模的完整推导
2.1 先别急着优化,写一版暴力代码
我给学生讲这类题时,一直坚持一个原则:如果题目场景比较复杂,先别空想,老老实实按照题意把一个最简单的过程写出来。这不丢人,反而能帮你验证自己理解题意没有跑偏。
模拟的思路很直接:维护一个变量cur表示当前轮到几号小朋友,初始值为1。然后开始发糖,每发一颗糖,cur就往后移动一位,如果cur已经到n了,下一颗糖回到1号。
cpp复制#include <bits/stdc++.h>
using namespace std;
int main() {
long long n, m;
cin >> n >> m;
long long cur = 1; // 当前小朋友编号,从1号开始
for (long long i = 1; i <= m; i++) {
if (i == m) {
cout << cur << endl; // 第m颗糖给cur号
break;
}
cur++; // 下一位小朋友
if (cur > n) {
cur = 1; // 绕回1号
}
}
return 0;
}
这段代码从功能上说完全正确,甚至样例测试也能通过。但问题在于:如果m等于10^18,for循环要执行10^18次,在绝大多数OJ环境里都是几秒甚至几十秒的耗时,直接超时。
很多第一次接触这类题的同学会困惑:明明功能没错,为什么不能算对?因为在竞赛和等级考试中,“能算出结果”只是及格线,“在限定时间内算出结果”才是真正的判分标准。这就像让你从1数到1万亿,你确实能数完,但考官等不了那么久。
2.2 列一张表,规律自己会跳出来
这时候最好的办法不是继续调代码,而是拿一组小数据在纸上推演。我建议自己动手列一下n=5时,第1颗到第12颗糖分别给谁。
| 糖果序号 | 拿到的小朋友编号 |
|---|---|
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
| 5 | 5 |
| 6 | 1 |
| 7 | 2 |
| 8 | 3 |
| 9 | 4 |
| 10 | 5 |
| 11 | 1 |
| 12 | 2 |
看到这个表,规律其实已经很明显了。糖果序号在“每一轮”内部是递增编号,每隔n颗就回到1号重新开始。这和时钟一样:钟面上只有12个数字,走了13个小时后时针又回到1的位置。所以问题本质上不是“发了多少颗”,而是“走了多少圈之后还剩几步”。
代码里那个“cur++,如果大于n就回到1”的操作,其实就是对n取余。取余本身就是在解决“绕圈之后落在哪个位置”的问题。
2.3 余数公式是怎么来的
现在来推导标准解法。我们仍然假设n个小朋友编号是1到n,糖果从第1颗开始发。
如果m恰好是n的倍数,比如n=5,m=10,第10颗糖给谁?看表,第5颗、第10颗都给5号,也就是编号n。这个结果用m % n算出来是0,但5号不是0号,所以直接输出m % n会出问题。
再看不是倍数的情况。n=5,m=12,第12颗给2号。如果直接用12 % 5,结果是2,恰好和编号一致。
看起来“能整除”和“不能整除”情况不一样。我们希望找到一个公式,无论是整除还是不整除都能统一处理。关键技巧是把编号整体平移一个单位,先按照0到n-1来编号,算完后再加1。
假设有一个“0号位置”对应真实世界的1号。也就是说,内部编号用0到n-1表示,第1颗糖发给内部编号0的人,第2颗糖发给内部编号1的人,依此类推。第m颗糖的内部编号就是:
内部编号 = (m - 1) % n
因为第1颗对应0,所以要减1。算完后加上1,就能还原成真实编号:
真实编号 = (m - 1) % n + 1
验证一下之前的数据:n=5,m=10,真实编号 = 9 % 5 + 1 = 4 + 1 = 5,正确。n=5,m=12,真实编号 = 11 % 5 + 1 = 1 + 1 = 2,也正确。
这个公式看起来简单,但它背后包含了一个非常重要的思维转换:与其一步步追踪“下一颗糖给谁”,不如直接问“第m颗糖相对于第1颗糖走了多少步”。走了m-1步,每走n步绕一圈,去掉整圈剩下的余数就是落点。任何循环过程,只要知道了周期长度n,都可以用取模来定位。
3. 标准C++解法与关键细节剖析
3.1 核心代码实现
推导完毕,标准解法其实只有短短几行。这也是编程有意思的地方:分析过程占了80%的时间,真正写代码可能不到10秒。
cpp复制#include <bits/stdc++.h>
using namespace std;
int main() {
long long n, m;
cin >> n >> m;
cout << (m - 1) % n + 1 << endl;
return 0;
}
这里面有几个我必须强调的细节。
第一,n和m都要声明成long long。有些同学习惯用int,如果题目中的m范围超过2^31 - 1,int类型存都不一定存得下,更别提参与运算。用int写这类题,等于是还没出发就把鞋子穿错了。
第二,括号一定要写全。(m - 1) % n + 1和m - 1 % n + 1完全是两个东西。C++里取模运算符%的优先级和乘除法一样,高于加减法,所以不加括号的话,m - 1 % n + 1会先算1 % n,再拿m去减,结果完全错乱。我见过不止一个学生栽在这个优先级上,代码看起来差不多,输出结果天差地别。
第三,这个解法在m很小甚至m=0时也成立。虽然题目给定m为正整数,但你用m=0去测试,公式也能给出0号,不会数组越界。这一点保证了你可以在边界情况放心使用。
3.2 如果编号从0开始,公式有什么区别
有些题目在描述时不会使用1到n的编号,而是直接用0到n-1,比如孩子们编号是0到n-1。这时候不需要加1,直接输出(m - 1) % n即可。
注意,这时如果不用减1,会出现什么问题?m=1时,(1 - 1) % n = 0,正确;m=5,n=5时,(5 - 1) % 5 = 4,也就是内部编号4,对应真实世界的5号,和上一节的结论一致。你会发现,不管编号从0开始还是从1开始,思路都围绕“第一颗对应的位置”展开,只要抓住这个锚点,就不会乱。
3.3 IO优化,不是玄学而是习惯
GESP三级不会故意卡IO,但使用cin和cout时,如果数据量不大,其实不需要特殊处理。然而我建议从小养成一个习惯:
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
这两行写在main函数开头,可以解除C++标准输入输出流与C标准IO的同步,让cin、cout更快。很多考生平时在自己电脑上测试没问题,一上OJ就偶尔超时,除了算法问题,IO缓冲也可能拖后腿。当然,这题核心是取模公式,时间复杂度已经是O(1),IO影响不大,但顺手写上也不会错。
4. 考场上最容易踩的四个坑
4.1 把“围成一圈”理解成“排队不断往返”
围成一圈和排成一队最大的区别是方向。排队发糖,发完n号就结束了,队伍不会自己回到1号。而围成一圈,n号的下一位是1号,形成的是环形逻辑。有些同学模拟时确实写了循环,但循环里用了“到n号之后再倒着走回来”的逻辑——那是“往返”而不是“绕圈”。这题里没有倒着走,永远是顺时针前进。
要判断自己是否理解正确,直接拿n=3,m=4这个简单样例检验:如果理解成排队往返,第4颗糖会发给2号(1->2->3->2);如果正确理解成绕圈,第4颗糖会发给1号(1->2->3->1)。你自己推导一下就会发现差别有多大。
4.2 直接用m % n,然后大谈整除情况
直接输出m % n是很多第一次接触环形问题的同学最自然的反应。当m不能整除n时,它给出的结果碰巧是对的;一旦m能整除n,m % n等于0,而真实编号应该是n。这个0会让程序输出一个不存在的“0号小朋友”。
网上有些题解直接推荐“1-index循环取模公式”,其实底层仍然避不开整除问题。遇到任何取模场景,我都建议先问自己一句:“我的编号体系里,0号代表什么?”如果0号有实际意义,可以直接用m % n;如果编号从1开始且没有0号,先用(m - 1) % n再+1最稳妥。
4.3 没看清问的是“剩余糖数”还是“最后一颗给谁”
我在考试监考和阅卷时发现,一个让人啼笑皆非的错误是:题目问的是“分完n个小朋友后老师手里还剩几颗糖”,有些学生答成了“最后拿到糖的小朋友编号”。两种问题的答案恰好是m % n和(m - 1) % n + 1的差别。
不要以为这种错误只发生在基础不好的学生身上。很多学生看完题目描述里写着“分糖果”三个字,脑子里就开始循环播放自己练过的同类题,题目后半段真正问什么反而没看仔细。所以我反复强调:把题目读完,尤其要把最后一行问题圈出来再动手。
4.4 小样例能过,大样例就超时的模拟惯性
这种题的隐藏陷阱就在数据范围。如果你不看数据范围,直接提交O(m)的模拟代码,小数据测试全对,但大数据会超时。GESP这种等级考试特别爱考“同一种思维在数据变大后是否还能成立”,所以每个题目的数据范围都不是随便写的。
建议考试时形成一套固定动作:读完题后先看数据范围,m是10^9以上,基本上在提醒你别用逐颗模拟;n是10^9、m也是10^9,更要走公式。数值范围本身就是题目给你的一级提示。
5. 横向扩展:一套思路解决同类“分糖果”题
5.1 逆问题:知道拿到糖的小朋友的编号,反推这是第几颗
掌握了正推公式后,可以尝试做一点变形训练。比如n=5,某颗糖发给了3号小朋友,问它是第几颗糖?
这个问题的答案理论上不唯一:第3颗、第8颗、第13颗都会给3号。所以逆问题时通常会补充条件,比如“这是第几轮发的糖”,或者“这是老师手上最后一颗糖,并且总共发了m颗”。结合轮数,你就能用乘法加余数把位置算回来。
我在训练学生时喜欢让他们做这种“正向变逆向”的变换,因为它能帮助你真正理解公式的结构,而不是死记硬背输出表达式。
5.2 进阶一点:发糖时跳过若干人
如果题目变成“1号开始,每次发给当前小朋友后,隔k-1个人再发下一个人”,这就从纯取模变成了约瑟夫环的雏形。在三级考试中不一定会直接考到,但作为思维扩展非常棒。
这类问题的核心仍然是确定“下一个人的位置”。如果当前人的位置是pos(用0到n-1表示),发完给下一个人,跳k步后的位置是:
下一个位置 = (pos + k) % n
如果从1到n编号,则是:
下一个真实编号 = (pos + k - 1) % n + 1
你会发现,所有环形模拟题到最后都是这几个公式在起作用。能熟练推导这题,以后遇到骑士巡游、约瑟夫环、猴子选大王等经典问题时也会轻松不少。
5.3 同场次考生经常一起复习的《小猫分鱼》
搜GESP三级经验帖时,很多人会把《分糖果》和《小猫分鱼》放在一起说。虽然名字都带“分东西”,但《小猫分鱼》的核心不是“循环定位”,而是“逆向递推”:已知最后一只猫分完后的剩余情况,要反推最开始有多少条鱼。
这类题的正确姿势是先设未知数,从后往前逐步还原,每一步用整除和余数判断可行性。它在编程实现上可能用到循环枚举初始值,也可能用到递归或递推。和《分糖果》共同的地方在于,你必须把题目转化成数学表达式,而不是真的用代码模拟一只一只猫分鱼的过程。
如果你手头有真题或者模拟题,建议把这两个题放在一起做一次专题训练:一道正向取模,一道逆向递推,做完后你对“循环过程”的理解至少上一个台阶。
5.4 解决GESP三级“分东西”类型题的通用清单
总结一下,我在带学生刷GESP三级时,遇到所有“分配类”的题目会按下面这个顺序思考。
先确定物品序列和人数的数量级,如果操作次数可能达到10^9甚至更大,立刻提醒自己不能靠模拟。接着判断过程有没有周期性,看是不是“发完一轮接着下一轮”,如果是,找一轮的长度n。再看编号是0基还是1基,0基直接用取模,1基要处理好整除和0号的问题。最后动手前用n=3, m=4这种极小的数据测试一轮,手动推演一遍再写代码。
这套清单可以避开绝大多数低级失误。
5.5 给备考九月场次同学的一点建议
如果你今年9月要考GESP三级,做题时千万别只看题解代码,更别背输出公式。这道题最好的复习方法是找一张A4纸,把n=5时的分配表从头画一遍,然后自己把公式从表格里“推”出来。你亲手推过一次之后,即使考试遇到同样的题换了一层马甲,也能快速认出它的循环本质。
我个人在集训时让学生做这道题,最后一定会追加一个小问题:“如果老师手里的糖数是0,输出什么才对?”这个小问题能立刻暴露一个学生是背了公式还是真懂原理。题目里虽然规定了m是正整数,但从训练角度,把边界情况想清楚,比多做十道同类型题都有用。
