TREE
Acme Design is a full service design agency.
OUR SERVICES
We offer a variety of trees.
CFO
George
Meet the Team
CEO
Elaine
Advisor
Susan
| Subordinates | Tree Matching | 
| Tree Diameter | Tree Distances I | 
| Tree Distances II | Company Queries I | 
| Company Queries II | Distance Queries | 
| Counting Paths | Subtree Queries | 
| Path Queries | Path Queries II | 
| Distinct Colors | Finding a Centroid | 
| Fixed-Length Paths I | Fixed-Length Paths II | 
| Prüfer Code | Tree Traversals | 
| Tree Isomorphism I | Tree Isomorphism II | 
CSES Tree + ADditonaL
| Subordinates | Tree Matching | 
| Tree Diameter | Tree Distances I | 
| Tree Distances II | Company Queries I | 
| Company Queries II | Distance Queries | 
| Counting Paths | Subtree Queries | 
| Path Queries | Path Queries II | 
| Distinct Colors | Finding a Centroid | 
| Fixed-Length Paths I | Fixed-Length Paths II | 
| Prüfer Code | Tree Traversals | 
| Tree Isomorphism I | Tree Isomorphism II | 
上一堂有教的東西 (複習)
定義
定義
有\(n\)個點的簡單無向連通圖\(G\)是一棵樹等價於:
\(G\)沒有環
\(G\)有\(n-1\)條邊
\(G\)中任兩點有一條唯一路徑
\(G\)拔掉一條邊會變不連通
\(G\)加上一條邊會有環

樹上DFS
vector<vector<int> > graph;
void dfs(int node,int parent){
    //do something
    for(auto x:graph[node]){
        if(x==parent) continue;
        dfs(x,node);
    }
}一些應用
- 存有層級的東西(E.g. 資料夾)
- 決策樹/遊戲樹
- 演化樹
- 各種噁心資料結構
- 到處都有樹
名詞解釋
有根樹:把一個點當成基底,稱為根
以1當根為例:
6是7的父節點(parent)
7是6的子節點(son)

5的祖先(ancestor)
7的子樹(subtree)
名詞解釋
有根樹:把一個點當成基底,稱為根
以1當根為例:
深度(depth):到根的距離

1
4
3
2
名詞解釋
有根樹:把一個點當成基底,稱為根
以1當根為例:
葉節點:沒有子節點的點

名詞解釋
樹直徑:樹上最長的一條路
中心(center):樹直徑中間一點(或兩點)

名詞解釋
重心:去除後,新出現的每棵樹\(|V|\leq|V_{ori}|/2\)
每棵樹都會有重心,有可能只有一個重心或是有兩個相鄰重心

名詞解釋
前序、中序、後序遍歷:
DFS時,把遍歷節點的順序紀錄起來,可以把樹變成陣列
前序(preorder):走到節點就紀錄
後序(postorder):離開節點時紀錄
Euler Tour:走到、離開時各紀錄一次
中序(inorder):僅限二元樹,兩邊遞迴中間紀錄
順帶一提,二元搜尋樹的中序遍歷會是sorted的

名詞解釋
vector<vector<int> > graph;
vector<int> pre,post;
void dfs(int node,int parent){
    //do something
    pre.push_back(node);
    for(auto x:graph[node]){
        if(x==parent) continue;
        dfs(x,node);
    }
    post.push_back(node);
}| Subordinates | Tree Matching | 
| Tree Diameter | Tree Distances I | 
| Tree Distances II | Company Queries I | 
| Company Queries II | Distance Queries | 
| Counting Paths | Subtree Queries | 
| Path Queries | Path Queries II | 
| Distinct Colors | Finding a Centroid | 
| Fixed-Length Paths I | Fixed-Length Paths II | 
| Prüfer Code | Tree Traversals | 
| Tree Isomorphism I | Tree Isomorphism II | 
CSES Tree + ADditonaL
樹DP
樹上做DP
跟一般的DP一樣,從小問題的答案算大問題
通常會先對子節點DFS得出答案,再回推自己的答案
如果是無根樹,為了方便有時候會以1為根
最基本的應用
以\(v\)為本節點,\(u\)是它的孩子
求子樹大小:\(dp_v=\sum dp_u +1\)
求深度:\(dp_u=dp_v+1\)
vector<vector<int> > graph;
vector<int> sz,dep;
void dfs(int node,int parent){
    sz[node]=1;
    dep[node]=dep[parent]+1;
    for(auto x:graph[node]){
        if(node==parent) continue;
        dfs(x,node);
        sz[node]+=sz[x];
    }
}來一個比較難的
樹上最大匹配
以1為根
令\(dp_{v,0}\)為不選\(v\)時\(v\)的子樹的最大匹配
\(dp_{v,1}\)為選\(v\)時\(v\)的子樹的最大匹配
轉移:\(dp_{v,0}=\sum max(dp_{u,0},dp_{u,1})\)
\(dp_{v,1}=max(dp_{v,0}-max(dp_{u,0},dp_{u,1})+dp_{u,1}+1)\)
這種紀錄選點或不選點的做法很常見,包含樹上最大獨立集、最小點覆蓋都可以這樣子
題單
| Subordinates | Tree Matching | 
| Tree Diameter | Tree Distances I | 
| Tree Distances II | Company Queries I | 
| Company Queries II | Distance Queries | 
| Counting Paths | Subtree Queries | 
| Path Queries | Path Queries II | 
| Distinct Colors | Finding a Centroid | 
| Fixed-Length Paths I | Fixed-Length Paths II | 
| Prüfer Code | Tree Traversals | 
| Tree Isomorphism I | Tree Isomorphism II | 
CSES Tree + ADditonaL
有一棵\(n\)個點的樹,中明想要在上面塗白色或黑色,但相鄰的點不能都是黑色
問總共有幾種塗色方法
\(n\leq10^5\)
有一棵\(n\)個點的樹,每個點住了一個人,現在他們都想要搬家,但都不想搬太遠,所以每個人都要換地方住,但是每個人搬家的距離加起來要最小
求最小值以及一種構造
\(n\leq10^5\)
有一棵\(n\)個點的樹,你要在每個點寫一個數字,其中點\(i\)的數字必須滿足\(l_i \leq i \leq r_i\)
定義漂亮度是每條邊兩個點差的絕對值加起來,請找出漂亮度的最大值
\(n\leq10^5\)
樹直徑
怎麼找直徑?
對每個點DFS/BFS,複雜度\(O(n^2)\)
怎麼找直徑?
對每個點DFS/BFS,複雜度\(O(n^2)\)
好爛喔
怎麼找直徑?
隨便選一個點DFS,從離他最遠的點再DFS一次,找到的最遠點就是直徑
例子

性質

另一個做法
題單
| Subordinates | Tree Matching | 
| Tree Diameter | Tree Distances I | 
| Tree Distances II | Company Queries I | 
| Company Queries II | Distance Queries | 
| Counting Paths | Subtree Queries | 
| Path Queries | Path Queries II | 
| Distinct Colors | Finding a Centroid | 
| Fixed-Length Paths I | Fixed-Length Paths II | 
| Prüfer Code | Tree Traversals | 
| Tree Isomorphism I | Tree Isomorphism II | 
CSES Tree + ADditonaL
裸的樹直徑
有一棵\(n\)個點的樹,求樹上嚴格次長樹直徑
\(n\leq10^5\)
有一張\(n\)個點、\(m\)條邊的圖,每個點有點權(可能為負),求圖上最長路徑
\(n\leq10^5\)
\(m=n-1\) 或 \(m=n\)
有一棵\(n\)個點的樹,問有幾種點集使得點集中每個點兩兩距離都是樹直徑
\(n\leq10^5\)
有一棵\(n\)個點的樹,一開始只有一個點,接下來每次會加一條邊跟一個點,接著問目前樹直徑長度
\(n\leq10^5\)
有一棵\(n\)個點帶邊權的樹,中明想要在上面開\(k\)家冰淇淋店,使得每個點離最近的冰淇淋店的距離最大值最小,求最大值
\(n\leq10^5\)
有一棵\(n\)個點帶正邊權的樹,有\(Q\)次修改,每次修改會改一條邊的長度,每次修改完輸出目前的樹直徑
\(n,Q\leq10^5\)
需要搭配後面的資料結構,可以學完再回來看
換根DP
直接看例題
簡化題目
簡化題目
給一棵\(n\)個點的樹,對點1算出其他點離它的距離總和
void dfs2(int node,int parent){
    for(auto x:graph[node]){
        if(x==parent) continue;
        dp[x]=dp[node]+1;
        dfs2(x,node);
    }
}
// ans = sum(dp)換根後的答案變化
想像根從點\(v\)換成他的兒子\(u\)
可以發現\(u\)的子樹的點的距離會-1
其他點的距離會+1
所以\(dp_u=dp_v-sz_u+(n-sz_u)\)
\(sz\)為子樹大小
全部的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]<<' ';
}
題單
| Subordinates | Tree Matching | 
| Tree Diameter | Tree Distances I | 
| Tree Distances II | Company Queries I | 
| Company Queries II | Distance Queries | 
| Counting Paths | Subtree Queries | 
| Path Queries | Path Queries II | 
| Distinct Colors | Finding a Centroid | 
| Fixed-Length Paths I | Fixed-Length Paths II | 
| Prüfer Code | Tree Traversals | 
| Tree Isomorphism I | Tree Isomorphism II | 
CSES Tree + ADditonaL
有一棵\(n\)個點的樹,中明想要在其中一些點塗黑色,這些點要相連
請對每個點算出:如果這個點一定要是黑色,那中明有幾種塗法(mod M)
\(n\leq10^5\)
\(2\leq M\leq10^9\)
有一棵\(n\)個點的樹,中明已經在上面塗白色或黑色了,現在你想要對每個點回答:
對每個點選一個包含他的連通子圖,使得白點數量減黑點數量最多,這個數字會是什麼?
\(n\leq2*10^5\)
樹壓平
回到遍歷
紅色是前序遍歷、藍色是後序

1
2
3
4
5
6
7
8
9
10
11
11
12
13
15
14
16
17
19
18
20
回到遍歷
紅色是前序遍歷、藍色是後序
有沒有發現什麼事?

1
2
3
4
5
6
7
8
9
10
11
11
12
13
15
14
16
17
19
18
20
回到遍歷
紅色是前序遍歷、藍色是後序
有沒有發現什麼事?

1
2
3
4
5
6
7
8
9
10
11
11
12
13
15
14
16
17
19
18
20
回到遍歷
紅色是前序遍歷、藍色是後序
有沒有發現什麼事?
子樹會在一個連續的區間!
(只有前序的時候也符合)

1
2
3
4
5
6
7
8
9
10
11
11
12
13
15
14
16
17
19
18
20
回到遍歷
紅色是前序遍歷、藍色是後序
有沒有發現什麼事?
子樹會在一個連續的區間!
(只有前序的時候也符合)
=> 砸資料結構!

1
2
3
4
5
6
7
8
9
10
11
11
12
13
15
14
16
17
19
18
20
有一棵\(n\)個點的樹,以1為根,每個點有點權
兩種操作:
1. 改一個點的權重
2. 查一個點的子樹點權和
\(n,q\leq2*10^5\)
前序遍歷後:單點改值、查區間和
=> BIT/線段樹
#include<iostream>
#include<vector>
#define ll long long
using namespace std;
vector<vector<int>> graph;
vector<ll> val,seg,order,out;
int cnt=0;
void dfs(int node,int parent){
    cnt++;
    order[cnt]=node;
    for(auto x:graph[node]){
        if(x==parent) continue;
        dfs(x,node);
    }
    out[node]=cnt;
}
void build(int l,int r,int ind){
    if(l==r){
        seg[ind]=val[order[l]];
        return;
    }
    int mid=(l+r)>>1;
    build(l,mid,ind*2);
    build(mid+1,r,ind*2+1);
    seg[ind]=seg[ind*2]+seg[ind*2+1];
}
ll query(int l,int r,int start,int end,int ind){
    if(start<=l&&r<=end) return seg[ind];
    if(r<start||end<l){
        return 0;
    }
    int mid=(l+r)>>1;
    return query(l,mid,start,end,ind*2)+query(mid+1,r,start,end,ind*2+1);
}
void modify(int l,int r,int pos,ll num,int ind){
    if(l==r){
        seg[ind]=num;
        return;
    }
    int mid=(l+r)>>1;
    if(pos<=mid){
        modify(l,mid,pos,num,ind*2);
        seg[ind]=seg[ind*2]+seg[ind*2+1];
    }
    else{
        modify(mid+1,r,pos,num,ind*2+1);
        seg[ind]=seg[ind*2]+seg[ind*2+1];
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n,q;
    cin>>n>>q;
    val.resize(n+1);
    seg.resize(4*n+4);
    order.resize(n+1);
    out.resize(n+1);
    graph.resize(n+1);
    for(int i=1;i<=n;i++) cin>>val[i];
    int a,b;
    for(int i=1;i<n;i++){
        cin>>a>>b;
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
    dfs(1,0);
    build(1,n,1);
    ll s,x,op;
    vector<int> revord(n+1);
    for(int i=1;i<=n;i++) revord[order[i]]=i;
    for(int i=0;i<q;i++){
        cin>>op;
        if(op==1){
            cin>>s>>x;
            modify(1,n,revord[s],x,1);
            
        }
        else{
            cin>>s;
            cout<<query(1,n,revord[s],out[s],1)<<"\n";
        }
    }
}有一棵\(n\)個點的樹,每條邊有邊權
兩種詢問:
1. 更改一條邊的邊權
2. 查詢a到b的路徑長
\(n,q\leq2*10^5\)
簡化問題
有一棵\(n\)個點的樹,每條邊有邊權
兩種詢問:
1. 更改一條邊的邊權
2. 查詢1到b的路徑長
\(n,q\leq2*10^5\)
簡化問題
有一棵\(n\)個點的樹,每條邊有邊權
兩種詢問:
1. 更改一條邊的邊權
2. 查詢1到b的路徑長
\(n,q\leq2*10^5\)

邊長1 -> 3
簡化問題
有一棵\(n\)個點的樹,每條邊有邊權
兩種詢問:
1. 更改一條邊的邊權
2. 查詢1到b的路徑長
\(n,q\leq2*10^5\)

邊長1 -> 3
簡化問題
有一棵\(n\)個點的樹,每條邊有邊權
兩種詢問:
1. 更改一條邊的邊權
2. 查詢1到b的路徑長
\(n,q\leq2*10^5\)
子樹加值!
前序遍歷後 -> 區間加值、單點詢問
-> BIT

邊長1 -> 3
回來原本問題
有一棵\(n\)個點的樹,每條邊有邊權
兩種詢問:
1. 更改一條邊的邊權
2. 查詢a到b的路徑長
\(dist(a,b)=dist(1,a)+dist(1,b)-2*dist(1,lca(a,b))\)
CODE
#include<iostream>
#include<vector>
#include<algorithm>
#include<utility>
#define ll long long
using namespace std;
vector<ll> in,vec,out,dep,bit;
vector<vector<ll>> anc;
vector<pair<pair<ll,ll>,ll>> edges;
vector<vector<pair<ll,ll>>> graph;
void dfs(int node,int parent){
    in.push_back(node);
    for(auto x:graph[node]){
        if(x.first==parent) continue;
        anc[x.first][0]=node;
        dep[x.first]=dep[node]+1;
        vec[x.first]=vec[node]+x.second;
        dfs(x.first,node);
    }
    out[node]=in.size()-1;
}
void build(int n){
    bit[1]=vec[in[1]];
    if(n>1) bit[2]=vec[in[1]];
    for(int i=2;i<=n;i++){
        bit[i]+=vec[in[i]]-vec[in[i-1]];
        if(i+(i&-i)<=n) bit[i+(i&-i)]+=bit[i];
    }
}
void modify(int l,int r,int num,int n){
    while(l<=n){
        bit[l]+=num;
        l+=(l&-l);
    }
    r++;
    while(r<=n){
        bit[r]-=num;
        r+=(r&-r);
    }
}
ll query(int pos){
    ll ans=0;
    while(pos>0){
        ans+=bit[pos];
        pos-=(pos&-pos);
    }
    return ans;
}
void build_anc(int n){
    for(int j=1;j<19;j++){
        for(int i=1;i<=n;i++) anc[i][j]=anc[anc[i][j-1]][j-1];
    }
}
int lca(int u,int v){
    if(dep[v]>dep[u]) swap(u,v);
    int diff=(dep[u]-dep[v]);
    for(int i=0;i<19;i++){
        if(diff&(1<<i)){
            u=anc[u][i];
            diff-=(1<<i);
        }
        if(diff==0) break;
    }
    if(u==v) return u;
    for(int i=18;i>=0;i--){
        if(anc[u][i]!=anc[v][i]){
            u=anc[u][i];
            v=anc[v][i];
        }
    }
    return anc[u][0];
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n,q;
    cin>>n;
    ll a,b,c;
    edges.resize(n);
    vec.resize(n+1);
    anc.resize(n+1,vector<ll>(19));
    out.resize(n+1);
    dep.resize(n+1);
    bit.resize(n+1);
    in.push_back(0);
    graph.resize(n+1);
    for(int i=1;i<n;i++){
        cin>>a>>b>>c;
        edges[i].first.first=a;
        edges[i].first.second=b;
        edges[i].second=c;
        graph[a].push_back(make_pair(b,c));
        graph[b].push_back(make_pair(a,c));
    }
    dfs(1,0);
    build(n);
    build_anc(n);
    cin>>q;
    vector<int> opin(n+1);
    for(int i=1;i<=n;i++) opin[in[i]]=i;
    for(int i=0;i<q;i++){
        cin>>c>>a>>b;
        if(c==1){
            int son;
            if(opin[edges[a].first.first]>opin[edges[a].first.second]) son=edges[a].first.first;
            else son=edges[a].first.second;
            modify(opin[son],out[son],b-edges[a].second,n);
            edges[a].second=b;
        }
        else{
            int gp=lca(a,b);
            cout<<query(opin[a])+query(opin[b])-2*query(opin[gp])<<"\n";
        }
    }
}樹壓平求LCA

從10走到8
-> 深度最淺的點就是他們的LCA
-> 只要經過一個點就把它記錄下來,就可以用RMQ的方法\(O(1)\)查詢LCA
這種作法叫做Euler Tour
題單
| Subordinates | Tree Matching | 
| Tree Diameter | Tree Distances I | 
| Tree Distances II | Company Queries I | 
| Company Queries II | Distance Queries | 
| Counting Paths | Subtree Queries | 
| Path Queries | Path Queries II | 
| Distinct Colors | Finding a Centroid | 
| Fixed-Length Paths I | Fixed-Length Paths II | 
| Prüfer Code | Tree Traversals | 
| Tree Isomorphism I | Tree Isomorphism II | 
CSES Tree + ADditonaL
裸的樹壓平
樹上啟發式合併
DSU的啟發式合併
直接看例題
直接看例題
int cnt[maxn];
void add(int v, int p, int x){
    cnt[ col[v] ] += x;
    for(auto u: g[v])
        if(u != p)
            add(u, v, x)
}
void dfs(int v, int p){
    add(v, p, 1);
    //now cnt[c] is the number of vertices in subtree of vertex v that has color c. You can answer the queries easily.
    add(v, p, -1);
    for(auto u : g[v])
        if(u != p)
            dfs(u, v);
}直接看例題
有一棵\(n\)個點的樹,根在1,每個點有顏色
對每個節點,輸出他的子樹有幾種不同顏色
\(n\leq2*10^5\)
暴力:\(O(n^2)\)
唬爛優化:算最大的子樹的答案時,把答案留著
最後再把其他子樹的答案加回去
\(O(nlogn)\)??
直接看例題
簡短證明:
看單一一個點\(v\)
\(v\)如果被算到,那他在的子樹一定不是重兒子,所以合併後子樹大小會變兩倍以上
\(v\)被算\(x\)遍之後子樹大小變成\(n\)
=> \(x\in O(logn)\)
全部的點複雜度\(\in O(nlogn)\)
實作
利用樹壓平的概念
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int> cnt,sz,ans,in,out,ord,vec;
vector<vector<int> > graph;
int timer=0;
void dfsz(int node,int parent){
    sz[node]++;
    ord.push_back(node);
    in[node]=timer++;
    for(auto x:graph[node]){
        if(x==parent) continue;
        dfsz(x,node);
        sz[node]+=sz[x];
    }
    out[node]=timer-1;
}
void dfs(int node,int parent,bool keep){
    int big=-1,maxi=-1;
    for(auto x:graph[node]){
        if(x==parent) continue;
        if(sz[x]>maxi){
            maxi=sz[x];
            big=x;
        }
    }
    for(auto x:graph[node]){
        if(x!=parent&&x!=big){
            dfs(x,node,0);
        }
    }
    if(big!=-1) dfs(big,node,1),ans[node]=ans[big];
    for(auto x:graph[node]){
        if(x!=parent&&x!=big){
            for(int i=in[x];i<=out[x];i++){
                cnt[vec[ord[i]]]++;
                if(cnt[vec[ord[i]]]==1) ans[node]++;
            }
        }
    }
    cnt[vec[node]]++;
    if(cnt[vec[node]]==1) ans[node]++;
    if(!keep){
        for(int i=in[node];i<=out[node];i++) cnt[vec[ord[i]]]--;
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int n;
    cin>>n;
    cnt.resize(n+1);
    graph.resize(n+1);
    ans.resize(n+1);
    sz.resize(n+1);
    in.resize(n+1);
    out.resize(n+1);
    vec.resize(n+1);
    for(int i=1;i<=n;i++) cin>>vec[i];
    vector<int> cp;
    for(int i=1;i<=n;i++) cp.push_back(vec[i]);
    sort(cp.begin(),cp.end());
    cp.resize(unique(cp.begin(),cp.end())-cp.begin());
    for(int i=1;i<=n;i++) vec[i]=lower_bound(cp.begin(),cp.end(),vec[i])-cp.begin()+1;
    int a,b;
    for(int i=1;i<n;i++){
        cin>>a>>b;
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
    dfsz(1,0);
    dfs(1,0,1);
    for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
}題單
| Subordinates | Tree Matching | 
| Tree Diameter | Tree Distances I | 
| Tree Distances II | Company Queries I | 
| Company Queries II | Distance Queries | 
| Counting Paths | Subtree Queries | 
| Path Queries | Path Queries II | 
| Distinct Colors | Finding a Centroid | 
| Fixed-Length Paths I | Fixed-Length Paths II | 
| Prüfer Code | Tree Traversals | 
| Tree Isomorphism I | Tree Isomorphism II | 
CSES Tree + ADditonaL
有一棵\(n\)個點的樹,每個點有一個小寫英文字母
有\(m\)個詢問,每個詢問問:
\(v_i\)的子樹裡深度是\(h_i\)的點,能否組成一個回文字串?
\(n,m\leq5*10^5\)
樹鍊剖分/輕重鍊剖分
有什麼東西是樹壓平不能做的?
有一棵\(n\)個點的樹,每條邊有邊權
兩種詢問:
1. 更改一條邊的邊權
2. 查詢a到b的路徑上最大值
沒辦法抵消了..
把樹變成鏈
一次壓平沒辦法,那把樹分成很多條鏈呢?
把樹變成鏈
把樹變成鍊
發現重邊會變成一條一條的鍊
現在我們想證明:樹上隨便一條路徑都只會經過
\(O(logn)\)條邊
把樹變成鍊
證明:
首先將路徑\((a,b)\)變成\((a,lca(a,b))\),\((b,lca(a,b))\)
當我要從a跳到他的lca時,如果換了一條鏈,就代表走到輕邊,因此走上去後子樹大小會變兩倍以上
被算\(x\)遍之後子樹大小變成\(n\)
=> \(x\in O(logn)\)
因為路徑上只會有\(O(logn)\)條鏈,所以可以對每條鏈開線段樹,單次詢問複雜度\(O(log^2n)\)
(因為要查\(O(logn)\)次線段樹)
實作
實作上其實只要開一棵線段樹就好:
如果我DFS時都先走重邊,那鍊上的點就會在壓平後變成一段區間
查詢的時候看哪個點深度比較深,把他往上跳到鍊頂
詳細看code
CODE
#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<<" ";
        }
    }
}題單
| Subordinates | Tree Matching | 
| Tree Diameter | Tree Distances I | 
| Tree Distances II | Company Queries I | 
| Company Queries II | Distance Queries | 
| Counting Paths | Subtree Queries | 
| Path Queries | Path Queries II | 
| Distinct Colors | Finding a Centroid | 
| Fixed-Length Paths I | Fixed-Length Paths II | 
| Prüfer Code | Tree Traversals | 
| Tree Isomorphism I | Tree Isomorphism II | 
CSES Tree + ADditonaL
caido推薦的實作題
重心分治
重心
重心:去除後,新出現的每棵樹\(|V|\leq|V_{ori}|/2\)
每棵樹都會有重心,有可能只有一個重心或是有兩個相鄰重心

找重心
隨便選一個點當根,預處理子樹大小
接著再DFS一次,如果有一個子樹大小>全部節點數/2,就往下DFS,沒有的話自己就是重心
找重心
隨便選一個點當根,預處理子樹大小
接著再DFS一次,如果有一個子樹大小>全部節點數/2,就往下DFS,沒有的話自己就是重心
#include<iostream>
#include<vector>
using namespace std;
vector<vector<int> > graph;
vector<int> sz;
int n;
void dfs_sz(int node,int parent){
    for(auto x:graph[node]){
        if(x==parent) continue;
        dfs_sz(x,node);
        sz[node]+=sz[x];
    }
    sz[node]++;
}
int dfs_cent(int node,int parent){
    for(auto x:graph[node]){
        if(x==parent) continue;
        if(sz[x]>n/2) return dfs_cent(x,node);
    }
    return node;
}
int main(){
    cin>>n;
    graph.resize(n+1);
    sz.resize(n+1);
    int a,b;
    for(int i=1;i<n;i++){
        cin>>a>>b;
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
    dfs_sz(1,0);
    cout<<dfs_cent(1,0);
}分治
分治的時候,都會從中間切一半
樹上沒有中間,但是有重心
重心分治
分治的時候,都會從中間切一半
樹上沒有中間,但是有重心
每次都從重心切下去!
算完經過重心的答案後,可以把重心拿掉,剩下的連通塊繼續遞回下去
分析方法基本上跟數列上的分治一樣,因為都切成很多半
例題
你有一棵\(n\)個節點的樹,你要給每個節點一個大寫字母。
對於兩個有相同字母的節點
x, y,它們之間的路徑上必須存在一個點 z,使得 z 的字母比它們兩個小。
\(1\leq n \leq 10^5\)
例題
練習
CODE
其實我寫爛了,傳上去會TLE,如果有人找到bug我請你飲料
不過框架大概是這樣子
#pragma GCC optimize("O3,unroll-loops")
#pragma GCC target("avx,avx2,bmi,bmi2")
#include<iostream>
#include<vector>
#include<algorithm>
#include<utility>
#define ll long long
using namespace std;
const int N = 2e5+5;
int sz[N],dep[N];
bool vis[N];
vector<vector<int> > graph;
ll cnt[N];
int n,k;
void dfs_sz(int node,int parent){
    sz[node]=1;
    for(auto x:graph[node]){
        if(x==parent||vis[x]) continue;
        dfs_sz(x,node);
        sz[node]+=sz[x];
    }
}
int dfs_cent(int node,int parent,int tot_sz){
    for(auto x:graph[node]){
        if(x==parent||vis[x]) continue;
        if(sz[x]*2>tot_sz) return dfs_cent(x,node,tot_sz);
    }
    return node;
}
void dfs_dep(int node,int parent){
    if(parent==-1) dep[node]=0;
    else dep[node]=dep[parent]+1;
    for(auto x:graph[node]){
        if(x==parent||vis[x]) continue;
        dfs_dep(x,node);
    }
}
ll dfs_calc_ans(int node,int parent){
    ll sum;
    if(dep[node]>k) sum=0;
    else sum=cnt[k-dep[node]];
    for(auto x:graph[node]){
        if(x==parent||vis[x]) continue;
        sum+=dfs_calc_ans(x,node);
    }
    return sum;
}
void dfs_build_cnt(int node,int parent){
    cnt[dep[node]]++;
    for(auto x:graph[node]){
        if(x==parent||vis[x]) continue;
        dfs_build_cnt(x,node);
    }
}
void dfs_reset(int node,int parent){
    cnt[dep[node]]=0;
    for(auto x:graph[node]){
        if(x==parent||vis[x]) continue;
        dfs_reset(x,node);
    }
}
ll decent(int node){
    dfs_sz(node,0);
    int cent=dfs_cent(node,0,sz[node]);
    dfs_dep(cent,-1);
    cnt[0]++;
    ll ans=0;
    for(auto x:graph[cent]){
        if(vis[x]) continue;
        ans+=dfs_calc_ans(x,cent);
        dfs_build_cnt(x,cent);
    }
    dfs_reset(cent,0);
    vis[cent]=1;
    for(auto x:graph[cent]){
        if(vis[x]) continue;
        ans+=decent(x);
    }
    return ans;
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n>>k;
    graph.resize(n+1);
    int a,b;
    for(int i=1;i<n;i++){
        cin>>a>>b;
        graph[a].push_back(b);
        graph[b].push_back(a);
    }
    cout<<decent(1)<<"\n";
}題單
| Subordinates | Tree Matching | 
| Tree Diameter | Tree Distances I | 
| Tree Distances II | Company Queries I | 
| Company Queries II | Distance Queries | 
| Counting Paths | Subtree Queries | 
| Path Queries | Path Queries II | 
| Distinct Colors | Finding a Centroid | 
| Fixed-Length Paths I | Fixed-Length Paths II | 
| Prüfer Code | Tree Traversals | 
| Tree Isomorphism I | Tree Isomorphism II | 
CSES Tree + ADditonaL
有一棵\(n\)個點帶正邊權的樹,有\(Q\)次修改,每次修改會改一條邊的長度,每次修改完輸出目前的樹直徑
\(n,Q\leq10^5\)
重心剖分/重心樹
重心樹
在對序列分治的時候,我們有時會畫一個分治樹
重心分治的時候,我們也可以畫分治樹
重心樹
在對序列分治的時候,我們有時會畫一個分治樹
重心分治的時候,我們也可以畫分治樹
樹的重心是重心樹的根,第一層分治的重心會是根的子節點,以此類推
重心樹

性質
重心分治時,我們處理到\(v\)時,他會影響到的節點們就會是在重心樹上的子樹
重心樹上\(v\)的祖先,就是當重心時影響到\(v\)的點
重心樹的深度是\(O(logn)\)
重心樹上兩點LCA在兩點最短路徑上
例題
例題
詢問\(v\)時,回答:
對重心樹上所有\(v\)的祖先\(x\),\(v\)經過\(x\)遇到的黑點答案
修改時,對重心樹上所有\(v\)的祖先\(x\)修改東西,使得我們可以回答詢問
樹同構
有兩棵樹,他們“長的”一不一樣?
長得一樣:編號拿掉後看起來一樣

簡化問題
有兩棵定根在1的樹,他們“長的”一不一樣?
長得一樣:編號拿掉後看起來一樣
對每種樹編號
如果我們對每種樹編號,那編號一樣的樹就長的一樣
怎麼編號?
對每種樹編號
如果我們對每種樹編號,那編號一樣的樹就長的一樣
怎麼編號?看子樹們的編號
#include<iostream>
#include<vector>
#include<algorithm>
#include<utility>
#include<map>
using namespace std;
map<vector<int>,int> m;
int cnt;
struct iso{
    vector<vector<int> > graph;
    int n;
    vector<int> id;
    iso(int _n){
        n=_n;
        cnt=0;
        graph.resize(n+1);
        id.resize(n+1);
        int a,b;
        for(int i=1;i<n;i++){
            cin>>a>>b;
            graph[a].push_back(b);
            graph[b].push_back(a);
        }
    }
    int dfs(int node,int parent){
        vector<int> children;
        for(auto x:graph[node]){
            if(x==parent) continue;
            children.push_back(dfs(x,node));
        }
        sort(children.begin(),children.end());
        if(m.count(children)) return m[children];
        else{
            m[children]=++cnt;
            return cnt;
        }
    }
    int get_id(){
        return dfs(1,0);
    }
};
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int t;
    cin>>t;
    while(t--){
        int n;
        cin>>n;
        m.clear();
        cnt=0;
        iso a=iso(n),b=iso(n);
        if(a.get_id()==b.get_id()) cout<<"YES\n";
        else cout<<"NO\n";
    }
也有hash的做法,但是我不會
回到原本問題
有兩棵樹,他們“長的”一不一樣?
=> 把它定根,看兩棵樹一不一樣
有什麼點很特別?
回到原本問題
有兩棵樹,他們“長的”一不一樣?
=> 把它定根,看兩棵樹一不一樣
有什麼點很特別?
重心!把重心當根
題單
| Subordinates | Tree Matching | 
| Tree Diameter | Tree Distances I | 
| Tree Distances II | Company Queries I | 
| Company Queries II | Distance Queries | 
| Counting Paths | Subtree Queries | 
| Path Queries | Path Queries II | 
| Distinct Colors | Finding a Centroid | 
| Fixed-Length Paths I | Fixed-Length Paths II | 
| Prüfer Code | Tree Traversals | 
| Tree Isomorphism I | Tree Isomorphism II | 
CSES Tree + ADditonaL
Prüfer code
今天沒有講的東西
- 樹背包
 - 樹分塊
 - Splay
 - Link-Cut Tree
 - 虛樹
 - 樹套樹
 - 矩陣樹定理
 
樹
By ck1110530
樹
- 721