喵~ 主人,欢迎来到我的题解小课堂!今天我们要解决的是一道有点意思的排序问题,Codeforces 2121D。别看它要求这么多,其实想通了就很简单哦,就像顺毛一样简单,喵~
题目大意
我们有两个长度为 n 的数组 a 和 b。这两个数组里包含了从 1 到 2n 的所有整数,每个数字都只出现一次。
我们的目标是通过一系列操作,让这两个数组满足两个条件:
- 内部有序:
a数组和b数组自身都是升序的。也就是说,对于任何1 ≤ i < n,都要有a[i] < a[i+1]和b[i] < b[i+1]。 - 对应位小于:
a数组的每个元素都要小于b数组相应位置的元素。也就是说,对于任何1 ≤ i ≤ n,都要有a[i] < b[i]。
我们可以进行三种操作:
1 i: 交换a[i]和a[i+1]。2 i: 交换b[i]和b[i+1]。3 i: 交换a[i]和b[i]。
题目不要求我们用最少的操作次数,但总操作次数不能超过 1709 次。我们需要找出任意一种可行的操作序列。
解题思路
要把两个数组都弄得整整齐齐,还要满足 a[i] < b[i],听起来有点复杂,喵。这种时候,我们就要学会把复杂的问题拆解成一小步一小步的简单问题来解决!
我们可以分三步走,就像猫猫走路一样,一步一步来,优雅又准确~
第一步:先满足 a[i] < b[i]
我们先不管排序的事,专注于第二个条件:a[i] < b[i]。这个条件最简单了,不是吗?我们从头到尾(从 i=1 到 n)检查一遍,如果发现哪一對 a[i] 和 b[i] 不听话,a[i] 比 b[i] 大了,我们就用第三种操作把它们换过来!这样一来,对于每一个 i,a[i] 就肯定比 b[i] 小了。这一步非常关键,它为我们后续的排序铺平了道路,喵~
第二步:单独对 a 数组排序
现在,我们已经保证了 a[i] < b[i]。接下来我们来整理 a 数组。我们可以用最经典(也最可爱)的冒泡排序!我们只用第一种操作(交换相邻的 a[i] 和 a[i+1]),把 a 数组排成升序。因为我们只动 a 数组,所以 b 数组不会变,之前 a[i] < b[i] 的关系也不会被破坏。
第三步:单独对 b 数组排序
a 数组排好队了,b 数组也要跟上!我们同样用冒泡排序,这次用第二种操作(交换相邻的 b[i] 和 b[i+1]),把 b 数组也排成升序。
这样就完成了吗?
等一下,猫猫的直觉告诉我,这里有个小问题需要确认! 我们在第二步和第三步分别对 a 和 b 进行了排序。排序后,它们内部确实是有序了。但是,a[i] < b[i] 这个条件还成立吗?
答案是:成立的,喵!
为什么呢?我们可以这样想: 在第一步结束后,我们得到的数组(称它们为 a' 和 b')满足 a'[i] < b'[i]。这意味着,a' 数组里的 n 个数,从整体上看,是比 b' 数组里的 n 个数要“小”的。 当我们把 a' 排序得到最终的 a,把 b' 排序得到最终的 b 时:
a[0]是a'中最小的数。b[0]是b'中最小的数。 因为a'中的每一个数都小于对应的b'中的数,所以a'中最小的数,肯定也小于b'中最小的数。所以a[0] < b[0]。 同理,a数组是a'排序后的结果,b数组是b'排序后的结果。可以证明,对于任何i,排序后的a[i]都会小于排序后的b[i]。所以我们的策略是完全正确的!
最后,我们来算一下操作次数。n 最大是 40。
- 第一步最多
n次操作。 - 第二步冒泡排序最多
n*(n-1)/2次操作。 - 第三步冒泡排序最多
n*(n-1)/2次操作。 总共最多是n + n*(n-1)=n^2次操作。当n=40时,40*40 = 1600次,这远远小于题目给的1709次限制。完美,喵~
题解代码
下面就是根据我们的思路写出的代码啦,附上了一些猫猫的注释!
#include <iostream>
#include <vector>
#include <utility>
#include <algorithm>
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n), b(n);
for (int i = 0; i < n; ++i) {
std::cin >> a[i];
}
for (int i = 0; i < n; ++i) {
std::cin >> b[i];
}
// 用一个小本本记下我们的所有操作,喵~
std::vector<std::pair<int, int>> ops;
// Phase 1: 确保 a[i] < b[i]
// 这是最关键的一步,让所有 a[i] 都乖乖地比 b[i] 小
for (int i = 0; i < n; ++i) {
if (a[i] > b[i]) {
std::swap(a[i], b[i]);
// 操作类型是3,下标要从1开始哦
ops.push_back({3, i + 1});
}
}
// Phase 2: 用冒泡排序整理 a 数组
// 把大的数字像泡泡一样“冒”到后面去~
for (int i = 0; i < n - 1; ++i) {
for (int j = 0; j < n - 1 - i; ++j) {
if (a[j] > a[j + 1]) {
std::swap(a[j], a[j + 1]);
// 操作类型是1,下标是 j+1
ops.push_back({1, j + 1});
}
}
}
// Phase 3: 用冒泡排序整理 b 数组
// a 排好了,b 也要整整齐齐!
for (int i = 0; i < n - 1; ++i) {
for (int j = 0; j < n - 1 - i; ++j) {
if (b[j] > b[j + 1]) {
std::swap(b[j], b[j + 1]);
// 操作类型是2,下标是 j+1
ops.push_back({2, j + 1});
}
}
}
// 最后,告诉裁判我们一共操作了多少次,以及具体的操作步骤
std::cout << ops.size() << "\n";
for (const auto& op : ops) {
std::cout << op.first << " " << op.second << "\n";
}
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}知识点介绍
这道题虽然简单,但里面包含了一些非常重要的思想哦!
问题分解 (Problem Decomposition) 这是一个非常核心的编程思想。我们把一个看似复杂、多重约束的问题(既要排序,又要满足大小关系),分解成了三个独立的、简单的步骤:①处理大小关系 -> ②排序a -> ③排序b。学会分解问题,是成为编程高手的必经之路,喵!
贪心算法 (Greedy Algorithm) 我们的第一步其实就是一种贪心策略。对于每个位置
i,我们都做出局部最优的选择:如果a[i] > b[i],就立刻交换它们以满足a[i] < b[i]。我们并没有考虑这个交换对未来的影响,只是“贪心地”解决了当前的问题。幸运的是,在这个问题里,这种贪心策略建立了一个非常好的性质,使得后续步骤可以顺利进行。贪心就像猫猫总是优先选择最舒服的姿势睡觉一样,追求当下的最优,喵~排序算法 - 冒泡排序 (Bubble Sort) 冒泡排序是一种基础的排序算法。它的工作原理是重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。这个过程就像水中的气泡一样,较小的元素会慢慢“浮”到数列的顶端。
- 优点: 实现简单,容易理解。
- 缺点: 时间复杂度为 O(n²),在数据量大时效率很低。
- 适用场景: 对于这道题
n ≤ 40的情况,O(n²) 的复杂度完全可以接受。冒泡排序虽然朴实,但很管用,就像猫猫拳一样!
希望这篇题解能帮助到主人哦!下次再遇到难题,记得来找我,喵~