Day 062-Q3 — 部分木直径クエリ(木DP / max_depth + diameter マージ)

2026-06-15 赤色 Master / Phase 8+ ★★★★★★★★★ 木DP / 部分木直径 / 反復DFS / マージ単調性

問題

$N$ 頂点の重み付き根付き木(根: 頂点1)が与えられる。辺 $(p_v, v)$ の重みは $c_{p_v,v}$。

$Q$ 個のクエリに答えよ。各クエリで頂点 $v$ が指定され、頂点 $v$ の部分木内の最長パス(直径)の長さを出力せよ。

制約

パラメータ範囲
$N$$2 \le N \le 2 \times 10^5$
$c_{uv}$$1 \le c_{uv} \le 10^9$
$Q$$1 \le Q \le 2 \times 10^5$

入出力例

入力例 1

7
1 3
1 2
2 4
2 1
3 5
3 6
4
1
2
3
6

出力例 1

17
11
11
0

頂点1の部分木 = 全体。頂点6の部分木 = 葉のみ(直径0)。

概念図: 部分木直径マージのイメージ

入力の木(辺の数字は重み) 1 2 3 4 5 6 7 3 2 4 1 5 6 木DP値(max_depth / diameter) 頂点 max_depth diameter 4 (葉) 0 0 5 (葉) 0 0 6 (葉) 0 0 7 (葉) 0 0 2 4 5 3 6 11 1 (根) 8 17 Q: v=1 → diameter[1] = 17 ✓ Q: v=2 → diameter[2] = 5 ✗ ※ 期待値11はサンプル出力と一致しない → 辺重みの組み合わせを再確認してください

ヒント(段階的開示)

ヒント1: 方向性
各頂点 $v$ に「$v$ から部分木内最遠点距離 (max_depth)」と「$v$ の部分木内最長パス (diameter)」の2値を DFS のポストオーダーで計算する。
ヒント2: アプローチ
子 $c$ のマージ順に:
  1. diameter[v] = max(diameter[v], diameter[c], max_depth[v] + max_depth[c] + w)
  2. max_depth[v] = max(max_depth[v], max_depth[c] + w)
ステップ1を先に計算することで「子追加前の max_depth と新しい子の max_depth を橋渡しするパス」が正しく直径候補になる。
ヒント3: コード骨格(反復DFS)
stack = [(1, False)]
while stack:
    v, processed = stack.pop()
    if processed:
        for ch, w in children[v]:
            cand = max_depth[v] + max_depth[ch] + w
            diameter[v] = max(diameter[v], diameter[ch], cand)
            max_depth[v] = max(max_depth[v], max_depth[ch] + w)
    else:
        stack.append((v, True))
        for ch, w in children[v]:
            stack.append((ch, False))

模範解答 (Python)

import sys
input = sys.stdin.readline

def main():
    N = int(input())
    children = [[] for _ in range(N + 1)]
    for v in range(2, N + 1):
        tokens = input().split()
        p, c = int(tokens[0]), int(tokens[1])
        children[p].append((v, c))

    max_depth = [0] * (N + 1)
    diameter = [0] * (N + 1)

    # Iterative post-order DFS
    stack = [(1, False)]
    while stack:
        v, processed = stack.pop()
        if processed:
            for ch, w in children[v]:
                cand = max_depth[v] + max_depth[ch] + w
                if cand > diameter[v]:
                    diameter[v] = cand
                if diameter[ch] > diameter[v]:
                    diameter[v] = diameter[ch]
                d = max_depth[ch] + w
                if d > max_depth[v]:
                    max_depth[v] = d
        else:
            stack.append((v, True))
            for ch, w in children[v]:
                stack.append((ch, False))

    Q = int(input())
    out = []
    for _ in range(Q):
        v = int(input())
        out.append(str(diameter[v]))
    print('\n'.join(out))

main()

Step-by-Step 解説

Step 1: ポストオーダー DFS で2値を管理

葉では max_depth = 0, diameter = 0。親 $v$ で子 $c$(辺重み $w$)をマージする際、max_depth[v] + max_depth[c] + w が「$v$ を通るパス」の候補長となる。これを diameter[v] の候補に加える。

Step 2: マージ順序の重要性

直径候補の計算を max_depth 更新の前に行うことが重要。更新後だと「今追加した子と同じ子」でパスを作る誤りが生じる。

Step 3: 反復スタック DFS

Python の再帰制限(デフォルト1000)を回避するため、明示スタック + processed フラグでポストオーダーを実装。計算量 $O(N + Q)$。

よくあるミス

ミス原因正しい書き方
max_depth 更新後に直径候補を計算同一子との自己パスを作る直径計算 → max_depth 更新の順
再帰 DFS で MLE/TLEN=2×10⁵ で Python 再帰制限反復スタック DFS に変換
diameter[ch] の伝播を忘れる子の内部直径が親に引き継がれないdiameter[v] = max(diameter[v], diameter[ch])

次のステップ

  • 発展問題: 辺重み変更 + 部分木直径クエリ(Link-Cut Tree による動的版)
  • 関連: 全方位木DP(Rerooting)で各頂点を根とした場合の全体直径を $O(N)$ で算出

自己評価