BZOJ2212:Tree Rotations(线段树合并 & 逆序数^)

现在有一棵二叉树,所有非叶子节点都有两个孩子。在每个叶子节点上有一个权值(有n个叶子节点,满足这些权值为1..n的一个排列)。可以任意交换每个非叶子节点的左右孩子。
要求进行一系列交换,使得最终所有叶子节点的权值按照遍历序写出来,逆序对个数最少。

Input

第一行n
下面每行,一个数x
如果x==0,表示这个节点非叶子节点,递归地向下读入其左孩子和右孩子的信息,
如果x!=0,表示这个节点是叶子节点,权值为x

1<=n<=200000

Output

一行,最少逆序对个数

Sample Input

3
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;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值