传送门
http://www.lydsy.com/JudgeOnline/problem.php?id=2212
题解
首先,考虑一个节点的两棵子树,逆序对分为三种:
①在左子树中的
②跨越左右子树的
③在右子树中的
明显旋转左右子树只会改变第二种,于是我们从底往上做启发式合并,每个点开个treap,对于左右子树直接统计逆序对和旋转后逆序对个数,取少的那个。这样每次只处理跨子树的逆序对个数,类似分治,最后加起来就是答案。
但是这样两个log很虚,有没有更好的算法呢?
答案是肯定的,就是厉害的线段树合并。将treap改成权值线段树,左右两边以mid作为参照,然后合并两棵子树,统计的答案为:
ans0 = sum[ls[x]] * sum[rs[y]];
ans1 = sum[ls[y]] * sum[rs[x]];
分别为转或不转的答案。经证明将n个logn的链合并起来就是nlogn的时空复杂度。不管你信不信,反正我信了。
那怎么合并呢?
首先底层root[now]表示now节点在权值线段树中的编号,先建好原树。然后将叶子按权值大小插入线段树底层,遇到非叶子节点先算左右子树答案,然后合并左右子树的线段树并计算答案。注意这里有两棵树别搞混了。
合并时,如果x为空返回y,y为空返回x。否则由于形态一样左右子树分别合并。
即ls[x] = Merge(ls[x], ls[y]);
rs[x] = Merge(rs[x], rs[y]);
在插入和合并中别忘了维护个数sum,大概就是这样的吧(等我发现其他题的套路后再修改与补充)。
跟线段树有关的数组要开maxn*Lg大小,跟原二叉树有关的只用开maxn。
线段树合并时间复杂度比treap+启发式合并优,但它跟权值有很大关系。总之这题并不难,但线段树合并还是有点奇妙的。
代码
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>
#define maxn 400004
#define Lg 18
using namespace std;
typedef long long LL;
int n, ro, cnt, sz;
int val[maxn], ch[2][maxn], root[maxn];
int ls[maxn*Lg], rs[maxn*Lg];
LL ans, ans0, ans1, sum[maxn*Lg];
void Build(int &now){
now = ++cnt;
scanf("%d", &val[now]);
if(!val[now]){
Build(ch[0][now]);
Build(ch[1][now]);
}
}
void PushUp(int now){
sum[now] = sum[ls[now]] + sum[rs[now]];
}
void Insert(int &now, int l, int r, int x){
now = ++sz;
if(l == r){
sum[now] = 1;
return;
}
int mid = (l + r) >> 1;
if(x <= mid) Insert(ls[now], l, mid, x);
else Insert(rs[now], mid+1, r, x);
PushUp(now);
}
int Merge(int x, int y){
if(!x) return y;
if(!y) return x;
ans0 += sum[ls[x]] * sum[rs[y]];
ans1 += sum[ls[y]] * sum[rs[x]];
ls[x] = Merge(ls[x], ls[y]);
rs[x] = Merge(rs[x], rs[y]);
PushUp(x);
return x;
}
LL Solve(int x){
ans = 0LL;
if(!val[x]){
ans = Solve(ch[0][x]) + Solve(ch[1][x]);
ans0 = ans1 = 0;
root[x] = Merge(root[ch[0][x]], root[ch[1][x]]);
ans += min(ans0, ans1);
}
else Insert(root[x], 1, n, val[x]);
return ans;
}
int main(){
scanf("%d", &n);
Build(ro);
printf("%lld\n", Solve(1));
return 0;
}
解救 这擦肩而过
想起我 想起我
这唯一的祈求