信息学奥赛刷题笔记:OpenJudge 1.10‘病人排队’的两种解法与避坑指南
在算法竞赛的进阶之路上,排序问题永远是绕不开的经典课题。今天我们要拆解的这道"病人排队"题目,表面看是简单的多关键字排序,实则暗藏多个算法选择的精妙之处。作为NOI/IOI系列赛事的常见题型,这类问题往往成为区分选手水平的关键——能否在有限时间内选择最优策略,往往决定了比赛排名。
这道题的特殊性在于它同时考察了三种核心能力:对排序稳定性的理解、多条件比较的逻辑整合能力,以及根据数据特征选择算法的判断力。在实际竞赛中,类似的问题可能以"医院分诊"、"航班优先级调度"或"任务处理顺序"等不同形式出现,但解题思路却高度相通。接下来,我将通过两种截然不同的解法,带大家深入理解排序算法在真实场景中的应用技巧。
1. 问题重述与核心难点
题目要求对病人登记信息进行特殊排序:年龄≥60岁的老年人群体需要按年龄降序排列(同龄保持登记顺序),而60岁以下的年轻人群体则只需保持原始登记顺序。最终输出所有老年人按规则排序后,再按序输出年轻人的ID。
看似简单的需求背后隐藏着几个技术痛点:
- 稳定性要求:老年人组内年龄相同时必须保持原始输入顺序
- 分组边界:需要准确区分老年人与年轻人的分界点(≥60岁)
- 效率考量:两种解法在不同数据规模下的性能差异显著
- 条件整合:如何将多条件判断转化为可执行的比较逻辑
特别值得注意的是,题目中"保持登记顺序"这一要求直接指向了排序算法的稳定性特性。在竞赛环境中,选手需要瞬间判断出哪些排序算法天生稳定(如归并排序、插入排序),哪些不稳定但可以通过额外处理变稳定(如快速排序)。
2. 解法一:分组后分别排序
2.1 基本思路与实现
这种解法的核心思想是"分而治之"——将输入数据按年龄阈值分为两个独立数组:
struct Patient { char id[10]; int age; }; vector<Patient> elderly; // 老年人组 vector<Patient> young; // 年轻人组对老年人组进行稳定的降序排序,年轻人组保持原样。最后先输出排序后的老年人,再按序输出年轻人。
使用STL的stable_sort实现版本:
bool compareAge(const Patient &a, const Patient &b) { return a.age > b.age; // 降序排列 } // 分组处理 for (int i = 0; i < n; ++i) { if (patients[i].age >= 60) { elderly.push_back(patients[i]); } else { young.push_back(patients[i]); } } // 稳定排序 stable_sort(elderly.begin(), elderly.end(), compareAge);2.2 关键细节与陷阱
这种解法看似直观,但藏着几个容易翻车的坑点:
稳定性选择:
- 错误做法:使用普通sort函数(可能破坏相同年龄元素的原始顺序)
- 正确做法:必须选用stable_sort或其它稳定排序算法
分组边界处理:
- 常见错误:将条件误写为
age > 60(漏掉刚好60岁的病例) - 防御性写法:建议使用
age >= 60并添加注释说明
- 常见错误:将条件误写为
输出顺序验证:
- 测试案例必须包含:多位同年龄老年人、边界值59/60岁病例
- 示例测试数据:
正确输出应为:A004、A001、A002、A005、A0035 A001 60 A002 60 A003 59 A004 85 A005 60
2.3 复杂度分析与适用场景
- 时间复杂度:O(n)分组 + O(m log m)老年人排序(m为老年人数量)
- 空间复杂度:O(n)额外空间
- 最佳场景:
- 老年人比例较低时(如m << n)
- 需要代码可读性优先的教学场景
- 对稳定性要求明确的题目条件
提示:在NOIP/省选级别竞赛中,当n≤1e5时此解法完全可行。但在IOI等高级别赛事遇到n≥1e6时,需考虑更优解法。
3. 解法二:整合单一比较条件
3.1 统一比较函数设计
这种解法的精髓在于将多条件排序规则压缩到单个比较函数中,核心逻辑如下:
- 任何老年人优先级高于任何年轻人
- 两位老年人比较时,年龄大的优先;同龄则按登记顺序
- 两位年轻人比较时,仅按登记顺序
用代码表达这一逻辑:
struct Patient { char id[10]; int age; int regOrder; // 登记序号 }; bool comparePatients(const Patient &a, const Patient &b) { bool aElder = a.age >= 60; bool bElder = b.age >= 60; if (aElder && bElder) { if (a.age != b.age) return a.age > b.age; return a.regOrder < b.regOrder; } if (!aElder && !bElder) { return a.regOrder < b.regOrder; } return aElder; // 只有一方是老人 }3.2 优化后的简洁版本
通过逻辑优化,比较函数可以简化为:
bool comparePatientsOptimized(const Patient &a, const Patient &b) { if ((a.age < 60 && b.age < 60) || a.age == b.age) { return a.regOrder < b.regOrder; } return a.age > b.age; }这种写法将四种情况压缩为两个判断:
- 都是年轻人或同龄老人:比较登记顺序
- 其他情况:直接比较年龄(隐含老年人优先)
3.3 性能对比与选择建议
| 对比维度 | 分组排序法 | 统一比较法 |
|---|---|---|
| 代码复杂度 | 较低(逻辑分离) | 较高(条件整合) |
| 稳定性依赖 | 必须使用稳定排序 | 任意排序算法均可 |
| 时间复杂度 | O(n + m log m) | O(n log n) |
| 空间复杂度 | O(n) | O(1)(原地排序) |
| 最佳数据场景 | 老年人占比<30% | 老年人占比>50% |
| 扩展性 | 修改单组规则方便 | 多条件修改更灵活 |
在实战中选择策略:
- 数据特征明显时:老年人极少用分组法,老年人多用统一法
- 规则可能变更时:统一比较法更容易调整条件权重
- 内存受限环境:优先选择原地排序的统一比较法
4. 调试技巧与验证方法
4.1 边界测试用例设计
针对这类排序问题,必须设计覆盖所有边界的测试数据:
- 年龄边界案例:
3 B001 59 B002 60 B003 61 - 同年龄老年人:
4 C001 70 C002 70 C003 70 C004 35 - 极值测试:
- 全老年人/全年轻人情况
- 最大规模数据(如1e5个病例)
4.2 调试输出技巧
在竞赛环境中,添加调试输出是快速定位问题的有效手段:
void debugPrint(const vector<Patient> &patients) { cerr << "Current Order:\n"; for (const auto &p : patients) { cerr << p.id << "(" << p.age << ", reg:" << p.regOrder << ") "; } cerr << "\n\n"; } // 在排序前后调用 debugPrint(patients);4.3 自动化验证脚本
对于高频出现的排序题型,建议准备验证脚本:
# verify.py import sys def check_output(input_file, output_file): # 实现自动校验逻辑 pass if __name__ == "__main__": check_output(sys.argv[1], sys.argv[2])使用方式:
./my_program < input.txt > output.txt python verify.py input.txt output.txt5. 竞赛应用扩展
5.1 变种题型应对
掌握核心思路后,可解决一系列相似问题:
多级优先级排序:
- 例如:VIP客户>老年人>普通客户
- 解决方案:在比较函数中添加优先级层次
动态规则变化:
- 例如:疫情期间老年人优先级调整
- 应对策略:将比较条件参数化
混合排序规则:
bool compareMixed(const Data &a, const Data &b) { if (a.priority != b.priority) return a.priority > b.priority; if (a.type == EMERGENCY && b.type == EMERGENCY) return a.arrivalTime < b.arrivalTime; ... }
5.2 性能优化技巧
当数据量达到1e6级别时,需要特别优化:
输入输出加速:
ios::sync_with_stdio(false); cin.tie(nullptr);内存访问优化:
- 使用连续内存存储结构体
- 避免链表等非连续结构
算法选择:
- 数据基本有序时,考虑插入排序变种
- 范围有限时(如年龄0-150),可用桶排序
5.3 常见错误汇总
根据竞赛统计,高频错误包括:
- 未处理同龄老人的稳定性(使用非稳定排序)
- 比较函数未满足严格弱序(导致运行时错误)
// 错误示例:未处理所有情况 bool cmp(Patient a, Patient b) { if (a.age >= 60 && b.age < 60) return true; if (a.age < 60 && b.age >= 60) return false; return a.age > b.age; // 漏掉同龄年轻人情况 } - 未正确记录原始顺序(需额外存储regOrder字段)
- 边界条件处理不当(如刚好60岁的分类)