樹上的路徑問題很難直接用陣列或線段樹處理;重心分解把「隨便兩點之間的路徑」變成「一定經過某個特定節點的路徑」。

樹的重心

移除某個節點後,剩下的每個連通子樹大小都 ≤ n/2——這個節點就是樹的重心。

     1
    / \
   2   3
  / \   \
 4   5   6

n = 6,移除節點 1:
  子樹 {2,4,5}(大小 3)、{3,6}(大小 2)
  最大子樹 3 ≤ 6/2 = 3 ✓ → 節點 1 是重心

重心的保證:每棵樹至少有一個重心,且找重心只需 O(n)(先算子樹大小,找滿足條件的節點)。

分解過程

1. 找當前樹的重心 c
2. 統計「所有經過 c 的路徑」
3. 移除 c(標記 removed[c] = true)
4. 對剩下的每個子樹遞迴重複

遞迴深度:每次移除重心後,每個子樹大小 ≤ n/2
→ 至多 log n 層,每層 O(n),總 O(n log n)

路徑統計(長度 ≤ k 的路徑數量)

long countPaths(int root, int k) {
    int c = findCentroid(root, subtreeSize[root]);
    removed[c] = true;
 
    List<Long> allDist = new ArrayList<>();
    allDist.add(0L); // 從 c 出發的路徑(c 本身)
 
    long count = 0;
    for (int child : adj[c]) {
        if (removed[child]) continue;
        List<Long> childDist = getDistances(child, c, 1);
 
        // 統計「一端在 child 子樹,一端在已處理子樹」且距離 ≤ k 的路徑
        count += countPairs(allDist, childDist, k);
        allDist.addAll(childDist);
    }
 
    // 遞迴處理每個子樹
    for (int child : adj[c])
        if (!removed[child])
            count += countPaths(child, k);
 
    return count;
}

countPairs(A, B, k):統計 a∈A, b∈B 且 a+b ≤ k 的對數。排序 A,對 B 中每個 b 二分搜尋,O((|A|+|B|) log n)。

容斥補正:直接統計 allDist 會算到「兩端都在同一個子樹」的路徑,通過分批統計(先統計、再加入 childDist)避免重複。

重心分解樹(Centroid Tree)

把每個重心作為父節點,子節點是它移除後各子樹的重心,形成「重心分解樹」。

重心分解樹高度 O(log n),任意節點到根路徑上的節點集合代表「原樹中包含這個節點的所有重心」——對每個重心做統計,等效於覆蓋所有路徑。


重心分解的直覺是:任何兩點路徑都必須經過某一層的重心——找到這一層,問題就分解了。