☰
谁拿了最多奖学金:OJ经典题中的条件判断与边界处理
2026/9/30 3:10:23 网站建设 项目流程

但凡刷过一点在线评测系统的人,大概率都见过“谁拿了最多奖学金”这道题。它常挂在各类OJ入门题单里,题号往往就是1269,出处是NOIP 2005普及组第一题。别看它标注难度不高,很多新手第一次提交照样会栽跟头,WA得莫名其妙。这道题真正考的不是高深的算法,而是三件看起来很简单、做起来却容易翻车的事:条件判断的完整性、输入数据的处理方式、以及“只在必要时保存数据”的程序设计意识。

今天我就把这道题从头到尾拆一遍,从题目背景、思路选型到代码实现、踩坑记录全部铺开。不管是刚接触结构体排序的初学者,还是想帮学弟学妹讲题的学长学姐,这篇文章应该都能给你一些能直接拿去用的东西。

1. 题目到底在考什么

1.1 五个奖学金条件的来源与规则

“谁拿了最多奖学金”这个题目背景是学校评选奖学金,每个学生有五个评选维度,每个维度对应一笔钱:

  • 院士奖学金:期末平均成绩高于80分,并且在读期间发表论文不少于1篇,奖励8000元。
  • 五四奖学金:期末平均成绩高于85分,并且班级评议成绩高于80分,奖励4000元。
  • 成绩优秀奖:期末平均成绩高于90分,奖励2000元。
  • 西部奖学金:期末平均成绩高于85分,并且来自西部省份,奖励1000元。
  • 班级贡献奖:班级评议成绩高于80分,并且担任班级干部,奖励850元。

输入数据里每个学生包含七个字段:姓名、期末平均成绩、班级评议成绩、是否班干部(Y/N)、是否西部省份学生(Y/N)、发表论文数。要求输出三行:获得奖学金总数最多的学生姓名、该学生的奖学金总数、所有学生奖学金总数的总和。

这道题的背景虽然设定在校园,但本质上是一个典型的多条件筛选与聚合计算问题。放在竞赛场景里,它就是用来检验选手能不能把文字描述准确翻译成代码逻辑,能不能把“满足多个条件才累加奖金”这件事处理干净。

1.2 题目的难度定位与常见误判

很多人一看题就觉得太简单了,五个if就结束了。但实际评测结果往往会给这些人一记响亮的耳光。这道题的难点不在于“会不会用排序”,而在于“能不能一次把所有细节都照顾到”。

我从辅导新手的经历里观察到,最常见的翻车点有三个:

  • 把“高于80”写成“大于等于80”,边界的等号搞错。
  • 读入“是否是班干部”和“是否是西部学生”时,用字符类型没有处理好,读到换行或空格。
  • 只记录每一笔奖金,最后忘了累加总额,或者总额的变量类型用得不对。

另外还有一个逻辑层面的坑:题目要求输出“获得最多奖学金的学生”,如果存在并列,应该输出在最前面出现的那个人。这一点在原题描述里其实有隐含约定,但很多人会在排序方案里把并列情况处理反了。

所以这道题真正考察的,是你能不能像一台严谨的机器一样,把条件逐条读透、逐条实现,而不是炫技。算法复杂度要求在这里几乎不构成压力,N最大也就100,O(N)甚至O(N log N)都能过,但前提是逻辑正确。

2. 解题思路:两种方案的取舍

2.1 方案一:把学生信息存进结构体再做排序

很多初学者看到“谁最多”这种字眼,第一反应是建一个结构体数组,把所有学生的信息读进来,然后写一个cmp函数,按奖学金金额从大到小排序,排完输出第一个。

struct Student { string name; int avgScore; int classScore; char isLeader; char isWest; int papers; int money; };

这个思路本身没有错,也能过。但它引入了一些不必要的风险。排序意味着你要为结构体写比较器,要把奖学金先算出来再排序,还要考虑并列时保持输入顺序的问题。一旦比较器写得不够严谨,比如奖学金相等时返回了false或者true的方向不对,结果就会错。

而且从程序效率的角度看,排序是完全没有必要的。题目只关心“最大值”和“总和”,这些都是可以在读入过程中同步维护的,不需要把所有学生都存下来。排序不仅多写了代码,还凭空增加了出错的空间。

2.2 方案二:边读边算,维护最大值和总额

更干净的做法是这样:每读入一个学生的数据,立刻用五个if计算他获得的奖学金总数。然后做两件事:

  • 把这个数累加进总额变量。
  • 如果这个数大于当前记录的最大值,就更新最大值,并把当前学生的姓名记录下来。

整个过程只需要一个循环,不需要结构体数组,不需要排序,内存占用是常数级别。代码结构也非常清晰,读数据、算钱、比较、更新,四步一气呵成。

int main() { int n; cin >> n; int total = 0; int maxMoney = 0; string maxName; for (int i = 0; i < n; i++) { string name, leader, west; int avg, cls, papers; cin >> name >> avg >> cls >> leader >> west >> papers; int money = 0; if (avg > 80 && papers >= 1) money += 8000; if (avg > 85 && cls > 80) money += 4000; if (avg > 90) money += 2000; if (avg > 85 && west == "Y") money += 1000; if (cls > 80 && leader == "Y") money += 850; total += money; if (money > maxMoney) { maxMoney = money; maxName = name; } } cout << maxName << endl; cout << maxMoney << endl; cout << total << endl; return 0; }

2.3 为什么“读入即处理”是更好的选择

从软件工程的角度看,“边读边算”体现的是一种流式处理思想:数据不需要完整驻留内存,读完一条就处理一条,处理完就能丢弃。这种思想在真实开发里非常常见,比如处理日志文件、统计流式数据等场景,数据量可能大到你根本没法全部塞进内存,但你只需要一个累加器和一个最大值变量就能完成任务。

在这道题里,N最大只有100,用哪种方案性能上没有任何差别。但我要强调的不是性能,而是“思维负担”。边读边算把“计算奖学金”和“寻找最大”这两件事紧紧耦合在一次遍历里,你不需要额外维护一个结构体数组与排序后的索引关系,也不需要担心比较器写错。代码量更少,心智负担更低,对新手来说绝对是首选。

如果你以后做到类似的题目,不妨先问自己一句:我真的需要把所有数据都存下来吗?很多时候答案是不需要。这个习惯能帮你省掉很多不必要的麻烦。

3. 完整代码实现与逐行解析

3.1 核心代码与变量设计

我用C++写了一个完整版本,并且加了注释,方便对照阅读。

#include <iostream> using namespace std; int main() { int n; cin >> n; int totalMoney = 0; // 所有学生奖学金总额 int maxMoney = 0; // 当前最高奖学金数额 string maxName; // 当前最高奖学金学生姓名 for (int i = 0; i < n; i++) { string name; int avgScore, classScore, paperCount; string isLeader, isWest; cin >> name >> avgScore >> classScore >> isLeader >> isWest >> paperCount; int money = 0; // 1. 院士奖学金:期末平均成绩 > 80,论文数 >= 1 if (avgScore > 80 && paperCount >= 1) { money += 8000; } // 2. 五四奖学金:期末平均成绩 > 85,班级评议成绩 > 80 if (avgScore > 85 && classScore > 80) { money += 4000; } // 3. 成绩优秀奖:期末平均成绩 > 90 if (avgScore > 90) { money += 2000; } // 4. 西部奖学金:期末平均成绩 > 85,是西部学生 if (avgScore > 85 && isWest == "Y") { money += 1000; } // 5. 班级贡献奖:班级评议成绩 > 80,是班干部 if (classScore > 80 && isLeader == "Y") { money += 850; } totalMoney += money; if (money > maxMoney) { maxMoney = money; maxName = name; } } cout << maxName << endl; cout << maxMoney << endl; cout << totalMoney << endl; return 0; }

这里我刻意把“是否班干部”和“是否西部学生”都用string类型读入,而不是用char。原因后面会专门讲,这是一个很重要的实战细节。

3.2 输入输出格式里容易被忽略的细节

这道题的数据输入格式是一行一个学生,字段之间用空格隔开。比如:

4 YaoLin 87 82 Y N 0 ChenRuiyi 88 78 N Y 1 LiXin 92 88 N N 0 ZhangQin 83 87 Y N 1

第一行是学生人数N,之后N行每行依次是:姓名、期末平均成绩、班级评议成绩、是否班干部、是否西部学生、论文数。

用cin >> 连续读取可以自动跳过空白字符(包括空格和换行),所以不需要专门处理换行符。但需要注意,如果你使用getline读整行,然后再拆分,就必须格外小心每行末尾的换行符和前面的残留字符。

输出的时候要严格按照题目要求,先输出最大学奖学金获得者的姓名,再输出该学生的奖学金数,最后输出总额,每行一个数。很多人因为输出顺序搞反,样例过不了,白白丢分。

3.3 手算一遍样例,验证逻辑

我们来把样例数据完整算一遍,确认代码逻辑能和手算结果对上。

第一个学生YaoLin:平均87,评议82,班干部Y,西部N,论文0。

  • 院士奖学金:平均87大于80,但论文0篇不满足大于等于1,所以不发放。
  • 五四奖学金:平均87大于85,评议82大于80,发放4000。
  • 成绩优秀奖:87不大于90,不发放。
  • 西部奖学金:平均87大于85,但西部标记为N,不发放。
  • 班级贡献奖:评议82大于80,班干部标记为Y,发放850。

YaoLin总额4850。

第二个学生ChenRuiyi:平均88,评议78,班干部N,西部Y,论文1。

  • 院士奖学金:88大于80,且论文1篇,发放8000。
  • 五四奖学金:平均88大于85,但评议78不大于80,不发放。
  • 成绩优秀奖:88不大于90,不发放。
  • 西部奖学金:88大于85,西部Y,发放1000。
  • 班级贡献奖:评议78不大于80,不发放。

ChenRuiyi总额9000。

第三个学生LiXin:平均92,评议88,班干部N,西部N,论文0。

  • 院士奖学金:论文0,不发放。
  • 五四奖学金:92大于85,评议88大于80,发放4000。
  • 成绩优秀奖:92大于90,发放2000。
  • 西部奖学金:不是西部学生,不发放。
  • 班级贡献奖:不是班干部,不发放。

LiXin总额6000。

第四个学生ZhangQin:平均83,评议87,班干部Y,西部N,论文1。

  • 院士奖学金:83大于80,论文1,发放8000。
  • 五四奖学金:83不大于85,不发放。
  • 成绩优秀奖:83不大于90,不发放。
  • 西部奖学金:83不大于85,不发放。
  • 班级贡献奖:评议87大于80,班干部Y,发放850。

ZhangQin总额8850。

总额等于4850加9000加6000加8850,一共28700。最高奖学金获得者ChenRuiyi,金额9000。这个结果与题目样例输出完全一致,说明五个条件和累加逻辑都没有问题。

4. 新手最容易踩的坑

4.1 字符读入的经典翻车现场

有些选手喜欢用char类型接收“是否班干部”和“是否西部学生”这两个字段。本身这么做没问题,但坑在于后续比较。假如你写的是:

char leader, west; cin >> name >> avg >> cls >> leader >> west >> papers; if (cls > 80 && leader == 'Y') ...

这样写其实也能正常通过。真正出问题的是另一种写法:先用cin读完整数,然后再想用scanf或者getchar去读字符,结果把上一行末尾的换行符给读了进去,造成数据错位。

我见过最典型的错误是有人用getline逐行读,然后手动拆字符串。一旦某一行学生没有班干部标记或者论文数没跟上,整个解析就乱套了。对这种固定格式的OJ输入,最稳妥的方式就是让cin的流运算符自己去处理空白字符,不要手动介入。

为了避免麻烦,我个人习惯把“是否”这类字段统一用string去接。程序读起来也更自然,和"Y"字符串比较比和字符比较更容易让人理解,而且完全不需要担心类型不匹配的问题。

4.2 边界条件:大于还是大于等于

题目里的“高于80分”是大于80,不是大于等于80。这个细节很多人第一次看会忽略,但只要你少写一个等号,遇到恰好80分的情况就会给出错误判断。

整理一下边界的正确写法:

条件正确写法
期末平均成绩高于80avg > 80
班级评议成绩高于80cls > 80
期末平均成绩高于85avg > 85
期末平均成绩高于90avg > 90
发表论文不少于1篇papers >= 1

有个小技巧是,题目描述里的“高于”“大于”都对应严格大于;“不少于”“至少”对应大于等于。把中文关键词和运算符号做一个映射记忆,基本不会错。

4.3 最大值的初始化和并列情况的处理

最大值变量初始化为0是没有问题的,因为每个学生的奖学金总额只会是非负数。第一次循环如果money大于0,自然能更新maxMoney。但如果你初始化为一个很大的数,比如9999999,那整个更新逻辑就废了,这个属于低级失误。

并列情况更需要注意。题目要求输出获得最多奖学金的学生,如果多个学生金额相同,应该取最先输入的那个。我代码里用的是:

if (money > maxMoney)

注意这里一定是“大于”,不是“大于等于”。如果写成了“大于等于”,那么当后面出现一个同等金额的学生时,会覆盖掉之前记录的姓名,最终输出的可能就不是最先出现的那个人,与题意不符。

其实这种“取第一个最大值”的写法也是很多实际业务场景中的通用规则。比如从日志里找最早达到某个阈值的记录,用大于号就能保证永远不会被后来的同值记录覆盖。

4.4 总额变量的取值范围

N的数据范围在题面上给的并不算大,一般不超过100。每个学生最高能拿的奖学金是8000加4000加2000加1000加850,等于15850元。100个人的总额上限也就1585000左右,int类型完全足够。

但如果你是从其他OJ上找到的改编版本,N有可能被放大,或者奖学金的单项金额被调整,这时用int可能就不够稳了。保险起见,可以把总额声明成long long,养成习惯,既不会多花你一秒,也能避免极端数据溢出。

我在实际教学里见过有人因为总额变量用了int,在自定义数据上爆掉的情况。虽然这不是原题的数据范围问题,但它提醒我们一个通用原则:凡是涉及累加和的数据,先想清楚最坏情况有多大,再决定类型,不要想当然。

5. 延伸:如果题目变了,代码怎么改

5.1 推广到“通用规则表”

这道题看起来固定了五个条件,但实际生产中你可能会遇到类似的问题:用户符合哪些条件就能获得对应奖励,奖励金额不同,条件数量还可能动态变化。这种时候,把五个if直接写死就很吃力。

一个更灵活的做法是抽出一个规则表,每条规则包含“触发条件”和“奖励金额”,然后用循环统一判断。比如:

struct BonusRule { int amount; bool (*check)(int avg, int cls, int papers, bool leader, bool west); };

如果你愿意,完全可以定义五个函数分别表示五条规则,然后放进数组里循环执行。这样新增一条规则时,不需要修改主循环逻辑,只要在规则数组里加一项。这种设计模式在真实的企业级开发里非常常见,也就是“策略模式”的简化版。

当然,对这道题来说,五个if更直观,也更不容易出bug。写出可扩展的代码不是目的,目的是让你意识到,简单的题目里也可以埋下设计的种子。

5.2 大数据量场景下的处理方式

如果N从100变成1000万,思路有没有变化?其实没有。边读边算本来就是O(N)复杂度,额外空间O(1),不管N多大,这个方案都能稳定运行。

但如果这时候你还坚持存结构体数组然后排序,那内存消耗就成了问题。假设一个学生结构体占50字节左右,1000万条数据就是500MB,普通OJ根本不可能让你跑过。

所以我在前面强调,读入即处理不仅是代码量更少的问题,它天然具备处理大规模数据的能力。很多初学者意识不到这种思路的价值,直到做海量日志处理或者流式计算项目时,才后悔当年没把这种思维刻进脑子里。

5.3 从竞赛题到业务场景的类比

如果把“谁拿了最多奖学金”映射到现实业务,就是一个典型的“多条件权益计算”场景。比如电商平台给用户发优惠券:

  • 注册满一年且消费金额超过5000,发100元券。
  • VIP用户且近30天有登录,发50元券。
  • 新用户首次下单,发30元券。

每个用户符合哪些条件,就累加哪些券。运营需要知道谁获得的总券额最高,同时也要统计本轮活动发放的总券额。这和奖学金题的结构一模一样。

从这个角度看,这道经典题给的价值并不只是在竞赛里拿分。它教给你的“逐条判断、立即累加、持续维护最大值”这套流程,在很多真实的数据统计场景里都有一模一样的应用。这也是为什么很多高校OJ和培训机构都愿意把它放在入门题单第一道题的原因之一。

6. 最后的实操建议

我在指导别人做这道题时,总习惯让他们先别急着写代码,拿出一张草稿纸,把五个条件用表格画出来,条件列、金额列、判断方向列都写清楚。做完这一步再动手,写起代码来会顺畅很多,也几乎不可能漏掉条件。

如果你已经写完了代码并提交,建议自己再多构造几组边界数据测试,不要只依赖样例。比如:

  • 全部条件都不满足的学生,看他贡献的money是不是0。
  • 平均成绩恰好80、85、90的学生,确认没有拿到对应的奖学金。
  • 论文数为0和论文数为1的对照。
  • 两个学生奖学金相同,确认输出的是先出现的那一个。

这种“自造测试数据”的习惯,比反复提交评测去碰运气要有效得多。

我个人早期的教训是:有一次把“是否西部学生”写成了先读char再比较字符串,结果读到一个换行符,整个判断全部错位,样例都过不了。后来我统一改成string读入,问题立刻消失。从那以后,但凡题目里出现Y/N这种标记,我都不用char去处理。

这道题虽然简单,但把它的每一个细节吃透,比草草写对一次提交更值得。条件如何拆解、数据如何流式处理、边界如何校准、并列如何取舍,这些思维方式的打磨,才是做竞赛题真正沉淀下来的东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询