這一篇來講講線段樹合並。
前置知識:動態開點線段樹
還是一樣先給一道例題:[JOI2012] Building2
題面是日文的,這里給出中文翻譯:
有n個城市,它們組成了一棵樹。 第i個城市有一座高度為Hi的建築。
你需要選擇一條盡量長路徑,設路徑中有k個點, 依次分別為i1,i2,⋯ik−1,ik,使得路徑滿足Hi1<Hi2<⋯<Hik,這k個點不一定要連續,求k的最大值。
概述:求樹上LIS( 最長上升子序列 ),不會求LIS也沒有關系,這只是一道例題。
先來考慮朴素做法:
取出樹上的所有路徑,對它們分別求LIS。
可以做,但是復雜度太高,不可行。
我們發散思維,容易想到一種做法:
對於每一個點x,我們發現經過它的一條路徑的LIS可以由兩部分組成。
以x開頭,x左邊的部分路徑如果是下降的,那右邊就要是上升的( 值h )。
以x開頭,x左邊如果是上升的,右邊就要是下降的。
畫個圖方便理解:

圖中藍色的顯然就是過x的LIS
根據上面的想法,我們求出以x開頭,往左的LIS=3,往右的LDS( 最長下降子序列 )=2。
以x開頭,往左的LDS=1,往右的LIS=2。
我們比較兩個結果( 相加-1,要把自己重復算的那一次減去 ),4和2,顯然答案就是4。
現在考慮怎么實現這個想法,
剛開始,所有點的LIS和LDS都是1( 自己 )。
然后我們考慮從底向上合並答案。
dfs遍歷到底,開始向上回溯,每次詢問找到左右子樹,直接合並答案。
如圖:

還是一樣,理解一下就好。看不懂沒關系,可以照着代碼自己畫幾組數據看看合並過程。
我們用動態開點線段樹維護一下這個合並,題就做出來了。
現在來講線段樹合並。
顧名思義,線段樹合並就是把兩棵線段樹合並成一棵。
可以在合並的過程中更新信息。
線段樹合並可以解決大部分更新答案需要合並的問題,dsu( 樹上啟發式合並 )能做的它也基本可以。
給出合並的代碼,以上面的例題為例:
inline void Merge(int&x,int y){ if(!x||!y){ x=x+y;return; } lis[x]=_fmax(lis[x],lis[y]); lds[x]=_fmax(lds[x],lds[y]); ret=fmax(ret,_fmax(lis[lc[x]]+lds[rc[y]],lds[rc[x]]+lis[lc[y]])); Merge(lc[x],lc[y]); Merge(rc[x],rc[y]); }
可以先學學可並堆(左偏樹),線段樹合並和可並堆是有挺多相似的地方的。
左偏樹的博客在我寫完線段樹系列后大概會寫一篇。畢竟日更博主...。
然后我們的其他操作就和動態開點線段樹基本一致了。
但是節點值的更新,是每次修改都要更新,這一點不像動態開點線段樹( 在葉子節點更新 )。
inline void Update(int&x,int l,int r,int pos,int val,int *h){ if(!x)x=++ncnt; h[x]=_fmax(h[x],val); if(l==r)return; int mid=(l+r)>>1; if(pos<=mid)Update(lc[x],l,mid,pos,val,h); else Update(rc[x],mid+1,r,pos,val,h); }
現在給出我寫的上面那道例題的標程。
注釋已經全部加好了,請放心食用。看不懂的地方可以在下方評論區里留言。
#include<bits/stdc++.h> using namespace std; #define getchar gc char buf[1000010],*pos,*End; inline char gc(){ if(pos==End){ End=(pos=buf)+fread(buf,1,1000000,stdin); if(pos==End)return EOF; }return *pos++; } //fread高速讀入 const int N=100010; struct Edge{ int u,v,nxt; }e[N<<1]; int head[N],tot; inline void addedge(int u,int v){ e[++tot].u=u; e[tot].v=v; e[tot].nxt=head[u]; head[u]=tot; }//前向星存圖 inline int _fmax(int x,int y){ return (((y-x)>>31)&(x^y))^y; }//快速max int ret; int root[N]; int lc[N*20],rc[N*20],lis[N*20],lds[N*20],ncnt;//開nlogn個點 inline void Merge(int&x,int y){//合並 if(!x||!y){ x=x+y;return; } lis[x]=_fmax(lis[x],lis[y]); lds[x]=_fmax(lds[x],lds[y]);//更新值 ret=_fmax(ret,_fmax(lis[lc[x]]+lds[rc[y]],lds[rc[x]]+lis[lc[y]])); Merge(lc[x],lc[y]); Merge(rc[x],rc[y]);//合並 } inline void Update(int&x,int l,int r,int pos,int val,int *h){ if(!x)x=++ncnt;//開點 h[x]=_fmax(h[x],val);//每個節點都更新值 if(l==r)return;//到葉子節點就可以返回了 int mid=(l+r)>>1; if(pos<=mid)Update(lc[x],l,mid,pos,val,h); else Update(rc[x],mid+1,r,pos,val,h);//遞歸更新值 } inline int Query(int&x,int l,int r,int L,int R,int *h){ if(l>r)return 0; if(!x)return 0; if(L<=l && r<=R)return h[x];//跟普通的動態開點線段樹一樣 int ret=0,mid=(l+r)>>1; if(L<=mid)ret=_fmax(ret,Query(lc[x],l,mid,L,R,h)); if(R>mid)ret=_fmax(ret,Query(rc[x],mid+1,r,L,R,h)); return ret; } int d[N],kcnt; inline int bis(int x){ return lower_bound(d+1,d+kcnt+1,x)-d;//離散化后查找值的二分 } int n,val[N]; int ans; inline void dfs(int x,int f){//dfs到底,回溯時進行合並 for(int i=head[x];i;i=e[i].nxt){ int y=e[i].v; if(y==f)continue; dfs(y,x); } ret=0; int nlis=0,nlds=0,ilis,ilds; for(int i=head[x];i;i=e[i].nxt){ int y=e[i].v; if(y==f)continue; ilis=Query(root[y],1,kcnt,1,val[x]-1,lis);//查找子樹的lis ilds=Query(root[y],1,kcnt,val[x]+1,kcnt,lds);//查找子樹的lds Merge(root[x],root[y]);//合並子樹 ans=_fmax(ans,ilis+1+nlds); ans=_fmax(ans,ilds+1+nlis);//更新答案 nlis=_fmax(nlis,ilis); nlds=_fmax(nlds,ilds);//更新節點的值 } ans=_fmax(ans,ret);//更新答案 Update(root[x],1,kcnt,val[x],nlis+1,lis); Update(root[x],1,kcnt,val[x],nlds+1,lds);//更新線段樹 } inline int read(){ int data=0,w=1;char ch=0; while(ch!='-' && (ch<'0'||ch>'9'))ch=getchar(); if(ch=='-')w=-1,ch=getchar(); while(ch>='0' && ch<='9')data=data*10+ch-'0',ch=getchar(); return data*w; } int main(){ n=read(); for(int i=1;i<=n;i++){ val[i]=read(); d[++kcnt]=val[i]; } sort(d+1,d+kcnt+1); kcnt=unique(d+1,d+kcnt+1)-d-1;//離散化 for(int i=1;i<=n;i++) val[i]=bis(val[i]); int x,y; for(int i=1;i<n;i++){ x=read();y=read(); addedge(x,y);addedge(y,x);//加邊 } dfs(1,0); printf("%d\n",ans); return 0; }
總結一下,遇到需要合並更新答案的題,就可以用線段樹合並解決。
那么本篇到這里就結束了,撰文不易,希望幫到各位。
下一篇更新線段樹優化建圖,本系列持續更新,求點贊求關注。
