D. Stay or Mirror - 题解
比赛与标签
比赛: Codeforces Round 1040 (Div. 2) 标签: greedy
题目大意喵~
主人你好呀~ 这道题是这样的喵~ (ฅ'ω'ฅ)
我们拿到一个长度为 n 的排列 p(也就是 1 到 n 这 n 个数,顺序打乱了而已)。 我们需要构造一个新的数组 a,长度也是 n。对于 p 里面的每一个数字 p_i,我们有两种选择来决定 a_i 的值:
- 保持不变:
a_i = p_i - 镜像变换:
a_i = 2n - p_i
我们的任务就是,为每一个 p_i 做出最喵的选择,使得最终构造出的数组 a 的逆序对数量最少。
一个逆序对指的是数组中一对下标 (i, j),满足 i < j 并且 a_i > a_j。
简单来说,就是通过二选一的操作,让新数组尽可能地有序,喵~
解题思路喵~
这道题看起来每个位置都有两种选择,2^n 种可能性,直接暴力肯定是不行的说。标签提示我们是贪心,那么我们来找找看有没有什么漂亮的贪心性质吧,喵~
1. 初步分析与基准情况
首先,咱们可以先算一个基准值。如果我们对所有的 p_i 都不做任何改变,也就是 a = p,这时候的逆序对数量是多少呢?这个很容易用 O(n^2) 的双重循环算出来,我们叫它 base_inversions 好了。
2. “小值”与“大值”的发现
接下来是关键的一步!我们观察一下两种选择 p_i 和 2n - p_i 的范围。
p_i是排列里的数,所以1 <= p_i <= n。我们叫它“小值”吧。2n - p_i呢?因为1 <= p_i <= n,所以n <= 2n - p_i <= 2n - 1。我们叫它“大值”好了。
看到了吗喵?任何一个“小值”都小于等于任何一个“大值”!(最特殊的情况是当 p_i = n 时,p_i 和 2n - p_i 都是 n)。
这个发现非常重要!它能大大简化我们对逆序对的分析:
- 如果
a_i是“大值”,a_j是“小值”,且i < j,那么a_i > a_j必然成立,这就一定会产生一个逆序对。 - 如果
a_i是“小值”,a_j是“大值”,且i < j,那么a_i < a_j必然成立,就一定不会产生逆序对。
3. 独立决策的奇迹
现在我们来考虑,对一个 p_i,我们把它从“小值”p_i 翻转成“大值”2n - p_i,逆序对数量会怎么变化呢?
最直观的想法是,这个变化量 delta_i 取决于其他的 a_j 是“小值”还是“大值”,这样决策之间就相互依赖了,没法贪心呀!QAQ
但是!奇迹发生了!我们可以通过一点点数学推导,证明每个位置的决策是独立的!
我们设 c_i 是一个选择变量,c_i = 0 表示我们选择 a_i = p_i(小值),c_i = 1 表示我们选择 a_i = 2n - p_i(大值)。
最终总的逆序对数量可以表示成一个关于所有 c_i 的函数。经过一番魔法般的推导(其实就是把所有情况的逆序对贡献加起来化简),我们可以得到一个超级优美的公式:
总逆序对 = base_inversions + Σ ( c_i * delta_i )
这里的 base_inversions 就是我们一开始算的全是“小值”时的逆序对数。而 delta_i 是一个只和 p_i 与它在原数组 p 中的前后元素大小关系有关的常数!
delta_i 的含义是:假设其他元素都不变(都是小值),只将 p_i 翻转成 2n - p_i,逆序对数量的变化量。
我们来算一下这个 delta_i:
- 原来
p_i与j < i的p_j产生的逆序对,是p_j > p_i的p_j的数量。我们记作count_before_greater。 - 原来
p_i与j > i的p_j产生的逆序对,是p_i > p_j的p_j的数量。 - 当
p_i变为2n - p_i(大值)后:- 对于
j < i,p_j是小值,2n - p_i是大值,所以p_j < 2n - p_i,逆序对为0。 - 对于
j > i,p_j是小值,2n - p_i是大值,所以2n - p_i > p_j,总是会产生逆序对。这样的j有n - 1 - i个。
- 对于
所以,变化量 delta_i = (翻转后的逆序对) - (翻转前的逆序对) delta_i = (n - 1 - i) - (count_before_greater + count_after_smaller)
这里 count_after_smaller 是 j > i 且 p_j < p_i 的数量。 我们知道 (count_after_smaller + count_after_greater) = n - 1 - i。 所以 count_after_smaller = n - 1 - i - count_after_greater。 代入一下,delta_i = (n - 1 - i) - (count_before_greater + n - 1 - i - count_after_greater)delta_i = count_after_greater - count_before_greater
这里的 count_after_greater 是指在 p 数组中,i 之后比 p_i 大的数的个数。count_before_greater 是 i 之前比 p_i 大的数的个数。
4. 贪心策略
既然总逆序对 I = base_inversions + Σ (c_i * delta_i),为了让 I 最小,我们就要让 Σ (c_i * delta_i) 最小。
因为每个 c_i 的选择(0或1)只影响 c_i * delta_i 这一项,所以我们可以对每个 i 独立做决策啦!
- 如果
delta_i < 0,说明翻转能减少逆序对,我们当然要翻转!选择c_i = 1,总变化量就加上这个负的delta_i。 - 如果
delta_i >= 0,说明翻转不会变得更好(甚至可能变差),那我们就不翻转啦。选择c_i = 0,总变化量加 0。
所以,最终的算法就是:
- 计算
base_inversions。 - 对每个
i从0到n-1: a. 计算delta_i = count_after_greater - count_before_greater。 b. 如果delta_i < 0,就把这个delta_i加到一个累加器total_delta里。 - 最终答案就是
base_inversions + total_delta。
这个算法的复杂度是 O(n^2),对于 n 最大 5000 且总和不超过 5000 的情况,是完全可以接受的!
代码实现喵~
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
void solve() {
int n;
std::cin >> n;
std::vector<int> p(n);
for (int i = 0; i < n; ++i) {
std::cin >> p[i];
}
// 步骤1:计算如果我们什么都不做 (a=p) 时的基础逆序对数量,喵~
long long base_inversions = 0;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (p[i] > p[j]) {
base_inversions++;
}
}
}
// 步骤2:对每个位置i,计算把它翻转带来的“收益”,如果收益是负的(即逆序对减少),就采纳它
long long total_delta = 0;
for (int i = 0; i < n; ++i) {
int current_val = p[i];
// 计算在 p[i] 之后,有多少个元素比 p[i] 大
int count_after_greater = 0;
for (int j = i + 1; j < n; ++j) {
if (p[j] > current_val) {
count_after_greater++;
}
}
// 计算在 p[i] 之前,有多少个元素比 p[i] 大
int count_before_greater = 0;
for (int k = 0; k < i; ++k) {
if (p[k] > current_val) {
count_before_greater++;
}
}
// 这就是我们推导出的变化量 delta_i
long long delta = (long long)count_after_greater - count_before_greater;
// 如果变化量是负数,说明翻转 p[i] 是个好主意,我们把它计入总变化量
if (delta < 0) {
total_delta += delta;
}
}
// 最终答案 = 基础逆序对 + 所有负的delta之和
std::cout << base_inversions + total_delta << " \n";
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}复杂度分析喵~
- 时间复杂度: O(n^2) 的说。计算
base_inversions需要两层循环,是O(n^2)。接着计算所有delta_i,对于每个i,我们又需要遍历它前面和后面的元素,这部分总共也是O(n^2)。所以总时间复杂度是O(n^2)。 - 空间复杂度: O(n) 的说。我们只需要一个数组
p来存储输入的排列,所以空间复杂度是O(n)。
知识点与总结喵~
这道题真是一道非常巧妙的贪心题呢!(๑•̀ㅂ•́)و✧
- 核心思想: 关键在于将一个复杂的、决策互相依赖的问题,通过数学变换,转化为了一个每个决策都相互独立的问题。
I = base_inversions + Σ (c_i * delta_i)这个公式就是解题的钥匙! - 边界分析: 对
p_i和2n - p_i的值域进行分析,得出“小值”和“大值”的结论,是简化问题的突破口。很多时候,注意题目给定的数值范围会带来意想不到的收获哦。 - 贪心证明: 虽然我们是贪心地选择了所有
delta_i < 0的情况,但背后是有严格的数学公式保证其正确性的。这告诉我们,一个好的贪心策略往往不是凭感觉,而是能够被证明的。 - 编程技巧: 对于这类问题,先计算一个基准值,再计算每个独立操作带来的变化量,是一种常见的解题模式。
希望这篇题解能帮助到主人哦!下次遇到难题也别怕,静下心来,说不定就能发现其中的小秘密啦~ 喵~