Điểm phân chia (hay còn gọi là phân chia theo trọng tâm, centroid decomposition) rất phù hợp để xử lý các bài toán về thông tin đường đi trên cây quy mô lớn.
Cho một cây có \(n\) đỉnh, \(m\) truy vấn, mỗi truy vấn cho một số \(k\), hỏi có tồn tại một đường đi độ dài \(k\) hay không.
\(n\le 10000,m\le 100,k\le 10000000\)
Ta chọn một đỉnh bất kỳ làm gốc \(\mathit{rt}\), mọi đường đi hoàn toàn nằm trong cây con của nó có thể chia thành hai loại: loại đi qua gốc hiện tại và loại không đi qua gốc. Với các đường đi qua gốc, lại chia thành hai loại: một đầu là gốc, hoặc cả hai đầu không phải gốc (loại sau có thể ghép từ hai đường đi loại trước). Do đó, với mỗi gốc \(rt\), ta tính trước đóng góp của các đường đi qua \(rt\), sau đó đệ quy xử lý các cây con cho các đường đi không qua \(rt\).
Trong bài này, với các đường đi qua \(rt\), ta liệt kê từng con \(ch\) của \(rt\), tính khoảng cách từ mọi đỉnh trong cây con \(ch\) đến \(rt\). Gọi \(\mathit{dist}_i\) là khoảng cách từ \(i\) đến \(rt\), \(\mathit{tf}_d\) là mảng đánh dấu xem đã có đỉnh nào ở các cây con trước có khoảng cách \(d\) đến \(rt\) chưa. Nếu với truy vấn \(k\) có \(tf_{k-\mathit{dist}_i}=true\), tức là tồn tại một đường đi độ dài \(k\). Sau khi xử lý xong cây con \(ch\), cập nhật các giá trị mới vào \(\mathit{tf}\).
Khi xóa mảng \(\mathit{tf}\), không nên dùng memset, mà nên lưu lại các vị trí đã dùng vào một hàng đợi để xóa, đảm bảo đúng độ phức tạp.
Trong mỗi tầng của phân chia, tổng số lần xử lý mỗi đỉnh là \(1\), nếu tổng cộng đệ quy \(h\) tầng thì độ phức tạp \(O(hn)\).
Nếu mỗi lần chọn trọng tâm (centroid) làm gốc, số tầng tối đa là \(O(\log n)\), tổng độ phức tạp \(O(n\log n)\). Vì vậy, phương pháp này còn gọi là trọng tâm phân chia (centroid decomposition).
Lưu ý: mỗi lần chọn lại gốc phải tính lại kích thước cây con, nếu không sẽ sai độ phức tạp hoặc sai kết quả.
Một cây mỗi đỉnh được gán một màu, định nghĩa \(s(i,j)\) là số màu trên đường đi từ \(i\) đến \(j\), \(\mathit{sum_{i}}=\sum_{j=1}^n s(i,j)\).Đối với mọi \(1\leq i\leq n\),tìm \(sum_i\).(\(1 \le n, c_i \le 10^5\))
Bài này kiểm tra sâu về tư duy điểm phân chia, rất phù hợp luyện tập nâng cao.
Trước hết, cần chuyển đổi ý nghĩa của \(\mathit{sum_i}\). Nếu tính trực tiếp như đề, rất khó hợp nhất thông tin giữa các cây con. Ta chuyển sang xét đóng góp của từng màu \(j\) cho \(\mathit{sum_i}\), gọi \(\mathit{cnt_j}\) là số đường đi qua \(i\) có màu \(j\), khi đó \(\mathit{sum_i} = \sum \mathit{cnt_j}\). Để tính \(\mathit{cnt_j}\), chỉ cần mỗi khi gặp màu mới thì \(\mathit{cnt_{col_u}}+=\mathit{size_u}\), với \(\mathit{size_u}\) là kích thước cây con gốc \(u\).
Trong quá trình điểm phân chia, cần thống kê:
Đường đi có một đầu là gốc, đóng góp cho gốc.
Đường đi có LCA là gốc, đóng góp cho các đỉnh trong cây con.
Phần 1 dễ xử lý, vì mỗi tầng chỉ cần duyệt toàn bộ cây con, dùng định nghĩa \(\mathit{sum_i}\) để cộng dồn.
Với phần 2, giả sử gốc \(u\) có con \(d\), chọn \(v\) trong cây con \(d\). Khi đó, đáp án cho \(v\) gồm:
Số màu xuất hiện trên đường \((u, v)\), gọi là \(\mathit{num}\), nhân với tổng kích thước các cây con khác \(d\) của \(u\), gọi là \(\mathit{siz1}\), đóng góp là \(\mathit{num}\times \mathit{siz1}\).
Với màu \(j\) không xuất hiện trên \((u, v)\), cộng thêm tổng \(\mathit{cnt_j}\) của các cây con khác \(d\).
#include<algorithm>#include<iostream>usingnamespacestd;#define rep(i, a, b) for (int i = (a); i <= (b); ++i)constexprintN=200005;inth[N],nxt[N*2],to[N*2],c[N],gr;voidtu(intx,inty){to[++gr]=y,nxt[gr]=h[x],h[x]=gr;}usingll=longlong;intn,nn,siz[N],mn,rt;boolvis[N];voidget_root(intu,intf){siz[u]=1;intmx=0;for(inti=h[u];i;i=nxt[i]){intd=to[i];if(vis[d]||d==f)continue;get_root(d,u);siz[u]+=siz[d];mx=max(mx,siz[d]);}mx=max(mx,nn-siz[u]);if(mx<mn)mn=mx,rt=u;}llans[N],sum;intcnt[N],v[N];// sum实时统计的是cnt[i]的和intnowrt;voidget_dis(intu,intf,intnow){// now为当前树链上的颜色数量(不含u)siz[u]=1;if(!v[c[u]]){sum-=cnt[c[u]];// 减去在之前子树中已经出现过的颜色信息now++;}v[c[u]]++;ans[u]+=sum+now*siz[nowrt];// 统计过u点的路径对u的贡献for(inti=h[u];i;i=nxt[i]){intd=to[i];if(d==f||vis[d])continue;get_dis(d,u,now);siz[u]+=siz[d];}v[c[u]]--;if(!v[c[u]]){sum+=cnt[c[u]];// 回溯}}voidget_cnt(intu,intf){if(!v[c[u]]){cnt[c[u]]+=siz[u];sum+=siz[u];// 将刚遍历过的子树的信息整合到cnt[i]和sum上去}v[c[u]]++;for(inti=h[u];i;i=nxt[i]){intd=to[i];if(vis[d]||d==f)continue;get_cnt(d,u);}v[c[u]]--;}voidclear(intu,intf,intnow){if(!v[c[u]])now++;v[c[u]]++;ans[u]-=now;ans[nowrt]+=now;for(inti=h[u];i;i=nxt[i]){intd=to[i];if(vis[d]||d==f)continue;clear(d,u,now);}v[c[u]]--;cnt[c[u]]=0;}voidclear2(intu,intf){cnt[c[u]]=0;for(inti=h[u];i;i=nxt[i]){intd=to[i];if(vis[d]||d==f)continue;clear2(d,u);}}intson[N];voiddivid(intu){vis[u]=true;inttot=0;nowrt=u;ans[u]++;for(inti=h[u];i;i=nxt[i]){if(vis[to[i]])continue;son[++tot]=to[i];}siz[u]=sum=cnt[c[u]]=1;v[c[u]]++;rep(i,1,tot){// 统计每个子树和它之前的所有子树中节点组合产生的贡献intd=son[i];get_dis(d,u,0);get_cnt(d,u);siz[u]+=siz[d];cnt[c[u]]+=siz[d];sum+=siz[d];}clear2(u,0);// 清空数组,记得不可以用memsetsiz[u]=sum=cnt[c[u]]=1;for(inti=tot;i>=1;--i){// 统计每个子树和它之后的所有子树中节点组合产生的贡献intd=son[i];get_dis(d,u,0);get_cnt(d,u);siz[u]+=siz[d];cnt[c[u]]+=siz[d];sum+=siz[d];}v[c[u]]--;clear(u,0,0);// 清空的同时统计答案for(inti=h[u];i;i=nxt[i]){// 继续向下进行点分治intd=to[i];if(vis[d])continue;nn=siz[d],mn=n+1,rt=0;get_root(d,u);divid(rt);}}intmain(){cin.tie(nullptr)->sync_with_stdio(false);cin>>n;intu,v;rep(i,1,n)cin>>c[i];rep(i,2,n)cin>>u>>v,tu(u,v),tu(v,u);rt=0,nn=n,mn=n+1;get_root(1,0);divid(rt);rep(i,1,n)cout<<ans[i]<<'\n';return0;}
Phân chia theo cạnh (Edge Centroid Decomposition)
Tương tự điểm phân chia, nhưng chọn một cạnh để chia cây thành hai phần có kích thước gần nhau nhất, rồi đệ quy xử lý hai phần.
Tuy nhiên, cách này không hiệu quả với cây nhiều nhánh như cây sao:
Nếu một đỉnh có nhiều con kích thước gần nhau, phân chia theo cạnh sẽ rất tệ.
Nếu cây là nhị phân, sẽ tránh được vấn đề này. Ta có thể chuyển cây đa nhánh thành cây nhị phân như xây segment tree:
Các đỉnh mới gán thông tin phù hợp với bài toán. Ví dụ, khi tính độ dài đường đi, gán trọng số cạnh gốc là \(1\), cạnh mới là \(0\).
Tổng số đỉnh tăng tối đa \(O(n)\), nên độ phức tạp vẫn \(O(n\log n)\).
Hầu hết các bài điểm phân chia đều có thể giải bằng phân chia theo cạnh (thường hằng số lớn hơn, nhưng không bị "hack" nặng), nên không cần ví dụ riêng.
Cây phân chia (Centroid Tree)
Cây phân chia là cây được xây lại từ cây gốc bằng cách phân chia theo trọng tâm, sao cho chiều cao cây mới là \(O(\log n)\).
Thường dùng cho các bài toán có truy vấn cập nhật động, không phụ thuộc hình dạng cây gốc.
Phân tích thuật toán
Mỗi lần tìm trọng tâm, liên kết nó với trọng tâm tầng trước thành cha-con, tạo thành cây mới có tối đa \(\log n\) tầng.
Nhờ vậy, nhiều thuật toán brute-force trên cây gốc sẽ chạy đúng và nhanh trên cây phân chia.
Cài đặt
Một mẹo nhỏ: mỗi lần truyền tổng kích thước tầng trước trừ đi kích thước con nặng nhất, sẽ ra tổng kích thước tầng hiện tại. Như vậy chỉ cần một DFS để tìm trọng tâm.
Last updated on this page:, Update history Found an error? Want to help improve? Edit this page on GitHub! Contributors to this page:OI-wiki All content on this page is provided under the terms of the CC BY-SA 4.0 and SATA license, additional terms may apply