#535. 「LibreOJ Round #6」花火
题意:给定一个序列,至多交换一次任意两个数,使得逆序对数量最小。
数据范围:\(n\le 5\times 10^5\)。
考虑什么时候不交换,只有 \(a_i=i\) 时不交换。
\(\bullet\) 暴力做法,枚举 \(i\),\(j\) 交换,再求逆序对数。复杂度 \(O(n^3\log n)\)。预计得分 \(10\) 分。
\(\bullet\) 优化,可以预处理出 \(f_{i,j}\) 表示 \(i\) 换到 \(j\) 减少的逆序对数量,同理处理出 \(g_{j,i}\) 表示 \(j\) 往前换到 \(i\) 变化的逆序对数,答案即为 \(\min\limits_{i<j}(ans-f_{i,j}-g_{j,i}+1)\)。复杂度 \(O(n^2)\)。预计得分 \(40\) 分。
\(\bullet\) 首先,我们交换的两数一定是 \(i<j\),\(a_i>a_j\),否则肯定不优,再观察交换两个数时变化的逆序对数量,发现为
\(1+2\times \sum\limits_{i<k<j}[a_j<a_k<a_i]\)
我们要使这部分最大,后面的部分实际上就是二维数点,求左上角 \((i,a_i)\),右下角 \((j,a_j)\) 的矩形中有多少点。
对于二维数点,有两种基本方法:考虑前缀和的思想,将询问拆成四部分计算贡献;考虑扫描线,先维护一位的偏序,再用线段树维护另一维。
考虑扫描线,预处理出所有矩形,依次计算贡献,复杂度 \(O(n\log n+逆序对数\times\log n)\)。预计得分没之前高。
\(\bullet\) 我们贪心发现,假如 \(k<i\),\(a_k>a_i\),我们完全可以把 \(i\) 换成 \(k\),让答案变得更优。同理 \(j\) 类似。
换言之,我们所选的 \(i\) 和 \(j\) 一定分别是前缀最大值和后缀最小值。可以预处理出两部分,复杂度变成 \(O(n\log n+前缀最大值数量\times 后缀最小值数量\times\log n)\)。在随机数据下,两者数量接近 \(\log n\),预计得分 \(80\) 分。
\(\bullet\) 我们发现瓶颈在于矩形数量过多,无法全部求出所有最大值。我们考虑反过来统计贡献,对于点 \((k,a_k)\),它能够贡献到哪些矩形中。应满足以下条件:
\(\begin{cases}i<k\\a_i>a_k\\j>k\\a_j<a_k\end{cases}\)
发现在前缀最大值序列中,\(k\) 影响的是一段区间;在后缀最大值中也是一段区间。若我们将前缀最大值和后缀最小值序列分别作为 \(y\) 轴和 \(x\) 轴,那么实际上就是对一个矩形加 \(1\) 的操作。求出哪个点权值最大,此时它影响的逆序对最多。需要的操作为区间加 \(1\),全局求最大值。同样可以用扫描线实现。复杂度 \(O(n\log n)\)。预计得分 \(100\) 分。
总结:对于一个操作,我们可以尝试表示出其影响其他值的条件。
考虑贪心的减少交换方案数,发现所选 \(i\),\(j\) 的 \(a_i\) 和 \(a_j\) 递增。
对于二维数点的形式,可以考虑用扫描线求出所有矩形的贡献;同时也可以反过来考虑,求一个点在哪些矩形内,若此时矩形的两维都是一段区间(与前面贪心联系),我们以此作为坐标轴的两维,枚举每个点,维护它能贡献哪些矩形,同样是扫描线的形式。
#include <bits/stdc++.h>
typedef long long ll;
ll read() {
ll x = 0, f = 1;
char c = getchar();
while(!isdigit(c)) {
if(c == '-') f = -1;
c = getchar();
}
while(isdigit(c)) {
x = (x << 3) + (x << 1) + (c - '0');
c = getchar();
}
return x * f;
}
ll T, o, n, ans;
ll a[500010], c[500010];
ll lowbit(ll x) {return x & (-x);}
void update(ll x) {
for(ll i = x; i <= n; i += lowbit(i)) {
c[i]++;
}
}
ll query(ll x) {
ll ret = 0;
for(ll i = x; i; i -= lowbit(i)) {
ret += c[i];
}
return ret;
}
struct node {
ll l, r, y, k;
} lne[1000010];
ll tot;
bool cmp(node a, node b) {
if(a.y != b.y) return a.y < b.y;
return a.k < b.k;
}
ll lazy[500010 << 2];
struct tr {
ll max;
} t[500010 << 2];
void pushup(ll u) {t[u].max = std::max(t[u << 1].max, t[u << 1 | 1].max);}
void pushdown(ll u) {
if(!lazy[u]) return;
t[u << 1].max += lazy[u];
t[u << 1 | 1].max += lazy[u];
lazy[u << 1] += lazy[u], lazy[u << 1 | 1] += lazy[u];
lazy[u] = 0;
}
void modify(ll u, ll l, ll r, ll L, ll R, ll k) {
if(L <= l && r <= R) {
t[u].max += k;
lazy[u] += k;
return;
}
ll mid = (l + r) >> 1;
pushdown(u);
if(L <= mid) modify(u << 1, l, mid, L, R, k);
if(R > mid) modify(u << 1 | 1, mid + 1, r, L, R, k);
pushup(u);
}
bool vis[500010];
ll st1[500010], st2[500010];
ll top1, top2;
ll binary(ll x) {
ll l = 1, r = top1, ret = top1;
while(l <= r) {
ll mid = (l + r) >> 1;
if(a[st1[mid]] >= a[x]) r = mid - 1, ret = mid;
else l = mid + 1;
}
return st1[ret];
}
ll binary2(ll x) {
ll l = 1, r = top2, ret = 1;
while(l <= r) {
ll mid = (l + r) >> 1;
if(a[st2[mid]] <= a[x]) r = mid - 1, ret = mid;
else l = mid + 1;
}
return st2[ret];
}
void Solve() {
bool flg = 0;
n = read();
for(ll i = 1; i <= n; i++) {
a[i] = read();
if(a[i] != i) flg = 1;
}
if(!flg) {
std::cout << "0\n";
return;
}
for(ll i = n; i >= 1; i--) {
ans += query(a[i]);
update(a[i]);
}
for(ll i = 1; i <= n; i++) {
if(i == 1 || a[i] > a[st1[top1]]) st1[++top1] = i, vis[i] = 1;
}
for(ll i = n; i >= 1; i--) {
if(i == n || a[i] < a[st2[top2]]) st2[++top2] = i, vis[i] = 1;
}
for(ll i = 1; i <= n; i++) {
if(vis[i]) continue;
ll l = binary(i), r = binary2(i);
// std::cout << l << " " << i << " " << r << "\n";
if(l < i && i < r) {
lne[++tot] = {i + 1, r, l, 1};
lne[++tot] = {i + 1, r, i, -1};
}
}
std::sort(lne + 1, lne + tot + 1, cmp);
ll ret = 0;
for(ll i = 1; i <= tot; i++) {
modify(1, 1, n, lne[i].l, lne[i].r, lne[i].k);
if(lne[i].y != lne[i + 1].y) ret = std::max(ret, t[1].max);
}
// std::cout << ret << "\n";
std::cout << ans - 2 * ret << "\n";
}
int main() {
Solve();
return 0;
}