PTA实验4-1-11这道“高空坠球”我第一眼扫过去觉得就是个循环累加的小题。真正提交的时候才发现越简单的题越容易在边界条件和初始值上翻车而且一翻就是WA。题目本身不复杂一球从M米高度自由下落每次落地后反弹回原高度的一半再落下求第N次落地时总共经过了多少米以及第N次反弹有多高。它几乎把循环递推、浮点数精度、输出格式这些C语言入门阶段最容易踩的坑全占了一遍。如果你正在刷PTA基础题库或者跟着翁凯老师的C语言公开课做实验题这篇文章可以直接拿来当解题笔记用。我会把推导过程、参考代码、常见WA原因都拆开讲清楚尽量让看完的人能自己复现出正确答案。1. 读懂题目这个“坠球”过程到底经历了什么1.1 题干里的关键信息与隐含条件先看题面。输入两个正整数M是初始高度N是落地次数输出两个值第N次落地时球经过的总路程以及第N次反弹高度保留两位小数。这里面有几个容易忽视的隐含条件。第一个“自由下落”意思是忽略空气阻力题目只关心几何路径不涉及任何物理公式。第二个“每次落地后返回原高度的一半”这里的“原高度”指本次落地之前的高度所以反弹高度序列是M、M/2、M/4、M/8这样往下走。第三个也是最重要的“第N次落地时”和“第N次反弹多高”——我们关心的是落地这个时间点落地之后紧接着就是反弹所以第N次反弹高度就是第N次落地后弹起的高度也就是M除以2的N次方。不同学校的PTA班级里实验题的编号可能略有不同有的叫“实验4-1-11”有的直接放“基础编程题目集”但题面就是这一道经典题。它考察的重点不是语法难度而是你能不能把一段连续的运动过程精确拆成一个个离散的“落地事件”并且用循环去模拟这些事件。1.2 把运动过程拆成“第一次”和“后续往返”很多人一上来就想着“球每弹一次就累加”结果把第一次落地前的纯下落也当成了“往返”路径直接多算。为了避免这个问题最好把整个过程拆成两段来看。第一次落地前球只是从M米高度落到地面经过的路程就是M这段没有“往返”。第一次落地后球弹起到M/2再落回地面这一段是上升加下落一共经过2乘以(M/2)也就是M。第二次落地后球弹起到M/4再落回地面这一段经过2乘以(M/4)也就是M/2。第三次落地后规律变成M/4、M/8……每次都是上一段的一半。用表格表示M1000、N5的情况会更直观落地次数本次增加的路程累计总路程本次落地后的反弹高度110001000500210002000250350025001254250275062.55125287531.25从表里能看出一条核心规律除第一次落地外每一次落地增加的路程都是上一次增加路程的一半。而反弹高度则是每次都在当前高度基础上减半。这就是递推的基础。1.3 先钉死两个边界N1和N2写代码之前我建议先把N1和N2这两个最小用例手算清楚它们就是后面调试的“基准答案”。N1球只落了一次总路程就是M第一次反弹高度是M/2。N2总路程是M加上第一次反弹再落下的距离M也就是2M第二次反弹高度是M/4。别看这两个用例简单很多错误代码在N1时就暴露了。比如有人习惯先让h除以2再累加或者把sum初始化成0N1时输出直接变成M/2和M/4样例都过不了。这两个基准值先记住后面无论代码怎么写都要满足这两个基本事实。2. 建立递推模型别硬模拟用“每次落地”做循环单位2.1 变量设计sum存累计路程h存反弹高度要让循环写起来不容易错关键是选好初始状态。我推荐的写法是sum初始化为M因为第一次下落已经真实发生了这M米必须算进总路程。h初始化为M/2表示第一次落地后球的反弹高度也就是下一次下落的高度。为什么不把sum初始化成0因为你要是从0开始累加就必须处理“第一次没有往返”的特例循环里容易多算或少算。为什么不把h初始化成M因为如果h是M那就意味着球还能弹回初始高度这和题意“反弹回原高度的一半”直接矛盾。这个初始化背后的逻辑可以这样理解我们让变量永远描述“当前已经完成的状态”。程序一启动第一次落地已经完成总路程自然是M第一次反弹高度也已经是M/2。剩下的循环就是不断推进后续的落地事件。2.2 循环核心先累加路程再更新下一次反弹高度从第二次落地开始循环核心代码就三行for (int i 2; i N; i) { sum 2 * h; // 上升h米再下落h米 h / 2; // 落地后高度减半作为下一次反弹高度 }逐行解释。第i次落地时球从上一个反弹高度h开始上升升到h后再落回地面所以这一段经过的路程是2h。累加完之后球已经落地接下来会反弹到本次落地前高度的一半所以h要除以2。操作的顺序不能反。如果先h/2再sum2h累加的路程就变成了2(h/2)少算了一段同时h也会在错误的时间点被更新最后连反弹高度也是错的。这里“先记录路程再更新状态”的顺序本质上是和物理事件的发生顺序保持一致球先走完这段路然后才落地反弹。用M1000、N5把整个循环过程展开循环变量i进入前sum进入前hsum 2*h 后h / 2 后初始1000500--21000500200025032000250250012542500125275062.55275062.5287531.25循环结束后sum是2875h是31.25和题目样例完全一致。2.3 用等比数列公式对拍验证循环写完了还可以用等比数列求和公式来做个“对拍”确认结果没有隐藏问题。总路程的构成是第一次下落M加上后续N-1次往返。第k次往返的路程是2乘以M/(2^k)所以总路程可以写成sum M 2M * (1/2 1/4 ... 1/2^(N-1))括号里是首项1/2、公比1/2、项数N-1的等比数列和为1 - 1/2^(N-1)。代入后sum M 2M * (1 - 1/2^(N-1)) 3M - M * 1/2^(N-2)N1时成立反弹高度则是h M / 2^N拿M1000、N5验证sum 3000 - 1000 * 1/8 2875h 1000 / 32 31.25。和循环结果吻合。这个公式平时写代码用不上因为公式版对N1的边界处理很别扭但用来验证循环是否正确非常方便。3. C语言实现从变量类型到输出格式的完整代码3.1 为什么变量必须用double这个题最大的隐藏考点之一就是变量类型。M和N是整数输入但路程和反弹高度在计算过程中会出现小数。比如M10、N3时反弹高度依次是5、2.5、1.25全是小数。如果用int存h整数除法直接截断h很快就变成0整个递推就废了。所以h和sum必须用浮点数。那为什么用double而不是float呢float的有效数字只有6到7位在M比较大或者N比较大的时候误差会被逐步放大。PTA的隐藏测试点里经常有M10000、N20这类数据double的15到16位有效数字完全够用float则有可能在中间步骤产生可见误差。这不是玄学是浮点数底层表示精度决定的。另外读入的时候要注意一个细节如果M声明成int那么计算反弹高度时不要直接写M/2C语言里的整数除法会把小数部分丢掉。正确写法是M/2.0或者把M强转成double再参与运算。3.2 输出格式的细节题目要求保留两位小数输出两个数中间用空格分隔末尾换行。对应的printf写法是printf(%.2f %.2f\n, sum, h);这里有两个常见问题。第一忘记写\nPTA会认为你输出缺了一部分直接格式错误。第二中间多打空格或者少打空格比如写成%.2f%.2f两个数字粘在一起判题系统照样不认。这些格式问题在PTA上不会报WA会报Presentation Error提交结果里能看到但如果你不注意也会白白浪费提交次数。关于%f和%lf有一点可以记牢scanf里读取double必须用%lfprintf里用%f就够%lf也合法但为了统一习惯输出时写%f最省心。3.3 完整参考代码与逐行注释把前面分析的东西组合起来就是一份可以直接提交的完整C语言代码#include stdio.h int main() { int M, N; scanf(%d %d, M, N); double sum M; // 第一次落地已经完成走了M米 double h M / 2.0; // 第一次落地后的反弹高度 for (int i 2; i N; i) { sum 2 * h; // 本次落地增加的路程升上去h米再落下来h米 h / 2; // 落地后反弹高度减半 } printf(%.2f %.2f\n, sum, h); return 0; }当N1时for循环一次都不执行sum保持Mh保持M/2输出就是10.00 5.00这样的正确结果。代码里只有一次除法就是M/2.0这里用2.0而不是2就是为了避免整数除法截断。这份代码本身没有用任何复杂的语法但它背后已经包含了完整的递推推导和边界处理逻辑。这也就是为什么我说这道题“简单但值得写”语法上不过关的人写不出逻辑上不过关的人容易写错。4. 常见错误与排查那些年我交过的WA4.1 错误一sum初始化成0第一次被算成了“往返”这个错误非常典型。有人觉得“总路程应该从0开始累加”于是写出了类似这样的循环double sum 0, h M / 2.0; for (int i 1; i N; i) { sum 2 * h; h / 2; }跑M1000、N5的时候输出的是1937.50 15.63和正确答案2875.00 31.25差了将近1000米。原因就是第一次落地只应该算M但这个写法把第一次也按“上升加下落”处理了导致整个累加的起点就错了。修复方式很简单要么把sum初始化为M循环从2开始要么保持sum从0开始但循环里特殊处理第一次。哪一种更不容易错我的经验是前者。把第一次落地当作已完成状态写进初始化里思维负担最小。4.2 错误二h多除了一个2反弹高度不翼而飞另一种隐蔽的错法是循环结束后多写一句h/2或者在循环里把更新顺序搞错。比如double sum M, h M / 2.0; for (int i 2; i N; i) { sum 2 * h; h / 2; } h / 2; // 多余的更新M1000、N5时sum倒是算对了是2875.00但h变成了15.63别的测试点全错。这种错最迷惑人的地方在于如果只盯着总路程看好像完全没问题反弹高度差一半不逐项检查很难发现。我的自查方法是每次提交前先用N2手工验证。N2时h应该是M/4不是M/8。你只要把N2这一个用例跑一遍这类错误马上就会现出原形。4.3 错误三整数除法让小数直接消失如果你学过C语言但你写出了这样的代码double h M / 2;那就要小心了。M是intM/2先按整数除法计算结果截断成整数然后再转成double赋给h。M5时h会变成2.0而不是2.5。初始反弹高度就已经错了后面的递推每一步都建立在错误数据上结果必然错。解决办法有两个写M/2.0或者写(double)M/2。我平时更习惯写M/2.0因为一眼就能看出除数是个浮点数意图清楚。调试这种错误的时候可以在初始化之后立刻printf打印一次h的值和手算值对比。这是定位初始值错误最快的方式。4.4 错误四边界数据N1没测隐藏测试点直接挂样例能过不代表提交能过这是PTA刷题人的共识。“高空坠球”这个题的样例是M1000、N5这个用例比较规矩掩盖了不少边界问题。真正扣分往往发生在隐藏测试点比如N1。N1的时候正确输出应该是M和M/2。如果你写的for循环次数是iN-1或者循环里先做了h/2再累加N1时输出的反弹高度就会变成M/4或者总路程变成0直接WA。对付隐藏测试点没有捷径就是主动构造边界用例。N1、N2、M1、M10、M10000这五个用例跑一遍题目能踩的坑基本就都踩到了。4.5 通用调试技巧手算小数据加printf打点最后分享一套我自己用着很顺的调试流程这套流程适用于PTA上几乎所有的递推模拟题。第一步手算小数据。拿M10、N1和M10、N2把预期输出写在纸上一个答案是10.00 5.00另一个是20.00 2.50。第二步在本地编译运行用这两组数据测试自己的代码输出不符就说明逻辑有错。第三步如果还查不出来就在循环里加一句临时的printf打印每次循环结束后的sum和h和手算的递推表对比很快就能定位到是哪一次迭代出了偏差。第四步确认无误后删掉调试输出提交。这套方法的关键价值在于手算和代码走的是同一条逻辑链任何一步不一致都是逻辑有漏洞的最好证据。5. 从这道题延伸出去PTA基础题的通用套路与进阶方向5.1 递推模拟题的共同解题思路“高空坠球”属于PTA题库里非常常见的“递推模拟”题型。它的特点是题目描述了一个连续变化的过程但每一步的状态更新规则固定只需要用循环逐次推进就能得到结果。这种题在PTA里到处都是。比如字符串逆序本质上是左右两个指针对称交换每一步更新指针位置二分查找每次根据中间值大小缩小搜索区间每一步更新左右边界复数四则运算每一步按复数的运算规则调整实部和虚部。这些题表面长得完全不一样核心思路却是同一个找到状态转移规则确定初始值用循环或函数把它实现出来。做这类题我总结了一个三步骤方法。第一步手算前几次变化把规律在纸上写出来不要一上来就写代码第二步确定循环的初始值和次数注意边界情况第三步用小数据验证输出。这个方法听着简单真能坚持做的人不多但确实是最省提交次数的路。5.2 和翁凯老师C语言课程的衔接我看到很多搜索词里都有“翁凯”和“pta题库答案c语言”猜测不少读者应该是在跟着翁凯老师的C语言课程做配套作业。翁凯老师的课程在讲完顺序结构、循环结构、数组和函数之后会布置PTA实验题。“实验4-1-11”通常就出现在循环结构这一章安排在条件语句之后、数组之前属于承上启下的位置。这个阶段你要练的不是高深的语法而是把文字题转化成代码的能力。高空坠球这道题考察的正是这一点读题、拆过程、找递推、写循环、验边界。哪怕你最后发现其实用一个for循环就搞定了这整个思考过程也会为后面学习递归、链表、指针打下基础。所以我建议这个阶段尽量不要直接去搜答案先自己推导一小时比照着答案抄十道题都更有用。5.3 如果题目升级从递推延伸到动态规划与天梯赛把高空坠球这道题稍微变形就能变成不少进阶题目的雏形。比如求“球在第N次反弹到达最高点时总共经过了多少米”初始化和循环次数就要调整再比如如果问“N趋近无穷大时总路程会收敛到多少”这就成了一个简单的等比数列极限题用3M这个结论就能快速验证你的循环代码是否合理。PTA天梯赛L2级别的题目里很多中等偏难的题看起来故事讲得天花乱坠核心其实都是状态转移和这道坠球题的思想一脉相承。模式匹配、二分查找函数、数值分析、两点间最短距离这些搜索热词背后对应的题也都在同一个能力圈里先把流程想清楚再翻译成C语言。基础题里练出来的递推思维就是这些进阶题的地基。最后说个我自己的习惯。每道PTA题交了Accepted之后我都会把错误的版本留下来隔几天再拿出来和正确版对比。高空坠球这道题让我记住的不只是“sum要初始化为M”这一个点更是一个通用原则循环次数和初始值必须与题意的计数方式严格对应。下次再遇到类似题先写两个最小用例把边界钉死再动手写循环能省下不少WA的时间。