
NOIP2009普及组真题解析用C搞定分数线划定从冒泡到STL sort的四种解法在信息学奥赛的备赛过程中排序算法是每个选手必须掌握的核心技能。2009年NOIP普及组的分数线划定题目恰好为我们提供了一个绝佳的练习平台让我们能够从多个角度理解排序算法的实际应用。这道题目看似简单却蕴含着丰富的算法思想和工程实践价值。对于初学者而言这道题的价值不仅在于完成题目要求更在于通过不同解法的对比深入理解算法效率、代码可读性和工程实践之间的平衡。我们将从最基础的冒泡排序开始逐步过渡到更高级的STL算法完整呈现一个竞赛选手的思维进化过程。1. 问题分析与基础解法1.1 题目要求解析题目要求我们根据考生的报名号和成绩确定面试的分数线。具体规则是计划录取人数的150%向下取整名考生的分数即为分数线所有不低于该分数线的考生都将进入面试。当有同分考生时按报名号升序排列。关键数据范围考生人数n1 ≤ n ≤ 5000计划录取人数m1 ≤ m ≤ n报名号k1000 ≤ k ≤ 9999成绩s1 ≤ s ≤ 1001.2 冒泡排序实现作为最直观的排序方法冒泡排序虽然效率不高O(n²)但对于n≤5000的数据规模完全足够。我们先来看基础实现#include iostream using namespace std; int main() { int k[5005], s[5005], n, m; cin n m; for(int i 1; i n; i) cin k[i] s[i]; // 冒泡排序 for(int i 1; i n - 1; i) for(int j 1; j n - i; j) { if(s[j] s[j1] || (s[j] s[j1] k[j] k[j1])) { swap(s[j], s[j1]); swap(k[j], k[j1]); } } int line s[int(m*1.5)]; int ct 0; for(int i 1; i n; i) if(s[i] line) ct; cout line ct endl; for(int i 1; i ct; i) cout k[i] s[i] endl; return 0; }这种解法的优势在于代码逻辑直观易于理解不依赖任何高级语言特性适合算法初学者理解排序过程但缺点也很明显排序效率较低代码可读性和维护性较差数据耦合度高报名号和成绩分开存储2. 结构体与自定义排序2.1 使用结构体组织数据为了提高代码的可读性和可维护性我们引入结构体来封装考生信息struct Student { int id; // 报名号 int score; // 成绩 };这种封装方式使得相关数据被组织在一起大大提高了代码的清晰度。同时我们可以利用C的sort函数进行高效排序。2.2 自定义比较函数STL的sort函数允许我们通过自定义比较函数来实现复杂的排序规则bool compare(const Student a, const Student b) { if(a.score ! b.score) return a.score b.score; // 成绩降序 return a.id b.id; // 报名号升序 }完整实现代码如下#include iostream #include algorithm using namespace std; struct Student { int id, score; }; bool compare(const Student a, const Student b) { if(a.score ! b.score) return a.score b.score; return a.id b.id; } int main() { Student stu[5005]; int n, m; cin n m; for(int i 0; i n; i) cin stu[i].id stu[i].score; sort(stu, stu n, compare); int line stu[int(m*1.5) - 1].score; int count 0; for(int i 0; i n; i) if(stu[i].score line) count; cout line count endl; for(int i 0; i count; i) cout stu[i].id stu[i].score endl; return 0; }这种解法的优势代码结构清晰可读性强排序效率高O(nlogn)数据组织更合理3. 进阶STL算法与Lambda表达式3.1 使用Lambda表达式C11引入的Lambda表达式可以让我们的代码更加简洁sort(stu, stu n, [](const Student a, const Student b) { return a.score b.score || (a.score b.score a.id b.id); });3.2 稳定排序的应用在某些特殊情况下我们可能需要保持相等元素的原始顺序。这时可以使用stable_sort// 先按报名号排序稳定 stable_sort(stu, stu n, [](const Student a, const Student b) { return a.id b.id; }); // 再按成绩排序稳定 stable_sort(stu, stu n, [](const Student a, const Student b) { return a.score b.score; });虽然对于本题来说普通的sort已经足够但了解stable_sort的特性对于解决更复杂的问题很有帮助。4. 性能分析与优化4.1 不同解法的性能对比解法类型时间复杂度空间复杂度代码复杂度适用场景冒泡排序O(n²)O(n)低教学、小规模数据STL sortO(nlogn)O(n)中通用场景稳定排序O(nlogn)O(n)中需要保持原始顺序计数排序O(nk)O(k)高数据范围小的情况4.2 计数排序的特殊解法当成绩范围有限如本题1-100分时可以使用计数排序实现O(n)时间复杂度的解法#include iostream #include vector #include algorithm using namespace std; int main() { vectorint scores[101]; // 每个分数对应一个报名号列表 int n, m; cin n m; for(int i 0; i n; i) { int id, score; cin id score; scores[score].push_back(id); } // 对每个分数的报名号列表排序 for(int i 1; i 100; i) sort(scores[i].begin(), scores[i].end()); int total 0, line 0; for(int i 100; i 1; --i) { total scores[i].size(); if(total int(m * 1.5)) { line i; break; } } // 重新计算实际人数可能有更多人同分 total 0; for(int i 100; i line; --i) total scores[i].size(); cout line total endl; for(int i 100; i line; --i) for(int id : scores[i]) cout id i endl; return 0; }这种解法在特定条件下成绩范围小性能最优但代码复杂度较高通用性不如基于比较的排序。5. 工程实践建议5.1 代码风格与可读性在竞赛编程中良好的代码风格同样重要使用有意义的变量名如studentCount而非n适当添加注释说明关键步骤保持一致的缩进和代码布局5.2 调试技巧排序类题目常见的调试问题边界条件处理如第m*1.5名正好有多个同分考生比较函数的正确性确保满足严格弱序数组下标越界特别是从0开始还是从1开始5.3 测试用例设计针对本题应该准备以下类型的测试用例常规情况有明确分数线同分考生较多的情况边界情况n和m的最小/最大值所有考生同分的情况例如// 测试用例1常规情况 5 3 1001 90 1002 85 1003 95 1004 90 1005 80 // 测试用例2多人同分 6 4 2001 80 2002 80 2003 85 2004 75 2005 85 2006 90在实际比赛中建议先手动计算预期结果再与程序输出对比。