蓝桥杯 2026 萌新首秀第二场 T3 图书归位(书架调整)
一句话概括解法:先用「奇偶性不变量」判断有无解,再从左到右贪心把每个数归位。这道题的核心考点不是敲代码,而是证明不清不变量就写不对判定条件。
一、题目复述
N 本书编号 1 ~ N 排在书架上,顺序可能是乱的。目标状态是 1, 2, 3, ..., N(第 i 个位置放编号为 i 的书)。
一次操作:任选连续的三个位置 x, x+1, x+2,交换 a[x] 与 a[x+2],中间的 a[x+1] 保持不动。
例如 1 2 3 4 5,选第 2 ~ 4 个位置,交换 2 和 4,得到 1 4 3 2 5。
求把书架恢复成 1, 2, ..., N 的最少操作次数;无论如何都无法完成时输出 -1。
数据范围:3 ≤ N ≤ 2000,输入保证是 1 ~ N 的一个排列。
二、第一步:把操作翻译成人话
「选连续三个位置、交换两端」听起来复杂,但作用到元素身上只有一句话:
一次操作 = 某个元素向左或向右跳 2 格,代价 1。
因为中间那个位置的书自始至终没被动过,我们完全可以忽略它,只看两端。
于是问题变成:在一个数组里,每次可以把一个元素搬动 2 格,最少几步能把它排好序?
三、核心知识点一:不变量(本题的胜负手)
3.1 观察
一个元素的位置只有两种可能:奇数下标或偶数下标。
而一次操作交换的是 a[x] 和 a[x+2]——这两个下标奇偶性必然相同(相差 2)。
所以:
一个元素所在位置的奇偶性,永远不会改变。
这就是本题的不变量,它直接决定了一半的答案。
3.2 画出「两条轨道」
把书架按奇偶性拆成两条互不干扰的轨道:
graph LR
subgraph 奇数位置轨道
O1["位置 1"] --- O3["位置 3"] --- O5["位置 5"] --- O7["位置 7"]
end
subgraph 偶数位置轨道
E2["位置 2"] --- E4["位置 4"] --- E6["位置 6"]
end
一次操作,只会在同一条轨道内把两个相邻的元素对调。两条轨道老死不相往来。
3.3 无解的判定条件
数字 i 的目标位置就是 i,所以它的目标奇偶性就是 i 的奇偶性。
结合不变量,立刻得到:
如果数字
i现在所在的下标pos与i的奇偶性不同,那么i永远无法回到位置 i → 无解,输出-1。
注意这里比较的是两个下标(当前位置 pos 和目标位置 i)的奇偶性,不是比较书号本身。这是本题最容易写错的地方。
更简洁的等价说法:奇数位置上必须全是奇数(如果能做到,偶数位置自然全是偶数)。
3.4 为什么奇偶性对上了就一定有解
这是容易被忽略的一步。上面只证明了「奇偶不匹配 → 无解」,反过来呢?
看奇数轨道:位置 1, 3, 5, ... 上的值也正好是 1, 3, 5, ... 这组奇数(因为排列里的奇数有 ⌈N/2⌉ 个,奇数位置也正好有 ⌈N/2⌉ 个)。
而一次操作在奇数组内部,就是把相邻的两个元素交换。而相邻交换可以生成任意排列(这就是冒泡排序能排好任何数组的原因)。
所以只要奇偶性匹配,就一定排得回去。判定条件既是必要的,也是充分的。
四、核心知识点二:最少次数到底是多少
4.1 换个角度:这是一道「相邻交换排序」题
把奇数轨道上的数字按位置顺序拉成一条序列:
1 | p = (a[1], a[3], a[5], ...) |
最终目标是 p = (1, 3, 5, ...)。而一次操作在这个序列上,恰好就是交换相邻两个元素。
问题于是变成:最少需要多少次相邻交换,才能把序列排成升序?
4.2 结论:最少相邻交换次数 = 逆序对数
这是经典结论,值得记住证明思路:
- 下界(为什么不能再少):交换两个相邻且逆序的元素,逆序对数量正好减少 1;交换两个相邻且不逆序的元素,逆序对数量反而增加 1。既然最终逆序对数必须是 0,设初始逆序对数为
inv,那么至少要做inv次有效交换。 - 可达(为什么能做到):冒泡排序每次只交换相邻的逆序对,恰好交换
inv次就把序列排好。
两者夹起来,最少次数就是 inv。
4.3 完整的答案公式
两条轨道互不影响,所以:
最少操作次数 = 奇数子序列的逆序对数 + 偶数子序列的逆序对数
代入样例 a = [5, 2, 3, 4, 1]:
| 轨道 | 序列 | 逆序对 | 个数 |
|---|---|---|---|
| 奇数位置 | (5, 3, 1) | (5,3)、(5,1)、(3,1) | 3 |
| 偶数位置 | (2, 4) | 无 | 0 |
| 合计 | 3 |
和样例输出一致。
这个视角的价值在于:它顺便证明了贪心是最优的——不再需要「感觉这样最短」这种模糊说法。
五、算法思路:从左到右贪心归位
5.1 步骤
按 i 从 1 到 N 依次处理,让位置 i 上的数变成 i:
- 若
a[i] == i,已经就位,跳过; - 在
i的右侧找到数字i所在的下标pos; - 检查
pos与i的奇偶性是否相同,不同就输出-1直接结束; - 反复执行
swap(a[pos-2], a[pos])、pos -= 2,直到数字i落到位置i。
5.2 为什么这样贪心是对的
三个关键点:
- 查找范围可以缩小:处理到第
i位时,位置1 ~ i-1已经固定是1 ~ i-1,所以数字i不可能在i的左边(除非它已经在i上)。因此只需从i+1往右找。 - 不会破坏已排好的部分:把数字
i从pos往左挪,每一步只涉及pos-2和pos两个位置,而pos-2 ≥ i,所以整段操作都在i及其右侧,前面1 ~ i-1完全不受影响。 - 每一步都是必要的:数字
i至少要向左移动(pos - i) / 2步,每步 2 格,我们一次不多做、一次不少做。这也和上一节的逆序对结论吻合。
5.3 代码
1 |
|
上面注释里提到的「等价写法」是指把查找和判定合并:
1 | for (int j = i + 1; j <= n; j++) { |
两种写法都对,区别只是「找到了先判定」还是「找完再判定」。
5.4 复杂度
外层 N 次,内层查找 O(N),移动 O(N),总体 O(N²)。N = 2000 时约 4×10⁶ 次运算,1 秒时限轻松通过。
六、样例逐步模拟
输入:
1 | 5 |
| 步骤 | 处理 | 操作(选哪三个连续位置) | 数组变化 | 次数 |
|---|---|---|---|---|
| 初始 | 5 2 3 4 1 |
0 | ||
| 1 | i=1,数字 1 在 pos=5 | 选 3~5,交换 a[3]、a[5] | 5 2 1 4 3 |
1 |
| 2 | 1 在 pos=3 | 选 1~3,交换 a[1]、a[3] | 1 2 5 4 3 |
2 |
| 3 | i=2,已就位 | 跳过 | 1 2 5 4 3 |
2 |
| 4 | i=3,数字 3 在 pos=5 | 选 3~5,交换 a[3]、a[5] | 1 2 3 4 5 |
3 |
输出 3,与样例一致。
七、易错点清单
- 奇偶判断比较错了对象:要比较下标
pos与下标i的奇偶性,不是比较a[pos]和i的值。写错这一条会得到莫名其妙的-1或死循环。 break的位置:必须放在if (a[j] == i)的大括号里面。放到外面就变成「永远在第i+1位就退出」,查找等于没做。pos忘了初始化:C++ 局部变量不初始化就是随机垃圾值(未定义行为),必须显式写int pos = -1;。- 无解时直接
return 0:判定失败必须立刻结束程序。如果只是continue或打个标记继续跑,后面的while会陷入死循环或访问越界。 while (a[i] != i)会不会死循环? 不会。因为奇偶性已经校验过,pos每次减 2,最终必然精确落到i。- 下标建议从 1 开始:本题的位置编号和书号天然对齐(位置
i放书i),用 1 起始下标能省掉大量-1换算。
八、拓展一:为什么不能用 BFS
「最少步数」四个字容易让人条件反射想到 BFS。但 BFS 的状态是整个排列:
- N = 10 时全排列约 3.6×10⁶,勉强能跑;
- N = 2000 时状态数远超宇宙原子数。
所以本题真正要考的是识别出结构性规律(不变量 + 贪心),而不是无脑搜。凡是「最少操作次数 + 有明确不变量」的题,都优先往这上面想。
九、拓展二:O(N log N) 的逆序对写法
既然答案就是「两条轨道的逆序对数之和」,就可以用树状数组把移动模拟整个省掉,N 开到 10⁶ 也没问题。
关键技巧:把每条轨道上的值压缩成轨道内的排名。设位置 i 上的值为 v(已保证 v 与 i 同奇偶),则
1 | 排名 = (v + 1) / 2 |
验证一下:
- 奇数轨道:
v = 1, 3, 5→ 排名1, 2, 3 - 偶数轨道:
v = 2, 4, 6→(2+1)/2 = 1、(4+1)/2 = 2、(6+1)/2 = 3
一个公式同时适配奇偶,很省事。
1 |
|
十、小结与模板记忆
- 不变量:
swap(a[x], a[x+2])不改变任何元素所在位置的奇偶性 → 奇偶不匹配即无解;奇数位置全是奇数则必有解。 - 最少次数:等于「奇数子序列逆序对数 + 偶数子序列逆序对数」,这个结论同时给出了贪心最优性的严格证明。
- 贪心:从左到右,每次把数字
i沿同奇偶轨道向左跳 2 格归位,操作区间始终在i及其右侧,不会破坏已排好的前缀。 - 模板:
1 | if (a[i] == i) continue; |
写这类「操作有固定步长」的题,先问自己两个问题:
- 什么量在操作前后不变?(这里是位置奇偶性)
- 在不变量的约束下,最少步数等于什么经典量?(这里是逆序对)
