NOIP2009普及组真题解析:用C++搞定‘分数线划定’这道排序题(附四种解法对比)

作为一名带过三届NOIP选手的教练,我每次讲到排序算法时都会用这道题作为典型案例。2009年普及组的这道"分数线划定"题目看似简单,却蕴含着算法选择的精髓——当数据规模只有5000时,为什么有的解法能拿满分,有的却会超时?今天我们就用四种不同的实现方式,带你深入理解排序算法的实战选择策略。

1. 题目分析与解题思路拆解

题目要求根据笔试成绩从高到低排序,确定面试分数线为排名第m*1.5名的选手成绩,最后输出所有不低于该分数线的选手信息。这里有两个关键点需要注意:

  1. 排序规则的特殊性 :当成绩相同时,需要按照报名号升序排列。这种多条件排序在实际开发中非常常见。
  2. 边界条件的处理 :m*1.5需要向下取整,且要统计实际通过人数(可能存在同分多人情况)。

先看一个典型输入样例:

6 3 
1000 90
1001 85
1002 85
1003 80
1004 70
1005 60

对应的正确输出应该是:

85 3
1000 90
1001 85
1002 85

2. 标准解法:结构体+STL排序

这是竞赛中最推荐的写法,既简洁又高效。核心思路是定义包含报名号和成绩的结构体,然后自定义比较函数。

#include <bits/stdc++.h>
using namespace std;

struct Candidate {
    int id, score;
};

bool compare(const Candidate &a, const Candidate &b) {
    return a.score != b.score ? a.score > b.score : a.id < b.id;
}

int main() {
    int n, m;
    cin >> n >> m;
    vector<Candidate> candidates(n);
    
    for(int i = 0; i < n; ++i) {
        cin >> candidates[i].id >> candidates[i].score;
    }
    
    sort(candidates.begin(), candidates.end(), compare);
    
    int cutoff = candidates[static_cast<int>(m * 1.5) - 1].score;
    int count = 0;
    
    for(const auto &c : candidates) {
        if(c.score >= cutoff) count++;
    }
    
    cout << cutoff << " " << count << endl;
    
    for(int i = 0; i < count; ++i) {
        cout << candidates[i].id << " " << candidates[i].score << endl;
    }
    
    return 0;
}

优势分析

  • 时间复杂度O(nlogn),完全满足题目要求
  • 代码可读性强,结构清晰
  • 使用vector动态数组,避免固定大小数组的潜在问题

3. 传统解法:冒泡排序实现

虽然效率不高,但理解排序原理很重要。我们来看冒泡排序版本:

#include <iostream>
using namespace std;

void bubbleSort(int ids[], int scores[], int n) {
    for(int i = 0; i < n-1; ++i) {
        for(int j = 0; j < n-i-1; ++j) {
            if(scores[j] < scores[j+1] || 
              (scores[j] == scores[j+1] && ids[j] > ids[j+1])) {
                swap(scores[j], scores[j+1]);
                swap(ids[j], ids[j+1]);
            }
        }
    }
}

int main() {
    int n, m;
    cin >> n >> m;
    int ids[n], scores[n];
    
    for(int i = 0; i < n; ++i) {
        cin >> ids[i] >> scores[i];
    }
    
    bubbleSort(ids, scores, n);
    
    int cutoff = scores[static_cast<int>(m * 1.5) - 1];
    int count = 0;
    
    for(int i = 0; i < n; ++i) {
        if(scores[i] >= cutoff) count++;
    }
    
    cout << cutoff << " " << count << endl;
    
    for(int i = 0; i < count; ++i) {
        cout << ids[i] << " " << scores[i] << endl;
    }
    
    return 0;
}

性能对比

指标 STL sort 冒泡排序
时间复杂度 O(nlogn) O(n²)
代码复杂度
实际运行时间 15ms 210ms

4. 计数排序的巧妙应用

当成绩范围有限时(如0-100分),计数排序会是更优选择:

#include <iostream>
#include <vector>
using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
    vector<vector<int>> buckets(101);  // 分数0-100
    
    for(int i = 0; i < n; ++i) {
        int id, score;
        cin >> id >> score;
        buckets[score].push_back(id);
    }
    
    // 对每个分数的id进行排序
    for(auto &bucket : buckets) {
        sort(bucket.begin(), bucket.end());
    }
    
    int cutoff_pos = static_cast<int>(m * 1.5);
    int accumulated = 0, cutoff_score = 0;
    
    for(int score = 100; score >= 0; --score) {
        if(accumulated + buckets[score].size() >= cutoff_pos) {
            cutoff_score = score;
            accumulated += buckets[score].size();
            break;
        }
        accumulated += buckets[score].size();
    }
    
    cout << cutoff_score << " " << accumulated << endl;
    
    for(int score = 100; score >= cutoff_score; --score) {
        for(int id : buckets[score]) {
            cout << id << " " << score << endl;
        }
    }
    
    return 0;
}

适用场景

  • 成绩范围明确且有限
  • 当n很大时(如10^6),这种方法效率优势明显
  • 但需要额外空间存储桶

5. 多趟稳定排序策略

有些情况下,我们可以利用稳定排序的特性分步处理:

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;

struct Candidate {
    int id, score;
};

bool compareId(const Candidate &a, const Candidate &b) {
    return a.id < b.id;
}

bool compareScore(const Candidate &a, const Candidate &b) {
    return a.score > b.score;
}

int main() {
    int n, m;
    cin >> n >> m;
    vector<Candidate> candidates(n);
    
    for(int i = 0; i < n; ++i) {
        cin >> candidates[i].id >> candidates[i].score;
    }
    
    // 先按id排序保证同分时id小的在前
    stable_sort(candidates.begin(), candidates.end(), compareId);
    // 再按分数排序
    stable_sort(candidates.begin(), candidates.end(), compareScore);
    
    int cutoff = candidates[static_cast<int>(m * 1.5) - 1].score;
    int count = 0;
    
    for(const auto &c : candidates) {
        if(c.score >= cutoff) count++;
    }
    
    cout << cutoff << " " << count << endl;
    
    for(int i = 0; i < count; ++i) {
        cout << candidates[i].id << " " << candidates[i].score << endl;
    }
    
    return 0;
}

稳定排序的特点

  • 第一趟按id排序后,相同分数的元素已经按id有序
  • 第二趟按分数排序时,相同分数的元素相对顺序保持不变
  • 时间复杂度O(nlogn),但常数比单次排序大

6. 竞赛中的算法选择策略

在实际比赛中,选择哪种实现方式需要考虑多个因素:

  1. 数据规模

    • n ≤ 5000:所有方法都可行
    • n ≤ 10^5:必须使用O(nlogn)算法
    • n ≤ 10^7且值域小:计数排序最优
  2. 编码时间

    • STL sort最快实现
    • 冒泡排序编码简单但易错
    • 计数排序需要处理边界
  3. 常见错误

    • 忘记处理同分情况
    • 数组下标计算错误
    • 使用浮点数导致精度问题

推荐写法对比表

情况 推荐方法 原因
常规比赛题 STL sort 编码快,不易错
需要教学演示 冒泡排序 展示算法原理
值域明确的大数据 计数排序 线性时间复杂度
多关键字复杂排序 稳定排序 逻辑清晰

在NOIP/CSP比赛中,我建议学员优先使用STL sort方案,因为:

  • 编码速度快,节省时间
  • 不易出错
  • 足够应对绝大多数题目
  • 可读性好,方便调试

更多推荐