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];
    }
}

來一個比較難的

CSES Tree Matching

求樹上最大匹配

匹配:選一些邊使得沒有邊共用一個點

可以想成倆倆結婚之類的

 

\(n\leq2*10^5\)

樹上最大匹配

以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一次,找到的最遠點就是直徑

例子

從 \(p\) DFS找到 \(a\) ,再從 \(a\) DFS找到 \(b\)

 

證明

性質

1. 樹上任一點的最遠點會是直徑上其中一點

2. 每個延伸塊的高度比直徑兩點中較近一點的距離小

 

證明

另一個做法

樹DP

對每個節點存:由他往下最長的路徑

則經過該節點最長的路徑就是他的孩子中(最大值+次大值-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\)個點、\(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

直接看例題

CSES Tree Distances II

給一棵\(n\)個點的樹,對每個點算出其他點離它的距離總和

 

\(n\leq2*10^5\)

簡化題目

CSES Tree Distances II

給一棵\(n\)個點的樹,對點1算出其他點離它的距離總和

 

\(n\leq2*10^5\)

簡化題目

CSES Tree Distances II

給一棵\(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)

換根後的答案變化

CSES Tree Distances II

想像根從點\(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的啟發式合併

把小的子樹併到大的子樹

 

應用在樹上:將子樹合併時,把小子樹答案併到大子樹的答案

直接看例題

CSES Distinct Colors

有一棵\(n\)個點的樹,根在1,每個點有顏色

對每個節點,輸出他的子樹有幾種不同顏色

 

\(n\leq2*10^5\)

直接看例題

CSES Distinct Colors

有一棵\(n\)個點的樹,根在1,每個點有顏色

對每個節點,輸出他的子樹有幾種不同顏色

 

\(n\leq2*10^5\)

 

暴力:\(O(n^2)\)

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);
}

直接看例題

CSES Distinct Colors

有一棵\(n\)個點的樹,根在1,每個點有顏色

對每個節點,輸出他的子樹有幾種不同顏色

 

\(n\leq2*10^5\)

 

暴力:\(O(n^2)\)

唬爛優化:算最大的子樹的答案時,把答案留著

最後再把其他子樹的答案加回去

\(O(nlogn)\)??

直接看例題

CSES Distinct Colors

簡短證明:

看單一一個點\(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);
}

分治

分治的時候,都會從中間切一半

 

樹上沒有中間,但是有重心

重心分治

分治的時候,都會從中間切一半

 

樹上沒有中間,但是有重心

 

每次都從重心切下去!

 

算完經過重心的答案後,可以把重心拿掉,剩下的連通塊繼續遞回下去

 

分析方法基本上跟數列上的分治一樣,因為都切成很多半

例題

CF 321C

你有一棵\(n\)個節點的樹,你要給每個節點一個大寫字母。

對於兩個有相同字母的節點
x, y,它們之間的路徑上必須存在一個點 z,使得 z 的字母比它們兩個小。

 

\(1\leq n \leq 10^5\)

例題

CF 321C

試著重心分治:

把重心設為A,接著遞迴下去,下一個重心設為B etc.

總共只會用到\(O(logn)\)種字母

 

時間複雜度\(O(nlogn)\)

練習

CSES Fixed-Length Path I

有一棵有\(n\)個點的樹,找有多少相異路徑長度為\(k\)

 

\(2\leq k \leq n \leq 2*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在兩點最短路徑上

例題

TIOJ 1171

有一個\(n\)個點帶邊權的樹,一開始點都是白色的

有兩種操作:

1. 把點塗成黑色

2. 查詢一個點離所有黑點的距離總和

\(n\leq10^5\)

\(q\leq10^6\)

例題

TIOJ 1171

詢問\(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

  • 359