
1. 这道题在考什么从生活场景到数学模型P5709 挂在洛谷入门题单里名字叫「Apples Prologue / 苹果和虫子」标签是「深基2.习6」也就是《深入浅出程序设计竞赛基础篇》第二章的第六道习题。很多人第一次点进去扫两眼题面心里想的是「这不就是除法吗」然后提交然后红字 WA然后开始怀疑人生。我自己当年也是这样前后交了七次才过现在回头看这七次里至少有五次踩的是同一个坑。先把题目本身说清楚八尾勇有 m 个苹果吃完一个苹果需要 t 分钟吃完一个立刻接着吃下一个现在时间已经过去了 s 分钟问她最多还可以完整地吃多少个苹果。输入是一行三个非负整数 m、t、s输出一个整数。题目难度标的是入门但它的坑点密度在整个「深基」题单里都算排得上号的。它适合谁来练我觉得三类人都该老老实实手动写一遍。第一类是刚学完输入输出、变量和整除的新手这道题能把你对「整数除法」的理解直接拉到及格线以上第二类是从其他语言转过来、准备在洛谷上刷题的人这道题的输入输出格式、数据类型选择、边界处理套路基本就是整个入门阶段的缩影第三类是自认为已经会了、想快速跳过的人——恰恰是这类人翻车最多。还有一个新手常见的困惑我得先打消掉题名叫「苹果和虫子」题面里却从头到尾在讲八尾勇吃苹果这个题名其实是翻译拼接留下的历史痕迹Apples Prologue 是另一道同名系列的题跟解法一点关系都没有。刷题时养成一个习惯——只认题面描述和输入输出说明别被题目名字带跑偏。这个习惯在后面的竞赛路上能省你不少时间。2. 把「吃掉」翻译成数学答案公式是怎么推出来的2.1 关键一步正在吃的那个苹果算不算这道题真正的分水岭就在这一个问题上s 分钟过去时如果她正咬着第 k 个苹果、还没啃完那这个苹果算不算「已经吃掉的」答案是算。因为题目问的是「还可以完整地吃多少个苹果」那个被咬了一半的苹果已经不可能再算作完整的了。所以我们要扣掉的不是「吃完的苹果数」而是「已经被占用的苹果数」——吃完的加上正在吃的那一个。举个具体的例子。m 5t 3s 7。3 分钟吃完一个7 分钟过去了她在第 3 分钟末吃完第 1 个第 6 分钟末吃完第 2 个第 7 分钟时正在吃第 3 个。此时被占用的是 3 个苹果剩下的完整苹果是 5 - 3 2 个。如果你偷懒写成 m - s / t算出来是 5 - 7/3 5 - 2 3多算了一个。这就是最经典的错误答案也是很多人第一次提交 WA 的原因。提示这类题有一句通用的判断口诀——「凡是问剩下的先想清楚正在进行的那个算不算消耗」。类似的场景还有「正在下载的文件算不算完成」「正在执行的进程占用多少内存」思路完全一致。2.2 向上取整的语义ceil(s/t) 到底代表什么把上面的推理一般化在 s 分钟里她消耗掉的苹果数量等于「s 分钟能覆盖多少个完整的 t 分钟区间」——但要注意哪怕最后一个区间只覆盖了一部分那个苹果也已经被占用了。这正是向上取整的定义。ceil(s / t) 表示「至少需要多少个 t 分钟的区间才能装下 s 分钟」。s 7、t 3 时ceil(7/3) 3因为 2 个区间只有 6 分钟装不下 7 分钟必须来第 3 个。而这第 3 个区间对应的苹果就是她正在吃的那个。对比一下向下取整也就是 C/Java/Python 里的整数除法 / 和 //floor(7/3) 2代表的是「完整吃完的苹果数」。两者差 1就差在正在咬的那一口上。这个区别用生活语言说就是除法给的是「吃完的」向上取整给的是「动过的」。题目要的是后者。理解到这一层你会发现整道题的计算其实只有两个动作先算「动过的」向上取整再拿总数去减最后兜住地板。2.3 公式的完整形式与样例验算把结论写成一行答案 max(0, m - ceil(s / t))其中 t 0 时答案恒为 0。外层那个 max(0, ...) 不是装饰。当 m 很小、s 很大时比如 m 3、t 10、s 100ceil(100/10) 10m 减去 10 等于 -7但苹果不可能吃出负数答案必须是 0。这一步在题面里没有明说但属于常识性约定也是评测数据必然会覆盖的。下面这张表是我自己手算的六组数据建议你也拿纸笔跟着走一遍比盯着代码看管用得多。mtsceil(s/t)m - ceil(s/t)输出场景说明537322正在吃第 3 个扣掉536233正好吃完第 2 个不多扣530055还没开始吃0310034-340没有苹果地板兜底31010010-70早就吃完了841177咬了一口也算动过第三行和第六行是最容易被忽略的两个角落s 0 时向上取整结果必须是 0因为 0 分钟什么都没动s 小于 t 时向上取整结果必须是 1因为已经在咬了。这两处的正确性完全依赖向上取整的实现方式一旦你用了浮点数就有可能在这里出错。3. 向上取整的四种写法哪种最不容易出事3.1 浮点写法 ceil(1.0 * s / t)直观但埋雷最符合直觉的写法是先转浮点再调库函数long long eaten (long long)ceil(1.0 * s / t);看起来很干净但这里面有两个隐患。第一浮点除法本身有精度问题。当 s 和 t 的数值很大时比如都到了 10^9 量级中间结果可能因为二进制无法精确表示而出现「本该是整数却算成了 2.0000000001」或者反过来「本该是 3 却变成了 2.9999999999」的情况ceil 一取答案就可能差 1。第二多余的浮点运算会让人对代码的确定性失去信心——你在本地跑十组数据都对不代表交上去那一组也对。我在早期刷题时就干过这种事本地测试全过、提交 WA 在两个点上盯着代码看了半小时最后把 ceil 换成整数写法一次就过了。从那以后凡是能纯整数算的我一律不碰浮点。3.2 整数偏移写法 (s t - 1) / t推荐主力整数向上取整的标准技巧就是先加上除数减一再做整除long long eaten (s t - 1) / t;原理很好懂如果 s 恰好能被 t 整除那么 (s t - 1) / t s/t (t-1)/t而 (t-1)/t 在整数除法里是 0结果正好等于 s/t如果 s 不能被 t 整除多出来的余数会把商「顶」上去一格结果就是 s/t 1。两种情况刚好覆盖向上取整的全部语义。不过这个写法有一个必须留意的副作用s t - 1 这一步是溢出的高发区。哪怕最终答案在 int 范围内中间这个加法也可能顶到类型上限。所以在 C 和 Java 里只要用这个写法我建议直接把 m、t、s 全部声明成 long longJava 里是 long别省那点内存。这个习惯看着小题大做但在别的题目上它救过我至少十次。3.3 分支写法与取模写法最稳妥的老派做法如果你对偏移写法的溢出风险还是不放心的可以退回到最朴素的分支版本long long eaten s / t; if (s % t ! 0) eaten;或者写成一行long long eaten s / t (s % t ! 0 ? 1 : 0);这个版本没有任何中间量被放大s 和 t 有多大都不会溢出只要它们自己没超逻辑也最贴近人脑的思考过程「先看完整吃完几个再看有没有正在吃的」。代价是多了几行代码可读性上也稍微啰嗦一点。我自己现在的默认选择是比赛或限时场景用偏移写法少打字平时练习和讲解用分支写法不容易错。两种都对看场合选。3.4 四种写法横向对比与选择建议写法表达式优势风险点我的使用场景浮点 ceilceil(1.0*s/t)语义直观一眼看懂精度不确定大数翻车基本不用整数偏移(s t - 1) / t简洁高效纯整数中间加法可能溢出限时比赛分支累加s/t (s%t!0)无溢出风险最稳代码稍长日常练习、讲解语言内建Python 的 -(-s//t)一行搞定无需特判可读性差偶尔偷懒最后一行那个 Python 技巧可以顺带记一下-(-s // t)是利用 Python 向下取整除法实现向上取整的经典写法效果等价于 ceil(s/t)而且全程整数运算。不过说实话写成这样给三个月后的自己看都要愣一下我个人还是更推荐老老实实写(s t - 1) // t。4. 三语言实现C、Python、Java 逐行拆解4.1 C 版本max(0, ...) 的类型陷阱先给你一版可以直接提交的完整代码#include bits/stdc.h using namespace std; int main() { long long m, t, s; cin m t s; if (t 0) { cout 0 \n; return 0; } long long eaten (s t - 1) / t; long long ans m - eaten; if (ans 0) ans 0; cout ans \n; return 0; }这段代码有几个地方值得单独抠一抠。第一bits/stdc.h 这个万能头文件在洛谷的评测环境里是可用的省得你背头文件但正式比赛时要确认评测机是否支持部分竞赛环境不提供。第二读入用 cin 就够了三个整数而已不用为了那点速度去写 scanf更不用关同步流。第三也是我最想强调的一点别写ans max(0, m - eaten);。std::max 是一个模板函数它要求两个参数类型完全一致。这里m - eaten是 long long而字面量 0 是 int模板推导会直接失败编译报一堆看不懂的错。想用 max 的话必须写成long long ans max(0LL, m - eaten);那个 0LL 的后缀就是在告诉编译器「我这是 long long 的零」。这个坑我第一次遇到的时候报错信息足足有十几行全是模板实例化的细节完全看不出问题在哪。后来学会了凡是涉及混合类型的 max/min一律手动补后缀或者干脆用 if 判断。4.2 Python 版本三行搞定与输入细节Python 的版本短得让人舒服m, t, s map(int, input().split()) if t 0: print(0) else: eaten (s t - 1) // t print(max(0, m - eaten))Python 里没有整数溢出这个概念s t - 1 再大也不怕这是它相对 C 的一个天然优势。//是向下取整除法配合偏移写法就得到了向上取整。max(0, ...)在 Python 里也不存在类型不匹配的问题因为 Python 的整数是动态类型的。但输入这里有一个新手常犯的错写成input().split()之后忘记map(int, ...)结果拿到的是三个字符串后面一算就报类型错误。还有一种写法是list(map(int, input().split()))然后再解包效果一样看你习惯。另外提醒一句如果题目给的是一次多组数据这道题不是但后面会遇到用 input() 循环读会慢那时候要换成 sys.stdin 批量读。这道题数据量小input() 完全够用。4.3 Java 版本类名、long 与快速读入Java 选手在洛谷上最容易踩的第一个坑跟算法无关提交的主类名必须叫 Main文件名也必须是 Main.java否则评测机直接编译失败你会看到一堆「找不到类」的报错然后怀疑是网站出了问题。这个坑每年都要放倒一批人。import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc new Scanner(System.in); long m sc.nextLong(); long t sc.nextLong(); long s sc.nextLong(); if (t 0) { System.out.println(0); return; } long eaten (s t - 1) / t; long ans Math.max(0L, m - eaten); System.out.println(ans); } }注意 Math.max 里的 0L跟 C 里的 0LL 是一个道理不写后缀的话Math.max(int, long) 这种调用在 Java 里会走隐式提升虽然不一定报错但返回值类型可能跟你预期的不一样埋着隐患。养成写全后缀的习惯省心。如果以后遇到输入量大的题目Scanner 会明显拖后腿那时候要换成BufferedReader br new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st new StringTokenizer(br.readLine()); long m Long.parseLong(st.nextToken());这套模板建议现在就背下来早晚用得上。这道题三个数Scanner 足够。4.4 输入输出格式为什么永远只输出一个数字这道题的输出要求是「一个整数」没有多余的字。我见过不少新手在代码里写cout 剩余苹果数 ans;本地看着挺友好交上去直接 WA人还一脸懵。评测机是做字符串比对的你输出的每一个字符都会被算进去多一个空格、多一个换行、多一个中文标点都不行。注意洛谷的评测规则里行末换行通常是允许的绝大多数题目会忽略行尾空白但行内多余空格、提示性文字、单位符号一律会导致 WA。最保险的做法是只输出答案本身加一个换行。C 里用cout ans \n;就够了别用 endl那个会强制刷新缓冲区在数据量大的题目上会拖慢速度。Python 的 print 默认带换行直接用。Java 的 println 同理。这种细节单看无所谓攒到一起就是一个稳定的编码习惯。5. 调试实录WA/RE 的典型面孔与排查路径5.1 五种最常见的错误答案及修法我把这道题在题解区和讨论区里反复出现的翻车姿势整理成了一张表你可以对照自己的提交记录看一眼提交结果典型原因具体表现修法WAt 0 未特判除零或答案错误最前面加if (t 0) 输出 0WA用 m - s/t 少扣一个恰好卡在非整除的数据点上改成向上取整WA负结果没兜底m 小 s 大时输出负数加 max(0, ...) 或 if 判断RE / 运行错误整数除零t 0 时直接崩同第一条特判必须放在除法之前CE / 编译错误max(0, long long) 类型不匹配一堆模板报错写 0LL 或改用 if第三行那个「负结果」特别有意思很多人在本地测的时候都用的 m 比较大的数据压根想不到答案会变成负数直到评测挂了才回头补。我自己的习惯是写完任何一道「剩余量」的题第一组测试数据就用 m 0、s 取一个很大的值专门看地板兜底有没有生效。这个自测动作只需要五秒钟能省下至少一次提交记录上的红字。5.2 自造测试数据与暴力对拍方案想要真正把这类题吃透光靠提交是不够的得学会自己造数据对拍。对拍的意思是写一个虽然慢但绝对正确的暴力程序再写一个你要验证的高效程序用随机数据喂给两个程序比对输出是否一致。这道题的暴力版本特别好写因为它可以直接模拟吃的过程long long simulate(long long m, long long t, long long s) { if (t 0) return 0; long long rest m, used 0; while (used s rest 0) { used t; rest--; } return rest; }这段模拟的逻辑是只要时间还没到 s就继续吃下一个苹果。当 used 加到超过 s 的时候说明最后一个苹果正在吃——注意此时 rest 已经减掉了正好对应「正在吃的也算占用」这个规则。如果 used 恰好等于 s说明最后一个刚好吃完rest 也减掉了同样正确。拿它跟公式版本对比m 5、t 3、s 7模拟过程中 used 依次是 3、6、9rest 依次是 4、3、2循环在 used 9 7 时退出返回 2。跟公式算的 2 完全一致。然后写个随机循环m 取 0 到 20t 取 0 到 10s 取 0 到 50全组合跑一遍只要有一次对不上你的公式就有问题。这套方法我强烈建议所有新手现在就学会它不只是解这一道题的工具而是你后面所有刷题日子的基本装备。5.3 提交环节的页面异常怎么处理偶尔会遇到提交之后页面卡住、提示提交失败或者显示一些看不懂的报错信息。这种情况先别急着怀疑自己的代码按下面的顺序排查一遍先确认本地能正常编译运行代码本身没问题检查网络连接是否正常试着打开一个新标签页看能否正常加载刷新页面重新登录一次有时候是登录状态过期了换一个浏览器或者关掉影响页面脚本的扩展插件再试如果用的是 Java第一时间检查类名是不是 Main稍等几分钟再提交偶发的服务波动刷新一下就好了。提示遇到页面层级的异常时最重要的一条是「先确认代码在本地是好的」。把变量隔离清楚你就能快速判断问题出在自己这边还是环境那边不用在那儿干着急。另外还有一个容易被误判的情况代码里main写成了mian、分号用了中文全角、括号没配对——这些编译期的低级错误表现出的现象有时候也是「提交异常」实际是你自己的锅。提交前花十秒钟扫一遍这几个地方比事后排查快得多。6. 从 P5709 延伸入门阶段该练的四种底层能力6.1 边界意识0 值、极值、单点覆盖这道题真正想教的不是除法而是边界意识。你回头数一数解题过程中涉及的特殊情况有多少个t 0、s 0、m 0、s 小于 t、答案算出来是负数。五个全在一个入门题里。养成一个固定的自检清单拿到任何题目先把变量取到最小值通常是 0和最大值各跑一遍再想想有没有「中间那个特殊点」比如这里 t 0 就是特殊的那个除数。这个动作花不了两分钟但它是区分「能过样例」和「能过全部测试点」的关键。很多新手刷题卡在「样例都对、提交就错」九成原因就是边界没覆盖全。6.2 数据类型与溢出的直觉第二个能力是数据类型的选择。这道题里m、t、s 三个数看起来都不大用 int 好像也行但只要你用了 (s t - 1) 这个偏移写法中间量就有被放大的可能。我一直跟人说「不确定就用 long long」是一条永远不会错的保险策略它浪费的那点空间在绝大多数题目里完全不值一提而它规避的风险却是实实在在的。顺便讲清楚一个概念有符号整数的溢出在 C 里是未定义行为不是说「会自动取模变成负数」那么温柔编译器有可能做出任何事。Java 里则是明确按补码回绕。所以别抱着「就算溢出我也能预测结果」的心态去省类型那是赌运气。6.3 下一步练什么顺序表、模拟、DP 的衔接这道题做完了接下来往哪个方向走如果你在「深基」题单上往下刷紧接着会遇到顺序表相关的题目比如询问学号那一类那里的核心是数组的读写和下标处理正好把这道题练出来的边界意识用上。再往后是模拟题就是那种「按题目描述一步步照着做」的题跟这里的暴力对拍程序思路一脉相承。再往后走长公共子序列这类动态规划题会开始出现——那时候你会发现真正难的不再是「某个操作怎么做」而是「状态怎么定义、转移怎么写」。回头看 P5709它其实已经埋了一个伏笔什么叫「已经被消耗」这个定义方式跟 DP 里定义状态是同一件事都是把模糊的现实问题翻译成精确的数学语言。最后分享一个我自己坚持了很多年的小习惯每刷完一道题在本地留一个文件夹里面放三样东西——通过的代码、暴力对拍程序、还有一张写着「我踩过的坑」的便签。这道题的便签上我写了两行「t0 必须特判」「max 记得写 0LL」。就这两行字在我后来做其他题的时候反复救过我。题是简单的把这些坑变成自己的肌肉记忆才是刷题真正的收获。