1. 开局:这道“平均气温”题,到底想考你什么?
后台有同学问:气温统计这类题,数据量明明不大,逻辑也很简单,怎么提交上去就是过不了?今天借着PA8题解报告——平均气温(Temperature)这道题,把那些容易被忽略的输入输出细节和浮点数精度问题彻底聊透。
这道题是各类算法课、数据结构课、编程实验里特别家常的一道:给你连续若干天的气温数据,叫你把平均值算出来。看着就是“求和除以个数”的小学运算,但真上了在线评测系统,不少人会在同一个坑里栽跟头。我见过一个班二十多个人提交,AC率不到一半的情况,错法还五花八门:有人用整数除整数,结果全是0;有人忘了处理小数位,差一个格式就给判错;还有人死在输入读取上,数据读不全,结果自然是错的。
这篇文章不打算只甩代码,我会把题面背后的考点逐个拆开,讲清楚为什么这么写、哪些地方容易翻车、翻车之后怎么排查。无论你是刚学编程的新手,还是带过这门课的助教,都能在这篇报告里找到点有用的东西。读完你能收获的不只是这道题的AC,还有一套对付同类输入输出题的通用方法论。
2. 题意拆解与问题本质分析
2.1 先看题面到底说了什么
这类题目的题干一般长这样:输入第一行是一个整数表示天数,接下来是若干行或一行的温度数据,可能带小数,也可能全是整数。要求输出这些温度的平均值,保留特定小数位。典型样例大概是:
5 1.0 2.0 3.0 4.0 5.0期望输出:
3.00有些变体还会要求保留一位、三位小数,甚至要求输出最高温度和最低温度。归根结底都是同一个框架:读入一组数据,做一次聚合计算,再按指定格式输出。
题目看起来是“算平均值”,但它的真实考察点其实有三个层面:
第一层是基础的循环累加和除法运算;第二层是输入流的处理能力,要能在不同风格的数据排布下稳定读取;第三层是浮点数的理解深度,知道为什么直接算可能不对、精度在哪里丢的、怎么控制小数输出。这三层能力对应着不同阶段的编程水平,也是这道题作为“PA8”意义所在——它不是一个纯考数学的题,而是一道综合性的程序题。
2.2 隐含的边界条件都有哪些
很多同学第一次AC失败,不是因为算法不会,而是因为没考虑到边界情况。平均气温这道题里,最容易被忽略的边界条件有这么几类:
- n为1时,平均值就是它本身,程序不能在此处崩溃或错误清空累加器;
- n为0时,题目一般不会给,但测试数据如果出现,得有应对方案,防止除数为零;
- 温度值为负数时,累加和可能为负,平均值输出时要带头负号;
- 数据量大的时候,比如几十万条数据,累加和可能会很大,用int很可能会溢出,需要用更大范围的类型;
- 温度带小数时,除法的取整逻辑要谨慎,尤其在不同语言中“/”运算符对整数和浮点数有完全不同的语义。
这些边界条件单独看都很简单,但组合在一起就容易出问题。我在本地测试时经常是随便造几组整数就能过,一提交就超时或者报错。后来养成一个习惯:写题之前先列一遍边界条件清单,再对照清单去设计测试用例。这个习惯帮我少走了很多弯路。
2.3 出题人真正希望你掌握的技能
结合这道题所在的课程进度来看,出题人大概率希望你在完成本题后能熟练做到几件事:正确处理标准输入输出、掌握循环结构、了解整型和浮点型的区别、使用格式化输出控制小数位。
听起来都是基础,但基础不代表简单。我辅导过不少同学,发现很多人卡在第8题左右,往往就是因为前面的题目能靠直觉做出来,到这道题开始需要真正理解“类型”这个抽象概念了。所以PA8这道平均气温题,既是知识的分水岭,也是培养严谨编程习惯的好机会。
3. 核心思路与方案选型的底层逻辑
3.1 为什么不推荐用数组存储全部数据
有同学拿到题就声明一个数组,把n个温度都存进去,最后再遍历求和。程序没问题,但对这道题来说,属于杀鸡用了牛刀。因为计算平均值只需要知道两样东西:总和、个数。完全可以在读取过程中边读边累加,不需要把数据留到后面再用。
用一个循环:
double sum = 0.0; for (int i = 0; i < n; ++i) { double x; cin >> x; sum += x; }处理完毕,内存占用恒定,时间复杂度是O(n),空间复杂度是O(1)。这种方式在数据规模达到十万、百万级别时也能平稳运行,而如果开数组,一旦题目隐藏的数据范围比你预估的大,还可能直接内存越界或者超限制。
3.2 使用double而非float,是为了什么
这是一个非常关键的选择。浮点数在计算机里是用二进制近似表示的,float类型只有约7位有效十进制数字,当你累加了上千个带小数的气温值时,误差会一层层累积,最后可能导致输出结果差一点点——而在线评测系统的判题往往要求误差在1e-2甚至更严格。double类型有约15位有效十进制数字,预留给累加误差的余量大多了。
我见过有人用float写了这道题,本地测试五个数据时结果都正常,一到评测机上就出现“Presentation Error”或“Wrong Answer”,最后换成double立刻AC。这类问题特别坑,因为不是逻辑错误而是精度错误,单靠肉眼很难察觉。
另一个选择是用整数计算:如果所有温度都精确到一位小数,可以先把所有数乘10转成整数,累加完再除以10。这样做的好处是避免浮点误差,坏处是可读性变差,而且一旦题目数据带两位小数甚至三位小数,你还得自己调整缩放系数。对于数据规模不超过百万级别的情况,double完全够用,不必绕这个弯子。
3.3 累加误差真的存在吗?聊聊Kahan补偿
现代CPU的浮点运算已经相当精确,但double在累加上亿个数时仍然可能出现系统性误差。比如1e16加上1.0,结果可能还是1e16,因为小数部分被挤掉了。平均气温题的数据范围通常不会大到触发这种情况,可如果你想把解法打磨得更坚实,可以在累加时使用Kahan补偿算法。
double sum = 0.0; double compensation = 0.0; for (int i = 0; i < n; ++i) { double x; cin >> x; double y = x - compensation; double temp = sum + y; compensation = (temp - sum) - y; sum = temp; }这段代码初看令人困惑,它的作用是记录每一步累加中丢失的低位信息,再在下一轮补回来。实际效果是让最终精度逼近真实值。
但在PA8这道题中,我会说:不必过度设计。用double加上普通的循环累加就够了,这个知识了解即可,如果是为了面试或者竞赛冲刺再去深究,日常学习阶段还是把基本功打牢更重要。很多初学者一上来就搞花哨的优化,反而忽略了最基础的逻辑正确性,这是本末倒置。
3.4 输出格式控制的几个关键动作
下一位输出是另一个大坑。常见的格式要求是保留两位小数,这就要用到格式化输出。
在C语言里用:
printf("%.2f\n", average);在C++里用:
cout << fixed << setprecision(2) << average << endl;在Python里用:
print(f"{average:.2f}")这里最容易被忽略的问题有两个:一是C++里如果忘记加fixed,setprecision(2)表示保留两位有效数字而非两位小数,结果可能输出科学计数法样式;二是格式化输出的四舍五入规则,不同编译器、不同语言的环境差异可能导致边缘舍入不一致。
我在实际做这道题时,还在输出前加了一行保护性判断:当平均值的绝对值小于某个极小值(如1e-9)时,直接设置成0。因为浮点数运算可能导致-0.0000001这种情况出现,格式化后输出成“-0.00”,评测系统可不管你数学上-0.00和0.00是否相等,字符串比对不一致就是判错。
4. 参考代码实现与关键步骤解释
4.1 C++完整参考实现
下面给出一版可以直接提交的C++参考代码:
#include <bits/stdc++.h> using namespace std; int main() { int n; while (cin >> n) { double sum = 0.0; double temperature; for (int i = 0; i < n; ++i) { cin >> temperature; sum += temperature; } double average = sum / n; if (fabs(average) < 1e-9) { average = 0.0; } cout << fixed << setprecision(2) << average << endl; } return 0; }有几个细节值得拿出来单独说。
第一,while (cin >> n)不是多此一举,它能自动处理输入流中的空白符,并且遇到文件结束符时循环自然退出。许多在线评测系统会组织多组测试数据,有这样的循环结构就不用担心只处理一组后程序就结束了。第二,读取温度数据时直接读double,不需要先读字符串再转换。第三,最后对平均值做了一次绝对值判断,把“-0.00”修正为“0.00”。
4.2 Python参考实现与API差异
Python代码更简洁,但需要注意input()的一次性读坑。如果n比较大,使用python的sys.stdin.read更稳:
import sys def main(): data = sys.stdin.read().split() if not data: return n = int(data[0]) temps = list(map(float, data[1:1+n])) avg = sum(temps) / n if n != 0 else 0.0 if abs(avg) < 1e-9: avg = 0.0 print(f"{avg:.2f}")把data[1:1+n]切出来再转float,是一种防御性写法,即使数据中间混有多余空格也不会断。这种写法在数据量百万级别时会占用较多内存,但在竞赛中通常够用。
4.3 C语言实现中的常见坑
如果是用C语言写,最简单的版本长这样:
#include <stdio.h> #include <math.h> int main() { int n; while (scanf("%d", &n) != EOF) { double sum = 0.0; double x; for (int i = 0; i < n; i++) { scanf("%lf", &x); sum += x; } double avg = sum / n; if (fabs(avg) < 1e-9) avg = 0.0; printf("%.2f\n", avg); } return 0; }记得scanf("%lf", &x)中%lf是double的占位符,如果写成%f则对应float类型,两者混用会把数据解析成一堆垃圾值。这是C语言初学者最容易在这里翻车的点。printf中的avg用%.2f而不是%.2lf,因为C语言会自动把double提升为printf可变参数中的double。当然用%.2lf在很多编译器下也能工作,但标准上%.2f更严谨。
4.4 多组数据的处理模式
上面三份代码都支持多组测试数据循环读取,这是这类题的一个隐含要求。在线评测系统的测试点经常是以文件为单位,一组输入文件里可能包含多个独立的n和数据块。如果你只会处理一组数据,很可能后面的数据全没读到,输出结果自然不对。
多组数据处理的核心思想是“把读取动作放在循环条件里”,读到哪组算哪组。一方面避免了数据残留,另一方面也让代码结构更接近生产环境中的流式数据处理模式。我个人在写所有涉及输入的问题时,都默认先考虑多组数据的情况,这是被评测系统训练出来的肌肉记忆。
5. 实测过程中高频遇到的三类错误
5.1 第一类:输出格式错误与换行符的锅
这类错误在评测结果里常常显示为“Wrong Answer”,而不是“Presentation Error”。很多同学没意识到,评测系统比对的是你输出的整个字节流,多一个空格、少一个换行、多输出一行空行,全部按错误处理。
平均气温这道题最常见的格式错误是:输出结果正确,但最后多打印了一个空行,或者每行结束时漏了换行。有些测试点对行尾的换行符号不敏感,但更多评测系统严格要求每一行输出后恰好有一个换行符。
我常用的排查方法是把输出重定向到文件里:
./your_program < test.in > test.out然后用十六进制编辑器或者cat -A检查输出文件的尾部字符。如果是C++的endl写多了,或者循环里在每组数据之间输出空行,都会看到明显的多余换行。
5.2 第二类:整数除法陷阱与隐式转换
这是最容易发生在C/C++代码里的大坑:n是int类型,sum是double类型,如果写sum / n没问题,因为sum是float或double时,n会自动被隐式转换成浮点类型再除法。但如果把sum声明为int,比如int sum = 0;然后读取整数温度,再写sum / n,得到的就是整数商,小数部分全被丢弃了。
我见过有同学这样写:
int sum = 0; ... printf("%.2f\n", sum / n);这里的sum / n在两数都是整数时执行整数除法,结果再被%.2f当作浮点数输出,输出的是个巨大的垃圾值或者0.00。这种错误的隐蔽之处在于:如果温度平均值恰好是整数,比如气温分别为1、2、3,平均值为2,输出看起来就完全正确,一旦平均值带小数,立刻产出错误结果。
正确的做法是确保分子或分母至少一方是浮点型,最直接的方式是把sum声明为double。如果题目要求输入是整数而你想用整数累加,最后输出前也要先转成浮点数再除,比如(double)sum / n。
5.3 第三类:数据范围估计不足导致的溢出
温度值看着都不大,但积累起来就可能超出int范围。假设n是100万,每天气温是40度,累加和就是4000万,这在int范围内;如果哪天题目改成统计100万次传感器读数,每个读数可能上千,累加和超过21亿就溢出了,int变成负数,平均值跟着错得离谱。
用double存累加和就没有这个问题,因为double可以表示约1.7e308范围内的数。极端情况下有精度损失,也不至于溢出。如果题目要求整型输出,则需要用long long类型来累加,这是许多竞赛老手的第一反应:见到求和第一个想到long long,而不是int。
5.4 附:常见问题速查表
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 输出0.00 | 整数/整数被当作整型除法 | 检查分子分母类型,确保至少一个浮点型 |
| 输出很多小数位但没有按格式输出 | 没用fixed或setprecision | C++中加fixed再setprecision(2) |
| 本地对,提交错 | 没有处理多组数据 | 改用while(cin >> n)或while(scanf(...) != EOF) |
| 输出结果为-0.00 | 浮点累加产生极小的负残差 | 输出前做绝对值判断并归零 |
| 结果差0.01 | 浮点精度或舍入规则不同 | 使用double并考虑输出前加1e-9修正 |
这张表可以贴在显示器边,排查任何类似统计题时都能用。很多问题并不是这次新犯的,而是过去就踩过,只是没有系统性地记录下来,下次又花了半小时重新排查。把经验沉淀成查表,效率能提升不少。
6. 从平均气温扩展出去的三个变体
6.1 变体一:顺带输出最高温和最低温
这道题做完后,试着加一点点难度:除了平均气温,再输出这组数据中的最高温和最低温,以及它们分别出现在第几天。
核心思路是维护两个变量:max_temp和min_temp,初始值可以设成第一个温度值,或者设成一个极大/极小值。每读取一个温度就更新它们。如果是多组数据,要记得每处理一组前重新初始化。
这个扩展训练的是最值维护技巧。初学的时候不少人是先把所有数据存进数组,遍历一遍求平均值,再遍历一遍求最大最小。如果改成流式处理,只需要一遍循环就能同时拿到总和、最大值、最小值、最值位置,代码复杂度不变,但思维的抽象层级提升了一档。
6.2 变体二:计算温度的方差和标准差
如果课程继续讲统计与概率,平均气温题很自然就会升级成“计算气温标准差”。公式是:先求出平均值,然后求每个温度与平均值差的平方和,再除以n,最后取平方根。
这里有一个值得注意的细节:如果先读一遍求平均值,再读第二遍求方差,输入流已经被消耗完了,那就必须用数组把数据存下来,或者用两次遍历。而如果使用流式读取一次性搞定平均值,那么你还没有办法同时算方差,因为方差公式需要用到平均值。实用做法是:第一遍读取时把数据存入vector,同时累加总和;第二遍遍历vector计算方差。空间换时间,代码结构也更清晰。
有时候题目会要求输出“温度是否波动较大”这种判断,本质就是在比较标准差是否超过阈值。这类题目不但考代码,还考公式理解,训练价值更高。
6.3 变体三:滑动窗口平均气温
再往后走,理论课可能会引入滑动窗口的概念:给出一长串逐小时气温数据,要求计算每连续k个小时的平均气温。这就不是简单算一个平均值了,而是要对每个长度为k的窗口都输出一个平均值。
朴素写法是双重循环,时间复杂度O(n*k),k一大就超时。优化的思路是保持一个“滑动总和”:从左往右移动窗口时,右边进一个数、左边出一个数,总和更新只需一次加法一次减法,总时间复杂度降为O(n)。这类题其实就是用了前缀和的简化版思想,练过一次之后,对数组下标和区间维护会敏感很多。
从平均气温这道题出发,能带出的变体链还有很长:累计频率、趋势判断、异常值检测……每往下走一个变体,难度就提高一个台阶。所以我很建议学有余力的同学:把最基本的平均气温题AC之后,别急着关掉页面,直接给自己出上面三个变体,每一个都亲自写一遍。过一段时间回头看,你会发现最初写PA8时那些小心翼翼的技巧,已经变成了基本功。
7. 写在最后:调试这类题目的一点私人体会
这道平均气温题,我前前后后应该写过不下十遍,每次教课、当助教或者帮同学答疑,都会发现新的细节。最想跟大家分享的一点体会是:处理输入输出类题目,最先要解决的不是算法,而是输入输出本身。很多同学一上来就盯着怎么求平均值,反而忽略了数据应该以什么形式进入程序、以什么形式离开程序。
调试的时候,我习惯准备三组测试数据:一组小数据,用于盯细节;一组中等数据,用于验证边界;一组大数据,用于测试性能。比如对于平均气温题,小数据我可能只给3天,中等数据给几百天,大数据给十万天,每次都先在自己的机器上跑通,再去评测系统提交。
气温统计只是一个入口,后面还有很大一片题海等着你去闯。如果这篇文章能帮你在PA8这道题上少走一点弯路,让“读入n个数并输出统计结果”这个模板可以在你脑海里定格成型,那这篇报告就有意义了。继续往下写,下一题见。