🌙 序幕:机房的叹息
刚学完结构体的学生小明,正对着屏幕上的 P5738《歌唱比赛》发愁。他的显示器上,代码行数已经突破了 40 行,变量定义得密密麻麻,像是一张错综复杂的蜘蛛网。
“教练,我这代码逻辑没问题啊,样例也过了,但为什么写起来这么累?”小明转过头,眼神里透着清澈的愚蠢。
我凑过去一看,好家伙!结构体、数组、排序函数 cmp(虽然没用上)、嵌套循环……他把这道题当成了“数据仓库管理系统”来写。
“小明啊,”我拉过一把椅子坐下,“你这不是在写算法,你这是在用 C++ 模拟 Excel 表格。”
🕵️ 第一部分:案发现场——“仓鼠症”患者的自白
让我们先看看小明的 V1.0 版本代码。这是一个非常典型的初学者杰作:什么都想存下来,什么都舍不得扔。
V1.0:重装坦克版(结构体流)
#include<iostream>#include<iomanip>#include<algorithm>usingnamespacestd;structstudent {int score[25]; // 存每个评委的分int maxs = -1; // 存最大值int mins = 11; // 存最小值double ave; // 存平均分double tot; // 存总分} students[105]; // 开了一个大数组存所有学生intmain(){int n, m;cin >> n >> m;double maxave = 0;// 典型的离线思维:先把所有数据读进来存好for(int i = 1; i <= n; i++) {for(int j = 1; j <= m; j++) {cin >> students[i].score[j];// ... 一系列复杂的更新逻辑 ... }// ... 计算平均分 ... }// ... 输出 ...}
❌ 诊断报告:
- 空间浪费(仓鼠症):题目问的是“最高分是多少”,并没有问“最高分的那个学生第 3 个评委给了几分”。你把 个分数全存下来,就像是为了吃一个鸡蛋,在家里养了一整个养鸡场。
- 逻辑臃肿:
students[i].score[j] 这种写法,光是打字都费劲。一旦 和 写反,立马越界。
🚧 第二部分:直觉的陷阱——“Excel 表格流”
在被我吐槽后,小明很不服气:“那我不定义结构体了,这题目输入是 行 列,我直接开个二维数组总行了吧?”
这就是 V1.5 版本,也是绝大多数同学在考场上的第一反应。
V1.5:直觉流(二维数组版)
#include<iostream>#include<algorithm>usingnamespacestd;// 全局大数组:像一张 Excel 表int scores[105][25]; intmain(){int n, m;cin >> n >> m;double max_avg = -1.0;for (int i = 0; i < n; i++) {int sum = 0; int cur_max = -1, cur_min = 11;for (int j = 0; j < m; j++) {cin >> scores[i][j]; // 像填表格一样存进去// 既然存进数组了,顺便处理一下 sum += scores[i][j]; cur_max = max(cur_max, scores[i][j]); cur_min = min(cur_min, scores[i][j]); }// 计算平均分... }return0;}
⚠️ 教练点评:这个写法比结构体简洁多了,逻辑也没问题。但是,它依然藏着两个隐患:
- 空间的代价:这道题 ,二维数组很小。但如果题目变成 呢?你的内存会瞬间爆炸(MLE)。在算法竞赛中,能不存的数据,尽量别存。
- i 和 j 的迷魂阵:新手最容易犯的错误就是写成
scores[j][i],或者在双层循环里搞混行列。数组维度越高,出错概率越大。
🧠 第三部分:思维的拔河——从“离线”到“在线”
我指着屏幕上的代码,问了小明一个问题:
“小明,假设你是比赛的主持人。当你念完第 1 号选手的最终得分后,你还需要记得第 1 号选手的第 3 个评委给他打了多少分吗?”
小明愣了一下:“好像……不需要了。”
“对!这就像流水线作业。”我在白板上画了一个传送带模型,“数据从输入流(cin)进来,你处理它,得到结果,然后就可以把原始数据扔进垃圾桶了。我们要的是结果,不是过程数据。”
这就是 OI 中重要的思维模型:在线处理(Online Processing)。
核心算法拆解
我们要计算的公式是:
这里面有三个坑:
- 整数除法的诅咒:在 C++ 里,
19 / 4 = 4。想要得到 4.75,你必须让分子变成浮点数。 - 极值的初始化:找最大值时,初始值要极小(如 -1);找最小值时,初始值要极大(如 11 或 INT_MAX)。
- 变量的重置:算完第 个同学,准备算第 个同学时,你的
sum、max、min 必须归位。
🚀 第四部分:代码的演化——封装的艺术
“既然我们要重复计算 次每个同学的得分,为什么不把‘计算一个同学得分’这个逻辑,打包成一个工具呢?”
我打开编辑器,敲下了 double get_score(int m)。
V2.0:函数封装版(特种兵流)
这时候,我们的代码从“重装坦克”变成了“特种兵”。没有结构体,没有二维数组,只有纯粹的逻辑。
#include<iostream> #include<algorithm> // 必须包含,用于 max, minusingnamespacestd;doubleget_score(int m){int sum = 0;int max_val = -1; // 初始化最大值:只要比0小就行int min_val = 999; // 初始化最小值:只要比10大就行// 循环读入 m 个评委的分数for(int i = 0; i < m; i++) {int x;cin >> x; // 读入一个,处理一个,扔掉一个 sum += x; max_val = max(max_val, x); min_val = min(min_val, x); }// 核心公式实现// 技巧:乘以 1.0 将整型表达式强制转换为浮点型,避免精度丢失return1.0 * (sum - max_val - min_val) / (m - 2); }intmain(){int n, m;cin >> n >> m;double ans = -1.0; // 记录全场最高分(擂主)// 就像流水线一样,处理 n 个同学for(int i = 0; i < n; i++) {// 调用函数,拿到当前同学的分数double current_score = get_score(m);// 打擂台:如果当前同学比历史最高分还高,更新擂主 ans = max(ans, current_score); }// 输出保留两位小数cout<<fixed<<setprecision(2)<<ans;return0;}
代码美学赏析:
main 函数极其清爽:主函数只负责“控制流程”和“打擂台”,具体的脏活累活全扔给 get_score。- 空间复杂度极致:无论 和 是 100 还是 100万,这段代码占用的内存永远是 (常数级)。
- 变量隔离:
sum、max_val 定义在函数内部,每次调用函数都会自动重新创建和初始化。你再也不用担心“忘记重置变量”导致的 Bug 了。
📝 第五部分:教练笔记——从这道题学到了什么?
看着 V2.0 的代码,小明的眼睛亮了:“教练,这代码看着真舒服,像刚洗完澡一样清爽。”
我合上笔记本,敲黑板划重点:
- 拒绝“Excel思维”:写代码不是填表格。如果数据只用一次,就不要把它存进数组里。阅后即焚是处理大数据量的基本素养。
- 函数的威力:当你的代码里出现两层嵌套循环,且逻辑较深时,试着把内层循环剥离成一个函数。这不仅是为了好看,更是为了隔离作用域,减少 Bug。
- 类型的敏感度:
int 除以 int 永远是 int。在涉及平均数计算时,哪怕分子分母都是整数,也要习惯性地写上 1.0 * ...。
最后送大家一句话:代码的复杂度,不应该体现在变量的数量上,而应该体现在逻辑的深度上。愿你们的代码,都能像手术刀一样精准、犀利。