樹論

不是數論

Lecturer : Lemon

圖論的樹枝 - 樹論

應該蠻明顯ㄉ

樹論是圖論的一部分

其實只要是這種類似植物的名稱

通常就是圖論的一種(?

e.g. 仙人掌(可能完全不會學到w)、森林

但線段樹可能不算(?

樹的性質在初階圖論(建中校培講義)裡面相當ㄉ重要

很多圖論的題目都和樹有關

所以聽完這堂課你們就可以輕鬆破台ㄌ

樹(圖論)

樹的性質

如果下面ㄉ名詞看不懂 建議去翻之前ㄉslides

  • 圖是一棵樹 \(\Longrightarrow\) 圖無環
  • 圖是一棵樹\(\iff\)圖連通且邊數 \( = \) 節點數 \( - 1\)
  • 圖是一棵樹\(\iff\)任意兩節點存在唯一的簡單路徑(不回頭)
  • 圖是一棵樹\(\iff\)去掉任意一條邊則不連通
  • 圖是一棵樹\(\iff\)連任意一條邊則有環

畫一棵樹

1

3

4

5

6

2

名詞定義

1

3

4

5

6

2

  • 樹根 : 選定一個點做「根」
  • 樹葉 : 度數為一的點(不是根的那些)
  • 子樹 : 移除一個點後剩下ㄉ樹們
  • 父(parent)子(child):離根較近者稱為父、                                       離根較遠者稱為子
  • 祖先(ancestor)、後代(descendant) :        父的父的父...、子的子的子...
  • 深度 : 一個點到根的距離
  • 高度 : 最大ㄉ深度

建一棵樹

如果題目的輸入告訴你

節點數\(n\)、邊的數量\(n-1\) 且圖連通

不要懷疑,他就4一棵樹

我們用存圖的方法(adjacency list之類ㄉ)來儲存邊

接著隨便選一個點做深度優先搜尋(dfs)

並且記錄每個點的parent和child

就okㄌ

CODE

#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 1e6 + 5;
vector<int> graph[MAXN];
vector<int> chd[MAXN];
int parent[MAXN];
void dfs(int u, int pa = -1) {
	parent[u] = pa; //save parent
	for(auto v : graph[u]) {
		if(pa != v) { //child_node != parent_node
			chd[u].push_back(v); //save children
			dfs(v, u);
		}
	}
	return;
}
int main() {
	int n;
	cin >> n;
	for(int i = 0; i < n - 1; ++i) {
		int u, v;
		cin >> u >> v;
		graph[u].push_back(v);
		graph[v].push_back(u);
	}
	dfs(1); //root
	for(int i = 1; i <= n; ++i) {
		cout << "Node " << i << ": \n";
		cout << "Parent is " << parent[i] << '\n';
		cout << "Child(ren) is(are) ";
		for(auto c : chd[i]) {
			cout << c << ' ';
		}
		cout << '\n';
	}
}

關於DFS

由於樹沒有環,

所以不用另外紀錄節點有沒有被走過

只要確定不要往回走即可

事實上

對於任意一張簡單圖(無自環、多重邊)

我們都可以透過DFS來將其轉變為類似樹的形式

但這是下下一次要講ㄉ ㄏㄏ

樹DP

樹有著非常好的分治結構

所以當我們利用樹做分治、DP

透過在每個節點上紀錄其子樹的答案

父節點便能夠透過子節點得到資訊

我們就可以維護正確的答案ㄌ

其實就4線段樹的運作邏輯

這也就是為甚麼它叫做樹w

深度 & 高度

做法只是在每個點多存一個深度ㄉ資訊

高度也4

留給讀者自行寫扣(?

我猜應該沒有很難w

例題

tcirc d025

tcirc d108

tcirc d112

tcirc d110

ZJ a265 (記得檢查它是一棵二元搜尋樹)

樹直徑ㄉ其中一種做法

從隨便一個點(通常會選樹根)DFS找最遠的點\(f\)

從\(f\)做DFS找最遠的點\(g\)

\(dis(f, g)\)即為所求

複雜度\(O(V)\)

樹直徑 :

最遠的兩點間距離

如果你想知道為什麼ㄉ話w

不嚴謹ㄉ證明我用講ㄉ(?

樹圓心ㄉ其中一種做法

樹圓心 :

和最遠的點距離最近的節點

(做樹根時高度最矮的點)

如果你還是想知道為什麼ㄉ話

證明我還是用講ㄉ

樹圓心一定在直徑上(不是因為它叫圓心)

所以在第二次DFS可以順便找出圓心

複雜度 : \(O(V)\)

例題

LCA

lowest common ancestor

最低共同祖先

基本上就字面上ㄉ意思w

為什麼我們想知道這ㄍ咚咚(?

ans : 由於兩個點的簡單路徑唯一

所以我們只要找到LCA

就等於找到路徑ㄌouob

Naive

一個很明顯的做法

對於其中一個點做DFS

直到碰到另外一個點

路徑上深度最小的點

就是LCAㄌ

預處理ㄉ時間複雜度 : \(O(V)\)

查詢ㄉ時間複雜度 : \(O(V)\)

好一點ㄉ做法

另一個蠻明顯ㄉ做法

我們會用上之前存ㄉparent

對於深度較大ㄉ點\(e\)、另一點\(f\)

我們發現 \(LCA (e, f) = LCA (parent(e), f)\)

if(depth[e] < depth[f]) swap(e, f); //讓e當深度較大ㄉ點
while(depth[e] != depth[f]) { //爬到一樣高
	e = parent[e];
}
while(e != f) { //一起往上爬
	e = parent[e];
	f = parent[f];
}

就做完ㄌ

預處理ㄉ時間複雜度 : \(O(V)\)

查詢ㄉ時間複雜度 : \(O(d) \in O(V)\)

二分搜

延續上一個做法

我們發現兩點LCA的祖先也是相同ㄉ

所以深度呈現單調性

考慮對深度二分搜ouo

我們同樣先把兩點爬到一樣高

假設現在的深度\(d\)

我們只要搜\(O(log d)\)次就能找到LCAㄌ

但很快你會發現

你往上爬的時間是\(O(d)\)

預處理ㄉ時間複雜度 : \(O(V)\)

查詢ㄉ時間複雜度是很棒ㄉ\(O(d log d) \in O(V log V)\)

倍增法csw algorithm

binary lifting 或 binary jumping

但我只推薦叫binary lifting

想法很簡單 : 

對於每個節點,我們儲存它的\(2^n\)輩祖先

ㄛ我要用畫ㄉ 窩不會用slides

栗子

倍增法

只要把祖先們都保存好(?

就可以在\(O(1)\)的時間搜尋

由於總共要搜\(O(log d)\)次

查詢ㄉ總複雜度 : \(O(log d) \in O(log V)\)

啊我們要怎麼存好祖先R

慢慢爬上去的時間複雜度是 \(O(V^2)\) 欸

果然還是用DFS好

倍增法

如果我們令 \(p[i][j]\)

代表第 \(i\) 個節點的第 \(2^j\) 輩祖先

很酷的事情4

\(p[i][j+1] = p[\ p[i][j]\ ][j]\)

我們就可以在\(O(1)\)的時間找到祖先

由於總共有\(O(log d)\)個祖先

又有\(O(V)\)個點

預處理ㄉ時間複雜度 : \(O(V log d) \in O(V log V)\)

倍增法

倍增法其實可以用在很多問題上

像是下禮拜ㄉ Sparse Table 也是類似的概念

當我們存取2的冪次作為區間的資料

我們在二分搜的時候

都能將時間複雜度壓在完美ㄉ\(O(log n)\)

例題

樹壓平

它很酷所以我才教ㄉ

對於一棵樹(一張圖)

我們在DFS的時候

可以記錄它被visit的時刻

和函式結束的時刻

又4我la

樹壓平

令被visit的時刻為 \(l\)

函式結束的時刻為 \(r\)

對於祖先\(u\)、後代\(v\)

顯然有 \(l_u \leq l_v \leq r_v \leq r_u\)

CODE

int l[MAXN], r[MAXN];
int timer = 0; //紀錄時間點
void dfs(int u, int pa = -1) {
	l[u] = (++timer); //讓時間點+1並記錄
	parent[u] = pa;
	for(auto v : graph[u]) {
		if(pa != v) {
			chd[u].push_back(v);
			dfs(v, u);
		}
	}
	r[u] = timer; //記錄離開的時間
	return;
}

用途

透過樹壓平

我們就把一個圖論問題

轉換成ㄌ相對應的區間序列問題

配合資料結構

可以達成子樹的加值、查詢等操作

例題

好多題都要樹鍊剖分QAQ

其實

上面講的很多問題都有不只一種的解法

兩個禮拜以後

Tarjan演算法

不要忘記

不要錯過

記得來小社上Tarjan演算法

因為非常好演算法

複雜度非常好

差不多一樣冰淇淋

再見

喔 下禮拜是線段樹

樹論

By lemonilemon

樹論

  • 296