现在有一棵二叉树,所有非叶子节点都有两个孩子。在每个叶子节点上有一个权值(有n个叶子节点,满足这些权值为1..n的一个排列)。可以任意交换每个非叶子节点的左右孩子。
要求进行一系列交换,使得最终所有叶子节点的权值按照遍历序写出来,逆序对个数最少。
Input
第一行n
下面每行,一个数x
如果x==0,表示这个节点非叶子节点,递归地向下读入其左孩子和右孩子的信息,
如果x!=0,表示这个节点是叶子节点,权值为x
1<=n<=200000
Output
一行,最少逆序对个数
Sample Input
3
0
0
3
1
2
0
0
3
1
2
Sample Output
1
思路:分治思想,一棵树无论其左儿子内部和右儿子内部怎么交换,左儿子相对右儿子的逆序数都是不变的,按此递归处理就行,朴素的写法是像归并排序那样每个区间都维护一段有序的数,但是会爆内存。因此要用新的姿势来进行线段树合并,参考了别人的写法感觉这东西比较神啊~。
# include <iostream>
# include <cstdio>
# include <cstring>
using namespace std;
typedef long long LL;
const int maxn = 4e5+30;
int cnt, CNT, son[maxn][2], SON[maxn][2];
int n, root, rt[maxn], sum[maxn], a[maxn];
LL ans=0, big=0;
void build(int &cur, int l, int r, int val)
{
cur = ++CNT;
if(l == r)
{
++sum[cur];
return;
}
int mid = l+r>>1;
if(val<=mid) build(SON[cur][0], l, mid, val);
else build(SON[cur][1], mid+1, r, val);
sum[cur] = sum[SON[cur][0]] + sum[SON[cur][1]];
}
void read(int &cur)
{
cur = ++cnt;
scanf("%d",&a[cur]);
if(a[cur])
{
build(rt[cur], 1, n, a[cur]);//若为叶子节点,建一颗线段树
return;
}
read(son[cur][0]);
read(son[cur][1]);
}
int merge(int L, int R)//将L和R两棵线段树合并
{
if(!L) return R;//任意一边为空返回另外一边的地址
if(!R) return L;
big += (LL)sum[SON[R][1]] * (LL)sum[SON[L][0]];//计算该区间的逆序数
SON[L][0] = merge(SON[L][0], SON[R][0]);//合并左子区间
SON[L][1] = merge(SON[L][1], SON[R][1]);//合并右子区间
sum[L] = sum[SON[L][0]] + sum[SON[L][1]];//★更新区间内的总数
return L;//将左子区间的地址赋给父亲
}
void dfs(int cur)
{
if(a[cur]) return;
int ls = son[cur][0], rs = son[cur][1];
dfs(ls);
dfs(rs);
big = 0;
LL tot = (LL)sum[rt[ls]] * (LL)sum[rt[rs]];//该区间总的二元数对(儿子间组成的数对)
rt[cur] = merge(rt[ls], rt[rs]);//合并两个儿子的线段树
ans += min(big, tot-big);//判断是否交换两个儿子
}
int main()
{
scanf("%d",&n);
read(root);
dfs(root);
printf("%lld\n",ans);
return 0;
}