[BZOJ2212]二叉树:线段树合并

这道题给人的第一感觉像是树形dp,我们可以按照树形dp的思路进行递归处理,统计一个结点左右子树贡献的答案时,我们分别算出是否交换左右子树的答案,取较大值并向上更新(这样是符合最优子结构的)。问题来了,我们如何快速把两棵子树合并并把答案贡献到父节点呢?线段树可以做到。为了让线段树支持合并操作,我们不能再用堆式的存储方法,而要使用动态开点的方法。我们以原二叉树中的每一个叶子节点为根建立线段树,注意我们建立的是权值线段树,segtree[x]中存储的是原二叉树上权值处在x结点代表的区间中的叶子结点的个数。建出n棵线段树的O(n^2)空间复杂度看起来不能接受,但实际上,我们使用动态开点的方法,建立的每棵线段树都不是完整的,因为有些结点根本不会用到,而我们只在需要的时候开点,因此,每棵线段树存的只是根节点到叶子结点的一条链,长度为logn,总空间复杂度为O(nlogn),可以接受。在合并的过程中,因为线段存储的是结点个数,我们可以方便地在O(logn)的时间内直接算出逆序对个数,自然也不需要考虑如何把逆序对个数存进线段树中。完整代码如下。

#include<cstdio>
#define maxn 400010
#define reg register
#define cmin(_x,_y) (_x<_y?_x:_y)
using namespace std;
int n,tot,w[maxn],lson[maxn],rson[maxn],root[maxn];//原二叉树的数据
int cnt,ls[maxn<<4],rs[maxn<<4],num[maxn<<4];//权值线段树的数据
long long ans,numl,numr;
int read()
{
	reg char ch=getchar();reg int in=0;
	while(ch>'9'||ch<'0') ch=getchar();
	while(ch<='9'&&ch>='0') in=(in<<1)+(in<<3)+ch-'0',ch=getchar();
	return in;
}
void input(reg int x)//按照题目要求,递归读入每个非叶子结点的左右儿子
{
	w[x]=read();
	if(!w[x])
	{
		lson[x]=++tot,input(tot);
		rson[x]=++tot,input(tot);
	}
}
int add(reg int x,reg int l,reg int r,reg int v)//动态开点
{
	if(l==r) num[x]=1;
	else
	{
		reg int mid=l+r>>1;
		if(v<=mid) ls[x]=add(++cnt,l,mid,v);
		else rs[x]=add(++cnt,mid+1,r,v);
		num[x]=num[ls[x]]+num[rs[x]];//num[x]的意义同上面讲的segtree[x]
	}
	return x;
}
int merge(reg int x,reg int y)//合并子树并更新答案
{
	if(!x||!y) return x+y;
	numl+=1ll*num[rs[x]]*num[ls[y]];//numl为不交换左右子树的逆序对个数
	numr+=1ll*num[ls[x]]*num[rs[y]];//numr为交换左右子树的逆序对个数
	ls[x]=merge(ls[x],ls[y]);
	rs[x]=merge(rs[x],rs[y]);
	num[x]=num[ls[x]]+num[rs[x]];//递归合并左右子树并pushup
	return x;
}
void work(reg int x)//在原二叉树上递归
{
	if(w[x]) return;
	work(lson[x]),work(rson[x]);
	numl=numr=0;
	root[x]=merge(root[lson[x]],root[rson[x]]);
	ans+=cmin(numl,numr);
}
int main()
{
	n=read(),input(++tot);
	for(reg int i=1;i<=tot;i++)
		if(w[i]) root[i]=add(++cnt,1,n,w[i]);
	work(1);
	return !printf("%lld",ans);
}

这里讲一下numlnumr的更新方法。我们在合并一个结点的左右子树之前,先更新numlnumr。因为我们使用的是权值线段树,所以num[ls[x]]代表满足w[i]<=segtree[x].mid的结点i的个数,而num[rs[x]]代表满足w[i]>segtree[x].mid的结点i的个数。如果不交换左右子树,逆序对增加的个数为num[rs[x]]*num[ls[y]],及左子树中满足w[i]>segtree[x].mid的结点i的个数和右子树中满足w[i]<=segtree[x].mid的结点i的个数的乘积。根据乘法原理,左子树中的num[rs[x]]个节点中的任何一个大于右子树中的num[ls[y]]个节点中的任何一个,它们两两之间可以构成逆序对,因此numl+=num[rs[x]]*num[ls[y]]numr的更新同理)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值