問題
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を構築。
オイラーツアーと二項ジャンプで O(N log N) 前処理、O(log N) クエリのLCAを構築。
2仮想木ノード集合の決定
クエリノードをDFS順ソート。隣接ノード間のLCAを追加することで、仮想木に必要な全ノードが確定。
クエリノードをDFS順ソート。隣接ノード間のLCAを追加することで、仮想木に必要な全ノードが確定。
3スタックで仮想木を構築
DFS順に処理しながら、スタックで現在のパスを管理。LCAより深いノードをポップしながら辺を張る。
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問題への応用