
1. 题目分析与建模思路1.1 先把物理过程在脑子里过一遍“PTA 实验4-1-11 高空坠球”这道题核心描述并不复杂一个球从某个初始高度落下每次落地后反弹回原高度的一半之后再落下如此反复。题目要求我们求出第N次落地时这个球一共经过了多少路程以及第N次反弹能弹到多高。我在PTA上刷题时第一次看到这个题目第一反应是“这不就是高中物理里的匀加速直线运动嘛套个公式就完事了”。但真正动手写代码的时候才发现题目坑人的地方不在于物理公式而在于怎么把“第N次落地”这个过程用循环准确地描述出来。很多人第一次交上去答案错误基本都是栽在“路程算多了一次”或者“反弹高度的定义没搞清”这两个点上。先把这个运动过程拆开看。球从高度 h 自由落下第一次接触地面时走的路程是 h此时发生了第一次反弹弹起到 h/2然后又落下来第二次接触地面时又走了 h/2 的下降路程。注意球的运动并不是“落下去、弹起来、再落下去”这么简单的三段式而是每一次“落地”都意味着一段完整的“下落 反弹”过程最后一次只落地不反弹或者最后一次反弹高度作为输出不再继续运动。题目里通常给定的输入是两个数初始高度 h 和落地次数 N。输出则是第 N 次落地时的总路程和此时球还能反弹到的高度。这就有个关键问题第 N 次落地之后球还会不会再反弹一次从物理上讲落地瞬间必然反弹但题目问的是“第 N 次反弹的高度”也就是第 N 次落地之后紧接着产生的那次反弹所达到的高度。这个高度在数值上等于第 N 次落地前那次下落高度的一半。为了说清楚我们可以把运动过程列一下第 1 次落地下落了 h反弹高度 h/2。第 2 次落地从 h/2 处落下走了 h/2反弹高度 h/4。第 3 次落地从 h/4 处落下走了 h/4反弹高度 h/8。第 N 次落地从 h / 2^(N-1) 处落下走了 h / 2^(N-1)反弹高度 h / 2^N。看到了吗第 N 次落地时的下落路程是 h / 2^(N-1)第 N 次反弹高度是 h / 2^N两者差一个因子 2很容易算混。而总路程则要累加所有下落段和除最后一次之外所有反弹段的路程因为最后一次反弹虽然发生了但题目让你“求”的是这次反弹的高度不要求你把这一段上升的路程也算进总路程里——换句话说总路程截止到第 N 次落地为止球在空中飞过的所有路径长度之和。1.2 为什么不能直接套公式有些同学可能会想每一段的路程都是等比数列直接用等比数列求和公式一行代码搞定何必用循环。这个思路本身是对的等比数列求和公式确实是这道题的最优解法之一下落路程总和h h/2 h/4 ... h/2^(N-1) h × (1 - (1/2)^N) / (1 - 1/2)反弹路程总和前 N-1 次h/2 h/4 ... h/2^(N-1)总路程 下落总和 反弹总和公式写法简洁但这里有一个隐蔽的问题题目里 N 的取值范围通常在 int 范围内而浮点数精度在累加或者计算幂次时会带来误差。更关键的是这是实验题教学目的是让学生练习“循环结构”而不是练“数学公式化简”。如果我们直接用等比数列求和公式虽然答案对但实验课的核心训练点就落空了。所以我在做题时选择了循环累加的方案这也符合 PTA 这个阶段实验 4主要讲循环控制的考核意图。循环本身并不复杂每次循环代表一次完整的“下落 反弹”过程累加下落距离再把反弹距离加进路程但不是每次都加最后更新当前高度为反弹高度进入下一轮。1.3 状态变量的设计是关键写这个程序前先想清楚要维护哪几个变量h当前高度初始值是输入的高度每次循环后更新为反弹高度。total累计路程初始为 0每次循环累加一段。i循环计数器从 1 到 N。变量之间的关系是循环第 i 次时球从当前高度 h 落下所以total h如果 i N说明这次落地后还要继续下一次运动还会反弹所以还要total h / 2然后更新h h / 2。如果 i N说明这是最后一次落地不再把反弹路程加进 total但仍要计算反弹高度作为输出。这里有一种常见的错误写法每次循环都先加下落 h再加反弹 h/2最后更新 h。这样在最后一轮会把第 N 次反弹的那段路程也加进 total结果比正确答案多了一段 h / 2^N。我第一次做这道题时就在这个细节上翻了车输出总路程一直比样例大一点点。后来把整个过程在纸上画了一遍才意识到“第 N 次落地”和“第 N 次反弹”不是同一时刻的概念——落地是一个瞬间反弹是落地之后紧接着发生的运动题目要的是截止到落地瞬间的总路程。注意理解“第 N 次落地时”这个时间截点是这道题唯一的也是最重要的思维门槛。2. 代码实现与核心细节处理2.1 完整的参考代码C 语言版搞清楚状态设计之后写代码就顺理成章了。下面我给出一个可以直接提交到 PTA 的 C 语言实现#include stdio.h int main() { double h; int N; double total 0; scanf(%lf %d, h, N); for (int i 1; i N; i) { total h; // 本次下落距离 if (i N) { total h / 2; // 前 N-1 次落地后还会反弹路程要加 } h h / 2; // 更新高度为本次反弹高度 } // 循环结束后h 已经是第 N 次反弹高度 printf(%.1f %.1f\n, total, h); return 0; }这个代码非常短核心逻辑就在 7 行循环里。我先把这段代码提交到 PTA 上能直接 ACAccepted。但如果你只是抄代码而不理解为什么if (i N)要这么写换一个题目条件或者换一种问法你照样不会做。下面我把这段代码的每一个关键点拆开讲。2.2 为什么变量类型要用 double 而不是 float题目里的初始高度可能是整数比如 100但反弹高度是 100、50、25、12.5……一路减半下去很快就出现小数。C 语言里 float 类型虽然也能表示小数但它只有 6 位有效数字当累加次数较多时误差会被放大。double 类型有 15 到 16 位有效数字对于这道题的量级完全够用。我见过有些同学用 float 写也能 AC因为 PTA 的测试样例一般不会把 N 给得特别大误差可能还在允许范围内。但作为一个有经验的开发者我建议所有涉及浮点运算的题目一律用 double这个习惯能帮你避免很多“看起来答案没问题但就是 WAWrong Answer”的玄学情况。另外scanf里对应 double 的格式符是%lf输出对应的是%fprintf 里%lf和%f在 C99 标准下效果相同但用%f更规范。很多新手在这里踩坑scanf用%f读 double 会导致读取失败变量值变成 0后面全盘皆输。2.3 循环变量的起点从 1 还是从 0我习惯让循环从 1 开始for (int i 1; i N; i)因为这样 i 的取值正好对应第几次落地语义非常清晰。如果写成for (int i 0; i N; i)也能跑但每次循环时 i 是 0 到 N-1判断“是不是最后一次落地”时就得写成if (i N - 1)可读性差一些还容易出错。从代码可读性的角度我强烈推荐循环计数器从 1 开始。编程不只是让机器执行更是让人看懂。等到你写更大的项目时就会明白变量命名和循环边界的“语义一致性”比省一两个字符重要得多。2.4 样例验证假设输入是100 3初始h 100total 0i1total 100total10013所以 total 50total150h50i2total 50total20023所以 total 25total225h25i3total 25total250i3不加反弹h12.5输出250.0 12.5手算验证一下第 3 次落地时球经过了 100第一次下落50第一次反弹50第二次下落25第二次反弹25第三次下落 250第三次反弹高度是 12.5。完全吻合。这就是这道题的标准答案。把这个样例吃透比做十道类似的题都管用。3. 常见问题与排查技巧实录3.1 我踩过的坑和排查方法问题一总路程比正确答案多一小段这个错误我在前面提过是“最后一次反弹路程被误加进 total”导致的。但还有另一种变体有些人把h h / 2写在了total h / 2之前结果 total 加的是更新后的 h也就是 h/2 的一半数值错得更离谱。排查这类问题我的经验是直接取小数据手工模拟。比如 h10, N2正确答案是 10 5 5 20第二次反弹高度是 2.5。你拿笔在纸上把循环走一遍很快就能定位是哪一步加错了。问题二输出格式错误PTA 对输出格式要求非常严格这道题要求输出两个数中间一个空格保留一位小数。有些同学写了printf(%.1f %.1f\n, total, h)这是对的。但如果你写成了printf(%.1f, %.1f\n, total, h)中间多了一个逗号PTA 直接判格式错误PEPresentation Error。还有一种格式问题是输出多余的空格或换行。PTA 的判定机制是逐字符比对多一个空格都会报错。提交前一定要把样例输出复制下来逐字符核对。问题三输入里可能有多个测试数据有些类似题目会要求“输入多组测试数据直到文件结束”但这道题从题目描述来看是单组输入。如果同学之前做过连续输入处理的题可能会惯性思维地写成while (scanf(%lf %d, h, N) ! EOF)这在单组数据的题目里也能运行但循环体里如果不重置 total就会导致后续计算累加错误。我的建议是每次做题先认真读题目描述看清楚是“单组输入”还是“多组输入”不要凭经验盲目套模板。问题四N 为 0 的情况题目如果没有明确说明 N 为正整数那就要考虑 N0 的边界情况。如果 N0球根本没有落地总路程应该是 0反弹高度应该是初始高度 h从未落地自然不存在反弹但输出初始高度是合理的。上面的循环代码对 N0 的情况会直接跳过循环total0h 保持输入值输出0.0 100.0假设 h100逻辑上说得通。但如果你的代码是先处理一次再循环N0 时就会出错。边界值测试是程序员的职业病刷题时尤其重要。3.2 浮点数精度问题的深入讨论有同学问过我如果 N 很大比如 1000循环 1000 次之后h 会变成多少答案是 h 会变得极小小到 double 都难以精确表示最后趋向于 0。但这道题里当反弹高度小于 0.05 时保留一位小数输出就会变成 0.0这是正常的不是 bug。不过这里有一个更深的问题当 h 非常小的时候total h / 2这种累加操作会因为浮点数的舍入误差产生微小偏差。在本题的测试数据范围内这个偏差不会大到影响 AC但如果你以后做数值计算相关的项目就要关注“误差累积”问题。简单说浮点数累加时如果加的数值远小于当前 total 的量级低位数字会被丢弃导致累加结果偏低。工程上常用 Kahan 求和算法来修正但这个知识点在 PTA 入门阶段不需要掌握了解一下有印象即可。3.3 常见错误速查表错误类型具体表现原因解决方案答案偏大总路程比正确值多最后一段反弹距离最后一次反弹被算入 total循环内用if (i N)控制变量类型错误输出全是 0.0 或精度明显不对scanf用%f读 double改用%lf变量声明为 double输出格式错误中间多逗号、少空格、小数位数不对没严格照题目输出格式复制样例格式逐字符核对结果完全不匹配循环次数不对、h 更新位置错误逻辑混乱循环边界写错手写模拟小数据逐步验证N0 出错输出与预期不符没有考虑边界情况先画出循环流程确认循环跳过后的输出值3.4 一个来自翁凯老师课堂的启发这个题目在 PTA 上被安排到实验 4正是 C 语言课程学到循环结构的时候。我记得翁凯老师在课堂上讲过一句话“循环程序写得好不好就看你能不能把循环不变式说清楚。”在这道题里循环不变式就是每次循环开始时h 保存的是本次落地前球所在的高度total 保存的是到目前上次循环结束时为止球已走过的路程。如果你写代码时脑子里有这个不变式那么循环体里每一步操作都是有依据的先加下落 h因为本次要从 h 落下判断是否要加反弹 h/2因为如果后面还有下一次落地这段反弹路程就不算到终点更新 h h / 2为下一次循环做准备。这个思维模式能迁移到几乎所有循环类题目上。以后你写二分查找、链表遍历、数值迭代本质上都是在维护某一个不变式。这也是刷 PTA 题目的真正价值——不是为了 AC 那一刻的快感而是为了锻炼这种“程序状态刻画”的能力。4. 题后延伸从这道题能学到什么4.1 从高空坠球到数值分析的初体验说实话高空坠球这道题本身难度不大代码十几行就写完了。但它的物理模型很有意思每次反弹高度减半。这是一个典型的等比递减过程在自然界中广泛存在——声音在介质中的衰减、光线穿过介质时的强度衰减、放射性物质的衰变都是类似的指数衰减模型。如果你把这道题的思路往深处再推一步就会发现它其实是一个“离散化的动力学模拟”用循环把连续运动拆成有限的步骤每一步用当前状态计算下一时刻的状态。这种“状态迭代”的方法是计算机模拟物理世界的基本手段。以后你学数值分析、计算物理、游戏物理引擎核心思想都是这样——只不过每一步的物理规律更复杂计算更精细而已。再换一个角度看这道题还能引出“级数收敛”的概念。总路程 h × (1 - (1/2)^N) / (1 - 1/2) h/2 × (1 - (1/2)^(N-1)) / (1 - 1/2)当 N 趋向无穷时这个路程会趋向一个有限值 3h。用通俗的话说球弹跳次数无限多但总路程不会无限大而是收敛在一个确定数值上。数学上这叫等比级数求和物理上这叫能量有限编程上这叫循环终止条件可以设定为“反弹高度小于某个阈值”。这些延伸也许现在用不上但当你学到更高级的课程时回头看会发现很多“简单题”其实是未来复杂问题的种子。4.2 关于 PTA 刷题的一些个人经验我在 PTA 上刷过不少题从最开始的字符串逆序、二分查找函数到天梯赛的 L2 题目再到一些复杂的模拟题。关于刷题我有几条比较深的体会第一先手算再写代码。我见过太多同学拿到题就开始敲键盘写完了发现不对再回头改。其实先把样例输入在草稿纸上手动算一遍搞清楚每一步的值是什么代码逻辑往往就浮出水面了。高空坠球这道题手算一遍样例之后循环怎么写基本就定下来了。第二提交前自己造测试数据。PTA 给的样例通常只是正常情况你自己要多想几个边界N0、N1、h0、h 很大、N 很大。把这些情况手算或心算一遍能挡掉八成以上的 WA。第三不要背代码要背思路。网上能搜到这道题的答案字符串逆序、复数四则运算、二分查找函数这些热门题的答案也满天飞。但如果你只是复制粘贴下次遇到变体照样不会。刷题训练的是“把问题抽象成程序逻辑”的能力这个能力只能通过自己写、自己错、自己改来获得。第四错题本是最高效的复习材料。把每次提交失败的代码和错误原因记录下来过两周再看你会发现自己曾经犯过的错误有多典型。我学 C 语言的时候光是把 PTA 上错过的题整理成笔记期末复习就省了一大半力气。4.3 后续可以尝试的进阶方向如果你把这道题做完了还有余力可以试着在原来的代码上做几个小改动把“反弹高度减半”改成“反弹高度乘以某个系数 k0k1”观察总路程随 k 的变化。把“输出第 N 次反弹高度”改成“输出反弹高度第一次低于某个阈值时一共落地了多少次”。这就是把固定次数的循环改成了条件循环while 循环。这里的循环条件和迭代更新都要相应调整是一个很有价值的变式练习。再把“单组输入”改成“多组输入直到 EOF”练习文件结束判断的写法。同时注意在每组输入开始前重置 total 等变量的初始值这个点很容易被漏掉漏掉以后输出会错得非常隐蔽。这些扩展练习都不难但能把一道简单题吃透切实解决“会一道题不会一类题”的问题。尤其是第二种变式它把“for 循环”和“while 循环”的切换方式练得明明白白。等你在 PTA 上遇到类似“猴子吃桃”“兔子繁殖”这类递推题时会发现自己对循环结构的理解完全不一样了。我自己在初学 C 语言时也曾经在这道题上卡了快一个小时不是代码不会写而是“第 N 次落地”这个边界条件想不明白。后来我把整个物理过程画成一张时间轴图把每一次落地和反弹都标出来才彻底搞懂。这大概就是编程学习里最常见也最重要的体验困扰你的往往不是语法而是对问题本身的理解不够精确。这道题的代码很短短到五分钟就能写完但把它蕴含的物理过程、循环设计、边界处理都想透你带走的东西远比这五分钟值钱得多。如果你现在正卡在某个 PTA 题目上我的建议是别急着搜答案先拿纸笔把过程画一遍。