ACM 一个月学习计划 · DAY 11

Day 11 · 结构体:把相关的数据打包

一个学生的「姓名 + 语文 + 数学 + 英语」本来要开四个数组,今天学会用一个 struct 全装下

今日目标:会用 struct 组织「一个学生的姓名+成绩」这类复合数据,能按规则排序并输出
预计用时:约 3 小时(含做题)  难度:入门 ~ 普及-
题目来源:洛谷 P5740 / P1093 / P1104 / P5143
← Day 10 · 函数:把代码写成模块

⏱今日时间安排

0 - 25 min看懂「为什么要结构体」——五个平行数组的痛点(第 1 节)
25 - 55 min定义语法 + 结构体数组 + 成员访问(第 2、3 节)
55 - 80 min今天的核心套路:读入 → 算派生字段 → 排序 → 输出(第 4 节)
80 - 180 min做 4 道题,每题限时 25 分钟
180 - 210 min对照本文题解,把没做出来的补掉 + 做自测
💡 和昨天内容的衔接

Day 10 你学了函数:把一段逻辑打包成有名字的一块。今天要学的是另一种「打包」—— 把几个相关的数据变量打包成一个整体。它们思想是一样的:给一堆东西起个名字,让代码更像人话。

昨天写 double dis(double x1,double y1,double x2,double y2) 时,参数已经开始变多了吧? 今天的结构体正是用来解决「参数/数据太多,散落一地」这个问题的。

1先看没有结构体时有多难受

一个真实的场景

老师给你 3 个同学的信息,要你按总分从高到低排序输出。信息长这样:

senpai 114 51 4
lxl 114 10 23
fafa 51 42 60

每行是:姓名、语文、数学、英语。现在问题来了——用你已经学过的知识,怎么存?

笨办法:开四个平行数组

string name[1005];                    // 姓名放一个数组
int chinese[1005], math[1005], english[1005];   // 三科成绩各放一个数组
int total[1005];                      // 总分再放一个

看着还行?那你试试按总分排序:

// 想交换第 i 个和第 j 个同学?
swap(name[i], name[j]);
swap(chinese[i], chinese[j]);
swap(math[i], math[j]);
swap(english[i], english[j]);
swap(total[i], total[j]);            // 少写一个就全乱了!
🚫 平行数组的三个致命问题

1. 一交换就要写 5 行,漏掉任何一个数组,这个同学的信息就「串味」了——姓名还是小明的,成绩变成小红的了。

2. 数不清:以后字段变成 10 个(还要加身高、班级、准考证号),交换就要写 10 行。

3. 没法把「一个同学」当整体传给函数。你想写一个 printStudent(i),函数签名得写成 printStudent(string, int, int, int, int),五个参数排着队,谁是谁全靠背。

🎯 结构体要解决的问题就一句话

把「本来就属于同一个东西的若干变量」,绑成一个整体,给它们起一个共同的名字。

「小明的姓名、小明的语文、小明的数学、小明的英语」这四样东西,本来就是一伙的, 凭什么要拆到四个数组里去?让它们待在一起,交换的时候整包一起换,多省事。

打比方

平行数组就像把一个人的身份证、学生证、银行卡分三个抽屉放——用的时候要去三个抽屉翻,搬家还要三个抽屉一起搬。

结构体就像一个文件袋,上面写着「小明」,里面装着他的所有材料。要交材料就整个袋子递过去, 要换人就整个袋子换掉。不存在「只换了半个袋子」这种事。

2结构体的定义与使用

语法长这样

struct Student {              // struct 关键字 + 结构体名字(自己起)
    string name;              // 成员 1:姓名
    int chinese;              // 成员 2:语文
    int math;                 // 成员 3:数学
    int english;              // 成员 4:英语
    int total;                // 成员 5:总分(自己算出来的,叫"派生字段")
};                            // ★★★ 这个分号千万别丢!★★★
⚠️ 三个最容易错的地方

① 末尾的分号 }; 必须有。这是 C++ 里极少数「花括号后面还要跟分号」的地方。 忘了它,编译器会报一堆莫名其妙的错(往往报在后面好几行,让你找不到北)。

② 结构体名字首字母习惯大写(Student、Person、Point), 变量名小写(a、p)。这不是语法要求,但是全世界的习惯,照着写别人一眼就懂。

③ 定义只是「画图纸」,不占内存。struct Student{...}; 只是告诉编译器「以后会有这么一种东西」, 真正占内存的是下面声明的变量。

定义之后怎么用

把结构体名字当成一种新的「类型」,像用 int 一样用它:

Student a;              // 一个学生
Student b[1005];        // 1005 个学生 —— 这叫「结构体数组」

cin >> a.name >> a.chinese >> a.math >> a.english;   // 输入
a.total = a.chinese + a.math + a.english;             // 算总分
cout << a.name << " " << a.total << endl;              // 输出
🎯 那个「点」是什么

a.name 读作「a 的 name」,意思就是从 a 这个文件袋里,取出 name 这一份材料。

这个点号 . 叫成员访问运算符。它是结构体的标志——看到 ×××.×××, 就知道前面是个结构体变量,后面是它的一个成员。

关键区别:a 是一个整体(一个学生),a.name 是一个普通的 string 变量。 你不能 cout << a,但可以 cout << a.name。

结构体的成员可以是什么?

成员类型例子说明
基本类型int age; double score;最常用
stringstring name;今天的 P5740 / P1104 都要用
数组int score[5];固定几科成绩时很方便
另一个结构体Date birthday;结构体可以套娃
构造函数Student(){ total = 0; }进阶用法,今天用不到
💡 现在不用背这些

今天四道题只用到前两种:string 和 int。等遇到了再回来看这张表。

3结构体数组:今天真正的主角

声明与访问

几乎每道结构体题都是「n 个结构体,排个序,输出几个」。所以结构体数组是最常用的形态:

struct Student {
    string name;
    int chinese, math, english, total;
} a[1005];                          // ★ 定义的同时顺便把数组也声明了

// 也可以分开写:
struct Student { ... };
Student a[1005];                    // 效果完全一样
💡 两种写法都对,看题目习惯

Day 6 讲数组时你就知道了:数组开在 main 外面(全局)会自动清零。 结构体数组开在全局也一样,所有成员自动是 0 / 空字符串,省事又安全。今天四道题我都开在全局。

访问第 i 个学生的姓名

结构体数组的每个元素都是一个完整的结构体,所以访问方式是「下标 + 点 + 成员」:

a[i].name        // 第 i 个学生的姓名
a[i].total       // 第 i 个学生的总分
a[i].chinese     // 第 i 个学生的语文成绩
⚠️ 括号的顺序不能反

正确:a[i].name  ❌ 错误:a.name[i](除非 name 真的定义成了数组)

记忆口诀:先锁定「第几个」,再取「哪一项」。先 a[i] 找到这个学生, 再从里面 .name 拿姓名。

完整骨架:读入 n 个学生

int n;
cin >> n;
for(int i = 0; i < n; i++){
    cin >> a[i].name >> a[i].chinese >> a[i].math >> a[i].english;
    a[i].total = a[i].chinese + a[i].math + a[i].english;   // ★ 顺手把总分算出来
}
🎯 「派生字段」这个动作,是今天的核心套路

题目只给你原始数据(三科成绩),但排序/比较需要的是加工过的数据(总分)。

所以读入的时候顺手把总分算出来存进结构体,后面排序时直接用 a[i].total, 不用每次都现算。算一次、用很多次——这就是「派生字段」的价值。

4结构体排序:比较规则怎么写

为什么结构体不能直接比大小

对 int 来说,「谁大谁小」是天然确定的。但两个 Student 谁应该排在前面? 比总分?比语文?比学号?编译器不知道,必须你来告诉它。

昨天的 int 数组排序是这样:

sort(a, a + n);                     // 从小到大,编译器懂

结构体要额外给一个「比较函数」当裁判:

bool cmp(const Student &x, const Student &y){
    return x.total > y.total;       // 返回 true:x 应该排在 y 前面
}

sort(a, a + n, cmp);                // 把裁判交给 sort
🎯 cmp 的唯一规则,背下来

cmp(x, y) 返回 true,意思就是「x 排在 y 前面」。

所以 return x.total > y.total; = 「总分大的排前面」= 降序。
想升序就写 return x.total < y.total;。

别去背「大于还是小于」,每次写的时候问自己一句:「我想让谁在前面?」

多关键字:一层一层往下比

很多题目不只一个排序条件,比如「总分高的在前;总分一样时语文高的在前;都一样时学号小的在前」。 写法就是一串 if,一层一层筛:

bool cmp(const Student &x, const Student &y){
    if(x.total != y.total) return x.total > y.total;         // ① 总分降序
    if(x.chinese != y.chinese) return x.chinese > y.chinese;  // ② 语文降序
    return x.id < y.id;                                      // ③ 学号升序
}
💡 注意每一行的 !=

if(x.total != y.total) 的意思是「总分不一样才轮到我做决定」。 一样的话就跳过这一行,交给下一个条件。

如果写成 if(x.total > y.total) return true; 就错了—— 总分相等时会直接跳到 return x.id < y.id;, 于是「总分小的」也会因为学号小而被判为排在前面。这是新手最常犯的错。

为什么参数写 const Student &x

这是 C++ 里传结构体的标准姿势,两个符号各管一件事:

写法含义好处
Student x把整个结构体复制一份传进来❌ 每个元素都复制,慢
Student &x传引用(别名),不复制快
const Student &x传引用 + 不许改✅ 又快又安全,标准写法

今天的 cmp 请统一写 const Student &x。等以后结构体里字段变多了,这个习惯能帮你省下大量时间。

5四道例题详解

下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译并跑过官方样例,输出是你看到的真实结果。

P5740 【深基7.例9】最厉害的学生 普及- · 结构体 + 求最值

题意:给出 N 个同学的姓名和三科成绩,找出总分最高的那位, 输出他的姓名和原始三科成绩(注意:不是输出总分,是输出原始的三科分数)。 如果有多个人总分并列最高,输出输入靠前的那位。

⚠️ 数据范围 / 关键约束

1 ≤ N ≤ 1000;姓名是长度不超过 8 的纯小写字母字符串;三科成绩都是不超过 150 的自然数。

成绩上限 150,三科总分最大 450,int 完全够。

思路

这题是结构体的「Hello World」,用打擂台的思路就够了,不需要真的排序:

  1. 把 N 个同学读进结构体数组,顺手算出每个人的 total。
  2. 假设第 0 个同学最厉害(best = 0)。
  3. 从第 1 个开始逐个挑战擂主:只要我的总分严格大于擂主,我就当新擂主。
  4. 输出擂主的姓名和三科成绩。

「并列时输出靠前的」怎么实现?关键在第 3 步的「严格大于」—— 用 > 而不是 >=。总分相等时擂台不换人,自然就保留了靠前的那位。

样例输入
3
senpai 114 51 4
lxl 114 10 23
fafa 51 42 60
样例输出
senpai 114 51 4

三个人总分:senpai = 114+51+4 = 169,lxl = 114+10+23 = 147,fafa = 51+42+60 = 153。最高的是 senpai。

完整代码

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

// 结构体:把「一个同学的全部信息」打包成一个整体
struct Student {
    string name;                    // 姓名
    int chinese, math, english;     // 三科成绩
    int total;                      // 派生字段:总分(读入之后算出来)
} a[1005];                          // 结构体数组:一次声明 1005 个同学

int main(){
    int n;
    cin >> n;
    for(int i = 0; i < n; i++){
        // 结构体数组的输入:a[i] 是一个整体,用 a[i].字段 逐个点名
        cin >> a[i].name >> a[i].chinese >> a[i].math >> a[i].english;
        a[i].total = a[i].chinese + a[i].math + a[i].english;
    }

    int best = 0;                   // best 记住「目前最厉害的同学」的下标
    for(int i = 1; i < n; i++){
        if(a[i].total > a[best].total){   // ★ 用 > 不用 >=:并列时保留靠前的
            best = i;
        }
    }

    cout << a[best].name << " " << a[best].chinese << " "
         << a[best].math << " " << a[best].english << endl;
    return 0;
}
✅ 实测结果

官方样例 → 输出 senpai 114 51 4 完全一致

自造并列测试(aaa 和 bbb 都是满分 100+100+100,aaa 在前)→ 输出 aaa 100 100 100 正确保留了靠前的同学

边界 N = 1(zoe 150 150 150)→ 输出 zoe 150 150 150 正确

易错点

  • 输出的是三科原始成绩,不是总分!题目说「输出最厉害的学生各项信息(姓名、各科成绩)」, 很多人顺手把 total 输出了,直接 0 分。
  • 比较用了 >=:并列时后面的会顶掉前面的,违反了「输出靠前的那位」。
  • 结构体定义末尾的 }; 分号忘了写。
  • 姓名是 string,要用 #include <string>(虽然很多编译器不写也能过, 但规范起见写上)。
P1093 [NOIP 2007 普及组] 奖学金 普及- · 多关键字排序

题意:n 个学生,每人有语文、数学、英语三科成绩。按下面的规则排出一个唯一确定的名次,然后输出前 5 名的学号和总分。

排序规则:① 总分从高到低;② 总分相同时语文从高到低;③ 总分和语文都相同时学号小的在前。

⚠️ 数据范围 / 关键约束

5 ≤ n ≤ 300;每科成绩在 0 到 100 之间;学号按输入顺序编号为 1 ~ n。n 至少是 5, 所以一定有 5 个人可以输出。

思路

这是结构体 + 多关键字排序的标准题。

  1. 读入时把学号也存进结构体(a[i].id = i;)——排序会把顺序打乱,学号必须跟着人走。
  2. 顺手算总分。
  3. 写三行 cmp,把三个优先级依次排好。
  4. sort 之后,前 5 个就是获奖的同学,输出 id 和 total。
🎯 为什么「学号必须存进结构体」

如果只把成绩存进数组,排序后你就不知道哪个成绩是谁的了。

学号是「身份标签」,必须和本人的数据绑在一起,一起被搬来搬去。这正是结构体最擅长的事—— 要是用平行数组,你就得开一个 id[] 数组,然后在 cmp 里手动同步交换,一不留神就对不上了。

💡 题目特意提醒的坑:前两名可能是「同分不同序」

题面里举了例子:7 号总分 279、5 号总分 279,但 7 号语文更高,所以必须输出 7 在前。 如果你只按总分排,两人顺序可能随机,就会 WA。三个关键字一个都不能少。

样例输入
6
90 67 80
87 66 91
78 89 91
88 99 77
67 89 64
78 89 98
样例输出
6 265
4 264
3 258
2 244
1 237

手算验证:1 号 237、2 号 244、3 号 258、4 号 264、5 号 220、6 号 265。按总分降序正好是 6、4、3、2、1。

完整代码

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

struct Student {
    int id;                         // 学号
    int chinese, math, english;     // 三科成绩
    int total;                      // 总分
} a[305];

// 比较函数:返回 true 表示 x 应该排在 y 前面
bool cmp(const Student &x, const Student &y){
    if(x.total != y.total) return x.total > y.total;         // ① 总分高的在前
    if(x.chinese != y.chinese) return x.chinese > y.chinese;  // ② 语文高的在前
    return x.id < y.id;                                      // ③ 学号小的在前
}

int main(){
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++){
        a[i].id = i;                                    // ★ 学号 = 输入行号,必须存下来
        cin >> a[i].chinese >> a[i].math >> a[i].english;
        a[i].total = a[i].chinese + a[i].math + a[i].english;
    }

    // 注意:数据存在 a[1] ~ a[n],所以是 sort(a+1, a+n+1, cmp)
    sort(a + 1, a + n + 1, cmp);

    for(int i = 1; i <= 5; i++){
        cout << a[i].id << " " << a[i].total << endl;
    }
    return 0;
}
⚠️ sort(a + 1, a + n + 1, cmp) 这个 +1 别写错

sort 的范围是左闭右开 [first, last)。数据在 a[1] 到 a[n], 所以要写到「最后一个元素的下一个位置」a + n + 1。

写成 sort(a + 1, a + n, cmp) 的话,最后一个同学压根没参与排序, 样例可能还过,但隐藏数据必挂。

✅ 实测结果

官方样例 1 → 输出 6 265 / 4 264 / 3 258 / 2 244 / 1 237 完全一致

官方样例 2(8 人,含两组同分需要比较语文/学号)→ 输出 8 265 / 2 264 / 6 264 / 1 258 / 5 258 完全一致

样例 2 里 2 号和 6 号都是 264 分、语文都是 88 分 → 按学号升序输出 2、6; 1 号和 5 号都是 258 分,1 号语文 80 > 5 号语文 78 → 1 在前。三个关键字全部生效。

易错点

  • 三个关键字的顺序写反。优先级是总分 → 语文 → 学号,一层一层往下筛。
  • 把 if(x.total != y.total) 写成 if(x.total > y.total), 会让总分相等的人错误地按学号重排。
  • 忘记存学号,排序后完全不知道输出谁。
  • sort 右端点漏了 +1,最后一个学生没排序。
  • 输出只有 5 行,别写成 n 行。
P1104 生日 普及- · 日期排序 + 稳定性

题意:给出 n 个人的姓名、出生年、月、日,按年龄从大到小输出姓名。 如果两个人年龄相同(年月日完全一样),输入靠后的同学先输出。

⚠️ 数据范围 / 关键约束

1 < n < 100;姓名长度 1 ≤ |s| < 20;保证年月日实际存在,年份在 [1960, 2020] 之间。

n 很小(最多 99 人),所以随便怎么写都不会超时——这题考的是「字段顺序」而不是效率。

思路

先想清楚一件事:「年龄大」等价于「出生日期早」。所以「年龄从大到小」就是「出生日期从小到大」。

日期怎么比大小?就是看日历的习惯:先比年,年相同再比月,月相同再比日。 这正好又是三段式 cmp:

先比 y(早的在前)→ 再比 m(早的在前)→ 再比 d(早的在前)
🎯 本题的真正考点:并列怎么办

题目要求「年龄相同则输入靠后的先输出」。注意这跟 Day 11 计划里写的「按输入顺序输出」 不太一样——是反过来的,靠后的先出来。

所以最后一层要写 return x.id > y.id;(编号大的排前面)。 把 id 当成「第四个关键字」塞进 cmp,是最稳的做法。

如果什么都不写会怎样?sort 不是稳定排序,并列元素的相对顺序是未定义的, 可能时对时错——这种「看起来能过、其实全靠运气」的代码最危险。

样例输入
3
Yangchu 1992 4 23
Qiujingya 1993 10 13
Luowen 1991 8 1
样例输出
Luowen
Yangchu
Qiujingya

Luowen 1991 年最早 → 年龄最大 → 排第一;Qiujingya 1993 年最晚 → 最小 → 排最后。

完整代码

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

struct Person {
    string name;        // 姓名
    int y, m, d;        // 出生年、月、日
    int id;             // 输入顺序(第几个读进来的)
} a[105];

// 比较函数:返回 true 表示 x 应该排在 y 前面(年龄大的在前)
bool cmp(const Person &x, const Person &y){
    if(x.y != y.y) return x.y < y.y;    // 出生年份小 → 年龄大 → 排前面
    if(x.m != y.m) return x.m < y.m;    // 同年再比月
    if(x.d != y.d) return x.d < y.d;    // 同月再比日
    return x.id > y.id;                 // ★ 生日完全相同:输入靠后的先输出
}

int main(){
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++){
        a[i].id = i;
        cin >> a[i].name >> a[i].y >> a[i].m >> a[i].d;
    }

    sort(a + 1, a + n + 1, cmp);

    for(int i = 1; i <= n; i++){
        cout << a[i].name << endl;
    }
    return 0;
}
✅ 实测结果

官方样例 → 输出 Luowen / Yangchu / Qiujingya(各占一行) 完全一致

自造并列测试(4 人:A、B 都是 2000-1-1,C 是 1999-5-5,D 是 2001-12-31) → 输出 C / B / A / D 正确

C 年龄最大排第一;A、B 生日相同,输入靠后的 B 先输出;D 最小排最后。

易错点

  • 把日期当成一个整数比(比如 y*10000+m*100+d)——这题碰巧也能过, 但更清楚的写法就是三段 if,可读性高得多。
  • 年月日的比较方向搞反。题目要「年龄大在前」=「出生早在前」= x.y < y.y, 不是 >。写下 cmp 之前先在纸上念一遍题目要求。
  • 并列时用 x.id < y.id:那会变成「输入靠前的先输出」,跟题目要求正好相反。
  • 姓名是 string 不是 char,输出时直接 cout << a[i].name。
P5143 攀爬者 普及- · 三维点排序 + 距离

题意:有 N 个标了三维坐标 (x, y, z) 的点,所有点的高度 z 互不相同。 要从最低的点爬到最高的点,经过每一个点,而且每走一步高度都必须上升。 一步的距离是两点的三维欧几里得距离,求总距离(保留三位小数)。

⚠️ 数据范围 / 关键约束

1 ≤ N ≤ 50000,答案保证在 double 的表示范围内。

题目没有给出 x、y、z 具体的取值范围,只保证了上面两条。所以算距离时建议转成 double 再算,别让 int 溢出。

思路

这题的巧妙之处在「把三维问题变成一维排序问题」。

题目的约束是「每一步高度 z 都必须上升」,而且「经过每一个点」。 那么攀爬路径其实已经被唯一确定了——就是把所有点按 z 从小到大排一排,依次走过去。

🎯 为什么唯一确定

假设按 z 排好是 P₁ < P₂ < P₃ < … < P_N。要求每步高度都上升, 那第一站只能是最低的 P₁(从最低点出发),第二站只能是 P₂(比 P₁ 高的点里最矮的),依此类推。

高度互不相同这个条件保证了顺序不会有歧义(万一有两点一样高,就没法排先后了)。 所以这题的「排序」不是用来选最优的,而是用来还原唯一路径的。

排完序,从 p[0] 顺着算到 p[n-1],把相邻两点的三维距离加起来就行。

三维距离公式

平面上两点距离你会了:√((x₂-x₁)² + (y₂-y₁)²)。三维就是再多加一项 z:

√( (x₂-x₁)² + (y₂-y₁)² + (z₂-z₁)² )

平方差了再开根,用 sqrt()(需要 #include <cmath>)。

样例输入
5
2 2 2
1 1 1
4 4 4
3 3 3
5 5 5
样例输出
6.928

按 z 排序后是 (1,1,1) (2,2,2) (3,3,3) (4,4,4) (5,5,5), 每相邻两点的距离都是 √(1+1+1) = √3 ≈ 1.7320508,四段共 ≈ 6.9282 → 保留三位小数 6.928。

完整代码

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
using namespace std;

struct Point {
    int x, y, z;        // 三维坐标
} p[50005];

// 按高度 z 从小到大排,就得到了攀爬的先后顺序
bool cmp(const Point &a, const Point &b){
    return a.z < b.z;
}

int main(){
    int n;
    scanf("%d", &n);
    for(int i = 0; i < n; i++){
        scanf("%d %d %d", &p[i].x, &p[i].y, &p[i].z);
    }

    sort(p, p + n, cmp);            // 排序之后,p[0] 最低,p[n-1] 最高

    double ans = 0.0;
    for(int i = 1; i < n; i++){
        // 三维欧几里得距离:先算差值再平方,最后开根号
        double dx = p[i].x - p[i-1].x;
        double dy = p[i].y - p[i-1].y;
        double dz = p[i].z - p[i-1].z;
        ans += sqrt(dx * dx + dy * dy + dz * dz);
    }

    printf("%.3f\n", ans);          // 保留三位小数
    return 0;
}
💡 这里为什么用 scanf / printf 而不是 cin / cout

N 最大 50000,要读 15 万个数。cin 在数据量大时比 scanf 慢一些, 用 scanf 是竞赛里的常规保险做法。

另一个办法是在 main 开头加一句 ios::sync_with_stdio(false);, 让 cin 也变快。两种都行,这题其实 cin 也能过(15 万个数不算多)。

printf("%.3f") 表示「保留 3 位小数」,会自动四舍五入; cout 的等价写法是 cout << fixed << setprecision(3) << ans;(需要 <iomanip>)。

✅ 实测结果

官方样例 → 输出 6.928 完全一致

边界 N = 1(只有一个点,不用移动)→ 输出 0.000 正确

最大规模 N = 50000(随机生成的 878 KB 输入文件)→ 输出 519450882.248, 运行耗时为毫秒级,远低于时限(复杂度 O(N log N),排序占主要时间)

易错点

  • 用 int 算平方和:(p[i].x-p[i-1].x) 的平方可能超出 int 范围。 先赋给 double 变量再平方,或者写 (double) 强转。
  • 忘了 #include <cmath>:sqrt 找不到。
  • 输出格式:题目要求保留 3 位小数,cout << ans 会打出 6.9282 这种一堆位数,直接 WA。
  • 数组开小了:N ≤ 50000,开 p[50005],别开成 1005。
  • 不要试图去找「最短路径」——路径是被题目条件唯一确定的,只需要排序 + 累加。

✗今日常见错误汇总

错误写法后果正确写法
struct Student { ... }
(末尾没分号)
编译报一堆看不懂的错,报错位置常在好几行之后 struct Student { ... };
a.name[i] 括号顺序反了,编译不过或取错东西 a[i].name
if(x.total > y.total) return true;
return x.id < y.id;
总分相等时,学号小的会被误判为在前面 → 多关键字排序整体错乱 if(x.total != y.total)
  return x.total > y.total;
sort(a+1, a+n, cmp) 右端点是左闭右开,最后一个元素没参与排序 sort(a+1, a+n+1, cmp)
不存学号/id 就排序 排完不知道哪份数据是谁的 a[i].id = i; 一起存进结构体
cmp 参数写 Student x 每次比较都复制整个结构体,数据大时明显变慢 const Student &x
P5143 用 int 存平方和 平方后溢出 → 距离算错 double dx = ...; 再 dx*dx
cout << ans 输出距离 小数位数不对,P5143 要求保留 3 位 printf("%.3f\n", ans);

✓自测:你真的会了吗

第 1 题:这个结构体定义有什么问题?
struct Student {
    string name;
    int score;
}

答:末尾少了分号,应该是 };。

这是 C++ 里一个著名的坑:结构体/类的定义右花括号后面必须跟分号。 忘了它,编译器会以为你还要继续声明变量(比如 } a[1005]; 就是合法的), 等到后面遇到别的语句才报错,于是错误信息指向的位置往往离真正的错误有好几行。

第 2 题:想让「分数高的在前,分数相同时名字字典序小的在前」,cmp 怎么写?
bool cmp(const Student &x, const Student &y){
    if(x.score != y.score) return x.score > y.score;   // 分数不同:高的在前
    return x.name < y.name;                            // 分数相同:名字小的在前
}

★ 关键是第一行用 != 判断「是否不同」,不同才由分数决定; 相同就自然落到第二行,由名字决定。

如果写成 if(x.score > y.score) return true;,那么分数相等时会掉到第二行, 于是「分数低但名字小」的人反而排在前面,全乱套了。

第 3 题:排序之后,怎么输出「总分第二名」?

假设已经 sort(a, a+n, cmp) 按总分降序排好了(a[0] 是第一)。

答:第二名是 a[1],直接

cout << a[1].name << " " << a[1].total << endl;

★ 记住:数组下标从 0 开始,所以「第 k 名」是 a[k-1]。 P1093 输出前 5 名循环写的是 i = 1..5(输出 a[1]~a[5]), 那是因为它在 a[1] 开始存数据。下标基础不同,读代码时一定要先看清楚。

第 4 题(手写代码):统计有多少个学生总分不低于 200 分
int cnt = 0;
for(int i = 0; i < n; i++){
    if(a[i].total >= 200) cnt++;
}
cout << cnt << endl;

★ 这题不需要排序,只需要遍历一遍结构体数组。结构体题不一定要排序—— 先看清题目要「求最值 / 数个数」还是「排顺序」,再决定用不用 sort。

▤速查卡(截图存手机)

【结构体定义】
  struct Student {
      string name;              // 成员用分号隔开
      int chinese, math, english, total;
  };                            // ★ 这个分号必须有!
  Student a[1005];              // 结构体数组

【访问成员】
  a[i].name        // 先锁定"第几个",再取"哪一项"
  a[i].total = a[i].chinese + a[i].math + a[i].english;   // 派生字段

【多关键字 cmp】
  bool cmp(const Student &x, const Student &y){
      if(x.total != y.total) return x.total > y.total;      // 总分降序
      if(x.chinese != y.chinese) return x.chinese > y.chinese; // 语文降序
      return x.id < y.id;                                    // 学号升序
  }
  sort(a + 1, a + n + 1, cmp);  // ★ 左闭右开,右端点 +1

【cmp 心法】
  cmp(x, y) 返回 true  =  x 排在 y 前面
  想降序写 >,想升序写 <
  并列条件用 if(... != ...) 筛,一层一层往下

【结构体题的固定套路】
  读入 → 存 id/派生字段 → 写 cmp → sort → 输出前 k 个 / 输出最值

【三维距离】
  double dx = p[i].x - p[i-1].x;   // 先转 double 再平方,防溢出
  ans += sqrt(dx*dx + dy*dy + dz*dz);
  printf("%.3f\n", ans);

→明天预告 · Day 12

排序:sort 与它的朋友们

今天你已经摸到了 sort 和 cmp 的门,明天要把它们彻底讲透: greater<int>() 降序、unique 去重、冒泡排序的原理, 以及 P1012「拼数」那个经典的拼接比较 a+b > b+a。

今天写的 cmp 明天还要用,而且会更难一点——记得回来复习这三行 if。