bzoj 2212: [Poi2011]Tree Rotations(线段树合并)

传送门

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

解救 这擦肩而过

想起我 想起我

这唯一的祈求

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值