Skip to content

喵~ 主人,欢迎来到我的题解小课堂!今天我们要解决的是一道有点意思的排序问题,Codeforces 2121D。别看它要求这么多,其实想通了就很简单哦,就像顺毛一样简单,喵~

题目大意

我们有两个长度为 n 的数组 ab。这两个数组里包含了从 12n 的所有整数,每个数字都只出现一次。

我们的目标是通过一系列操作,让这两个数组满足两个条件:

  1. 内部有序: a 数组和 b 数组自身都是升序的。也就是说,对于任何 1 ≤ i < n,都要有 a[i] < a[i+1]b[i] < b[i+1]
  2. 对应位小于: a 数组的每个元素都要小于 b 数组相应位置的元素。也就是说,对于任何 1 ≤ i ≤ n,都要有 a[i] < b[i]

我们可以进行三种操作:

  1. 1 i: 交换 a[i]a[i+1]
  2. 2 i: 交换 b[i]b[i+1]
  3. 3 i: 交换 a[i]b[i]

题目不要求我们用最少的操作次数,但总操作次数不能超过 1709 次。我们需要找出任意一种可行的操作序列。

解题思路

要把两个数组都弄得整整齐齐,还要满足 a[i] < b[i],听起来有点复杂,喵。这种时候,我们就要学会把复杂的问题拆解成一小步一小步的简单问题来解决!

我们可以分三步走,就像猫猫走路一样,一步一步来,优雅又准确~

第一步:先满足 a[i] < b[i]

我们先不管排序的事,专注于第二个条件:a[i] < b[i]。这个条件最简单了,不是吗?我们从头到尾(从 i=1n)检查一遍,如果发现哪一對 a[i]b[i] 不听话,a[i]b[i] 大了,我们就用第三种操作把它们换过来!这样一来,对于每一个 ia[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 数组也排成升序。

这样就完成了吗?

等一下,猫猫的直觉告诉我,这里有个小问题需要确认! 我们在第二步和第三步分别对 ab 进行了排序。排序后,它们内部确实是有序了。但是,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 次限制。完美,喵~

题解代码

下面就是根据我们的思路写出的代码啦,附上了一些猫猫的注释!

cpp
#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;
}

知识点介绍

这道题虽然简单,但里面包含了一些非常重要的思想哦!

  1. 问题分解 (Problem Decomposition) 这是一个非常核心的编程思想。我们把一个看似复杂、多重约束的问题(既要排序,又要满足大小关系),分解成了三个独立的、简单的步骤:①处理大小关系 -> ②排序a -> ③排序b。学会分解问题,是成为编程高手的必经之路,喵!

  2. 贪心算法 (Greedy Algorithm) 我们的第一步其实就是一种贪心策略。对于每个位置 i,我们都做出局部最优的选择:如果 a[i] > b[i],就立刻交换它们以满足 a[i] < b[i]。我们并没有考虑这个交换对未来的影响,只是“贪心地”解决了当前的问题。幸运的是,在这个问题里,这种贪心策略建立了一个非常好的性质,使得后续步骤可以顺利进行。贪心就像猫猫总是优先选择最舒服的姿势睡觉一样,追求当下的最优,喵~

  3. 排序算法 - 冒泡排序 (Bubble Sort) 冒泡排序是一种基础的排序算法。它的工作原理是重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。这个过程就像水中的气泡一样,较小的元素会慢慢“浮”到数列的顶端。

    • 优点: 实现简单,容易理解。
    • 缺点: 时间复杂度为 O(n²),在数据量大时效率很低。
    • 适用场景: 对于这道题 n ≤ 40 的情况,O(n²) 的复杂度完全可以接受。冒泡排序虽然朴实,但很管用,就像猫猫拳一样!

希望这篇题解能帮助到主人哦!下次再遇到难题,记得来找我,喵~

Released under the MIT License.