淺談樹鏈剖分
阿新 • • 發佈:2020-08-27
我們知道一顆樹的dfs序可以有多種,所以可以通過一些暗箱操作把dfs序給安排了,從而能夠更好的維護樹上的區間資訊
什麼是樹剖
樹剖的核心是恰當的把樹剖分成若干條鏈,鏈拼接起來就成了一段區間,然後利用資料結構來維護。
一些概念
重兒子:子樹節點最多的兒子
輕兒子:重兒子以外的兒子
重邊:父節點與重兒子所連的邊
輕邊:父節點與輕兒子所連的邊
重鏈:重邊組成的路徑
輕鏈:輕邊組成的路徑
重鏈頭:一條重鏈中深度最淺的點。特別地,與輕邊相連的葉子節點也算重鏈頭
圖中加粗邊為重邊,紅點為重鏈頭
樹鏈剖分流程
第一步,對這棵樹進行dfs,處理出每個節點的父節點、深度、重兒子
void dfs1(int u) { size[u]=1; //size表示子樹大小,初始size為1 for(int x=Root[u];x!=0;x=Next[x]) if(v[x]!=fa[u]){ d[v[x]]=d[u]+1;fa[v[x]]=u; //處理深度和父親節點 dfs1(v[x]);size[u]+=size[v[x]]; //子節點size值處理後,更新父節點size if(size[v[x]]>son[u])son[u]=v[x]; //取size最大的為重兒子 } }
第二步,連線重鏈,安排dfs序
void dfs2(int u,int t)//u為當前節點,t為u所在重鏈的重鏈頭 { id[u]=++tot,cur[tot]=a[u],top[u]=t; //處理dfs序和重鏈頭 //id為時間戳,cur為新dfs序對應的值,top為重鏈頭 if(son[u])dfs2(son[u],t);//先處理重兒子 //注意,重兒子和它的父節點都是同一個重鏈頭 for(int x=Root[u];x!=0;x=Next[x]) if(v[x]!=fa[u]&&v[x]!=son[u])//如果是輕鏈底端,則自己做重鏈頭 dfs2(v[x],v[x]); }
至此,我們對樹的剖分就結束了
接下來我們用線段樹來維護剖分好的樹
以查詢樹上\(x\)到\(y\)的和為例
剛才的dfs維護了top陣列
於是可以通過不斷的向上跳到重鏈頭,直到\(x\)和\(y\)位於一條重鏈,再用線段樹查詢出跳過的區間值。這樣就可以\(O(nlogn)\)的計算出\(x\)到\(y\)的和。
int calsum(int x,int y) { ll res=0; while(top[x]!=top[y]){ //兩點不在一條重鏈 if(d[top[x]]<d[top[y]]) //設x為深度較深的點 swap(x,y); res+=query(id[top[x]],id[x],1); //對x所在重鏈求和 x=fa[top[x]]; //跳到重鏈頭的父親,也就是下一條重鏈上 } if(d[x]>d[y])swap(x,y); res+=query(id[x],id[y],1); //計算同一條重鏈上x到y的區間和 return res; }
一些例題
樹鏈剖分模板題
樹剖+線段樹綜合應用
樹剖修改題