Ví dụ động lực: cho cây \(N = 10^5\), cần đếm cặp \((u,v)\) có \(\text{dist}(u,v) = K\). Liệt kê mọi cặp mất \(O(N^2)\). Nếu chặt cây tại centroid\(C\), mọi đường đi hoặc qua \(C\) (đếm bằng tần suất khoảng cách \(O(\text{size})\)) hoặc nằm gọn trong một nhánh (đệ quy). Mỗi tầng kích thước giảm một nửa → \(O(N \log N)\). Đó là lý do ta cần centroid.
Định nghĩa: Cho cây có \(N\) đỉnh. Centroid (trọng tâm) là đỉnh mà khi loại bỏ nó, mỗi thành phần liên thông còn lại có kích thước \(\leq \lfloor N/2 \rfloor\).
Tồn tại và duy nhất: Mọi cây có \(\geq 1\) đỉnh đều có đúng một hoặc hai centroid. Nếu có hai centroid thì chúng kề nhau.
graph TD
A((1)) --- B((2))
A --- C((3))
B --- D((4))
B --- E((5))
C --- F((6))
C --- G((7))
Cây 7 đỉnh. Đỉnh 1 là centroid vì khi loại bỏ nó:
- Thành phần bên trái (gốc 2): 3 đỉnh \(\leq \lfloor 7/2 \rfloor = 3\) ✓
- Thành phần bên phải (gốc 3): 3 đỉnh \(\leq 3\) ✓
Ý tưởng: Bắt đầu từ đỉnh bất kỳ, đi xuống "phía nặng nhất". Nếu đỉnh hiện tại có con với subsize \(> N/2\), đi vào con đó. Khi dừng lại → đó là centroid.
#include<bits/stdc++.h>usingnamespacestd;constintMAXN=100005;vector<int>adj[MAXN];intsubsize[MAXN];boolremoved[MAXN];intn;voiddfs_size(intu,intparent){subsize[u]=1;// Bước 1: mỗi nút tự đếm chính nófor(intv:adj[u]){if(v!=parent&&!removed[v]){// Bước 2: bỏ cha và nhánh đã táchdfs_size(v,u);// Bước 3: tính size con trướcsubsize[u]+=subsize[v];// Bước 4: cộng dồn vào u}}}intfind_centroid(intu,intparent,inttotal){for(intv:adj[u]){// Bước 5: nhánh nào quá nửa total thì centroid nằm trong đó -> đi tiếpif(v!=parent&&!removed[v]&&subsize[v]>total/2){returnfind_centroid(v,u,total);}}returnu;// Bước 6: không còn nhánh nặng -> u chính là centroid}intmain(){ios_base::sync_with_stdio(false);cin.tie(NULL);cin>>n;for(inti=0;i<n-1;i++){intu,v;cin>>u>>v;adj[u].push_back(v);adj[v].push_back(u);}dfs_size(1,0);cout<<find_centroid(1,0,subsize[1])<<"\n";return0;}
#include<bits/stdc++.h>usingnamespacestd;constintMAXN=100005;vector<int>adj[MAXN];intsubsize[MAXN];boolremoved[MAXN];intcentroid_parent[MAXN];intn;voiddfs_size(intu,intparent){subsize[u]=1;for(intv:adj[u])if(v!=parent&&!removed[v]){dfs_size(v,u);subsize[u]+=subsize[v];}}intfind_centroid(intu,intparent,inttotal){for(intv:adj[u])if(v!=parent&&!removed[v]&&subsize[v]>total/2)returnfind_centroid(v,u,total);returnu;}voiddecompose(intu,intparent_centroid){dfs_size(u,-1);// Bước 1: tính lại size cho thành phần hiện tạiintc=find_centroid(u,-1,subsize[u]);// Bước 2: tìm trọng tâm ccentroid_parent[c]=parent_centroid;// Bước 3: nối c vào centroid treeremoved[c]=true;// Bước 4: tách c khỏi cây gốcfor(intv:adj[c])// Bước 5: đệ quy từng nhánh còn lạiif(!removed[v])decompose(v,c);}intmain(){ios_base::sync_with_stdio(false);cin.tie(NULL);cin>>n;for(inti=0;i<n-1;i++){intu,v;cin>>u>>v;adj[u].push_back(v);adj[v].push_back(u);}decompose(1,-1);for(inti=1;i<=n;i++)cout<<i<<": parent = "<<centroid_parent[i]<<"\n";return0;}
#include<bits/stdc++.h>usingnamespacestd;constintMAXN=100005;vector<int>adj[MAXN];intsubsize[MAXN];boolremoved[MAXN];intn,k;longlongans=0;voiddfs_size(intu,intp){subsize[u]=1;for(intv:adj[u])if(v!=p&&!removed[v]){dfs_size(v,u);subsize[u]+=subsize[v];}}intfind_centroid(intu,intp,inttotal){for(intv:adj[u])if(v!=p&&!removed[v]&&subsize[v]>total/2)returnfind_centroid(v,u,total);returnu;}voidget_dists(intu,intp,intd,vector<int>&out){out.push_back(d);// Bước 1: ghi khoảng cách từ centroid tới ufor(intv:adj[u])// Bước 2: lan sang kề chưa táchif(v!=p&&!removed[v])get_dists(v,u,d+1,out);}voidcount_paths(intu){vector<int>total(n+1,0);// total[d] = số đỉnh cách centroid d ở nhánh đã duyệttotal[0]=1;// Bước 3: chính centroid cách chính nó 0for(intv:adj[u]){if(removed[v])continue;// Bước 4: bỏ nhánh đã táchvector<int>dists;get_dists(v,u,1,dists);// Bước 5: lấy mọi khoảng cách trong nhánh vfor(intd:dists)// Bước 6: ghép với nhánh cũ: d + (k-d) = kif(k-d>=0)ans+=total[k-d];for(intd:dists)total[d]++;// Bước 7: nhập nhánh v vào total}}voiddecompose(intu){dfs_size(u,-1);// Bước 8: tính size thành phầnintc=find_centroid(u,-1,subsize[u]);// Bước 9: tìm centroidcount_paths(c);// Bước 10: đếm mọi đường qua c trước khi táchremoved[c]=true;// Bước 11: tách c rồi đệ quyfor(intv:adj[c])if(!removed[v])decompose(v);}intmain(){ios_base::sync_with_stdio(false);cin.tie(NULL);cin>>n>>k;for(inti=0;i<n-1;i++){intu,v;cin>>u>>v;adj[u].push_back(v);adj[v].push_back(u);}decompose(1);cout<<ans<<"\n";return0;}
Giải thích: Tại mỗi centroid, total[d] đếm số đỉnh cách centroid \(d\) cạnh từ các subtree đã duyệt. Khi xét subtree mới, ta chỉ đếm đường đi giữa subtree mới và các subtree cũ → tránh đếm trùng.
#include<bits/stdc++.h>usingnamespacestd;constintMAXN=100005,LOG=17;vector<int>adj[MAXN];intsubsize[MAXN],depth[MAXN],up[MAXN][LOG],centroid_parent[MAXN];boolremoved[MAXN],colored[MAXN];multiset<int>color_dist[MAXN];intn;voiddfs_lca(intu,intp){up[u][0]=p;for(inti=1;i<LOG;i++)up[u][i]=up[u][i-1]==-1?-1:up[up[u][i-1]][i-1];for(intv:adj[u])if(v!=p){depth[v]=depth[u]+1;dfs_lca(v,u);}}intlca(intu,intv){if(depth[u]<depth[v])swap(u,v);for(inti=LOG-1;i>=0;i--)if(up[u][i]!=-1&&depth[up[u][i]]>=depth[v])u=up[u][i];if(u==v)returnu;for(inti=LOG-1;i>=0;i--)if(up[u][i]!=up[v][i]){u=up[u][i];v=up[v][i];}returnup[u][0];}intdist(intu,intv){returndepth[u]+depth[v]-2*depth[lca(u,v)];}voiddfs_size(intu,intp){subsize[u]=1;for(intv:adj[u])if(v!=p&&!removed[v]){dfs_size(v,u);subsize[u]+=subsize[v];}}intfind_centroid(intu,intp,inttot){for(intv:adj[u])if(v!=p&&!removed[v]&&subsize[v]>tot/2)returnfind_centroid(v,u,tot);returnu;}voiddecompose(intu,intpc){dfs_size(u,-1);intc=find_centroid(u,-1,subsize[u]);centroid_parent[c]=pc;removed[c]=true;for(intv:adj[c])if(!removed[v])decompose(v,c);}voidtoggle(intu){intcur=u;// Bước 1: leo từ u lên gốc centroid tree (O(log N) tầng)while(cur!=-1){intd=dist(u,cur);// Bước 2: khoảng cách trên cây gốc qua LCA// Bước 3: tô thì thêm d, xóa thì gỡ một bản sao d khỏi multisetif(colored[u])color_dist[cur].erase(color_dist[cur].find(d));elsecolor_dist[cur].insert(d);cur=centroid_parent[cur];// Bước 4: lên centroid cha}colored[u]=!colored[u];// Bước 5: đảo trạng thái màu}intquery(intu){intans=INT_MAX,cur=u;// Bước 6: đáp án nhỏ nhất qua mọi tổ tiên centroidwhile(cur!=-1){if(!color_dist[cur].empty())// Bước 7: đỉnh tô gần nhất trong cụm curans=min(ans,dist(u,cur)+*color_dist[cur].begin());cur=centroid_parent[cur];}returnans==INT_MAX?-1:ans;}intmain(){ios_base::sync_with_stdio(false);cin.tie(NULL);cin>>n;for(inti=0;i<n-1;i++){intu,v;cin>>u>>v;adj[u].push_back(v);adj[v].push_back(u);}depth[1]=0;memset(up,-1,sizeof(up));dfs_lca(1,-1);decompose(1,-1);intq;cin>>q;while(q--){intt,u;cin>>t>>u;if(t==1)toggle(u);elsecout<<query(u)<<"\n";}return0;}