Day 018-Q1 — 仮想木(Virtual Tree)

2026-05-01 赤色 Master / Phase 8+ ★★★★★★★★★ Virtual Tree / Auxiliary Tree

問題

N頂点の根付き木(根は頂点1)が与えられる。各辺には重みがある。Q個のクエリでK個の頂点集合 S が指定される。S の全頂点のLCAの関係を保ちつつ、S に含まれない頂点を圧縮した「仮想木」を構築し、仮想木上での辺重み総和を求めよ。

制約

$N, Q \le 2 \times 10^5$
各クエリKの合計 $\le 2 \times 10^5$
$1 \le w \le 10^9$

入出力例

入力例 1

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

出力例 1

15
11

ヒント (段階的開示)

ヒント1: 方向性
仮想木とはクエリ頂点とそれらのLCAのみを残した圧縮木。オイラーツアー上でソートして構築する。
ヒント2: アプローチ
1. オイラーツアー(in-order)でクエリ頂点をDFS順ソート。2. スタックで仮想木の辺を構築(隣接するLCAを順次追加)。3. 仮想木上の辺重みは元の木での距離。
ヒント3: 誘導
nodes = sorted(set(nodes), key=lambda x: euler_in[x])
extras = [lca(nodes[i], nodes[i+1]) for i in range(len(nodes)-1)]
all_nodes = sorted(set(nodes + extras), key=lambda x: euler_in[x])
# スタックで仮想木構築

模範解答 (Python)

import sys
from collections import defaultdict
input = sys.stdin.readline

def main():
    sys.setrecursionlimit(300000)
    N = int(input())
    graph = defaultdict(list)
    for _ in range(N-1):
        u, v, w = map(int, input().split())
        u -= 1; v -= 1
        graph[u].append((v, w))
        graph[v].append((u, w))

    LOG = 18
    parent = [[-1]*N for _ in range(LOG)]
    depth = [0]*N
    dist = [0]*N
    euler_in = [0]*N
    euler_out = [0]*N
    timer = [0]

    stack = [(0, -1, 0, 0, False)]
    while stack:
        v, p, d, dd, returning = stack.pop()
        if returning:
            euler_out[v] = timer[0]; timer[0] += 1
            continue
        parent[0][v] = p if p != -1 else v
        depth[v] = d; dist[v] = dd
        euler_in[v] = timer[0]; timer[0] += 1
        stack.append((v, p, d, dd, True))
        for u, w in graph[v]:
            if u != p:
                stack.append((u, v, d+1, dd+w, False))

    for k in range(1, LOG):
        for v in range(N):
            parent[k][v] = parent[k-1][parent[k-1][v]]

    def lca(u, v):
        if depth[u] < depth[v]: u, v = v, u
        diff = depth[u] - depth[v]
        for k in range(LOG):
            if (diff >> k) & 1: u = parent[k][u]
        if u == v: return u
        for k in range(LOG-1, -1, -1):
            if parent[k][u] != parent[k][v]:
                u = parent[k][u]; v = parent[k][v]
        return parent[0][u]

    def edge_dist(u, v):
        l = lca(u, v)
        return dist[u] + dist[v] - 2*dist[l]

    Q = int(input())
    for _ in range(Q):
        line = list(map(int, input().split()))
        K = line[0]
        nodes = [x-1 for x in line[1:K+1]]
        nodes_sorted = sorted(set(nodes), key=lambda x: euler_in[x])
        to_add = []
        for i in range(len(nodes_sorted)-1):
            to_add.append(lca(nodes_sorted[i], nodes_sorted[i+1]))
        all_nodes = sorted(set(nodes_sorted + to_add), key=lambda x: euler_in[x])

        vtree_edges = []
        stack2 = []
        for v in all_nodes:
            if not stack2:
                stack2.append(v)
            else:
                l = lca(v, stack2[-1])
                if l != stack2[-1]:
                    while len(stack2) >= 2 and depth[stack2[-2]] >= depth[l]:
                        vtree_edges.append((stack2[-2], stack2[-1]))
                        stack2.pop()
                    if stack2[-1] != l:
                        vtree_edges.append((l, stack2[-1]))
                        stack2.pop()
                        stack2.append(l)
                stack2.append(v)
        while len(stack2) >= 2:
            vtree_edges.append((stack2[-2], stack2[-1]))
            stack2.pop()

        ans = sum(edge_dist(u, v) for u, v in vtree_edges)
        print(ans)

main()

Step-by-Step 解説

1LCAの前処理
オイラーツアーと二項ジャンプで O(N log N) 前処理、O(log N) クエリのLCAを構築。
2仮想木ノード集合の決定
クエリノードをDFS順ソート。隣接ノード間のLCAを追加することで、仮想木に必要な全ノードが確定。
3スタックで仮想木を構築
DFS順に処理しながら、スタックで現在のパスを管理。LCAより深いノードをポップしながら辺を張る。
4辺重みの計算
仮想木の各辺は元の木上の距離(dist[u] + dist[v] - 2*dist[lca(u,v)])で計算。

よくあるミス

ミス原因正しい書き方
LCAを追加しない仮想木が不完全になる隣接ノード間のLCAを必ず追加
euler_inだけでLCA判定ancestor判定が不正確euler_in/outの両方で判定
同じノードの重複set()で重複除去が必要set(nodes_sorted + to_add)

次のステップ

  • 発展: 仮想木上でのDPを実装(仮想木の辺を使った木DP)
  • 応用: Steiner Tree問題への応用

自己評価

自分の回答

気づき・メモ