Skip to content

E. Jeff and Permutation - 题解

比赛与标签

比赛: Codeforces Round 204 (Div. 1) 标签: greedy, *2200 难度: *2200

题目大意喵~

主人,你好呀~!(ฅ'ω'ฅ) 这道题是关于一个叫 Jeff 的男孩子和他讨厌的“逆序对”的故事哦。

我们拿到一个长度为 n 的整数序列 p。对于序列里的每一个数 p_i,我们都可以选择把它变成 -p_i,也可以选择不改变它。我们的目标是,通过这些操作,让最终序列里的“逆序对”数量变得最少最少!

一个“逆序对”是指,在序列 a 中,存在两个下标 ij,满足 i < j 并且 a_i > a_j

简单来说,就是我们可以随意改变每个数字的符号,来让整个序列的逆序对总数最小。最后,只要告诉 Jeff 这个最小的数量就可以啦,喵~

解题思路喵~

这道题看起来有点复杂呢,因为改变一个数字的符号,可能会影响它和序列里其他所有数字的关系,感觉牵一发而动全身的说!但是,只要我们找到正确的思考角度,问题就会变得意外地简单哦!

首先,我们来观察一下操作的本质。不管我们怎么变,数字 p_i 的最终值要么是 |p_i|,要么是 -|p_i|。它的绝对值是不会变的!我们不妨先把所有数都变成它们的绝对值,得到一个新序列 a,其中 a_i = |p_i|。现在的问题就变成了:对于每个 a_i,我们是保留它(正数),还是把它变成 -a_i(负数),才能使总逆序对最少呢?

这里有一个非常重要的性质:任何一个正数都比任何一个负数要大(0 除外)。这意味着,我们最终得到的序列,可以看成是被我们选择的符号分成了“正数集团”和“负数集团”。

那么,我们该如何为每一个 a_i 做决定呢?一个一个试吗?那可太慢啦!这里就需要一点点贪心的智慧了,喵~

核心思想: 我们可以对每一个数字 a_i 独立地做出最优决策!

让我们来分析一下,对于固定的一个 a_i(位于第 i 个位置),它的符号选择会如何影响逆序对的数量。一个逆序对 (k, l) 总是成对出现的,我们来考虑 a_i 会和哪些 a_j 形成逆序对。

最奇妙的地方来啦!我们来分析 a_i 和其他数 a_j 的关系,特别是当 a_j 的绝对值比 a_i 小的时候。

假设我们正在为 a_i 决定符号,我们来比较一下两种选择带来的后果:

  1. 选择 +a_i (让它为正)

    • 考虑一个 a_j,如果 a_j < a_i。那么无论 a_j 是正还是负,+a_i 都比它大(因为 +a_i > a_j > -a_j)。
    • 所以,如果 j > ia_ja_i 右边),a_i > a_j 就会形成一个逆序对。
    • 如果 j < ia_ja_i 左边),a_i > a_j 不会形成逆序对。
    • 因此,选择 +a_i 会和所有在它右边且绝对值比它小的数形成逆序对。
  2. 选择 -a_i (让它为负)

    • 同样考虑一个 a_j,如果 a_j < a_i。那么无论 a_j 是正还是负,-a_i 都比它小(因为 a_j > -a_j > -a_i)。
    • 所以,如果 j < ia_ja_i 左边),a_j > a_i 就会形成一个逆序对。
    • 如果 j > ia_ja_i 右边),a_i < a_j 不会形成逆序对。
    • 因此,选择 -a_i 会和所有在它左边且绝对值比它小的数形成逆序对。

看到这里的规律了吗,喵?(ΦωΦ)

对于 a_i 来说,它和那些绝对值比它小的数产生的逆序对数量,只取决于我们给 a_i 的符号!

  • 变成 +a_i,贡献的逆序对数 = 右边比 a_i 小的数的个数。
  • 变成 -a_i,贡献的逆序对数 = 左边比 a_i 小的数的个数。

你可能会问,那和绝对值比 a_i 大的数呢?神奇的是,这个贪心策略的美妙之处就在于,我们不需要考虑它们!可以证明,当我们对每个 a_i 都做出上述的局部最优选择后,全局的结果也是最优的。每个数都只关心如何最小化与比自己“弱小”的数的冲突,最终整个序列就达到了和谐,喵~

所以,我们的总策略就是:

  1. 把输入的所有数 p_i 变成它们的绝对值 a_i
  2. 遍历每一个 a_i (从 i = 0n-1)。
  3. 对于当前的 a_i,数一数在它左边有多少个数比它小(smaller_before),再数一数在它右边有多少个数比它小(smaller_after)。
  4. a_i 对总逆序对数量的最小贡献就是 min(smaller_before, smaller_after)
  5. 把所有 i 的这个最小贡献加起来,就是最终的答案啦!

代码实现喵~

cpp
#include <iostream>
#include <vector>
#include <cmath>
#include <numeric>
#include <algorithm>

int main() {
    // 使用C++标准库的快速IO,让程序跑得更快喵~
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n;
    std::cin >> n;

    std::vector<int> p(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> p[i];
        // 第一步:把所有数都变成非负的绝对值
        p[i] = std::abs(p[i]);
    }

    long long total_inversions = 0;
    // 第二步:遍历每一个元素 p[i]
    for (int i = 0; i < n; ++i) {
        long long smaller_before = 0; // 记录左边比 p[i] 小的数的数量
        long long smaller_after = 0;  // 记录右边比 p[i] 小的数的数量

        // 第三步:计算 smaller_before
        for (int j = 0; j < i; ++j) {
            if (p[j] < p[i]) {
                smaller_before++;
            }
        }

        // 第三步:计算 smaller_after
        for (int j = i + 1; j < n; ++j) {
            if (p[j] < p[i]) {
                smaller_after++;
            }
        }

        // 第四步:取两者中的较小值,累加到总数中
        // 这代表了 p[i] 贡献的最小逆序对数量
        total_inversions += std::min(smaller_before, smaller_after);
    }

    // 第五步:输出最终结果
    std::cout << total_inversions << std::endl;

    return 0;
}

复杂度分析喵~

  • 时间复杂度: O(n^2) 的说。 我们有一个主循环,它会遍历数组中的 n 个元素。在主循环的内部,又有两个独立的循环,分别用来统计当前元素的左边和右边。这两个内部循环在最坏情况下也需要遍历 n 个元素。所以总的时间复杂度就是 O(n * n) = O(n^2)。对于题目给出的 n <= 2000 的范围,2000 * 2000 = 4,000,000 次计算,是完全可以接受的!

  • 空间复杂度: O(n) 的说。 我们主要用了一个 vector 来存储输入的 n 个数字的绝对值。所以空间消耗是和 n 成正比的,也就是 O(n)。

知识点与总结喵~

这道题真是一道非常巧妙的贪心题呢!它告诉我们,有时候一个看似全局性的复杂问题,可以通过找到正确的切入点,分解成一系列独立的、简单的局部问题来解决。

  • 核心算法: 贪心 (Greedy Algorithm)。解题的关键在于发现并证明那个可以独立决策的贪心性质。
  • 问题分解: 将最小化总逆序对的问题,分解为最小化每个元素与“比它小的元素”之间产生的逆序对数量之和。
  • 重要观察:
    1. 数字的绝对值是固定的,我们只能改变符号。
    2. 任何正数 > 任何负数,这个性质是构建贪心策略的基石。
    3. 对于一个数 a_i,它与比它小的数 a_j 形成的逆序对数量,只取决于 a_i 的符号和 j 相对于 i 的位置,而与 a_j 的符号无关!这真是太神奇了!

希望这篇题解能帮助到你,主人!要继续加油,享受解题的乐趣哦!喵~ ( ´ ▽ ` )ノ

Released under the MIT License.