Day 104-Q2 — 重心分解(Centroid Decomposition)

2026-07-27 赤色 Master / Phase 8+ ★★★★★★★★☆ 距離K以下の頂点対数え上げ

問題

$N$頂点の木が与えられる。辺$i$は頂点$u_i,v_i$を結び重み$w_i$を持つ。2頂点$u,v$の距離$\mathrm{dist}(u,v)$をパス上の辺重みの総和とする。整数$K$が与えられるので、$\mathrm{dist}(u,v)\le K$を満たす頂点対$(u,v)$($u

重心分解は、木を「取り除くと残りの各連結成分のサイズが元の半分以下になる頂点=重心」で分割し、「重心を経由するパス」をまとめて処理してから残りの部分木に再帰する分割統治法。深さは$O(\log N)$で、全体$O(N\log^2 N)$で全パスの統計を計算できる。

入力形式

N K
u_1 v_1 w_1
...
u_{N-1} v_{N-1} w_{N-1}

制約

$2 \le N \le 2\times10^5$
$1 \le K \le 10^{14}$
$1 \le w_i \le 10^9$
与えられるグラフは木

入出力例

入力例1

4 3
1 2 1
2 3 2
3 4 3

出力例1

4

距離は $d(1,2)=1,d(1,3)=3,d(1,4)=6,d(2,3)=2,d(2,4)=5,d(3,4)=3$。3以下は$(1,2),(1,3),(2,3),(3,4)$の4組。

概念図: 重心を通るパスの数え上げ

木 1-2-3-4(重み1,2,3)・重心を c=2 とする w=1 w=2 w=3 1 c=2 3 4 d(c,1)=1 d(c,c)=0 d(c,3)=2 d(c,4)=5 距離をソート: [0, 1, 2, 5] (K=3, two-pointerで走査) 0 1 2 5 i→ ←j 0+5=5>3 → j-- / 0+2=2≤3 → (0,1),(0,2)を採用・i++ / 1+2=3≤3 → (1,2)採用 この1回のcentroidパスで (1,2),(1,3),(2,3) の3組を発見 残りの (3,4) は c=2 を削除した後、部分木 {3,4} への再帰で発見される

ヒント(段階的開示)

ヒント1: 方向性
木上のすべてのパスは、適切な1頂点で分割すれば「その頂点を通るパス」と「それを含まないより小さい部分木内で完結するパス」に分けられる。この頂点として重心を選ぶと、部分木サイズが毎回半分以下になり再帰の深さが$O(\log N)$に収まる。
ヒント2: アプローチ
重心$c$を削除し、$c$から各頂点への距離をDFSで求める。ソートしてtwo-pointerで「距離の和が$K$以下」のペア数を数える。ただしこれは同じ子部分木内のペアまで含んでしまうので、各子部分木ごとに同じ計算をして引き算する。その後$c$を除いた各部分木に再帰する。
ヒント3: 誘導(コード骨格)
def count_pairs_le_k(dists, K):
    dists.sort()
    i, j = 0, len(dists) - 1
    cnt = 0
    while i < j:
        if dists[i] + dists[j] <= K:
            cnt += j - i
            i += 1
        else:
            j -= 1
    return cnt

def decompose(v):
    c = 重心(v)
    削除(c)
    all_dists = [0] + (cから各部分木への距離すべて)
    ans += count_pairs_le_k(all_dists, K)
    for 各子部分木 sub:
        ans -= count_pairs_le_k(sub の距離リスト, K)
    for 各子部分木 sub:
        decompose(sub)

模範解答 (Python)

import sys


def solve():
    data = sys.stdin.buffer.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    k = int(data[idx]); idx += 1
    graph = [[] for _ in range(n + 1)]
    for _ in range(n - 1):
        u = int(data[idx]); v = int(data[idx + 1]); w = int(data[idx + 2])
        idx += 3
        graph[u].append((v, w))
        graph[v].append((u, w))

    sys.setrecursionlimit(500000)

    removed = [False] * (n + 1)
    subtree_size = [0] * (n + 1)

    def calc_size(v, parent):
        subtree_size[v] = 1
        for to, w in graph[v]:
            if to != parent and not removed[to]:
                calc_size(to, v)
                subtree_size[v] += subtree_size[to]
        return subtree_size[v]

    def find_centroid(v, parent, tree_size):
        for to, w in graph[v]:
            if to != parent and not removed[to] and subtree_size[to] > tree_size // 2:
                return find_centroid(to, v, tree_size)
        return v

    def collect_dists(v, parent, dist, out):
        out.append(dist)
        for to, w in graph[v]:
            if to != parent and not removed[to]:
                collect_dists(to, v, dist + w, out)

    def count_pairs_le_k(dists):
        dists.sort()
        i, j = 0, len(dists) - 1
        cnt = 0
        while i < j:
            if dists[i] + dists[j] <= k:
                cnt += j - i
                i += 1
            else:
                j -= 1
        return cnt

    ans = 0

    def decompose(v):
        nonlocal ans
        tree_size = calc_size(v, -1)
        centroid = find_centroid(v, -1, tree_size)
        removed[centroid] = True

        neighbors = [(to, w) for to, w in graph[centroid] if not removed[to]]

        all_dists = [0]
        child_dist_lists = []
        for to, w in neighbors:
            sub = []
            collect_dists(to, centroid, w, sub)
            child_dist_lists.append(sub)
            all_dists.extend(sub)

        ans += count_pairs_le_k(all_dists)
        for sub in child_dist_lists:
            ans -= count_pairs_le_k(sub)

        for to, w in neighbors:
            decompose(to)

    decompose(1)
    print(ans)


solve()
計算量: $O(N\log^2 N)$(重心分解 $O(\log N)$ 層 × 各層 $O(N\log N)$ のソート)。ランダム木300ケースで愚直全点対計算と一致することを確認済み。

Step-by-Step 解説

1重心を求める
DFSで部分木サイズを求め(`calc_size`)、「取り除くとどの方向の連結成分もサイズが半分以下」になる頂点を`find_centroid`で見つける。
2重心を通るパスをまとめて数える
重心$c$から到達可能な全頂点への距離を集め($c$自身は距離0)、ソートしてtwo-pointerで距離の和が$K$以下のペア数を数える。
3同一部分木内の過大カウントを補正
$c$の各子部分木ごとに同じ計算をして、$c$を経由しない同一部分木内ペアの分を引く。
4再帰
$c$を削除済みにし、各子部分木に同じ手順を再帰適用。重心分解の性質上、深さは$O(\log N)$。

よくあるミス

ミス原因正しい書き方
重心を求める際に古いサイズを使い続ける再帰のたびにその連結成分でサイズ再計算が必要`decompose(v)`の最初に必ず`calc_size`をやり直す
同じ部分木内のペアを引き算し忘れる重心を通らないペアまで過大カウントする子部分木ごとに`count_pairs_le_k`を計算し全体から引く
`removed`フラグの更新を忘れる削除済み重心を再訪問し二重カウント・無限ループの原因重心を選んだら即座に`removed[centroid]=True`
two-pointerで`j-i`個まとめて数えるのを忘れる1組ずつ+1しようとして計算量が悪化・実装も煩雑`dists[i]+dists[j]<=K`成立時は`j-i`個すべて条件を満たすことを利用

次のステップ

  • 発展: 「距離がちょうど$K$」のパスを数える古典問題(Codeforces 161D相当)との関係を確認する
  • 発展: 頂点重みを付け「距離が$K$以下のペアの重みの積の総和」など集計内容を一般化する
  • 次回予告: 線形漸化式の高速計算(行列累乗・Companion Matrix)— $N\le10^{18}$項目を高速に求める

自己評価

自分の回答

気づき・メモ