[BZOJ2212]二叉樹:線段樹合併
阿新 • • 發佈:2019-02-19
這道題給人的第一感覺像是樹形,我們可以按照樹形的思路進行遞迴處理,統計一個結點左右子樹貢獻的答案時,我們分別算出是否交換左右子樹的答案,取較大值並向上更新(這樣是符合最優子結構的)。問題來了,我們如何快速把兩棵子樹合併並把答案貢獻到父節點呢?線段樹可以做到。為了讓線段樹支援合併操作,我們不能再用堆式的儲存方法,而要使用動態開點的方法。我們以原二叉樹中的每一個葉子節點為根建立線段樹,注意我們建立的是權值線段樹,中儲存的是原二叉樹上權值處在結點代表的區間中的葉子結點的個數。建出棵線段樹的空間複雜度看起來不能接受,但實際上,我們使用動態開點的方法,建立的每棵線段樹都不是完整的,因為有些結點根本不會用到,而我們只在需要的時候開點,因此,每棵線段樹存的只是根節點到葉子結點的一條鏈,長度為
#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); }
這裡講一下和的更新方法。我們在合併一個結點的左右子樹之前,先更新和。因為我們使用的是權值線段樹,所以代表滿足的結點的個數,而代表滿足的結點的個數。如果不交換左右子樹,逆序對增加的個數為,及左子樹中滿足的結點的個數和右子樹中滿足的結點的個數的乘積。根據乘法原理,左子樹中的個節點中的任何一個大於右子樹中的個節點中的任何一個,它們兩兩之間可以構成逆序對,因此(的更新同理)。