最近帮一个学弟排查洛谷P1068 [NOIP 2009 普及组] 分数线划定,他排序写了,样例也过了,交上去却一直不是满分。拿去对照之后发现,问题几乎都出在同一个地方:他把“m * 1.5 向下取整”当成了最终要输出的人数。这个理解看似只差一步,实际上已经偏离了题目规则,最后名单自然也不对。
P1068是一道很适合用来练结构体排序和边界条件的普及组题目,也是CSP-J备考过程中很多人会反复遇到的经典模型。只看标题,会觉得这是一道“排个序、输出几个人”的水题;但真正动手写过之后,你会发现它对读题能力的要求比排序本身更高。这篇文章会从题目规则拆解开始,讲清楚分数线与录取人数的关系,再给出C++和Python两种可提交的完整实现,最后把几个容易让人卡住的边界点一起捋一遍。
1. 1.5倍入围线的本质:为什么线以第k名为准,人数却可能更多
1.1 原题要你做的事到底是什么
先把题面转换成人话。大概意思是:有n名选手,原本计划录取m人。由于报名人数相对较多,实际流程并不直接录取前m名,而是把计划人数乘以1.5后向下取整,得到一个新的位置k。
接下来,按成绩从高到低排序,如果成绩相同,就按报名号从小到大排序。排序结束后,取第k名选手的成绩作为“面试分数线”。
到这里都很直观,麻烦的是下一句:所有成绩不低于这个分数线的选手,全部进入面试。
这句话意味着,最终人数并不一定等于k,而是可能大于k。因为第k名选手的分数,可能和第k+1名、第k+2名完全一样。只要这些人都达到分数线,按照同分规则就都应该被输出。
很多第一次写这题的选手会形成一种直觉:既然是“录取m人”,那我排序之后取前m个不就行了?如果题目说的是直接录取前m人,那确实没问题。但P1068用的是“先画一条分数线,过线的人全部入围”的规则,所以不能直接用m,也不能直接用k去截断输出。
1.2 一个边界序列直接说明问题
假设当前输入是n=6,m=3。先算一下位置:m乘1.5等于4.5,向下取整后,k=4。
如果按成绩排序后出现下面这个序列:
| 排序位置 | 笔试成绩 | 是否进入面试 |
|---|---|---|
| 1 | 95 | 进入面试 |
| 2 | 90 | 进入面试 |
| 3 | 88 | 进入面试 |
| 4 | 80 | 进入面试 |
| 5 | 80 | 进入面试 |
| 6 | 66 | 淘汰 |
第4名选手的成绩是80分,所以面试分数线就是80。此时第5名选手的成绩也是80分,和第4名同分,按照规则也必须进入面试。最终实际人数不是4,而是5。
如果程序写成了这样:
cpp复制int ans = m * 3 / 2;
for (int i = 0; i < ans; i++) {
// 直接输出前ans个人
}
那么第5名就会被漏掉。在一个n还比较小的测试数据里,这种错误可能不影响样例,但一旦遇到分数并列的情况,结果就会出错。
这也解释了为什么这题虽然叫“分数线划定”,却不能简单地翻译成“输出前k名”。关键在于,线是画在“分数”上的,不是画在“名次”上的。
1.3 “报名号升序”在这道题里的角色
排序规则中还有一句话:成绩相同的人,按报名号由小到大排列。这句话不是摆设。
设想这样一个场景:两个选手都是85分,报名号分别是1002和1001。如果排序时只按成绩降序,而不看报名号,那么这两个人在最终名单里的先后顺序是不确定的。哪怕第k名已经落在85分这一组里,后面所有85分的人也要一起输出,此时如果他们的相对顺序没有按报名号排好,输出就会和标准答案不一致。
所以在写比较器时,成绩降序是主关键字,报名号升序是次关键字。两个条件要一起写进排序规则,不能只处理其中一个。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 双关键字排序:把“成绩降序、报名号升序”落进代码
2.1 C++里写结构体比较器
P1068的数据规模并不大,但结构体排序是普及组很常见的代码基本功。建议直接用C++标准库里的sort,不要自己手写排序。
先定义结构体:
cpp复制struct Player {
int id; // 报名号
int score; // 笔试成绩
};
比较器可以写成:
cpp复制bool cmp(const Player& a, const Player& b) {
if (a.score != b.score) return a.score > b.score;
return a.id < b.id;
}
sort函数内部会反复调用cmp,用它来判断a是否应该排在b前面。当前两个选手分数不同时,分数线高的排在前,所以用大于号;分数相同时,报名号小的排在前,所以用小于号。
有基础的同学可能想写成一行:
cpp复制sort(players.begin(), players.end(), [](const Player& a, const Player& b) {
return a.score != b.score ? a.score > b.score : a.id < b.id;
});
这种lambda写法也没有问题,尤其是现在不少C++版本都支持C++11以上的特性,洛谷上选择C++17完全可以跑。
但有一点要提醒:第二层比较千万别写成 a.id > b.id。这个错误在初级选手里出现频率很高,一旦写反,输出顺序和题目要求正好相反,偏偏在有些数据里因为成绩刚好没有重复,样例依然能过,导致调错时更隐蔽。
2.2 比较器里的严格弱序问题
很多讲排序的文章会提“严格弱序”
