排序是竞赛里最常用的工具之一。今天把 sort、自定义 cmp、去重、冒泡与逆序对一次讲透
| 0 - 25 min | sort 的三种基本用法 + 左闭右开的坑(第 1 节) |
| 25 - 55 min | 自定义 cmp 与「严格弱序」这条铁律(第 2 节) |
| 55 - 80 min | 去重三连 sort → unique → erase(第 3 节) |
| 80 - 105 min | 冒泡排序与逆序对(第 4 节)——P1116 的本体 |
| 105 - 200 min | 做 4 道题,每题限时 25 分钟 |
| 200 - 220 min | 对照本文题解 + 做自测 |
昨天你用 sort 和 cmp 排了学生(结构体)。今天要把它彻底讲清楚:
sort 到底怎么用、cmp 有什么必须遵守的规矩、去重怎么和排序配合。
另外今天还会打开「排序的黑盒子」——学一遍冒泡排序。 你可能会问:既然有 sort,为什么还要学又慢又笨的冒泡? 因为 P1116 车厢重组要的不是「排好序」,而是「交换了多少次」。 而「冒泡排序的交换次数」正好等于一个非常重要的量——逆序对个数。
#include <algorithm> // ★ sort 住在这个头文件里,别忘了 sort(a, a + n); // ① 数组升序 sort(a, a + n, greater<int>()); // ② 数组降序 sort(a, a + n, cmp); // ③ 按你自己的规则排
如果用 vector:
sort(v.begin(), v.end()); // vector 升序 sort(v.begin(), v.end(), greater<int>()); // vector 降序
sort 而不是自己写
标准库里的 sort 是经过极限优化的混合排序(快排 + 堆排 + 插入排序),
复杂度 O(n log n)。你自己手写的快排几乎不可能比它快,而且容易写错。
n = 10⁶ 的时候,O(n log n) 大约是 2×10⁷ 次操作,跑起来只要零点几秒。 而 O(n²) 是 10¹² 次,要跑好几分钟——这就是为什么「会排序」是硬门槛。
sort(a, a + n)因为 C++ 标准库的区间约定是左闭右开 [first, last):
包含 first 指的元素,不包含 last 指的元素。
| 数据存放方式 | 正确的 sort 写法 | 说明 |
|---|---|---|
| a[0] ~ a[n-1] | sort(a, a + n) | 右端点 = 起始 + n |
| a[1] ~ a[n] | sort(a + 1, a + n + 1) | 右端点还要再 +1 |
| vector v 全部 | sort(v.begin(), v.end()) | end() 指向末尾的「下一个」 |
数据存在 a[1] ~ a[n],却写成 sort(a + 1, a + n)——
最后一个元素根本没参与排序。
这种错误特别阴险:样例可能照样过,因为样例小、最后一个恰好不用动;到大测试点才突然 WA。
校验方法:sort 的右端点写成 a + 起始下标 + 元素个数,永远错不了。
greater<int>() 是什么东西它是标准库给你的一个「现成的比较规则」,意思是「大的排前面」。 加一对括号是因为它是个临时对象,要传一个实例给 sort。
sort(a, a + n); // 升序:1 2 3 4 5 sort(a, a + n, greater<int>()); // 降序:5 4 3 2 1
() 会怎样
写 greater<int> 而不加括号,编译器会报一大串「模板参数推导失败」之类的错。
记住口诀:greater 后面一定要带一对小括号。
同理,排序 long long 或 double 时要用 greater<long long>()、
greater<double>()——尖括号里写元素的类型。
sort 必须知道「谁该排在前面」。对 int、double、
string(字典序)这些类型,C++ 内置了默认规则;
但两个自定义结构体谁大谁小,编译器不知道,必须你写 cmp——这就是第 2 节。
cmp(x, y) 返回 true,意思就是「x 应该排在 y 前面」。
所以:
· return x < y; → 小的在前 → 升序
· return 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; // ③ 学号升序
}
每一行的含义是「这一项不一样,就由这一项说了算」,一样就往下走。
这就是昨天 P1093 的写法,今天要补充的是:为什么第一行必须用 != 而不能用 >。
如果写成 if(x.total > y.total) return true;,
那么两人总分相等时不会返回,会直接落到第 ③ 行,
于是学号小的会被判为排在前面,哪怕总分更低——名次直接错乱。
听起来很学术,其实只有两句人话:
| 规则 | 人话 | 例子 |
|---|---|---|
| cmp(x, x) 必须是 false | 自己不能排在「自己」前面 | ❌ x <= y 会让 cmp(x,x) 为 true |
| 不能出现「循环谁在前面」 | a 在 b 前、b 在 c 前 ⇒ a 必在 c 前 | 我们写的多关键字 cmp 天然满足 |
<= 或 >=
bool cmp(int x, int y){
return x <= y; // ❌ 大错!相等时也返回 true
}
标准规定 sort 的 cmp 必须是严格小于(strict)。你写 <= 就破坏了约定,
行为变成未定义——排序库内部会做出错误的分区假设,
可能越界访问内存直接崩溃(RE),也可能排出来的顺序是错的。
我实际的测试结果(老实说):在本机 MinGW g++ 6.3.0 / -O2 下,
我用 <= 的 cmp 排 20 万个随机数,程序没有崩、结果也恰好是升序的。
但这不代表这样写是安全的——它是未定义行为,换个编译器、换个标准库、换份数据就可能崩或排错。
计划书里写「会 RE」是一种风险警告,而不是保证。
结论:永远写严格的 < 或 >,一个等号都不要多。
const T &
传引用避免复制整个结构体,加 const 表示「只读不改」。
数据量大时这个细节能省下可观的时间。昨天的 const Student &x 就是这个道理。
要把一堆数「去重 + 排序」,标准做法只有三步:
sort(a, a + n); // ① 先排序,让相同的数挨在一起 int m = unique(a, a + n) - a; // ② 去掉相邻的重复项,m 是剩余个数 // 如果用的是 vector,还要第三步: v.erase(unique(v.begin(), v.end()), v.end()); // ③ 真正删掉多余的尾巴
unique 到底干了什么
它不是「删除」,而是「搬家」:把不重复的元素依次挪到数组前面,重复的丢到后面去。
返回值是新的「逻辑末尾」的指针,所以 unique(...) - a 就是去重后的元素个数。
对数组来说,后面的垃圾数据你不去管就行了(用 m 当新的长度)。
对 vector 来说,size() 没变,所以要 erase 把尾巴真删掉。
unique 只比较相邻两个元素。如果数组是 3 1 3 1,
它什么都删不掉(3 和 1 不相邻)。
所以顺序绝对不能反:必须先 sort,再 unique。排完序相同的数一定挨在一起,unique 才能一网打尽。
如果题目里的数值范围很小(比如 1 ~ 1000),用标记桶更简单,连排序都省了:
bool vis[1005] = {false};
for(int i = 0; i < n; i++){ int x; cin >> x; vis[x] = true; }
// 从小到大扫一遍桶,有旗子的就是出现过的数,顺便天然有序
for(int v = 1; v <= 1000; v++)
if(vis[v]) cout << v << " ";
| 方法 | 复杂度 | 适用场景 |
|---|---|---|
| sort + unique | O(n log n) | 值域很大也能用(通用首选) |
| 标记桶 | O(n + 值域) | 值域小的时候更简单更快 |
今天的 P1059 两种写法都行,我会把两种代码都给你,而且都实测过。
核心想法就一句话:从左到右把相邻两个数比一比,前面的大就交换,一轮下来最大的那个就「冒」到了最右边。
拿 4 3 2 1 走一遍第一轮:
| 比较 | 操作 | 数组变成 |
|---|---|---|
| 4 和 3 | 4 > 3,交换 | 3 4 2 1 |
| 4 和 2 | 4 > 2,交换 | 3 2 4 1 |
| 4 和 1 | 4 > 1,交换 | 3 2 1 4 |
| 第一轮结束 | 最大的 4 冒到最右,共交换 3 次 | 3 2 1 | 4 |
然后对前面 3 2 1 重复,再对 2 1 重复……
for(int i = 0; i < n - 1; i++){ // 一共做 n-1 轮
for(int j = 0; j < n - 1 - i; j++){ // 后面 i 个已经排好,不用再管
if(a[j] > a[j+1]){ // 相邻逆序
swap(a[j], a[j+1]);
}
}
}
| 排序算法 | 复杂度 | 特点 |
|---|---|---|
| 冒泡排序 | O(n²) | 慢,但能顺便数出「交换次数」 |
| std::sort | O(n log n) | 快,竞赛首选,但数不出交换次数 |
n ≤ 1000 的时候 O(n²) 是 10⁶ 次操作,完全没问题,所以今天的 P1116 可以直接用冒泡。
在一串数里,如果前面一个数比后面一个数大,这两数就构成一对逆序对。
例子:4 3 2 1 的逆序对有:
(4,3) (4,2) (4,1) (3,2) (3,1) (2,1) —— 正好 6 对。
为什么?看冒泡的内层循环:每发现一对「相邻且前大后小」,就交换一次。 而每一次交换,恰好消掉一对相邻逆序,并且让整个数组的逆序对总数减少 1; 没有交换的时候,说明已经没有相邻逆序了,而此时数组必定已经有序(逆序对为 0)。
所以:数组有序 ⟺ 逆序对为 0 ⟺ 不再有交换, 交换次数从头到尾正好把逆序对一对一对清空,因此两者相等。
我实测验证过:写了两个程序,一个用冒泡数交换次数,一个用归并排序数逆序对, 对 12 组随机数据(n 从 10 到 65)跑出来结果完全一致,比如 n=65 那组两个都是 1043,n=58 那组都是 692。
题目里的「桥旋转 180 度交换相邻两节车厢」,就是冒泡里的 swap(a[j], a[j+1])。
问「最少多少步」——因为每次只能交换相邻车厢,这个「最少步数」正好就是逆序对个数。
n ≤ 1000 用冒泡就够。但如果 n 变成 10⁵,O(n²) 就挂了,这时要用归并排序在 O(n log n) 内数出逆序对:
归并的时候,如果右半段的 a[j] 小于左半段的 a[i],
那么左半段剩下的 mid - i + 1 个数全都比 a[j] 大,一次就贡献这么多逆序对。
这段代码我也写了并编译跑过,附在 P1116 的题解里,当作课外加餐。
下面每道题的代码我都用 g++ -std=c++14 -O2 -Wall 实际编译并跑过官方样例,输出是你看到的真实结果。
题意:明明生成了 N 个 1 到 1000 之间的随机整数,其中重复的只保留一个。 要求先输出去重后有多少个不同的数(M),再把这些数从小到大排好序输出。
N ≤ 100,每个数在 1 ~ 1000 之间(题面原文是「生成了 N 个 1 到 1000 之间的随机整数 (N ≤ 100)」, 在说明里只标注了「NOIP 2006 普及组 第一题」,没有额外的分档数据表)。
数值范围这么小,说明桶做法完全可行——这正是本题最省事的解法。
题目要求的是「去重 + 排序」两件事,而这两件事有天然的联系:排完序之后,相同的数一定挨在一起, 于是「去重」就退化成了「删掉相邻的重复项」。
两种写法:
sort 排序 → unique 去重相邻重复项 → 得到新的长度 m。vis[1005],读到哪个数就给哪个桶插旗;
最后从 1 扫到 1000,有旗子的数字天然就是升序且不重复的。10 20 40 32 67 40 20 89 300 400 15
8 15 20 32 40 67 89 300 400
10 个数里 20 和 40 各出现了两次,去重后剩 8 个。
#include <iostream>
#include <algorithm>
using namespace std;
int a[105];
int main(){
int n;
cin >> n;
for(int i = 0; i < n; i++) cin >> a[i];
sort(a, a + n); // ① 排序
// ② 去重:unique 把不重复的元素挪到前面,返回「新区间末尾」的指针
int m = unique(a, a + n) - a;
cout << m << endl; // ③ 先输出去重后的个数
for(int i = 0; i < m; i++){
if(i) cout << " "; // 数字之间一个空格,末尾不多打
cout << a[i];
}
cout << endl;
return 0;
}
#include <iostream> using namespace std; int a[105]; bool vis[1005]; // 桶:vis[v] 标记数字 v 出现过 int main(){ int n; cin >> n; for(int i = 0; i < n; i++){ cin >> a[i]; vis[a[i]] = true; // 出现过的数,在它自己的桶里插一面旗 } int m = 0; for(int v = 1; v <= 1000; v++){ if(vis[v]) m++; // 从小到大扫桶,顺便数出有多少个不同的数 } cout << m << endl; int printed = 0; for(int v = 1; v <= 1000; v++){ if(vis[v]){ if(printed) cout << " "; cout << v; printed++; } } cout << endl; return 0; }
官方样例 → 两种写法都输出:
8 15 20 32 40 67 89 300 400
与样例输出完全一致。
自造边界测试「5 个数全是 7」→ 两种写法都输出 1 / 7 正确
if(i) cout << " "; 的写法更规范。vis[1005]),
开成 vis[1000] 在读入 1000 时会越界。题意:给出 n 个正整数,把它们首尾相接拼成一排,组成一个最大的整数,输出这个整数。
1 ≤ n ≤ 20,1 ≤ aᵢ ≤ 10⁹。
注意两件事:① 每个数最大 10 亿(10 位数),n 最多 20 个, 拼起来最长有 200 位左右——远超 long long 的范围,必须用字符串处理。 ② n 很小,说明这题考的不是算法效率,而是比较规则想不想得对。
先说一个错误但非常诱人的想法:把数字当字符串,按字典序从大到小排,然后拼起来。
它对样例居然是对的!但它是错的。看这个反例:
| 输入 | 字典序降序排(❌ 错) | 正确答案(✅) |
|---|---|---|
| 3 31 | "31" > "3" → 拼成 313 | 331 > 313 → 拼成 331 |
| 9 91 | "91" > "9" → 拼成 919 | 991 > 919 → 拼成 991 |
(上面两组我都真的编译运行验证过:我的正确程序输出 331 和 991; 专门写了一个「字典序降序」的错误版本,输出的正是 313 和 919。)
不要单独看 a 和 b,而要看「a 拼 b」和「b 拼 a」哪个更大:
如果 a + b > b + a,那么 a 应该排在 b 前面
回到 {3, 31}:"3"+"31" = "331","31"+"3" = "313",
因为 "331" > "313",所以 3 排在 31 前面,拼出来是 331。✅
再看 {9, 91}:"9"+"91" = "991","91"+"9" = "919",
991 > 919,所以 9 排在 91 前面,得到 991。✅
三个理由,说服你自己:
{"12","1212"} 这种),
cmp 两边都返回 false,视为「等价」。所以它可以安全地交给 sort。题面给了 1 ≤ aᵢ,也就是说输入里不会出现 0。
如果真的有 0(比如 0 1),拼接结果可能是 "01" 这种不合法的整数形式,
那时才需要特判。这题不用操心。
3 13 312 343
34331213
4 7 13 4 246
7424613
样例 1:343 > 312 > 13(两两拼接比较后),拼成 34331213。
样例 2:顺序是 7、4、246、13 → 拼成 7424613。
#include <iostream> #include <string> #include <algorithm> using namespace std; string s[25]; // ★ 核心比较规则:如果 a 接在 b 前面比 b 接在 a 前面更大,a 就排在前面 bool cmp(const string &a, const string &b){ return a + b > b + a; } int main(){ int n; cin >> n; for(int i = 0; i < n; i++) cin >> s[i]; sort(s, s + n, cmp); for(int i = 0; i < n; i++) cout << s[i]; cout << endl; return 0; }
读入直接读成 string,不做任何转换——因为最后要的就是拼接出来的字符串,
中间用不用 int 根本没意义,而且 int 也存不下 10 位数拼起来的结果。
官方样例 1 3 / 13 312 343 → 输出 34331213 完全一致
官方样例 2 4 / 7 13 4 246 → 输出 7424613 完全一致
关键反例 2 / 3 31 → 输出 331 正确(朴素字典序降序会输出 313,错)
关键反例 2 / 9 91 → 输出 991 正确(朴素字典序降序会输出 919,错)
⚠️ 一个非常重要的实测发现:我专门写了一个「按字典序降序」的错误版本,
拿它去跑两个官方样例,竟然都输出正确(34331213、7424613 全对);
只有在我自造的反例 {3,31}、{9,91} 上才露出马脚(输出 313、919)。
结论:样例全过 ≠ 代码正确。P1012 是「样例骗人」的典型案例,比较规则必须从原理上想清楚。
int / long long 去拼:20 个数各 10 位,拼起来最多 200 位,
什么整数类型都存不下,必须用 string。a > b(字典序降序):样例能过,隐藏数据必挂,见上面的反例。a + b >= b + a:多了个等号,破坏严格弱序,属于未定义行为,
可能崩也可能排错。必须用严格的 >。题意:n 个候选人,每人有一个票数,票数可能非常大(最多 100 位)。 输出票数最多的那个人的号数(第几行输入的),再输出他的票数。保证没有并列。
1 ≤ n ≤ 20;票数可能大到 100 位数字;保证不存在票数相同的候选人。
100 位是什么概念?long long 最多存 19 位。所以只能把票数当字符串读进来。
第一步:谁长谁大。一个数的位数多,它一定更大 (因为两个数都没有前导零:5 位数最小是 10000,4 位数最大是 9999,10000 > 9999 恒成立)。
第二步:一样长就按字典序比。位数相同时,字符串的字典序比较等价于数值比较——
因为逐位比较就是「从最高位开始比」,第一位谁大谁就大。而 string 的 >
正是逐字符比较 ASCII 码,数字字符 '0'~'9' 的 ASCII 也是递增的,所以完全对得上。
实现上用「打擂台」比排序更简单,毕竟 n 只有 20:
又是昨天那个道理:要输出「第几个人」,就必须在扫描时把号数一直带在身边。
这里用一个 bestId 变量跟着擂主字符串一起更新就行了。
5 98765 12365 87954 1022356 985678
4 1022356
5 个票数里,1022356 是唯一一个 7 位数,其他都是 5 位或 6 位数,所以位数最多的一定最大 → 第 4 个人当选。这题样例本身就体现了「先比长度」的重要性:985678(6 位)比 98765(5 位)大, 但比 1022356(7 位)小。
#include <iostream>
#include <string>
using namespace std;
int main(){
int n;
cin >> n;
string best = ""; // 目前票数最多的那个数
int bestId = 0; // 它的号数
for(int i = 1; i <= n; i++){
string s;
cin >> s;
bool better = false;
if(bestId == 0){
better = true; // 第一个候选人,直接当擂主
}else if(s.size() != best.size()){
better = (s.size() > best.size()); // ① 位数多的数一定更大
}else{
better = (s > best); // ② 位数相同,用字典序比
}
if(better){
best = s;
bestId = i;
}
}
cout << bestId << endl;
cout << best << endl;
return 0;
}
官方样例 → 输出 4 / 1022356 完全一致
自造大数据测试(3 个候选人:一个 99 位数 999…9、一个 100 位数 999…98、一个 12345)
→ 输出 2 / 那个 100 位数 正确
这个测试说明:100 位数字用 string 存完全没问题,
位数多的那个(100 位)确实更大,程序选出了第 2 个候选人。
「先比长度」这一步到底有多必要?我专门写了个只比字典序的错误版本做了对照:
1 / 98765 是错的(正确答案是 4 / 1022356)。
因为 '9' 的 ASCII 码比 '1' 大,字典序认为 98765 > 1022356。2 / 9 / 10:正确版输出 2 / 10;
错误版输出 1 / 9。字典序认为 "9" > "10",但数值上 9 < 10。结论:大数比大小,「先比长度」绝对不能省。(这题比 P1012 友好——错误做法连样例都过不了, 而不是像 P1012 那样样例全过、隐藏数据才挂。)
long long 读票数:100 位数字直接溢出,读进来就是垃圾值。必须用 string。"9" 会被判为大于 "10",答案就错了。better 只在严格更大时为 true)就会保留靠前的那位。题意:一条桥最多容纳两节车厢,转 180 度就能交换相邻两节车厢。 要用这个操作把车厢按编号从小到大排好,问最少要转多少次。
车厢总数 N ≤ 1000,N 个数互不相同。
还有一个藏在输入格式里的坑:题面特意注明 「实际上数据中并不都在同一行,有可能分行输入」。
所以如果你用 getline 按行读、或者假设「第二行正好有 N 个数」,就会翻车。
用 cin >> a[i] 逐个读则完全免疫——因为 cin 会
自动跳过空格和换行,根本不在乎数据是怎么分行的。这也是我一直推荐用 cin >>
读数字的原因之一。
题目绕了一大圈,本质就是一句:你有一个操作,可以交换数组里相邻的两个元素,问最少几次能让数组升序。
而「最少次数」这个问题的答案非常干净:
下界:每次相邻交换最多只能消掉 1 个逆序对,所以步数 ≥ 逆序对个数。
达成:冒泡排序每次发现相邻逆序就交换,恰好每次都消掉 1 个,最终把逆序对清零,步数 = 逆序对个数。
两边一夹:最少步数 = 逆序对个数 = 冒泡排序的交换次数。
所以这题的代码骨架就是「冒泡排序 + 一个计数器」。N ≤ 1000 时 O(N²) 是 10⁶ 次操作,轻松通过。
4 4 3 2 1
6
4 3 2 1 的逆序对:(4,3)(4,2)(4,1)(3,2)(3,1)(2,1),共 6 对 →
需要 6 次旋转。也可以手数:第一轮 3 次交换、第二轮 2 次、第三轮 1 次,3+2+1 = 6。
#include <iostream>
#include <algorithm>
using namespace std;
int a[1005];
int main(){
int n;
cin >> n;
for(int i = 0; i < n; i++) cin >> a[i]; // cin 自动跳过换行,分行输入也不怕
int cnt = 0;
// 冒泡排序:每一轮把当前最大的车厢「冒」到最右边
for(int i = 0; i < n - 1; i++){
for(int j = 0; j < n - 1 - i; j++){
if(a[j] > a[j+1]){ // 相邻两节逆序,必须旋转一次
swap(a[j], a[j+1]);
cnt++; // 每交换一次记一步
}
}
}
cout << cnt << endl;
return 0;
}
如果 N 变成 10⁵,冒泡的 O(N²) 就超时了。这时用归并排序,在合并两段的时候顺手把逆序对统计出来:
#include <cstdio> #include <iostream> using namespace std; int a[1005], tmp[1005]; long long cnt = 0; // 逆序对个数 // 归并排序:顺便统计跨越左右两半的逆序对 void mergeSort(int l, int r){ if(l >= r) return; int mid = (l + r) / 2; mergeSort(l, mid); mergeSort(mid + 1, r); int i = l, j = mid + 1, k = l; while(i <= mid && j <= r){ if(a[i] <= a[j]){ tmp[k++] = a[i++]; }else{ tmp[k++] = a[j++]; cnt += (mid - i + 1); // 左半段剩下的 i..mid 全都比 a[j] 大 } } while(i <= mid) tmp[k++] = a[i++]; while(j <= r) tmp[k++] = a[j++]; for(int t = l; t <= r; t++) a[t] = tmp[t]; } int main(){ int n; scanf("%d", &n); for(int i = 0; i < n; i++) scanf("%d", &a[i]); mergeSort(0, n - 1); cout << cnt << endl; return 0; }
合并时如果右半段的 a[j] 比左半段的 a[i] 小,
说明左半段从 i 到 mid 这 mid - i + 1 个数全都不小于 a[j],
而且它们在原数组里都排在 a[j] 前面——一次就贡献了这么多逆序对。
今天的题用不上,但它是后面「CDQ 分治」「树状数组求逆序对」的思想起点,值得先混个脸熟。
官方样例 4 / 4 3 2 1:冒泡版输出 6,归并版也输出 6 与样例完全一致
边界测试:已有序输入 3 / 1 2 3 → 输出 0(不用旋转) 正确
N = 1(1 / 1)→ 输出 0 正确
最大规模 N = 1000 完全降序(逆序对最多的情况)→ 输出 499500,
与理论值 1000×999÷2 = 499500 完全吻合,运行毫秒级完成
交叉验证:用 12 组随机数据(n 从 10 到 65)分别跑冒泡版和归并版, 12 组结果全部相同(例如 n = 65 那组两版都是 1043,n = 58 那组都是 692,n = 12 那组都是 20), 实测支持了「冒泡交换次数 = 逆序对个数」这条结论。
getline 读数据:题面明说数据可能分行,分行读会把一行当成一整份,直接错。
用 cin >> 或 scanf 逐个读数字最稳。a[1001],开 a[1005] 更保险。j < n - 1 而不是 j < n - 1 - i:
多跑一些不影响正确性(只是慢一点),但内层访问到 a[n] 就越界了,必须带 - i。| 错误写法 | 后果 | 正确写法 |
|---|---|---|
| sort(a+1, a+n, cmp) | 左闭右开,最后一个元素没参与排序,样例可能还过 | sort(a+1, a+n+1, cmp) |
| sort(a, a+n, greater<int>) | 少了括号,编译报模板推导失败 | greater<int>() |
| bool cmp(int x,int y){return x<=y;} | 破坏严格弱序 → 未定义行为,可能越界崩溃或排错 | return x < y; |
| if(x.total > y.total) return true; return x.id < y.id; |
第一关键字相等时,会被后面的条件错误接管 | if(x.total != y.total) return x.total > y.total; |
| 去掉重时先 unique 再 sort | unique 只删相邻重复,没排序时一个都删不掉 | 先 sort,再 unique |
| vector 只用 unique 不 erase | size() 没变,尾巴上还留着垃圾数据 | v.erase(unique(v.begin(),v.end()), v.end()) |
P1012 用 a > b 当 cmp |
两个样例全过但隐藏数据必挂({3,31} 会输出 313) | return a + b > b + a; |
| P1781 用 long long 读票数 | 100 位数字溢出,读到垃圾值 | string s; cin >> s; 先比长度再比字典序 |
| P1116 用 getline 读车厢 | 数据可能跨行,按行读全乱 | cin >> a[i],自动跳过空白符 |
| 变量 int 存逆序对数 | n 大时 n²/2 可能超 int(n=1000 时 50 万没超,但习惯要养好) | long long cnt; |
答:sort(a, a + n, greater<int>());
三个要点:① 需要 #include <algorithm>;
② 右端点是 a + n(左闭右开,不含 a[n]);
③ greater<int> 后面的括号不能丢。
bool cmp(int x, int y){
return x <= y;
}
答:用了 <=,不满足严格弱序(cmp(x,x) 返回了 true,等于说「自己排在自己前面」)。
sort 要求的是严格小于。写 <= 属于未定义行为:可能让排序库越界读写直接崩溃,
也可能悄悄排出错误的顺序。正确写法是 return x < y;。
★ 补充一句真话:我在本机 MinGW g++ 6.3.0 / -O2 上实测过这个错误 cmp, 排 20 万随机数时并没有崩、结果也恰好是对的。 但这不代表安全——未定义行为的意思是「标准不保证任何结果」,换个环境就可能出事。 别拿编译器的宽容当运气。
3 31 3 三个数拼起来最大是多少?用 a + b > b + a 的规则两两比一遍:
· 3 和 31:"331" > "313" → 3 在前
· 3 和 3:"33" == "33" → 等价,谁先都一样
· 31 和 3:"313" < "331" → 3 在前
答:排序结果是 3 3 31,拼起来是 3313。
验证一下确实是最大:3313(3-3-31)、3313(3-31-3 也是 3313)…… 而 3133 更小。✅
5 4 3 2 1 的逆序对有几个?冒泡要交换几次?答:逆序对 = 任取两个数都是「前大后小」= C(5,2) = 10 个。 冒泡交换次数也是 10 次(两者永远相等)。
手数验证:4 + 3 + 2 + 1 + 0 = 10。✅
一般结论:完全降序的 n 个数,逆序对是 n(n-1)/2 个——
这就是为什么 1000 个降序车厢的答案是 1000×999÷2 = 499500。
答:因为 unique 只比较相邻的两个元素,把相邻的重复项合并掉。
如果数组是 2 5 2,两个 2 不相邻,unique 一个都删不掉。
排完序之后(2 2 5),相同的数一定挨在一起,unique 才能一次清干净。
★ 一句话:sort 负责把重复的凑到一起,unique 负责把它们合并。分工明确,顺序不能换。
【sort 三件套】 #include <algorithm> sort(a, a + n); // 升序(左闭右开!) sort(a, a + n, greater<int>()); // 降序,括号别丢 sort(a, a + n, cmp); // 自定义规则 sort(v.begin(), v.end()); // vector // 数据在 a[1]~a[n] → sort(a+1, a+n+1, cmp) 【cmp 的铁律】 cmp(x, y) 返回 true = x 排在 y 前面 只能写严格 < 或 >,绝不能写 <= / >= 多关键字:if(x.k1 != y.k1) return x.k1 > y.k1; if(x.k2 != y.k2) return x.k2 > y.k2; return x.k3 < y.k3; 参数用 const T &x 【去重三连】 sort(a, a + n); int m = unique(a, a + n) - a; // 数组:m 就是新长度 v.erase(unique(v.begin(), v.end()), v.end()); // vector 要 erase 【桶去重(值域小时更好用)】 bool vis[1005] = {false}; vis[x] = true; for(int v = 1; v <= 1000; v++) if(vis[v]) cout << v << " "; 【冒泡排序 + 数交换次数】 int cnt = 0; for(int i = 0; i < n - 1; i++) for(int j = 0; j < n - 1 - i; j++) if(a[j] > a[j+1]){ swap(a[j], a[j+1]); cnt++; } // cnt = 逆序对个数 = 最少相邻交换次数 【大数比较(字符串)】 先比长度:s.size() != t.size() → 长的更大 再比字典序:s > t 【拼接最大数(P1012)】 bool cmp(const string &a, const string &b){ return a + b > b + a; // ★ 不是 a > b! } 【输入防坑】 cin >> x 自动跳过空格和换行 → 数据分行也无所谓 不要用 getline 读数字序列
贪心入门:局部最优通向全局最优
今天学的排序,明天马上要派上大用场——「排序 + 贪心」是竞赛里最常见的组合拳: P1223 排队接水(按接水时间从小到大排)、P1094 纪念品分组(排序 + 双指针)、 P4995 跳跳、P3817 小 A 的糖果,全都建立在你今天掌握的排序能力之上。
另外,今天没搞懂的 P1012 拼接比较,明天还会以别的形式出现,记得复习一下 cmp 的心法。