1. 这道GESP三级真题,到底在考什么
每年GESP(编程能力等级认证)三级考试结束,讨论最热烈的往往不是压轴大题,反而是某些看似“很简单”的题。这道“分糖果”(一本通编号4126,原题编号【GESP2506三级】)就是典型代表:题目讲的是分糖果,代码写起来也就二三十行,但每年都有不少考生在这上面栽跟头,甚至有的同学考完对照答案都还搞不明白自己错在哪。
先说题目大意:有n盒糖果,每盒糖果数量不同,小A和小B约定,小A可以把自己盒子里多余的糖果分给小B,但有一个限制——每次只能从糖果数量最多的那一盒里取糖,并且取出的糖只能平均分给除这盒以外的其他所有盒子。小A的目标是,在有限次操作之后,让糖果数量最少的那一盒的糖果数尽可能多,问这个最大值是多少。
乍一听很像“小学奥数里的均分问题”,实际动手一算,会发现里面的弯弯绕不少。这道题挂在“编程启蒙”分类下,但它出现在GESP三级卷子里,显然不只是考察“读题”和“写循环”这么简单。
我平时带学生刷信息学奥赛题目,GESP三级的定位是“初步掌握算法设计能力”,重点考察的内容包括:枚举、模拟、简单贪心、基础排序、二分思想等。这道分糖果题,表面上是模拟,真正高效的解法却是“贪心+二分答案”,一步到位考察了好几个核心知识点,算是一道性价比非常高的题目——题面短、思路不难、但对你算法思维的扎实程度要求不低。
这篇文章我就以这道题为线索,完整拆解一下:正确的贪心结论怎么来、代码怎么写才不容易错、哪些边界条件最坑人,以及如果你是准备GESP三级或者刚开始信息学奥赛入门,这道题能给你带来哪些可复用的思维方法。
2. 直观思路为什么会超时:从模拟到贪心的关键转折
第一次看到“分糖果”这道题,绝大多数人的反应是:直接按题意模拟操作过程,每次找出最大值那盒,把多余的糖果均分给其他盒子,重复执行,直到无法继续或者达到目标值。
理论上这确实是“最忠实于题目描述”的做法,但它有两个致命问题。
2.1 模拟操作的“无底洞”在哪里
先看一个具体例子。假设有三盒糖果,数量分别是1、2、100。
按照题目规则,第一次操作时糖最多的是第三盒(100颗)。如果从这盒里取出糖果均分给另外两盒,能取出多少?这里先留个悬念——题目其实对“取出多少”有隐含约束,但哪怕是每次只取一部分出来分,操作次数也会非常惊人。如果n比较大、糖果总数比较大,模拟操作循环的次数可能高达百万、千万甚至更多,三重循环一叠加,稳稳超时。
这是很多初学者容易忽略的一点。信息学奥赛不是“不管你跑多久,只要结果对就行”,而是必须在限定时间(通常是1秒)内跑完所有测试点。三级考试虽然没有四、五级那么严苛,但时间复杂度的概念从这一级开始就必须建立。
2.2 从“模拟过程”转向“验证答案”
既然不能一步步模拟,就要换思路。请注意题目问的是一个“最大值”——让最少的那盒尽可能多。这种“求最大/最小值的极值问题”,在算法竞赛里有一个非常经典的套路:不直接去构造操作过程,而是二分答案,然后验证某个答案是否可行。
用人话说就是:我不去模拟整个分糖果的过程,而是先猜一个最终的“最少盒糖果数”,然后判断:按照规则,能不能通过若干次操作,让每一盒的糖果数都不少于这个数?
如果能,说明这个目标还不够高,可以再往上猜;如果不能,说明目标太高了,得往下调整。这种“猜答案+验证可行性”的思路,正是二分答案的核心思想,也是这道题真正想考察的东西。
从“模拟整个过程”到“二分验证答案”,这个思维转变,就是这道题最关键的分水岭。跨过去了,代码自然水到渠成;跨不过去,就容易陷在模拟的泥潭里出不来。
2.3 为什么这里的单调性成立
二分答案能够使用的前提,是答案具有单调性:如果目标值x可行,那么比x小的目标值一定也可行;如果x不可行,那么比x大的目标值一定也不可行。
对于分糖果这道题,单调性非常直观——目标值设得越低,每盒要达到这个数就越容易;目标值设得越高,操作起来就越难。从“每个盒子至少有0颗糖”到“每个盒子至少有1亿颗糖”,可行性从“一定可以”逐渐过渡到“大概率不行”,中间一定存在一个分界点,这个分界点就是我们要求的最大值。
正是因为有这个单调性,我们才可以用二分法高效逼近答案。如果题目改成分糖果时要满足什么非单调的奇怪条件,那这个解法就废了,这也是为什么不少题目虽然长得像二分,实际却不能用二分的原因——因此做题时一定要先确认单调性再动手。
3. 正确解法推导:从数学规律到代码落地
前面说了大方向,这一节我们把细节彻底讲透。
3.1 关键结论:只有“最大堆的存量”能用来补差
题目里那句“从小A的糖果盒中取出多余的糖果分给小B的盒子”,其实暗示了一个非常重要的规则细节:每次操作取糖的来源,是当前糖果数最多的那一盒,并且取出的糖要均分给其他所有盒子。
很多同学卡在这里:如果每次取完,原来最多的那盒不再是最多了怎么办?如果一次取太多导致某些盒子反而超过了目标值怎么办?全纠结在这些过程细节里,越想越乱。
正确的做法是跳出来看全局。我们不用关心每轮具体是谁在“贡献”糖果,只需要关心:经过若干次操作后,除了最终剩下的那盒“永远最多”的糖果以外,其他所有盒子的糖果数是否都能达到目标值x。
为什么可以这样看?因为每次操作都从当前最多的那盒里取糖,而取出来的糖是均分给其他盒子的。在整个过程中,我们可以把“给其他盒子补糖”这件事统一理解为:从所有盒子中糖果最多的那一盒里,不断抽取糖果补给其他不足的盒子。等到其他盒子都补到了目标值x及以上,剩下的所有糖果自然都堆在最后那一盒里——反正题目求的是“最少的那盒的糖果数”,某一盒特别多完全不影响答案。
于是核心问题就变成了一个非常朴素的数学判断:
- 设所有盒子的糖果总量为sum;
- 想让“其他”n-1盒都至少有x颗糖,这些盒子至少需要的总量是 (n-1) * x;
- 如果剩下那一盒里原本的糖果数足够支撑这个“补给”,就说明x可行。
再细化一层。实际操作中,不是每一盒都需要补,有的盒子原本就比x多,有的比x少。只有比x少的盒子需要从“最大堆”那里获得补给。所以更准确的判定是:
- 计算所有糖果少于x的盒子,距离目标x还差多少,把差量全部加起来,得到一个总缺口need;
- 再看“最大堆”里除了保留x颗之外,有多少“富余”可以拿出来补这个缺口。只要富余量大于等于need,x就可行。
3.2 三步判定法:从“需求”到“供给”的核对
具体落地时,我建议把判定逻辑拆成三步,不容易漏、不容易错。
第一步,找到糖果数最多的那一盒,设它的数量为maxCandy。
这里有个小陷阱:如果通过多次操作,原来最多的那盒被分出去了不少糖,后来另一盒成了最多的,怎么办?其实不用慌,因为我们判断的是“是否存在可行方案”,而每次从当前最多的盒子里取糖,最后所有“多余”的糖会集中到某一个盒子里。你可以想象成:系统内所有盒子只需要保留x颗,其余全部收拢到某个“总仓库”,需要补给时从仓库里拨给不够的盒子。只要有仓库里的存量够用,就等价于存在一种操作顺序能达成目标。这是这类题里最常用也最重要的“整体思维”。
第二步,计算所有盒子的总缺口need。
遍历所有盒子(不包括最终的“仓库”那一盒),对所有糖果数小于x的盒子,累加 (x - 当前糖果数) 得到need。
第三步,计算“仓库”的富余量。
所谓富余量,就是除“仓库”那一盒外,其他所有盒子原本超过x的部分也都可以视作“补给来源”……等等,这里要特别小心一个逻辑。
严格来说,因为每一轮取糖都只能从“当前最多”的那一盒取,所以真正能够长期充当“补给源”的其实是最初那一盒最大堆。但其他盒子如果本身超过x,它并不需要补给,也不会主动贡献出去。所以判定时并不需要把“其他盒子超过x的部分”算进富余量。
正确的逻辑是:如果总糖果量sum中,除“仓库”外的n-1盒要达到x,总共至少需要 (n-1) * x 颗糖。如果sum减去这(n-1)盒原有的全部糖果后,剩余归“仓库”所有的量,依然不少于need,那么x就可行。
用公式表达就是:
maxCandy - x >= need其中need是“这n-1个盒子距目标x的缺口总和”。注意maxCandy是初始最大盒的糖果数,因为在操作过程中,它的糖果会不断减少但始终是“补给源”,直到最后稳定。
3.3 举个例子:直观验证判定逻辑
假设有四盒糖果,分别为:3, 5, 8, 20,n=4,总糖果sum=36。
现在判断目标值x=6是否可行。
先看缺口。当前糖果小于6的盒子:3(差3)、5(差1),总缺口need=3+1=4。
再看最大堆,maxCandy=20。如果x=6,最大堆最多可以贡献20-6=14颗糖给其他盒子。14 >= 4,说明x=6可行——把3那盒补到6需要3颗,5那盒补到6需要1颗,从20那盒里取出4颗均分补过去就够,剩下16颗依然在最重的盒子里,不影响“最少盒”超过6。
再看x=8是否可行。缺口:3(差5)、5(差3)、6?等等,8那盒正好是8,差0,所以need=5+3=8。最大堆能贡献20-8=12颗。12 >= 8,可行。
再看x=12。缺口:3(差9)、5(差7)、8(差4),need=9+7+4=20。最大堆能贡献20-12=8颗。8 < 20,说明x=12不可行。也就是说最终答案应该在8和12之间。
这个判定过程,本质上就是在回答:在保证“仓库”最后也至少有x颗的前提下,从仓库拿出去的糖能不能把其他所有盒子都托到x以上。理解了这一点,代码其实就一行公式的事。
3.4 二分框架怎么搭
判定函数ready(x)写好后,外层套一个二分即可:
- 左边界lo = 0(最少不可能为负数);
- 右边界hi = sum(再多也不可能超过总糖果数除以n,但直接取sum也没问题,反正二分很快);
- 当lo < hi时,取mid = (lo + hi + 1) / 2(注意是向上取整的二分,避免死循环),若ready(mid)为真,说明还能往更高处试,lo = mid;否则hi = mid - 1。
最终lo就是答案。
向上取整的二分很多人第一次写容易卡住。我建议直接记住:凡是求“最大值最小”或“最小值最大”的题目,二分更新时建议写成 lo = mid 的形式,对应的mid取上中位数;凡是求“最小值最大”或“最大值最小”的反向,则 hi = mid,mid取下中位数。这样写不容易死循环。
下面是完整的C++参考代码:
#include <bits/stdc++.h> using namespace std; int n; long long a[100005]; bool check(long long x) { long long need = 0; long long maxCandy = 0; for (int i = 0; i < n; i++) { maxCandy = max(maxCandy, a[i]); if (a[i] < x) { need += x - a[i]; } } return maxCandy - x >= need; } int main() { cin >> n; long long sum = 0; for (int i = 0; i < n; i++) { cin >> a[i]; sum += a[i]; } long long lo = 0, hi = sum; while (lo < hi) { long long mid = (lo + hi + 1) / 2; if (check(mid)) lo = mid; else hi = mid - 1; } cout << lo << endl; return 0; }这里check函数的return语句其实可以再优化——因为need在x很大时会非常大,而maxCandy - x可能是负数,所以返回false非常自然。这段代码里我用long long全程兜底,避免int溢出问题,这是竞赛代码必须养成的习惯。
4. 边界条件与常见错误:这些坑我见过无数人踩
算法思路讲完了,但很多同学“思路懂了,代码还是错”,原因几乎都出在边界条件的处理上。下面这些坑,我带学生集训时反复强调过,这里逐一列出来。
4.1 数据范围与溢出问题
GESP三级真题的数据范围,n通常可以达到10^5级别,每盒糖果数也可能到10^9级别。那么总糖果数sum最大就是10^14级别,远超int的范围。如果代码里用int存sum、need、maxCandy,一旦测试数据给满,直接溢出变成负数,check函数就会出错,最后答案全错。
这是最没技术含量、但最容易丢分的一类错误。解决办法就一条:涉及求和、比较大小的变量,全部声明为long long。别嫌丑,竞赛里这条习惯能救你无数次。
4.2 二分的边界更新
有的同学会把二分写成:
long long mid = (lo + hi) / 2; if (check(mid)) lo = mid; else hi = mid;这样写最大的问题是:当lo和hi相邻(比如lo=2, hi=3)时,mid永远等于2,如果check(2)为真,lo还是2,陷入死循环。这也是二分答案题最经典的死循环陷阱。
正确的写法应该是:
long long mid = (lo + hi + 1) / 2; if (check(mid)) lo = mid; else hi = mid - 1;道理很简单:因为你是“求最大值”,mid可行时你要把下界往上提(lo=mid),所以mid必须取上中位数,否则可能出现“永远试不到更高的那个值”的情况。
4.3 check函数里的“最大堆”到底取哪个
有同学会问:随着操作的进行,原来最大的那盒可能不再是最大的,那check里还拿初始最大值去减x,是不是不严谨?
这里要再解释一遍。我们的check函数判断的并非“模拟某一种操作过程能否成功”,而是“在理想调度下,所有盒子能否同时达到x”。因为每一轮都从当前最大的盒子里取糖,最终所有多余糖果会被聚拢到某一盒,这一盒是谁并不重要,重要的是它“有足够的糖可以拿来补”。初始最大值盒子的糖果总量,减去它最终必须保留的x颗,就是整个系统能拿出去补缺口的“总预算”。只要这个总预算不小于所有盒子的总缺口,就必然存在一种取糖顺序能让所有盒子都达标。
理解这个“总预算”视角,你才能真正理解这道题的贪心本质。如果只背代码不理解原理,换个题目稍微变一变,就不认识了。
4.4 单个盒子糖果数特别多或特别少的极端情况
比如n=1时,只有一盒糖果。按照题目的分配逻辑,没有其他盒子可分,最少的那一盒就是它自己,答案就是这盒的糖果数。但我的代码里n=1时,need=0,maxCandy=a[0],check(x)返回a[0]-x >= 0,即x <= a[0]都可行,二分出的答案正好就是a[0],也没问题。
再比如某个盒子糖果数是0,也没关系,把它当成一个需要补很多糖的盒子即可。
还有所有盒子糖果数相等的情况,比如三盒全是5。目标是5时,need=0,maxCandy-x=0,可行——所以答案是5,符合直觉。
4.5 无解情况的处理
题目保证了一定存在某种意义上的答案(至少每盒可以保留0颗糖,所以答案至少是0),所以不需要额外判无解。但如果你用二分答案时左边界是0,右边界是sum,最终一定会收敛到一个数,所以这里安全。
5. 从一道题到一类题:这套思维能迁移到哪里
刷题不能只盯着题面本身。这道“分糖果”虽然短小,但它背后的方法论能帮你解决不少看起来八竿子打不着的题目。
5.1 “最大值最小/最小值最大”类问题
信息学奥赛里有一大类题,提问方式非常相似:“让某某东西的最大值尽可能小”、“让某某东西的最小值尽可能大”。经典的包括“数列分段”、“跳石头”、“进击的奶牛”等。这些题无一例外,都可以用二分答案+可行性判定来解决。
它们共同的思维特征是:问的是极值,但直接求极值非常困难;如果给你一个候选答案,验证它是否可行却相对容易。于是我们二分这个候选答案,反复验证,逼近最终极值。这种“正难则反”的思维,是算法竞赛里最常用的策略之一。
5.2 贪心思想的嵌入
这道题的check函数本身也是一个贪心——我们并不关心具体怎么分,只是用“总预算>=总缺口”这个贪心条件判断可行性。像这种“不关心过程,只看总量约束”的思路,在很多资源分配类题目中都能看到,比如“木棒切割”、“分披萨”、“装水问题”等。
做题时遇到“每次操作都有明确规则但过程极长”的题,第一反应就应该是:能不能跳过过程,只看结果是否满足某个条件?如果能,大概率就是二分答案的套路。
5.3 对GESP三级的备考启发
从这道题往回看,GESP三级真正想考察的能力其实是三个:
- 阅读并理解题意,能从长题面里抽象出核心数学模型;
- 掌握二分答案框架,知道什么时候能用、怎么写不出bug;
- 代码基本功扎实,包括循环、排序、long long使用、边界条件处理。
如果你正在准备GESP三级,我建议不要眉毛胡子一把抓地刷题,先把“二分答案”这个专题学透。把上面说的“数列分段”“跳石头”全部独立做一遍,再回来看这道分糖果,你会觉得它不过是一只纸老虎。
6. 竞赛环境与备考实操:GESP三级考生最容易忽略的细节
最后聊一点题外话,但我觉得对三级考生特别重要。
GESP考试用的是标准的OI竞赛环境,编程软件通常是Code::Blocks或者Dev-C++,评测系统对代码的输入输出格式要求极其严格。很多同学平时在本地IDE上跑得好好的,一提交就0分,原因往往不是算法错,而是:
- 没有写
#include <bits/stdc++.h>或缺少必要的头文件,本地编译器自动帮你补全了,但评测机不会; - 输入输出没用对,多了空格、少了换行、输出了一堆调试信息;
- 数组开小了,比如题目说最多10^5个盒子,有人只开了10005,越界后结果乱掉;
- 忘记用
long long,在极端数据下溢出。
这些都是我每年带学生参加GESP考试时反复叮嘱的。所以备考阶段,强烈建议你从第一道题开始就用标准竞赛模式写代码:不开-std=c++11之外的花哨特性,不依赖本地编译器的特殊行为,每次提交前自查数组大小和数据类型。
另外说一个很多考生不知道的小技巧:GESP考试使用文本比对判题,输出的行末空格会导致答案错误。如果你的代码里用cout << ans << endl,完全没问题;但如果用cout << ans << " ",就等着吃亏吧。统一用\n或endl输出换行,别画蛇添足。
还有,考试时如果遇到这道题类似的“二分答案”题,实在不确定二分边界就多调试几组小数据。比如手算出n=3、三盒分别是1、2、100的答案,再用代码跑一遍。这种小数据自查,往往能在关键时刻救你一命。
7. 写在最后:这道题教给我的三件事
每年讲这道“分糖果”,我都觉得它是GESP三级里最有教学价值的一题。它没有复杂的算法模板,没有繁琐的代码实现,但它强迫你完成一次思维上的“跳跃”——从模拟过程,到验证答案。
我在实际教学里发现,能独立想出二分答案解法的学生,不是因为他提前背过“二分答案”这个模板,而是因为他养成了“遇到复杂过程先想能否跳到结果”的思维习惯。这种思维习惯,远比会背几个模板算法值钱。
如果你正在准备GESP三级或刚刚开始信息学奥赛之旅,刷完这道题之后,不妨再找几道同类型题目练手。每道题多问自己一句:“如果我不模拟全过程,能不能直接判断某个答案行不行?”想通了这一句,你就已经超越了大多数只会背代码的选手。
最后再分享一个实操小技巧:做题时养成“先写暴力解法,再拿暴力解法验证高效解法”的习惯。分糖果这道题你可以先写一个“每次找最大值模拟操作”的暴力程序,再用上面的二分代码跑同一组随机数据,两边结果一对比,你就能确认自己的贪心判定没有问题。这个“对拍”技巧,是信息学竞赛选手中非常通用且高效的debug方式,建议从入门阶段就练起来。