题解:洛谷 B3968 成绩排序

【题目来源】

洛谷:B3968 [GESP202403 五级] 成绩排序 - 洛谷

【题目描述】

有 名同学,每名同学有语文、数学、英语三科成绩。你需要按如下规则对所有同学的成绩从高到低排序:

  1. 比较总分,高者靠前;
  2. 如果总分相同,则比较语文和数学两科总分,高者靠前;
  3. 如果仍相同,则比较语文和数学两科的最高分,高者靠前;
  4. 如果仍相同,则二人并列。

你需要输出每位同学的排名,如遇 \(x\) 人并列,则他们排名相同,并留空后面的 \(x-1\) 个名次。例如,有 \(3\) 名同学并列第 \(1\),则后一名同学自动成为第 \(4\) 名。

【输入】

第一行一个整数 \(N\),表示同学的人数。

接下来 \(N\) 行,每行三个非负整数 \(c_i,m_i,e_i\) 分别表示该名同学的语文、数学、英语成绩。

【输出】

输出 \(N\) 行,按输入同学的顺序,输出他们的排名。

注意:请不要按排名输出同学的序号,而是按同学的顺序输出他们各自的排名

【输入样例】

6
140 140 150
140 149 140
148 141 140
141 148 140
145 145 139
0 0 0

【输出样例】

1
3
4
4
2
6

【核心思想】

  1. 问题分析:给定 \(N\) 名同学的语文、数学、英语成绩,需要按四重优先级排序(总分 → 语数总分 → 语数最高分 → 编号),并处理并列情况(并列者排名相同,后续排名跳过)。最后按输入顺序输出每位同学的排名。这是一个多关键字自定义排序 + 并列排名处理问题。

  2. 算法选择

    • 结构体封装:将学生成绩、派生指标和排名封装为结构体
    • 双重排序:先按成绩规则排序计算排名,再按编号排序恢复输入顺序输出
  3. 关键步骤

    • 读入数据:读取 \(N\) 和每位同学的 \(c_i, m_i, e_i\)
    • 预处理派生指标
      • \(tot = c_i + m_i + e_i\)(总分)
      • \(ys = c_i + m_i\)(语数总分)
      • \(max\_ys = \max(c_i, m_i)\)(语数最高分)
    • 第一次排序\(cmp\)):按 \(tot \downarrow \to ys \downarrow \to max\_ys \downarrow \to id \uparrow\)
    • 计算排名(处理并列):
      • \(cnt \leftarrow 1\)
      • 遍历排序后的学生:
        • 若当前学生与前一名在 \(tot, ys, max\_ys\) 上均相同:\(rk = rk_{prev}\)(并列)
        • 否则:\(rk = cnt\)(新排名)
        • \(cnt \leftarrow cnt + 1\)
    • 第二次排序\(cmp2\)):按 \(id\) 升序恢复输入顺序
    • 输出结果:按 \(id\) 顺序输出 \(rk\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \log N)\),两次排序
    • 空间复杂度:\(O(N)\),存储学生数组
  5. 多关键字排序与并列处理的核心思想

    • 派生指标预处理:将排序所需的复合指标(语数总分、语数最高分)在读入时计算并存储,避免排序时重复计算
    • 并列判定条件:仅比较前三项指标(总分、语数总分、语数最高分),编号仅用于打破平局,不参与并列判定
    • 排名计数器的跳跃\(cnt\) 始终递增,但 \(rk\) 在并列时保持不变,实现"留空后面的 \(x-1\) 个名次"的效果
    • 双重排序策略:第一次排序确定排名,第二次排序恢复输入顺序,确保按"输入顺序"输出排名而非按排名输出学生
    • 适用于复杂排序规则、并列处理、按原始顺序输出的多关键字排序类问题

【算法标签】

普及- #其他排序

【代码详解】

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

int n;  // 学生人数

// 学生结构体,存储各科成绩和相关信息
struct Stu {
    int yw, sx, yy;  // 语文、数学、英语成绩
    int tot;         // 总分
    int ys;          // 语文+数学总分
    int max_ys;      // 语文和数学中的最高分
    int id;          // 学生编号
    int rk;          // 最终排名
};

Stu stu[10005];  // 学生数组

// 排序比较函数1:用于成绩排序
bool cmp(Stu x, Stu y) {
    if (x.tot != y.tot) return x.tot > y.tot;        // 规则1:总分高的在前
    else if (x.ys != y.ys) return x.ys > y.ys;       // 规则2:语文+数学高的在前
    else if (x.max_ys != y.max_ys) return x.max_ys > y.max_ys; // 规则3:语文或数学单科高的在前
    else return x.id < y.id;                         // 规则4:编号小的在前
}

// 排序比较函数2:用于按学生编号排序输出
bool cmp2(Stu x, Stu y) {
    return x.id < y.id;
}

int main() {
    cin >> n;  // 输入学生人数

    // 输入并处理每个学生的成绩数据
    for (int i = 1; i <= n; i++) {
        int ci, mi, ei;
        cin >> ci >> mi >> ei;
        stu[i].id = i;           // 记录学生编号
        stu[i].yw = ci;           // 语文成绩
        stu[i].sx = mi;           // 数学成绩
        stu[i].yy = ei;           // 英语成绩
        stu[i].ys = ci + mi;      // 语文+数学总分
        stu[i].max_ys = max(ci, mi); // 语文或数学最高分
        stu[i].tot = ci + mi + ei;   // 三科总分
    }

    // 第一次排序:按成绩规则排序
    sort(stu + 1, stu + n + 1, cmp);

    int cnt = 1;  // 排名计数器
    // 计算每个学生的实际排名(处理并列情况)
    for (int i = 1; i <= n; i++) {
        // 检查是否与前一名学生成绩完全相同
        if (i > 1 && stu[i].tot == stu[i-1].tot &&
            stu[i].ys == stu[i-1].ys &&
            stu[i].max_ys == stu[i-1].max_ys) {
            stu[i].rk = stu[i-1].rk;  // 并列排名
        } else {
            stu[i].rk = cnt;          // 新排名
        }
        cnt++;  // 排名计数器递增
    }

    // 第二次排序:按学生编号排序,方便输出
    sort(stu + 1, stu + n + 1, cmp2);

    // 输出每个学生的排名
    for (int i = 1; i <= n; i++) {
        cout << stu[i].rk << endl;
    }

    return 0;
}

【运行结果】

6
140 140 150
140 149 140
148 141 140
141 148 140
145 145 139
0 0 0
1
3
4
4
2
6
posted @ 2026-07-17 15:40  团爸讲算法  阅读(11)  评论(0)    收藏  举报