進階樹論
建國中學 賴昭勳
Euler Tour & DFS
樹壓平
樹的DFS順序 - 1
1
2
3
4
5
6
7
8
9
10
前序遍歷:
1, 2, 9, 5, 8, 7, 6, 10, 3, 4
性質:進入一個節點後,走完該子樹的所有節點之後才會離開。
他的用處:用序列表示樹
1
2
3
4
5
6
7
8
9
10
紀錄每個點的左界和右界,代表該點的子樹的區間。
int ti = 0; int lef[maxn], rig[maxn]; //[lef, rig) void dfs(int n, int par) { lef[n] = ti++; for (int v:adj[n]) { if (v != par) { dfs(v, n); } } rig[n] = ti; }
考慮這種問題
樹上每個點上有一個數值ai,
處理 q 筆操作。
- 改變某個點的值
- 詢問以v為子樹的數值和
變成線段樹!
在樹上歐拉遍歷 - 2
進去和離開點時都把點加到序列裡面,
也就是說序列長度為2n
1
2
3
4
5
6
7
8
9
10
進入:1, 2, 9, 5, 8, 7, 6, 10, 3, 4,
離開: 9, 8, 5, 7, 2, 3, 10, 4, 6, 1
解決路徑相關的問題!
尋找根到任意節點的路徑
1
2
3
4
5
6
7
8
9
10
In:1, 2, 9, 5, 8, 7, 6, 10, 3, 4,
Out: 9, 8, 5, 7, 2, 3, 10, 4, 6, 1
考慮區間[in[1],in[v]]
所有只出現一次in的點都在(1,v)的路徑上,in跟out都出現的點應該被「抵銷」掉。
如果題目求的數值是可以用扣的(ex.路徑和),那可以將(u,v)拆成
(1,u)+(1,v)−2∗(1,lca(u,v))
還有一種樹壓平-3
在歐拉遍歷時每次遇到一個點就把他紀錄下來。
額外紀錄一個深度陣列
1
2
4
5
6
8
7
3
arr: 1, 2, 7, 7, 2, 5, 8, 8, 5, 2, 1, 6, 3, 3, 6, 4, 4, 6, 1
dep: 0, 1, 2, 2, 1, 2, 3, 3, 2, 1, 0, 1, 2, 2, 1, 2, 2, 1, 0
用這種方法找路徑
1
2
4
5
6
8
7
3
arr: 1, 2, 7, 7, 2, 5, 8, 8, 5, 2, 1, 6, 3, 3, 6, 4, 4, 6, 1
紀錄每個點的首次出現(in)和最後一次出現(out)時間
對於兩個節點u,v (in[u]<in[v]):
1. v在u的子樹內 (ex. 1-8)
[in[u],in[v]]
2. v 不在u的子樹內 (ex. 5-4)
[out[u],in[v]]
用樹壓平做O(nlogn)/O(1) LCA
一樣定義in[v],out[v]分別為點v第一個、最後一個出現在序列的位置
1
2
4
5
6
8
7
3
arr: 1, 2, 7, 7, 2, 5, 8, 8, 5, 2, 1, 6, 3, 3, 6, 4, 4, 6, 1
dep: 0, 1, 2, 2, 1, 2, 3, 3, 2, 1, 0, 1, 2, 2, 1, 2, 2, 1, 0
Sparse Table!
對於兩個點u,v,lca(u,v)會位於
in[u],in[v]中間,且in[u],in[v]必定在[in[lca(u,v)],out[lca(u,v)]]中間
也就是說lca(u,v)是[in[u],in[v]]中間深度最小的一個!
以下是練習題
大家自己判斷要怎麼樹壓平!
給你一棵樹,每個點上有值vi,對於每個點u,找出他子樹內有幾個點v值比u大。
n≤105,vi≤109
給你一棵樹,每個點上有值ai,有q筆詢問,每次問兩個點u,v的路徑間的xor和。
n,q≤105,ai≤109
給一棵有根樹,上面每個點是 1 或 0 (一開始全部是0),接下來有q筆操作。
1. 改一個子樹裡面每個點的值 (0變1, 1 變 0)
2. 詢問一個點的值
n,q≤105
給你兩棵有n個節點的樹T1,T2,
請找出有多少對邊e1∈T1,e2∈T2,使得當我們分別把這兩條邊拔掉後,兩棵樹上所製造的兩個連通塊點集可以一一對應。
1
2
2
3
3
5
4
1
4
5
如左圖有三組解:
n≤105
註:想想看確定性(不用random/hash)的解
例題5:數樹問題
樹DP/樹分治
在樹上DP有什麼特別的?
- 通常需要考慮不定數量個轉移點(例如子節點數量)
- 有根樹 vs 無根樹
- 有時候他根本不是DP,而是分治或Greedy
- 有不同的應用/變形
樹DP常見的想法:
把根拔掉,拆成一棵棵的子樹
簡單的例題:樹上最大匹配
給定一棵樹,選出一個邊的集合S,使得S裡面任兩條邊都沒有共用點,並且∣S∣盡量大。
O(n)
1
2
3
4
5
6
7
8
9
10
解一:DP
令dp[0][v]代表以v為根的子樹中,不把v連到子節點的最大邊數。
dp[1][v]代表把v連到其中一個子節點的最大邊數。
dp[0][v]=∑max(dp[0][u],dp[1][u])
dp[1][v]要怎麼辦?
解一:DP
所有子節點u中,至少要有一個使用dp[0][u],其他的可以用max(dp[0][u],dp[1][u])
也就是說:
把u跟v配對所付出的「代價」
解二:其實Greedy就好了
從葉節點開始選,如果可以配對就配對,並把配對的兩個節點拔掉。
1
2
3
4
5
6
7
8
9
10
為什麼是對的?
選那一條邊的答案 ≥不選
(如果不選就等同於那個葉節點不存在)
其實樹DP大概就這樣
但他超難><
以下的題目會有某些 OI 題的雷
給你一棵樹,每個節點上面有一個人,現在每個人都必須去一個不同的節點,而且每個節點最後只能有一個人,問所有人移動距離的最小總和(並輸出移動方法)。
n≤105
例題2: Appleman and Tree
給你一棵樹,每個節點是黑色或白色,問有多少種方法把樹分成任意多個連通塊,使得每個連通塊內洽有一個黑點。
(答案模109+7)
n≤105
給你一棵n個節點的樹和距離D,請選出最多個點,使得任兩點的距離至少為D。
全方位木DP
なに
在大部份的樹DP問題,我們都可以直接選根
但有時候我們必須考慮所有可能的根
要怎麼有效率的做這件事呢?
來看一個裸題
在一棵樹上,點有點權ai,請找出一個點v,使得∑1≤i≤ndist(i,v)⋅ai 最大
n≤2×105
O(n2)顯然可做,但是要怎麼優化?
優化暴力?
先隨便選一個點DFS,用一個變數紀錄答案,並對每個點存兩個值:
- 子樹大小
- 子樹內的ai總和
當我要從目前的根u移動到相鄰的點v為根時...
u
v
4
5
6
7
8
9
10
只會改到u,v的資訊!
O(1)更新
並維護答案
我們做得到的事
在O(1)內把根往一條邊移動
這樣就夠了
對樹做歐拉遍歷,每經過一條邊就改變根的位置,總共改變O(n)次,而且每個點都會被看到。
這就是全方位木DP,又稱Rerooting Technique
例題1: TIOJ 魔法鏈
給你一棵樹,你每次可以選一個葉子(只有連到一個點)的節點,把他的編號紀錄在一個序列,並拔掉那個點。問可以形成的序列數。
註:本題的n≤2000,但其實可以做到n≤2×105
2
5
7
8
9
合法序列有:
9, 8, 5, 7, 2
7, 8, 5, 2, 9
...
不合法序列有:
9, 5, 8, 7, 2
給你一棵樹,點有點權ai,一個「好的」根節點v會符合,對於任意一條從v到點u的路徑,路徑上都不會有權值相同的點。問有幾個好的根節點。
n≤2×105
Binary Jumping
倍增法
還記得倍增法的LCA怎麼做嗎?
對每個點維護他的1,2,4,...,2k倍祖先,即可以在O(logn)時間內找到一個點的x倍祖先。
其實,這個技巧不一定要在樹上也可以做。
一個常見的變形是:一個點指向另一個(可能有環)
你有一個 1~n 的排列p和一個長度為m的序列a,
保證1≤ai≤n。
接著有q筆詢問,每次問區間[l,r],回答a裡面是否存在一個在[l,r]內的子序列,是排列p的某個循環移位 (Cyclic shift)
n,m,q≤2×105
假設我固定子序列的第一項,如果他是答案的話,考慮最後一項....
提示->
倍增法還可以做什麼
把一條路徑分成logn條不重疊的路徑!
當我們在處理可合併的東西的時候(ex. min, max, sum, gcd, ...),在做倍增表的時候就可以一邊紀錄這個東西,並用相同的方法在O(logn∗合併時間)找答案。
LCA 的一個(有點廢話的)性質
-
每一對點會有恰好一個LCA,因此可以用枚舉LCA的方式枚舉路徑
- 其實我原本要寫更多性質的但我想不到了
給你一張圖,邊有邊權,對於每一條邊,找到包含那條邊的最小生成樹權值。
n,m≤2×105,圖連通
給你一棵樹,有q筆詢問,每次問三個點a,b,c,回答有多少點d,使得如果你把d當根節點,lca(a,b)=c。
n,q≤105,
補充:強強的LCA
使用O(n)記憶體和預處理時間,一樣O(logn)跳k倍祖先
然後其實倍增表能做到的所有事情他都可以做到(因為路徑一樣可以被拆解)。
Small to large Merging
啟發式合併
啟發式合併?
簡單來說,就是當我們要合併兩個大小為a,b (a≥b)的資料結構(可以是序列,set,map...)時,把小的合併到大的。
他跟樹上問題的關係?
1
2
3
4
5
6
7
8
9
10
假設我對一個子樹必須維護O(子樹大小)的東西
那在合併子樹答案的時候就可以用這個方法。
他的複雜度是...?
什麼?多一個 log 而已
有n個點,每次合併大小為a,b的連通塊時需要O(min(a,b))的時間。
考慮一個點所在的連通塊大小,每次他「被合併到其他連通塊」時,這個大小會增加為原本的至少2倍,也就是說一個點只會被算到O(logn)次。
有n個點,所以總共合併 O(nlogn)次
給你一棵有根樹,每個點上有顏色ci,一個子樹內的顏色為「重要」的,代表沒有其他顏色在那個子樹內出現比該顏色更多次(也就是說可能有多個重要的顏色)。
對於每個點,找到該子樹所有重要顏色的編號(ci)總和。
n≤105
例題:忍者調度問題
給你一棵有根樹以及預算上限M,每個點上是一個忍者,有出動費用ci和領導值li,如果你選了v為領導人,則他的滿意度為
lv×預算內能在v子樹內出動的忍者數量
請找出最大的滿意度。
n≤105,M,ci,li≤109
給你一棵n個葉節點的二元樹,每個葉子上有一個值ai,且所有ai形成一個排列。
你可以選擇任意個非葉節點並把他們的左右小孩交換。在交換之後,我們用前序遍歷這棵二元樹並上葉節點上的值紀錄成一個序列,
求這個序列最少的逆序數對數。
n≤2×105
Heavy Light Decomposition
樹鏈剖分
Binary Jumping 的問題
一個點會被O(n)個區間覆蓋到,所以如果點上要修改,會有O(n)個值改變。
我們需要把樹上路徑切成O(logn)條不相交的路徑,而且每個點都只被一個紀錄的路徑覆蓋到。
所以就有樹鏈剖分了

每個點都屬於一條樹鏈
考慮點v所在的樹鏈d(v)和他所有的小孩u。
子樹大小最大的一個小孩會有d(u)=d(v)。
其他的小孩屬於一條新的樹鏈。
這樣的複雜度

定義up(v)為點v所在樹鏈的「頂端」的節點。
那考慮一個點v走到根的路徑中,依序經過的樹鏈頂端的子樹大小。
跳到另一個樹鏈時,樹鏈頂端的子樹大小至少會加倍。
至多只會經過O(logn)條樹鏈
那樹鏈剖分之後呢?
任意一條路徑可以被拆成O(logn)條樹鏈組成的路徑
把一條樹鏈當成序列的一個區間,一條路徑會變成O(logn)個區間,可以用線段樹維護。
那我們就會做這種問題
給你一棵樹,點有點權ai,處理q筆操作。
- 把點v的值改成x
- 詢問u到v路徑的最大ai值
n,q≤105
More (hard) Problems
以下有很多 OI 題
很多我曾經會的題
跟很多我不會的題
卡常
在一個n個點的有根樹上,每個點有顏色1≤ci≤K,接著有q筆操作:
1. 把一個點的顏色改為x
2. 詢問以v為根的子樹有多少種不同的顏色
n≤5×105,q≤3×105,SLOWJUDGE
這題有兩種解法w
進階樹論 (資讀)
By justinlai2003
進階樹論 (資讀)
- 1,417