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
樹
- 390