小D遇到了一个有趣的序列变换问题。给定一个长度为n的整数序列a₁,a₂,...,aₙ,我们需要通过最少的相邻交换操作,将这个序列变成一个"好序列"。所谓好序列,就是先非严格递增,后非严格递减的序列。换句话说,存在某个位置k,使得对于所有i<k有aᵢ≤aᵢ₊₁,对于所有i≥k有aᵢ≥aᵢ₊₁。
这个问题在实际中有很多应用场景,比如优化某些排序算法的中间步骤,或者处理数据分布的形状调整。理解这个问题的解法,不仅能帮助我们解决这个特定问题,还能培养对序列操作和贪心算法的直觉。
解决这个问题的核心思路是贪心算法。观察发现,要让序列变成先升后降的形状,最小值应该出现在序列的最左端或最右端。基于这个观察,我们可以采取以下策略:
这个策略之所以有效,是因为每次处理最小值时,我们做出了局部最优的选择,而这些局部最优选择最终会导向全局最优解。
当序列中存在多个相同的最小值时,我们需要特别注意处理顺序。正确的做法是:
例如,对于序列[2,1,1,3],我们应该先处理第一个1(移动到最左需要1次交换)或最后一个1(移动到最右需要1次交换),而不是中间的1。
最直接的实现方式是:
这种方法的复杂度是O(n²),对于n≤1e5的数据规模来说显然不够高效。
为了将复杂度降低到O(n log n),我们可以使用平衡树(如Treap)来维护序列。具体思路是:
平衡树可以高效地支持查询、插入和删除操作,每个操作的时间复杂度都是O(log n),因此整体复杂度为O(n log n)。
虽然作者坚持使用Treap,但实际上树状数组(Fenwick Tree)是更简单高效的选择。树状数组可以实现:
cpp复制#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxn = 3e5 + 18;
int ch[maxn][2], rnd[maxn], val[maxn], siz[maxn], tot, rt;
// 创建新节点
int add(int num) {
tot++;
ch[tot][0] = ch[tot][1] = 0;
rnd[tot] = rand();
siz[tot] = 1;
val[tot] = num;
return tot;
}
// 更新节点大小
void push_up(int i) {
siz[i] = siz[ch[i][0]] + siz[ch[i][1]] + 1;
}
// 合并两棵Treap
int merge(int l, int r) {
if(!l || !r) return l + r;
if(rnd[l] < rnd[r]) {
ch[l][1] = merge(ch[l][1], r);
push_up(l);
return l;
} else {
ch[r][0] = merge(l, ch[r][0]);
push_up(r);
return r;
}
}
// 按值分裂Treap
void split(int rt, int v, int &l, int &r) {
if(!rt) { l = r = 0; return; }
if(val[rt] <= v) {
l = rt;
split(ch[l][1], v, ch[l][1], r);
push_up(l);
} else {
r = rt;
split(ch[r][0], v, l, ch[r][0]);
push_up(r);
}
}
// 查询并计算最小交换次数
int process(int id) {
int a, b, mid;
split(rt, id, a, b);
split(a, id - 1, a, mid);
int res = min(siz[a], siz[b]);
rt = merge(a, merge(mid, b));
return res;
}
// 删除元素
int del(int id) {
int a, b, mid;
split(rt, id, a, b);
split(a, id - 1, a, mid);
int res = min(siz[a], siz[b]);
rt = merge(a, b);
return res;
}
int n, ans = 0;
pair<int, int> a[maxn];
void main_() {
cin >> n;
for(int i = 1; i <= n; i++) {
cin >> a[i].first;
a[i].second = i;
}
sort(a + 1, a + n + 1);
// 初始化Treap
for(int i = 1; i <= n; i++) {
rt = merge(rt, add(i));
}
for(int i = 1, j; i <= n; i = j) {
for(j = i; j <= n && a[j].first == a[i].first; j++);
int l = i, r = j - 1;
while(l <= r) {
int cnt_l = process(a[l].second);
int cnt_r = process(a[r].second);
if(cnt_l <= cnt_r) {
ans += cnt_l;
del(a[l++].second);
} else {
ans += cnt_r;
del(a[r--].second);
}
}
}
cout << ans << endl;
}
我们需要证明每次选择移动最左或最右的最小值是最优的。假设存在一个全局最优解,其中某个中间的最小值被先移动。我们可以通过交换操作顺序,使得最左或最右的最小值先被移动,而不增加总交换次数。因此,贪心选择是安全的。
在每次移除一个最小值后,剩下的问题仍然是相同性质的子问题。我们做出的局部最优选择不会影响后续子问题的最优解,因此具有最优子结构。
空间复杂度主要是存储Treap和原序列,为O(n)。
这种序列变换问题在以下场景中可能遇到:
这个问题可以延伸到更一般的序列变换问题。例如:
理解这个问题的解法,有助于我们掌握贪心算法的设计思路,以及如何利用高级数据结构(如平衡树、树状数组)来优化算法效率。在实际编程比赛中,这类问题经常出现,掌握其解法可以大大提升解题能力。