E. Jeff and Permutation - 题解
比赛与标签
比赛: Codeforces Round 204 (Div. 1) 标签: greedy, *2200 难度: *2200
题目大意喵~
主人,你好呀~!(ฅ'ω'ฅ) 这道题是关于一个叫 Jeff 的男孩子和他讨厌的“逆序对”的故事哦。
我们拿到一个长度为 n
的整数序列 p
。对于序列里的每一个数 p_i
,我们都可以选择把它变成 -p_i
,也可以选择不改变它。我们的目标是,通过这些操作,让最终序列里的“逆序对”数量变得最少最少!
一个“逆序对”是指,在序列 a
中,存在两个下标 i
和 j
,满足 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
决定符号,我们来比较一下两种选择带来的后果:
选择
+a_i
(让它为正):- 考虑一个
a_j
,如果a_j < a_i
。那么无论a_j
是正还是负,+a_i
都比它大(因为+a_i > a_j > -a_j
)。 - 所以,如果
j > i
(a_j
在a_i
右边),a_i > a_j
就会形成一个逆序对。 - 如果
j < i
(a_j
在a_i
左边),a_i > a_j
不会形成逆序对。 - 因此,选择
+a_i
会和所有在它右边且绝对值比它小的数形成逆序对。
- 考虑一个
选择
-a_i
(让它为负):- 同样考虑一个
a_j
,如果a_j < a_i
。那么无论a_j
是正还是负,-a_i
都比它小(因为a_j > -a_j > -a_i
)。 - 所以,如果
j < i
(a_j
在a_i
左边),a_j > a_i
就会形成一个逆序对。 - 如果
j > i
(a_j
在a_i
右边),a_i < a_j
不会形成逆序对。 - 因此,选择
-a_i
会和所有在它左边且绝对值比它小的数形成逆序对。
- 同样考虑一个
看到这里的规律了吗,喵?(ΦωΦ)
对于 a_i
来说,它和那些绝对值比它小的数产生的逆序对数量,只取决于我们给 a_i
的符号!
- 变成
+a_i
,贡献的逆序对数 = 右边比a_i
小的数的个数。 - 变成
-a_i
,贡献的逆序对数 = 左边比a_i
小的数的个数。
你可能会问,那和绝对值比 a_i
大的数呢?神奇的是,这个贪心策略的美妙之处就在于,我们不需要考虑它们!可以证明,当我们对每个 a_i
都做出上述的局部最优选择后,全局的结果也是最优的。每个数都只关心如何最小化与比自己“弱小”的数的冲突,最终整个序列就达到了和谐,喵~
所以,我们的总策略就是:
- 把输入的所有数
p_i
变成它们的绝对值a_i
。 - 遍历每一个
a_i
(从i = 0
到n-1
)。 - 对于当前的
a_i
,数一数在它左边有多少个数比它小(smaller_before
),再数一数在它右边有多少个数比它小(smaller_after
)。 a_i
对总逆序对数量的最小贡献就是min(smaller_before, smaller_after)
。- 把所有
i
的这个最小贡献加起来,就是最终的答案啦!
代码实现喵~
#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)。解题的关键在于发现并证明那个可以独立决策的贪心性质。
- 问题分解: 将最小化总逆序对的问题,分解为最小化每个元素与“比它小的元素”之间产生的逆序对数量之和。
- 重要观察:
- 数字的绝对值是固定的,我们只能改变符号。
- 任何正数 > 任何负数,这个性质是构建贪心策略的基石。
- 对于一个数
a_i
,它与比它小的数a_j
形成的逆序对数量,只取决于a_i
的符号和j
相对于i
的位置,而与a_j
的符号无关!这真是太神奇了!
希望这篇题解能帮助到你,主人!要继续加油,享受解题的乐趣哦!喵~ ( ´ ▽ ` )ノ