1. 程式人生 > >[BZOJ2212]二叉樹:線段樹合併

[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的更新同理)。