初識樹鏈剖分

2022-05-11 16:15:39 字數 4204 閱讀 7656

首發於摸魚世界&更好的閱讀體驗

到現在也只會照著std打板子..

雖然這樣,毒樹鏈剖分還是乙個非常優雅的演算法。

前置芝士:\(dfs\),線段樹

樹鏈剖分可以把樹上的區間操作通過把樹剖成一條條鏈,利用線段樹資料結構進行維護,從而達到\(o(nlogn)\)的優秀時間複雜度。

比如這樣的操作:

在一棵樹上,將\(x\)到\(y\)路徑上點的點權加上\(w\),並要求支援查詢兩個點\(x,y\)路徑間的點權和。

乍一看,兩個操作都很簡單。修改操作可以用樹上差分\(o(1)\)亂搞,靜態查詢可以用\(lca\)完成。

但是合起來就沒有辦法了:每次查詢之前都需要\(o(n)\)預處理,資料略大直接\(t\)飛。

於是樹剖出場了。

區間修改&查詢是線段樹的強項,但是它只能對一段連續的區間進行查詢。於是我們需要想辦法讓樹上需要操作的路徑變成一段連續的區間。

引入乙個概念:重兒子,也就是乙個節點的兒子中\(size\)最大的。連線到重兒子的邊即為重邊

重兒子組成的,就是重鏈

比如在這棵樹中,連續的紅邊組成的就是一條條重鏈。我們用\(top[u]\)記錄節點\(u\)所在重鏈的頂端。特別地,沒有被重邊連線的節點,\(top[u]=u\),即它們所在重鏈的頂端就是自身。注意到,當\(u\)是一條重鏈的頂端(\(top[u]=u\))時,它的父節點一定在另一條重鏈上

始終記住我們的目標:把在樹上區間操作轉化為在一段連續的區間進行操作。

考慮如何用\(dfs\)給樹上的每個節點在區間內找到乙個合適的位置。我們發現,從根節點出發,優先走重邊,這樣的\(dfs\)序似乎有點特殊。

例如上圖,優先走重邊的\(dfs\)序為:\(124798356\)。很顯然,這樣的\(dfs\)序滿足同一條重鏈上的點\(dfs\)序連續。所以用線段樹維護的,就是重鏈上的資訊

這樣操作之後,我們可以做到的是:\(o(logn)\)對一條重鏈上的資訊區間修改,區間查詢。

對於兩個節點\(u,v\),我們可以通過不斷地跳重鏈,直到兩個節點在同一條重鏈上。這個是很好實現的,因為只需要跳到\(fa[top[u]]\),就到了一條新的重鏈。

**實現僅樹剖部分是不麻煩的。我們需要維護的資訊有\(dep\)(節點深度),\(fa\)(父節點),\(son\)(重兒子),\(sz\)(子樹節點數,用來判重兒子),這些可以用一次\(dfs\)完成。

void dfs1(int u,int f,int d)//fa,dep,son,sz

}}

接下來,就需要把這棵樹每個節點壓到線段樹維護的序列的乙個位置了。就像上文說的一樣,按照優先重邊的\(dfs\)序壓入線段樹即可。於是記錄乙個\(id[i]\)表示原樹中節點\(i\)對應的線段樹中的下標。\(rk[i]\)反過來記錄線段樹中下標為\(i\)的原數編號。

由於預處理了父節點,所以\(dfs2\)傳參只需要\(u\)(當前節點)和\(t\)(當前重鏈頂端節點)。在遍歷兒子之前先\(dfs2(son[u],t)\),因為\(u\)和\(u\)的重兒子在同一條重鏈上。接下來才遍歷輕(非重)兒子\(v\),但是傳參為\(dfs2(v,v)\),因為\(v\)就是新的一條重鏈的起點。

void dfs2(int u,int t)//top,id,rk

}

再回到最開始的問題:

在一棵樹上,將\(x\)到\(y\)路徑上點的點權加上\(w\),並要求支援查詢兩個點\(x,y\)路徑間的點權和。

答案就顯得很明了了。

如果是查詢,先保證\(dep[x]>dep[y]\),然後就和\(lca\)類似的,利用重鏈加速:每次把\([top[x],x]\)這條重鏈的和累加到答案上,再使\(x\)跳到另一條重鏈上,即\(x=fa[top[x]]\),直到\(x,y\)在同一條重鏈上,再把兩個點之間的資訊統計累加一下即可。

int getsum(int x,int y)

修改同理。

於是我們發現,雖然我們採用了優先重邊的\(dfs\)序,但它畢竟遍歷的都是自己的兒子節點。所以...還可以支援子樹操作。因為一棵子樹在重邊優先的\(dfs\)序中編號也是連續的。並且這個編號很容易算,因為我們維護了乙個\(sz\)資訊。所以樹中\(x\)節點的子樹對應的就是線段樹維護的\([id[x],id[x]+sz[x]-1]\)這個區間

於是還是板子一般的線段樹區間修改&查詢。

可以注意到線段樹部分基本沒講,因為每個人寫線段樹的方法可能不太一樣,蒟蒻我分享的只是樹剖的思想。

另外,為什麼樹剖每次操作是\(o(logn)\)呢?利用線段樹的子樹操作自然是\(o(logn)\),剩下的就是那個像\(lca\)一樣的跳重鏈。

證明:從任意節點向根節點跳重鏈,經過的重鏈和輕邊(非重邊)都是\(log\)級別的。

考慮到每走一條輕邊,子樹大小至少翻倍,否則這就不是條輕邊了。於是經過的輕邊就最多為\(log_2 n\)條。而重鏈和輕邊的交替出現的,所以數量也在這個級別。

於是每次操作就只有\(o(logn)\)的時間複雜度。

模板題

以下是**

#include#define int long long

#define ls (k<<1)

#define rs (k<<1|1)

using namespace std;

const int n=1e5+10;

struct node

t[n<<2];

int a[n];

int n,m,r,mod;

int sum;

int head[n<<1],to[n<<1],nxt[n<<1],cnt;

int sz[n],fa[n],dep[n],son[n];

int top[n],id[n],rk[n],tot;

inline int read()

while(ch>='0'&&ch<='9')

return x*f;

}void add(int u,int v)

void dfs1(int u,int f)

return;

}void dfs2(int u,int t)

}void build(int k,int l,int r)

int m=l+r>>1;

build(ls,l,m);

build(rs,m+1,r);

t[k].w=t[ls].w+t[rs].w;

return;

}void down(int k)

void addsum(int k,int x,int y,int p)

down(k);

int m=l+r>>1;

if(x<=m)addsum(ls,x,y,p);

if(y>m)addsum(rs,x,y,p);

t[k].w=t[ls].w+t[rs].w;

return;

}void asksum(int k,int x,int y)

down(k);

int m=l+r>>1;

if(x<=m)asksum(ls,x,y);

if(y>m)asksum(rs,x,y);

t[k].w=t[ls].w+t[rs].w;

return;

}//-----------------------------

int getsum(int x,int y)

void update(int x,int y,int p)

signed main()

{ n=read(),m=read(),r=read(),mod=read();

for(int i=1;i<=n;i++)a[i]=read();

for(int i=1;i**的確是長,也不算容易調,但是真正妙的是利用輕重鏈的思想進行的化樹為鏈。

感謝閱讀。

樹鏈剖分 樹鏈剖分講解

好了,這樣我們就成功解決了對樹上修改查詢邊權或點的問題。下面放上 vector v maxn int size maxn dep maxn val maxn id maxn hson maxn top maxn fa maxn 定義 int edge 1,num 1 struct tree e ma...

演算法入門 樹鏈剖分 輕重鏈剖分

目錄 3.0 求 lca 4.0 利用資料結構維護資訊 5.0 例題 參考資料 資料結構入門 線段樹 發表於 2019 11 28 20 39 dfkuaid 摘要 線段樹的基本 建樹 區間查詢 單點修改 及高階操作 區間修改 單點查詢 區間修改 區間查詢 標記下傳 標記永久化 閱讀全文 樹鏈剖分用...

樹鏈剖分 樹剖換根

這是一道模板題。給定一棵 n 個節點的樹,初始時該樹的根為 1 號節點,每個節點有乙個給定的權值。下面依次進行 m 個操作,操作分為如下五種型別 換根 將乙個指定的節點設定為樹的新根。修改路徑權值 給定兩個節點,將這兩個節點間路徑上的所有節點權值 含這兩個節點 增加乙個給定的值。修改子樹權值 給定乙...