我們通常所講的樹鏈剖分其實是輕重鏈f剖分。
樹剖是什麼?是一種讓你的碼量不得不超過 2.4kb 的可以維護一棵樹路徑的資料結構。
首先,如何維護乙個數列的區間和或者是區間修改使兩者的時間複雜度為 \(o(\log_2 n)\) 呢?顯然,線段樹可以輕鬆維護。
但是如果我們把這個問題挪到樹上;
然後,如何在一棵樹上維護 \(u\) 到 \(v\) 兩點簡單路徑上的點權和或是修改點權使他們的時間複雜度仍然為 \(o(\log_2n)\) 呢?那就把出題人暴打一頓
線段樹是維護不了的,那麼我們該怎麼辦才能使兩個操作的時間複雜度為 \(o(\log n)\) 呢?
(靈光乍現)我們可以給每個節點編個號,割成若干個不相交的編號連續的鏈,然後用線段樹一條一條維護。這樣,將點 \(u\) 到 \(v\) 的簡單路徑就可以變成若干條鏈,每條鏈用 \(o(\log_2 n)\) 維護,總時間複雜度也是 \(o(\log_2 n)\)。
但是——
怎麼編號?怎麼割成鏈?
輕重鏈剖分就是用一種方法編號和割成鏈從而達到使維護樹兩點路徑 \(o(\log_2 n)\) 的東西。
下面,我們以板子來扯樹鏈剖分。
首先,在學樹鏈剖分之前,我們要明白一些定義:
名字意思
備注重兒子(\(son\))
父親節點上子樹大小最大的節點
輕兒子不是重兒子的節點
輕邊任意節點到輕兒子的邊
重邊任意節點到重兒子的邊
重鏈若干收尾相銜接的重邊
頂端必為輕兒子,落單的輕兒子也是重鏈
如果是這樣,我們就會發現,一棵樹必定會被拆成若干條重鏈,why?
首先,只要乙個節點不是根或是葉子,肯定是有重兒子的,那麼剩下的點是輕兒子,會被算成重鏈。
那麼他們相不相交呢?
每一條重鏈的頂端都是輕兒子,因為與輕兒子相連的邊是輕邊,無法構成重鏈,所以不會相交。因為每一條重鏈是連續的,不能通過輕邊伸到另一條邊去,所以也不會相交。
可是會有兩條重鏈叉在一起嗎?
顯然是不會的,因為叉在一起必然有若干個公共點,要求每條鏈編號連續,然而這樣無論如何編號不能連續,是不會的。(感性畫圖理解
嗯,很有道理。
下面,我們就要來開始樹剖預處理啦~
樹剖的預處理有兩次 dfs。
第一次 dfs,我們要維護很少的資訊(
名稱含義
\(siz_u\)
以 \(u\) 為根的子樹大小
\(fa_u\)
\(u\) 的父親
\(de_u\)
\(u\) 的深度(離根節點的距離)
\(son_u\)
\(u\) 的重兒子(沒有為 \(0\))
這是很輕鬆就能維護的,**:
void dfs1(int u,int fat)
//u 是當前點,fat 是 u 的父親
}
名稱
用途\(ind_u\)
\(u\) 的 dfs 序,也就是線段樹種 \(u\) 的編號。
\(who_c\)
dfs 序為 \(c\) 的節點在樹上的編號。
\(top_u\)
節點 \(u\) 的鏈的頂端。
在 dfs 的時候,傳參是兩個: \(u\) 和 \(t\) 表示當前在 \(u\) 號點,鏈頂是 \(t\)。
這裡的 dfs 略微複雜一點。首先,如果有重兒子,就沿著重兒子往下跑,這樣的話沿著 \(t\) 下去的鏈的節點都是連續的(自己想想 dfs 的性質就懂了)。然後我們來遍歷剩下的點,如果不是重兒子,說明是輕兒子,此時一條鏈是斷了的,我們以這個輕兒子做新的鏈頂。
**:
void dfs2(int u,int t)
\),跳出這條鏈,去新鏈跳。轉入操作 1。
因為 \(u\) 和 \(v\) 在同一條鏈上,編號連續。我們讓 \(u\) 變成深度小的那個,然後用線段樹修改這條鏈上的路徑。
前面鋪了那麼長,相信這個各位可以很快理解。
void ask_1(int u,int v,int z)
第二個問題是求和,和第乙個問題大庭相徑,只不過把 2 的修改變成了求和。
int ask_2(int u,int v)
可是第三個和第四個怎麼維護?怎麼樹剖?
這裡其實會運用到乙個 dfs 序的性質:
以任意點為根的子樹,子樹 dfs 序必然連續。
這其實也是樹剖思想的根基,這個只要你會 dfs,應該很好理解。
那麼 \(u\) 的編號是 \(ind_u\),那麼它子樹 dfs 序最大的乙個應該是什麼?
因為是連續的,所以應該加上 \(siz_u\),但是要減去 \(u\) 自己,所以是 \(ind_u+siz_u-1\),這就是最後乙個葉子節點的編號。
void ask_3(int u,int z)
int ask_4(int u)
這裡其實也算是提醒我自己,因為這一點我卡了 20min 板子/ch
順便附上板子的線段樹**;
void build(int l,int r,int now)
build(l,mid ,ls(now));
build(mid+1,r,rs(now));
push_up(now);
}//普通查詢和修改,這題和板子沒有差別。
int ask(int l,int r,int s,int t,int now)
void updata(int l,int r,int s,int t,int now,int val)
int mid=(s+t)>>1;
push_down(now,s,t);
if(l<=mid)updata(l,r,s,mid ,ls(now),val);
if(r> mid)updata(l,r,mid+1,t,rs(now),val);
push_up(now);
}
話說 08 年 zjoi 這題是不是用來送分的啊/fad/fad/fad/fad/fad
首先,這裡要維護兩個值:\(s\) 和 \(m\),\(s\) 是和,\(m\) 是最大值。
不難寫出線段樹**,一些小細節見注釋:
int ls(int now)
int rs(int now)
void push_up(int now)
void build(int l,int r,int now)
int mid=(l+r)>>1;
build(l,mid,ls(now));
build(mid+1,r,rs(now));
push_up(now);
}int ask_max(int l,int r,int s,int t,int now)//區間最大值
int ask_sum(int l,int r,int s,int t,int now)//區間和
void updata(int q,int s,int t,int now,int val)
//這裡是單點修改,q 是單點修改的位置
if(q<=mid)updata(q,s,mid,ls(now),val);//找位置
else updata(q,mid+1,t,rs(now),val);//找位置
push_up(now);
}
至於樹剖的預處理基本上是題題相似,所以和最開始擺出來的兩個 dfs 一模一樣。
然後就是查詢的問題了。
qsum
和qmax
其實和我們上面寫的查詢基本一致,只不過乙個求和,乙個求最大值,**:
int qmax(int u,int v)
int qsum(int u,int v)
void change(int u,int val)
嗯嗯沒錯,樹剖是一種很不錯的求 lca演算法。
其實,每次跳鏈頂的操作,也可以看成找祖宗。當兩者跳到了同一條鏈上,很顯然就是深度小的是 lca,畢竟 lca 是可以乙個節點乙個節點往上爬。
樹剖常數特別小,像我這樣的大常數辣雞還是 ios+cin/cout 黨,也能不卡一點常就能跑到總時間 2.3s 左右,比倍增 2.7s 快了不少。
int get_lca(int u,int v)
就是碼量略微大了一丁點,不過個人認為樹剖更好寫。
好啦,樹鏈剖分的學習筆記就寫到這裡啦~
完結撒花✿✿ヽ(°▽°)ノ✿
樹鏈剖分學習筆記
寫 又犯了很sb的錯誤,線段樹寫錯了。好像每次都會把r l 1寫成l r 1,然後就只有20分。寫的比較醜,壓了壓之後190行。基本上是我打過的最長的乙個模板了 然後簡單介紹一下樹剖吧。樹鏈剖分,就是把樹剖分成鏈,然後用資料結構來維護這些鏈,使得詢問 修改的複雜度達到o logn o l ogn 不...
樹鏈剖分學習筆記
樹鏈剖分 mod estc oder modestcoder modest code r如果你是重兒子,你就在重路徑上。如果你是輕兒子,暴力沿著祖先向上爬最多log nlogn logn 次就可以遇到重路徑。或者到根 而樹上操作基本就是找祖先 也許有人喜歡我的碼風 include include d...
樹鏈剖分學習筆記
前言 書上只講了重鏈剖分,菜雞也只會這一種,想看其他的是別想了。要會樹鏈剖分,首先你需要了解一些概念。我們把乙個節點的所有兒子節點中子樹節點數最大的稱為重兒子,也就是size最大的子節點。size的定義我在講換根dp時說過,因此不再贅述。對於每個節點的重兒子,我們用 son x 來記錄它,父親節點到...