BZOJ 2212 线段树启发式合并

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

考虑题中的唯一操作,交换两个孩子。
对每个节点考虑两个孩子对答案的贡献:左孩子的贡献 + 右孩子的贡献 + 左孩子比右孩子大产生的贡献。
交换两个孩子只会使得第三种贡献变化,且只对当前节点有影响。
因此我们只需要考虑如何计算逆序对。

对节点建权值线段树,考虑将左右孩子的线段树合并时,不反转的情况下,每次统计左孩子对右孩子的影响的数量,然后递归合并下去。反转的情况下就是考虑右孩子对左孩子的影响。
其实这里本质上和cdq分治是一样的,可以做到统计贡献的不重不漏。

#include <bits/stdc++.h>
#define all(x) x.begin(), x.end()
using namespace std;

const int maxn = 4400000;
int n;
int cid;

int root[maxn], l[maxn], r[maxn], v[maxn];
struct Seg {
    int l, r, sum;
} tr[maxn];

void pushup(int x) {
    tr[x].sum = tr[tr[x].l].sum + tr[tr[x].r].sum;
}

int update(int pos, int l, int r) {
    int x = ++cid;
    int m = l + r >> 1;
    if(l == r) {
        tr[x].sum = 1;
        return x;
    }
    if(pos <= m)
        tr[x].l = update(pos, l, m);
    else
        tr[x].r = update(pos, m+1, r);
    pushup(x);
    return x;
}

int sz = 1;

void read(int x) {
    scanf("%d", &v[x]);
    if(v[x] == 0) {
        l[x] = ++sz;
        read(l[x]);
        r[x] = ++sz;
        read(r[x]);
    } else {
        root[x] = update(v[x], 1, n);
    }
}

long long ans = 0, s1 = 0, s2 = 0;

int mergetree(int x, int y, int tp) {
    if(!x) return y;
    if(!y) return x;
    if(tp == 1) {
        s1 += 1LL * tr[tr[x].r].sum * tr[tr[y].l].sum;
        s2 += 1LL * tr[tr[x].l].sum * tr[tr[y].r].sum;
    } else {
        s2 += 1LL * tr[tr[x].r].sum * tr[tr[y].l].sum;
        s1 += 1LL * tr[tr[x].l].sum * tr[tr[y].r].sum;
    }
    tr[x].l = mergetree(tr[x].l, tr[y].l, tp);
    tr[x].r = mergetree(tr[x].r, tr[y].r, tp);
    pushup(x);
    return x;
}

void dfs(int x) {
    if(!x)
        return ;
    dfs(l[x]), dfs(r[x]);
    if(!v[x]) {
        s1 = s2 = 0;
        if(tr[root[l[x]]].sum > tr[root[r[x]]].sum)
            root[x] = mergetree(root[l[x]], root[r[x]], 1);
        else
            root[x] = mergetree(root[r[x]], root[l[x]], 2);
        ans += min(s1, s2);
    }
}

void init() {
    cid = 0;
}

int main() {
    init();
    scanf("%d", &n);
    read(1);
    dfs(1);
    printf("%lld\n", ans);
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值