{Tree}

cjtsai

我上次講小社是11/17欸 燒雞

\(INDEX\)

  • 樹直徑
  • 樹低批
  • 樹重心
  • 樹壓平
  • LCA

這我

  • 20729 蔡嘉晉
  • 失蹤的世宗
  • judging

 

 

 

 

 

  • 店言學術
  • 資訊校隊
  • 北市賽佳作
  • 這次不放照片
  • 放個我新買的rog風暴軸
  • 真香

對不起我偷了一堆東西

{樹}

可以吃嗎

  • 每個節點都只有有限個子節點或無子節點
  • 沒有父節點的節點稱為根節點
  • 每一個非根節點有且只有一個父節點
  • 樹裡面沒有環路(cycle)

名詞定義

根節點

葉節點

父節點

子節點

正向情說為甚麼

要用黑底白圖

所以我把它調成白底了

存樹

  • 不定根(不指定根節點)
    • 跟圖一樣存adj[][]
  • 定根
    • 也可以存adj
    • 或是一個一維陣列紀錄每個點他爸(如果沒有要往下走)

樹的性質

  • N個點的樹
    • 因為其除根節點外皆只有一個往上連結的邊
    • 故其共有N-1條邊
  • 反之
    • 若一張連通圖有N-1條邊 其必為樹
    • 有些題目可能會給這些條件不說他是樹來搞人

 

  • 樹必無環

樹論

  • 對樹這種資料結構進行各種神奇的算法來得到期望得答案
    • 樹DP
    • 樹壓平
    • 樹剖
    • ...

{樹直徑}

最長的那條

樹直徑

  • 找到兩個點,他們的距離是所有任兩個點的距離中最長的
  • How?
    • Brute Force
    • 全點對最短路 Floyd-Warshall
    • \(O(N^3)\)
  • 認真做
    • 從隨便一個點BFS找到最遠的點
    • 再從最遠的那個點BFS一次找到離他最遠的點
    • 這兩個點就是樹直徑
    • \(O(N)\)

動畫

  • 希望我做的出來

WHY

CODE

  • TEXT
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
const int maxn = 2e5+5;
vector<int> adj[maxn];

void dfs(int i, vector<int> & dep, vector<bool> & visited) {
	visited[i] = 1;

	for (auto &v : adj[i]) {
		if (!visited[v]) {
			dep[v] = dep[i]+1;
			dfs(v, dep, visited);
		}
	}
}


signed main(){
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);

	int n;
	cin >> n;
	for (int i = 0; i < n-1; ++i) {
		int a, b;
		cin >> a >> b;
		adj[a].push_back(b);
		adj[b].push_back(a);
	}

	vector<int> dep(n+1, 0);
	vector<bool> visited(n+1, 0);
	dfs(1, dep, visited);
	int maxDeepNode = 1;
	for (int i = 1; i <= n; ++i) {
		if (dep[i] > dep[maxDeepNode]) maxDeepNode = i;
	}
	
	fill(dep.begin(), dep.end(), 0);
	fill(visited.begin(), visited.end(), 0);

	dfs(maxDeepNode, dep, visited);

	int diameter = 0;
	for (auto &i : dep) diameter = max(i, diameter);
	cout << diameter << endl;

	return 0;
}

{樹DP}

低批好難

複習

  • DP
    • 從小區間的答案求得大區間的答案
  • 樹DP
    • 從子樹的答案求得根的答案

  • 子樹大小
    • 顯然
    • 若u為v的子節點們
    • \(sz[v]=(\sum{sz[u]})+1\)
    • 子樹大小包含該點本身
#include<bits/stdc++.h>
using namespace std;
vector<vector<int>> sub(200007);
vector<int> ans(200007, 0);
int solve(int ind){
    for(int x:sub[ind]){
        solve(x);
        ans[ind]+=ans[x]+1;
    }return ans[ind];
}
 
int main(){
    ios_base::sync_with_stdio(false);cin.tie(0);
    int n;cin>>n;
    for(int i=2; i<=n; i++){
        int a;cin>>a;
        sub[a].push_back(i);
    }
    solve(1);
    for(int i=1; i<=n; i++) cout<<ans[i]<<' ';
}

例-2

  • TREE MATCHING
    • 給你一顆n個點的樹
    • 請幫某些邊塗色 每個點只能碰到一個有顏色的邊
    • 求最多能塗幾條邊

樹DP

  • 狀態
    • \(dp[v][0]\) : v的子樹中不選v的最大匹配
    • \(dp[v][1]\) : v的子樹中選v的最大匹配
  • 轉移  (v=current u=child)
    • dp[v][0] = \(\sum max(dp[u][0], dp[u][1])\)
      • 每個子樹的東西都拿最大的
    • dp[v][1] = \( dp[v][0]+max(dp[u][0]+1-max(dp[u][0], dp[u][1])) \)
      • 一定要有一顆子樹只能是\(dp[u][0]\) 其他拿大的就好

CODE

  • TEXT
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
vi adj[200005];
int dp[200005][2];
 
void dfs(int v, int p) {
    for (int to : adj[v]) {
        if (to != p) {
            dfs(to, v);
            dp[v][0] += max(dp[to][0], dp[to][1]);
        }
    }
    for (int to : adj[v]) {
        if (to != p) {
            dp[v][1] = max(dp[v][1], dp[to][0] + 1 + dp[v][0] -
                                         max(dp[to][0], dp[to][1]));
        }
    }
}
 
int main() {
    ios_base::sync_with_stdio(0);cin.tie(0);;
    int n;
    cin >> n;
    for (int i = 0; i < n - 1; i++) {
        int u, v;
        cin >> u >> v;
        u--, v--;
        adj[u].pb(v), adj[v].pb(u);
    }
    dfs(0, -1);
    cout << max(dp[0][0], dp[0][1]) << '\n';
}

GREEDY

  • 剛剛那題其實也可以貪婪
  • 從葉子開始 能拿就拿 不能拿就跳過
  • 一定能拿到最多的
  • WHY?

換根DP全方位木DP

  • 在一些不指定根的題目中
  • 他可能會問你把每個點當根的時候一個問題的答案是甚麼
  • 像是每個點他到其他所有點的距離和
  • CSES Tree Distances II

 

  • 暴力\(O(N^2)\)
  • 一定出事

 

  • 如果我有一個人他的答案
  • 要怎麼算他孩子的答案?

換根

  • 所有人的距離和
  • 如果走下去還是他的孩子 距離會-1
  • 反之+1

 

  • \(dp[u]=dp[v]-sz[u]+(n-sz[u])\)
    • \(sz[u]\) : 他的子樹
    • \(n-sz[u]\) : 不是他的子樹

CODE

#include<iostream>
#include<vector>
#include<algorithm>
#define ll long long
using namespace std;
vector<ll> sub,dp;
vector<vector<int>> graph;
int n;
void dfs1(int node,int parent){
    for(auto x:graph[node]){
        if(x==parent) continue;
        dfs1(x,node);
        sub[node]+=sub[x];
    }
    sub[node]++;
}
void dfs2(int node,int parent){
    for(auto x:graph[node]){
        if(x==parent) continue;
        dp[x]=dp[node]+1;
        dfs2(x,node);
    }
}
void dfs3(int node,int parent){
    for(auto x:graph[node]){
        if(x==parent) continue;
        dp[x]=dp[node]-sub[x]+n-sub[x];
        dfs3(x,node);
    }
}
int main(){
    cin>>n;
    int a,b;
    sub.resize(n+1);
    dp.resize(n+1);
    graph.resize(n+1);
    for(int i=1;i<n;i++){
        cin>>a>>b;
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
    dfs1(1,0);
    dfs2(1,0);
    for(int i=2;i<=n;i++) dp[1]+=dp[i];
    for(int i=2;i<=n;i++) dp[i]=0;
    dfs3(1,0);
    for(int i=1;i<=n;i++) cout<<dp[i]<<' ';
}

{樹重心}

Acme Engineering is a full service software agency.

這啥

  • 以這個點為根的話所有子樹大小皆小於[2/n]
  • 取所有點到任意一點的距離和 重心為其最小者

 

  • 必定只有一或二個 若二個必相鄰

 

  • 有啥用
  • 重心分治/剖分 下下次有機會講

HOW

  • 用剛剛低批出來的子樹大小(隨便選一個點當根都可以)
  • 全部點數量減掉他的子樹的大小就會得到他上面的點的數量
  • 然後就往比較大的那個方向一直走就會找到了

CODE

  • TEXT
int siz[maxn];
void dfs(int n, int par) {
    siz[n] = 1;
    for (int v:adj[n]) {
        if (v != par) {
            dfs(v, n);
            siz[n] += siz[v];
        }
    }
}
int tot; //總節點數
int get_centroid(int n, int par) {
    for (int v:adj[n]) {
        if (v != par && siz[v] * 2 > tot) {
            return get_centroid(v, n);
        }
    }
    return n;
}
int main() {
    dfs(1, 0);
    int centroid = get_centroid(1, 0);
}

{樹壓平}

熱壓吐司

完全不是

  • 把樹壓成一條鍊
  • 這樣就可以套前幾堂講的資料結構(線段樹等)

 

  • 可以實現對某個點他的子樹進行操作
  • 或是簡單的樹鍊操作
    • 可反悔的 像是和 最大值就不行

遍歷

  • 怎麼把一個點他的所有孩子們放在同一個區間?

 

 

  • 對他做DFS
  • 紀錄他進入跟出去的時間戳記

有甚麼用?

  • 綠 入時間戳
  • 紅 出時間戳

1

2

3

4

5

6

7

8

9

10

11

12

13

15

14

16

17

18

21

19

20

22

有甚麼用?

  • 綠 入時間戳
  • 紅 出時間戳

1

2

3

4

5

6

7

8

9

10

11

12

13

15

14

16

17

18

21

19

20

22

換成點編號

  • 綠 入時間戳
  • 紅 出時間戳

1

2

3

4

5

6

7

8

9

10

11

12

13

15

14

16

17

18

21

19

20

22

1

2

3 

4

5

6

7 

8

9

 

10

11

換成點編號

  • 綠 入時間戳
  • 紅 出時間戳

1

2

3

4

5

6

7

8

9

10

11

12

13

15

14

16

17

18

21

19

20

22

1

2

3 

4

5

6

7 

8

9

10

11

1

2

3

4

5

6

7

8

9

10 

11

所以怎樣

  • 每個點他的子樹會被包含在該點的入時間戳跟出時間戳中間
  • 開個陣列紀錄後就能用線段樹進行操作

 

  • 同時也能做簡單的樹鍊操作
  • 可回復式的(SUM)
  • 入時間戳的值設成正值
  • 出時間戳的值設成負值

CODE

  • TEXT
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define pii pair<int,int>
#define pb push_back
#define all(x) x.begin(),x.end()
#define ff first
#define ss second
vector<int> pa(300000+1), in(300000+1, -1), out(300000+1);
vector<vector<int>> ch(300000+1);
vector<int> st(1000010, 0);
vector<int> dfn;
int n;
void dfs(int root){
	dfn.pb(root);
	for(int x:ch[root]){
		dfs(x);
	}
	dfn.pb(root);
}

int query(int l, int r) {
	int sum=0;
	for(l+=2*n,r+=2*n;l<r;l>>=1,r>>=1){
		if(l&1)sum+=st[l++];
		if(r&1)sum+=st[--r];
	}
	return sum;
}
int build(){
	for(int i=2*n; i<4*n; i++)st2[i]=dfn[i-2*n];
	for(int i=2*n-1;i>=0; i--)st2[i]=min(st2[i*2+1], st2[i*2]);
}


signed main(){
	ios_base::sync_with_stdio(false);cin.tie(0);
	int q;cin>>n>>q;

	for(int i=2; i<=n; i++) {cin>>pa[i];ch[pa[i]].pb(i);}
	int root=1;
	dfs(root);
	for(int i=0; i<2*n; i++){
		if(-1==in[dfn[i]])in[dfn[i]]=i;
		else out[dfn[i]]=i;
	}
	while(q--){
		int det;cin>>det;
		if(det==1){
			int a, b;cin>>a>>b;
			int d=b;
            //線段樹的modify指令被我inline了
			for(int i=in[a]+2*n; i; i>>=1){
				st[i]+=d;
			}
			for(int i=out[a]+2*n; i; i>>=1){
				st[i]+=d;
			}

		}if(det==2){
			int u;cin>>u;
			int ans=query(in[u], out[u]+1)/2;
			cout<<ans<<'\n';
		}
	}
	}

}


{LCA}

最低共同祖先

Lowest Common Ancestor

生物課?

  • 確實是 最低 共同祖先
  • 但資訊會把樹轉過來就變成最高了 小問題

最低共同祖先

  • 兩個點他們各自的直系血親中重複的第一個
  • 一定存在因為根是所有人他祖先

 

  • 有甚麼用?
  • 快速得知兩個點他們路徑上最低的點
  • 進而求得路徑或定向

Brute Force

  • 對每個點紀錄他的深度(從根走下來的距離)
  • 從一個點DFS到另外一個點看路徑上深度最低的點
  • \(O(N)\)

 

 

可能好一點

  • 兩個人先走到同個深度的祖先
  • 然後一起往上走 直到兩個人走到同樣的祖先
  • 他就一定是這兩個點的LCA

 

  • 聽起來在某些情況下可能會快一點
  • 但還是\(O(N)\)

 

  • 要怎麼往上走?

倍增法

  • 一種在線性搜尋時可能可以把複雜度多帶一個Log的東西

 

  • 如果你有認真上海放音的課
  • 可能記得有個東西叫做sparse table
  • 透過記錄長度為2的n次方的區間的資料並進行合併得到詢問區間

倍增on tree

  • 紀錄每個點的 \(2^n\) 代祖先

 

  1. 比較下面的那個點先\(O(logN)\)跳到一樣的高度
  2. 兩個同樣高度的點k從大到小比較\(2^k\)代祖先
  3. 如果不一樣就往上走
  4. 最後他們停在LCA上了

CODE

  • TEXT
int dep[maxn];
int anc[18][maxn]; 
// anc[i][j] 代表 j 的 2^i 倍祖先
// 紀錄到每一點的 2^17 倍祖先 (大於1e5) 

void dfs(int n, int fa, int d) {
    anc[0][n] = fa; // 一倍祖先就是父親
    dep[n] = d;
    for (int v : adj[n]) {
    	if (v != fa) dfs(v, n, d+1);
    }
}

void setupLCA() {
    dep[0] = -1;
    dfs(1, 0, 0);
    for (int i = 1; i < 18; i++) {
        for (int j = 1; j <= n; j++) {
            anc[i][j] = anc[i-1][anc[i-1][j]];
        }
    }
}

int lca(int a, int b){
    if (dep[a] < dep[b]) swap(a, b);
    for (int i = 17;i >= 0;i--) {
    	if (dep[anc[i][a]] >= dep[b]) {
        	a = anc[i][a];
        }
    }
    if (a == b) return a;
    for (int i = 17;i >= 0;i--) {
    	if (anc[i][a] != anc[i][b]) {
            a = anc[i][a];
            b = anc[i][b];
        }
    }
    return anc[0][a];
}

int main() {
    // 省略輸入圖
    setupLCA();
    while (q--) {
        int u, v; 
        cin >> u >> v;
        cout << lca(u, v) << endl;
    }
}

歐拉迴路做LCA

  • 歐拉迴路
    • 把所有點走過一次
    • 跟樹壓平有點小差別
    • 從下往上走再往上走都要記錄到
    • 一個點可能出現多次

euler tour

  • 順序

1

2

3 

4

5

6

7 

8

9

 

10

11

order 1 2 3 4 5 6 7 8 9
vertex 1 2 3 4 3 5 3 2 1
order 10 11 12 13 14 15 16 17 18
vertex 6 7 6 8 9 8 6 1 10
order 19 20 21
vertex 11 10 1
  • 取range min深度最小的那個點就會是LCA了

{樹鍊剖分}

希望我不會燒雞

  • CSES 2134
  • 帶修改詢問路徑上最大值

樹剖

  • 如果在鍊上遇到無法抵銷的操作
  • 如最大值
  • 就沒辦法樹壓平了
  • 把樹切成一段一段的每段都只是一條鏈
  • 就可以炸資結了!

樹鏈

有啥用

  • 每次需要求一條樹鏈的時候
  • 我們可以把要求分成兩半
  • 假設是 a 到 b 的鏈
  • 拆成 a to lca(a, b) && lca(a, b) to b
  • 這樣就會是兩條只往下走的樹鏈了

 

  • 所以我們就可以透過查詢若干條樹鏈的不同區間合併出所求的樹鏈

  • 要怎麼切樹才不會讓複雜度爛掉(?
     
  • 還記得啟發式合併嗎

 

  • 每次一個節點有多於一個子節點的時候時
  • 選擇子樹大小最大的那個連上現在的節點延長樹鏈
  • 其他的就新建一條樹鏈

 

  • 這樣每次要合併樹鏈的時候
  • 子樹大小都會是兩倍
  • 只要合併\(log \ n\)次樹鏈一定能得到任意樹鏈

蒿土實作

  • 其實線斷樹只要開一顆
  • 因為你把他壓平之後
  • 如果先走重邊(子樹比較大的那個)
  • 樹鏈都會在同個區間

 

  • 查詢的時候就可以往上跳
  • 先跳深度比較深的那個

  • TEXT
#include<iostream>
#include<vector>
#include<algorithm>
#pragma GCC optimize("O3,unroll-loops")
#pragma GCC target("avx,avx2")
using namespace std;
const int N=2e5+5;
int in[N],out[N],sz[N],nxt[N],par[N],vec[N],seg[4*N],dep[N];
vector<int> ord;
vector<vector<int> > graph;
int cnt=0;
void dfs_sz(int node,int parent){
    sz[node]=1;
    par[node]=parent;
    if(node!=1) dep[node]=dep[parent]+1;
    int szz=graph[node].size();
    int mx=0;
    for(int i=0;i<szz;i++){
        if(graph[node][i]==parent) continue;
        dfs_sz(graph[node][i],node);
        sz[node]+=sz[graph[node][i]];
        if(sz[graph[node][i]]>mx){
            mx=sz[graph[node][i]];
            swap(graph[node][i],graph[node][0]);
        }
    }
}
void dfs_nxt(int node,int parent){
    in[node]=++cnt;
    ord.push_back(node);
    for(auto x:graph[node]){
        if(x==parent) continue;
        if(x==graph[node][0]) nxt[x]=nxt[node];
        else nxt[x]=x;
        dfs_nxt(x,node);
    }
    out[node]=cnt;
}
void build(int l,int r,int ind){
    if(l==r){
        seg[ind]=vec[ord[l]];
    }
    else{
        int mid=(l+r)>>1;
        build(l,mid,ind*2);
        build(mid+1,r,ind*2+1);
        seg[ind]=max(seg[ind*2],seg[ind*2+1]);
    }
}
void modify(int l,int r,int num,int pos,int ind){
    if(l==r){
        seg[ind]=num;
        return;
    }
    int mid=(l+r)>>1;
    if(pos<=mid) modify(l,mid,num,pos,ind*2);
    else modify(mid+1,r,num,pos,ind*2+1);
    seg[ind]=max(seg[ind*2],seg[ind*2+1]);
}
int query(int l,int r,int start,int end,int ind){
    if(r<start||end<l) return 0;
    if(start<=l&&r<=end) return seg[ind];
    int mid=(l+r)>>1;
    return max(query(l,mid,start,end,ind*2),query(mid+1,r,start,end,ind*2+1));
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n,q;
    cin>>n>>q;
    graph.resize(n+1);
    nxt[1]=1;
    dep[1]=1;
    ord.push_back(0);
    int a,b;
    for(int i=1;i<=n;i++) cin>>vec[i];
    for(int i=1;i<n;i++){
        cin>>a>>b;
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
    dfs_sz(1,0);
    dfs_nxt(1,0);
    build(1,n,1);
    int op;
    for(int i=0;i<q;i++){
        cin>>op;
        if(op==1){
            cin>>a>>b;
            modify(1,n,b,in[a],1);
            vec[a]=b;
        }
        else{
            cin>>a>>b;
            int ans=vec[a];
            while(nxt[a]!=nxt[b]){
                if(dep[nxt[a]]<dep[nxt[b]]) swap(a,b);
                ans=max(ans,query(1,n,in[nxt[a]],in[a],1));
                a=par[nxt[a]];
            }
            if(dep[a]<dep[b]) swap(a,b);
            ans=max(ans,query(1,n,in[b],in[a],1));
            cout<<ans<<" ";
        }
    }
}

題單

考幹

他們都是連結

幹訓

你會來這堂課的話

大概要準備一下上機考

剩差不多一個月ㄌ

過幾天大概會發公告講規則

Tree

By cjtsai

Tree

樹們

  • 107