尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

陶陶摘苹果P1046:从模拟题建立编程竞赛的边界思维

陶陶摘苹果P1046:从模拟题建立编程竞赛的边界思维 记得我第一次带学生刷信息学竞赛的入门题单很多人的第一反应是找一道看起来最难的题证明自己。我的建议恰恰相反先从 P1046 陶陶摘苹果这种“签到题”开始。这道来自 NOIP 2005 普及组的第一题算法含量几乎为零却能一次性暴露你在读题、建模、输入输出、边界处理四个环节上的坏毛病。尤其是正在为 CSP-J 2026 备赛的同学把这个题吃透比盲目刷十道难题更有价值。下面我会从题面拆解开始把这道经典入门题背后的竞赛思维完整过一遍并附上可以直接复制使用的 C 实现。1. 题面拆解苹果、陶陶和那把30厘米的板凳很多同学 WA 在这么简单的题上不是代码写得不对而是题面没有读完整。我们先把原题的信息点一条一条拎出来。1.1 题目到底讲了什么场景陶陶家的院子里有一棵苹果树每年秋天结10个苹果。她伸手能碰到的高度是题目单独给的不是一个固定写死的值。够不到的时候她会踩上一个30厘米高的板凳再试一次。只要陶陶能碰到苹果苹果就会掉下来就算摘到了。这句话里藏着两个关键信息。第一她能碰到的高度是一个输入变量不是题面里写死的110厘米。第二板凳高度是固定的30厘米但真正决定采摘能力的是“伸手最大高度 30”这个组合值。如果把这两个信息抽象成数学语言每一个苹果的高度记作 a_ii 从1到10陶陶的伸手高度记作 h那么她能够到的最大高度就是upper h 30只要 a_i upper第 i 个苹果就一定能被摘到。这里特别要注意题面说“碰到的苹果就会掉下来”所以等于的情况必须算进去不能写成小于号。很多人在草稿纸上推的时候总是下意识写成 a_i upper觉得至少要超过一点才算摘到但竞赛题目既然写了“碰到就算”我们就得严格按这句话执行。我自己的做题习惯是读题之后不急着写代码先列出题面中所有数字的含义和范围。这张信息表看起来啰嗦却非常管用。它能让你在动手之前就发现类似“加板凳这个动作必须体现在程序里”这样的隐藏条件而不是写到一半才想起来。数据含义取值范围a1 到 a10每个苹果到地面的高度通常为 100~200 的整数h陶陶伸手能碰到的最大高度通常为 100~120 的整数30板凳高度固定常量30upper实际采摘上限等于 h30130~150这张表里最后一行就是很多同学 AC 不了的根本原因。他们认为题目读完了但实际上漏掉了“板凳”这个额外条件。做模拟题最忌讳的就是凭感觉压缩信息觉得自己理解了就跳过。把数字和动作全部登记下来再开始设计逻辑看起来慢实际是最快的。1.2 输入输出格式与样例验证输入有两行。第一行是10个苹果距离地面的高度用空格分隔第二行是陶陶手伸直后能够到的最大高度。输出只需要一个整数陶陶能够摘到的苹果数目。我们看样例输入100 200 150 140 129 134 167 198 200 111 110陶陶伸手高度 h110加上板凳 30得到 upper140。逐一对比前10个数100 小于等于 140可以摘到200 大于 140摘不到150 大于 140摘不到140 等于 140可以摘到129、134、111 都小于 140可以摘到剩下 167、198、200 都摘不到。数一下能摘到的一共是 100、140、129、134、111 这5个所以样例输出是 5。如果你在草稿纸上手算一遍会发现这个样例特意把 140 放在里面就是提醒你等号不能漏掉。如果你算出的是6个那一定是把 150 也算进去了如果算出4个那就是把 140 漏掉了。这两种错误在提交时都会直接变成 WA。很多刚入门的人觉得手算样例浪费时间直接读一遍就敲代码。但竞赛里最常见的翻车就是“我以为我懂了其实没懂”。手动走一遍样例相当于用最朴素的方式验证自己对题面的理解。连样例都核对不上代码写得再漂亮也没有意义。1.3 贴近生活的背景为什么在竞赛里这么重要NOIP 的入门题特别喜欢用生活场景来包装算法摘苹果、买文具、算电费、开灯关灯。这种题目表面上是讲故事实际上是在考察一个问题你能不能从一段自然语言里抽取出清晰的数学模型。陶陶摘苹果如果去掉生活背景本质上就是“给你10个数和一个阈值统计不超过阈值的数有多少个”。但如果你不具备这种提炼能力就会被故事里的板凳、苹果、院子这些词绕晕。以后你还会遇到大量更复杂的场景题比如小鱼的游泳时间、校门外的树、不高兴的津津。它们的内核往往都很简单真正难的是准确识别出“题目要我算什么”。从这个角度看P1046 是你建立这种提炼能力的第一块跳板值得多花一点时间认真对待。2. 把“摘苹果”翻译成程序逻辑不用算法就是最好的算法有些同学总觉得竞赛题必须用什么高深算法看到这题反而不知道怎么写。实际上“模拟”本身就是一种非常重要的解题思想把题目描述的过程用程序原样执行一遍不玩任何花活。2.1 从自然语言到伪代码这道题的核心流程非常短一共五步读入10个苹果高度存到数组里读入陶陶的伸手高度 h把 h 增加 30得到 canReach遍历数组统计有多少个元素小于等于 canReach输出统计结果。写成伪代码就是for i 1 to 10: read apple[i] read h canReach h 30 ans 0 for i 1 to 10: if apple[i] canReach: ans ans 1 print ans这里有一个初学者经常想不明白的点为什么第一步要先把10个苹果高度存下来不能边读边判断因为输入顺序是“先10个高度再 h”。如果你在第一次循环里直接比较 apple[i] 和 canReach此时 h 还没读进来canReach 根本不存在。这种“先出现的数据要留到后面才用”的情况正是数组存在的意义。也可以换个角度理解你手里拿着一个篮子去摘苹果但你得先知道自己的手能伸多高。题目偏要把苹果的高度先告诉你把手的高度放最后。那么你就得先把看到的苹果高度记在纸上等知道了手的高度再回头对照纸上的记录逐个判断。数组就是那张纸。2.2 为什么不需要排序、二分、前缀和有初学者会问要不要先对苹果高度排序然后用二分查找快速统计有多少个小于 upper 的苹果从算法设计上看排序加二分的复杂度是 O(nlogn)在这个只有10个元素的题里和直接遍历的 O(n) 几乎没有区别。但代码复杂度和出错概率会明显上升。排序之后如果下标处理不仔细反而容易把答案算错。竞赛中的复杂度选择永远要跟着数据范围走。当 n 是 10、100、1000 这种级别时直接枚举往往是最稳的方案。P1046 的数据范围决定了“纯模拟”就是最优解不存在更聪明的算法路线。等以后遇到 n 等于 10^5、10^6 的题目再考虑排序、二分、双指针这些优化手段也不迟。这就是我一直强调的算法不是越高级越好而是在满足题目限制的前提下越简单越可靠越好。很多同学在入门阶段就养成“什么题都想套一个高级算法”的习惯反而把简单的题做复杂还容易出错。学会判断一道题需不需要优化本身就是一个重要的竞赛技能。2.3 复杂度与边界条件的直觉养成时间复杂度是 O(10)如果推广成 n 个苹果就是 O(n)。空间复杂度要看实现方式用数组存10个数是 O(n)但因为数组大小固定为10也可以把它视为 O(1)。这两种说法在竞赛分析中都常见关键是你得知道它在说什么而不是背结论。既然题目只给10个苹果为什么不让循环变量 i 从1到10、数组下标也从1开始呢完全可以但要注意数组长度至少开 11。很多人写 C 时数组声明成 int a[10]循环却写 for (int i1; i10; i)访问 a[10] 时已经越界。这种越界访问不会立刻报错可能读到脏数据也可能刚好不影响结果所以特别难排查。我习惯的统一策略是一切从0开始循环写成 i 10。这个习惯看起来只是风格问题实际上能帮你规避一大类下标错误。以后处理字符串、数组、动态规划时统一的下标习惯会节省大量调试时间。你要是现在就把这个习惯固定下来后面会感谢它。3. 完整代码与逐行注释从 cin 到 scanf 都给你写清楚代码部分我给出两种风格一种是新手最容易理解的数组加 for 循环另一种是竞赛中更稳的 scanf/printf 版本。两段代码的核心逻辑完全一致你可以根据自己目前的输入输出习惯选择。选择本身不重要重要的是把每一步都看懂。3.1 写法一cin/cout 普通数组#include bits/stdc.h using namespace std; int main() { int apple[10]; // 第一步读入10个苹果高度 for (int i 0; i 10; i) { cin apple[i]; } // 第二步读入陶陶伸手高度并加上板凳高度 int h; cin h; h 30; // 第三步遍历统计 int ans 0; for (int i 0; i 10; i) { if (apple[i] h) { ans; } } cout ans endl; return 0; }这段代码里最关键的一行是h 30;。它直接改变了后续所有比较的基准值。如果把它注释掉样例答案会从5变成1。我建议在比赛时把这一行写成int reach h 30;后面统一用 reach 比较保留 h 的原始值。这样万一你后面还想输出或使用原始 h不至于被改动的值干扰。另外注意#include bits/stdc.h这个头文件很多在线评测系统都支持用起来非常方便。但有些本地编译器或比赛环境可能不识别它。如果你遇到编译错误可以改成包含iostream等具体头文件。我平时教学会直接让学生用万能头因为能少记一堆头文件名把注意力集中在算法上。3.2 写法二scanf/printf 版本#include cstdio int main() { int a[10]; for (int i 0; i 10; i) { scanf(%d, a[i]); } int h; scanf(%d, h); h 30; int cnt 0; for (int i 0; i 10; i) { if (a[i] h) { cnt; } } printf(%d\n, cnt); return 0; }很多 OI 选手习惯用 scanf/printf因为它比 cin/cout 快而且在格式化输出时更好控制。不过对于本题这种只有10个数的规模cin/cout 完全不会超时选哪种全看个人习惯。初学者不要在这个选择上纠结重要的是把数据结构和判断逻辑写对。等你真正参加比赛发现输入输出影响性能时再切换也来得及。3.3 一个经常被问到的点能不能不存数组有人会想既然苹果只有10个能不能在读入时直接用一个变量顶过去如果 h 在苹果高度之前读入当然可以边读边判断。但本题输入顺序是“先10个高度再 h”所以你必须把苹果高度先记住这是数组存在的意义。如果你实在不想开普通数组也可以用 vector apple(10); 然后同样下标访问。vector 的好处是动态大小以后遇到“n 由输入决定”的题目时可以直接适用坏处是初学者要理解“容器”的概念。对于这道题普通数组完全够用。不过我建议你借此机会把 vector 的基本用法看了因为后面很多题都会用到它。也有很多同学为了“省空间”定义10个独立变量 apple1 到 apple10逐个读入、最后统一判断。在10个数的场景下这样确实可行可读性和扩展性却非常差。一旦题目把10改成1000整个思路就得推翻。老老实实用数组或 vector是这道题教给你的第一课。4. 提交上去总是判错一份可以复现的排查思路P1046 的讨论区里永远不缺“样例过了但 WA”的帖子。下面这些问题我几乎每年都能在学生代码里看到。如果你也 WA 了不妨按这个顺序排查一遍。4.1 最隐蔽的失分点把30厘米忘在一边有相当一部分同学读题后把“手伸直后能够到的最大高度”误当成最终采摘高度代码直接写成if (apple[i] h)。用样例验证时他们算出答案是1却以为这是正确答案结果自然 WA。这个错误的本质不是不会加数字而是没有把“板凳”这个额外条件纳入模型。要避免它最有效的办法就是在本文第1章那张信息表里明确写出“采摘上限 h 30”。你一旦在纸上写过这个公式写代码时就不容易忘记。读题时把每一个数和它的作用写出来特别是那些名字里带“最大”“额外”“固定”的修饰词这些词往往是题目的隐藏考点。4.2 下标从1开始导致的数组越界如果坚持让苹果编号从1到10代码可以写成for (int i 1; i 10; i) { cin a[i]; }但这时数组至少要声明为int a[11]而不是int a[10]。因为 C 数组下标从0开始a[10] 已经越界了。越界访问不会立刻报错它可能读到脏数据也可能刚好不影响结果这种“随机性”是排查起来最头疼的。你可能本地跑一次对提交一次错反复几次都找不到原因最后发现是数组开小了。统一从0开始、循环写成 i 10是我强烈推荐的做法。它能帮你彻底避开这类问题而且当你以后面对二维数组、字符串处理时这个习惯能极大降低心智负担。下标风格看起来是小事实际影响的是你整个竞赛生涯的调试效率。4.3 判断条件写反或漏掉等号计数时写a[i] h属于方向性错误说明你统计的是“摘不到的苹果”。这种错误通常是因为写代码时心里想的是“从高到低判断”手却写反了。还有一种常见情况是写a[i] h把等于上限的苹果漏掉。样例中的 140 正好等于上限如果你用小于号答案会从5变成4。要精准找到这类错误最有效的办法是构造边界测试数据。我每次讲题都会让学生做下面这个表格里的四组测试输入特征预期输出测试目的10个苹果高度全部为2000验证没有苹果能摘到10个苹果高度全部为10010验证全部能摘到苹果高度恰好等于 h30至少1验证等号是否被包含h 取到范围的最小值与手算一致验证 h 是否正确加上30把这四条自己跑一遍比在提交页面反复猜测有效得多。举个例子当输入为100 120 130 140 150 160 170 180 190 200 110手算结果上限 140能摘到 100、120、130、140共4个。如果条件写成 h答案就是3问题立刻暴露。这种练习做多了你自然会形成构造边界数据来验证代码的意识。4.4 输入输出细节带来的玄学错误使用 scanf 时有人会在格式字符串里写%d 多带一个空格。在标准输入下通常没问题但某些评测环境下可能遇到预料之外的行为。最稳妥的写法就是%d配合变量不要画蛇添足。输出端常见的错误是printf(%d, ans)忘记换行。多数评测机不会因为少换行判错但养成%d\n的习惯能让你的输出更规范未来处理多组输出时也不容易出错。还有一个容易被忽略的细节如果你在main函数里定义了int ans;但没有初始化在部分编译器上它的初值是随机值。虽然很多环境下默认是0但这不是语言标准保证的。比赛时不初始化就累加等于把结果交给运气。所有计数器一律写int ans 0;这个习惯要从第一道题开始养成。4.5 判断“到底哪一步错了”的系统排查法如果你在评测端看到 Wrong Answer先不要急着改一版乱试。我的排查顺序是把样例在本地跑一遍确认输出完全一致检查代码中所有涉及 h、30、10 的常量是不是都来自题面检查循环范围是否越界用上面表格里的边界数据做一轮测试如果还不对把读入后的 h 和数组元素打印出来人工复核一遍模型。这套流程看上去简单却能在绝大多数入门题上帮你定位问题。很多同学失败是因为跳过第2、4步直接进入“盲改”状态。改一次提交一次运气好碰对了运气不好就越改越乱。竞赛比的不是谁提交次数多而是谁能一次性把问题想清楚。5. 从一道入门模拟题看竞赛刷题的正确姿势P1046 来自 NOIP 2005 普及组当年是第一题。在比赛里它的定位是让大部分选手稳定拿分所以难度刻意压得很低。这种“签到题”对于今天的备赛者来说价值并不在于算法而在于建立竞赛思维里的第一块基石。5.1 为什么模拟题值得反复咀嚼所谓“模拟题”就是题面描述什么代码就做什么。陶陶摘苹果的过程拆开来看是“读入-存储-计算-计数-输出”这个五步曲。以后你会遇到大量模拟题比如按规则走路、按规则转格子、按规则处理字符串底层全是这个五步曲的变形。如果看不懂一道模拟题通常不是代码能力问题而是题面信息提取能力不够。我经常说把题面当成一份说明书凡是出现数字、动作、条件的地方都要在草稿纸上登记。P1046 的训练点恰恰在这里。它让你明白生活化的文字只是外衣里面包的始终是输入、判断、输出这几个基本步。掌握这层抽象能力你在竞赛里会觉得很省力。5.2 给 CSP-J 2026 备赛选手的刷题建议现在距离新一轮 CSP-J 还有时间如果你打算系统准备我的建议是把近十年普及组真题的 T1 全部按顺序刷一遍。每道题不求快但求把四个问题写在自己的题解笔记里题目在做什么我用了什么数据结构判断条件是什么边界在哪里。刷完这些 T1 后你会对“简单题”形成一种肌肉记忆看到类似题目快速建模稳定一次 AC。这不是天赋而是反复训练的结果。尤其是现在很多在线评测系统都有题目题单你可以按“入门-顺序结构-分支结构-循环结构”的方式推进。P1046 实际上就处在这个路径的最前面的位置。我建议你用一页纸记录每天刷题时犯的错误不用多三五条就行。一周后回看你会发现自己的错误高度集中比如“忘了初始化”“下标越界”“判断条件漏等号”。知道自己的薄弱点在哪里之后的训练就有针对性了。5.3 值得接力的几个同级别题目学完 P1046 后想趁热打铁可以按下面这个顺序继续练P1421 小玉买文具整除、取余和价格换算的组合考验单位统一P1422 小玉家的电费分段计费考察分支结构和浮点数处理P1047 校门外的树区间标记问题是模拟题中稍复杂的一种P3954 成绩读入三个分数按权重求和并输出整数。这些题难度都停留在普及组第一题的附近适合用来检验你是否真正理解了“把题面翻译成代码”这一核心能力。如果每道题都能独立完成再回头写一遍 P1046你一定会发现自己看问题的角度已经不一样了。我教这道题时还有个习惯让学生把样例数据改成一版“全摘不到”和“全摘得到”的极端数据再跑一遍代码。这种测试意识会在以后每一场比赛中帮到你比背十篇题解都管用。陶陶摘苹果虽然简单却像一面镜子照出的恰恰是你写竞赛代码时最底层的习惯。把它理顺了后面的路会顺畅很多。
返回列表