Day 084-Q1 — Offline LCA (Tarjan) + 木上距離クエリ一括処理 $O((N+Q)\alpha(N))$

2026-07-07 赤色 Master / Phase 8+ ★★★★★★★★★ Tarjan LCA・Union-Find・木上距離

問題

$N$ 頂点の根付き木(根は頂点 $1$)が与えられる。各辺には重み $w_i$ がある。$Q$ 個のクエリ $(u_i, v_i)$ が与えられ、各クエリで頂点 $u_i$ と $v_i$ の間の距離(辺重みの和)を答えよ。

距離は $\text{dist}(u, v) = \text{dep}[u] + \text{dep}[v] - 2 \cdot \text{dep}[\text{lca}(u,v)]$ で求められる($\text{dep}[v]$ は根から $v$ への辺重みの和)。Tarjan のオフライン LCA アルゴリズムを用いて全クエリを $O((N + Q) \alpha(N))$ で処理せよ。

制約

パラメータ範囲備考
$N$$\le 2 \times 10^5$頂点数
$Q$$\le 2 \times 10^5$クエリ数
$w_i$$0 \le w_i \le 10^9$辺重み

入出力例

入力例1

5 3
1 2 3
1 3 5
2 4 2
2 5 4
4 5
3 4
1 5

出力例1

6
10
9

概念図: Tarjan の Offline LCA アルゴリズム

Tarjan Offline LCA — DFS + Union-Find 1 root 2 3 4 5 3 5 2 4 dep=0 dep=3 dep=5 dep=5 dep=7 クエリ処理: dist(4, 5) の計算 lca(4,5) = 2 (DFS終了時に Union-Find で判定) dist(4,5) = dep[4] + dep[5] - 2×dep[2] = 5 + 7 - 2×3 = 6

ヒント

ヒント1(方向性)

距離クエリは LCA を使えば $\text{dist}(u,v) = \text{dep}[u] + \text{dep}[v] - 2 \cdot \text{dep}[\text{lca}(u,v)]$ で計算できる。Binary Lifting を使ったオンライン LCA は $O(N \log N)$ 前処理・$O(\log N)$ クエリだが、Tarjan のオフラインアルゴリズムなら全クエリを DFS 中に $O((N+Q)\alpha(N))$ で処理できる。

ヒント2(アプローチ)

Tarjan の LCA アルゴリズムの手順:(1) DFS で頂点を訪問し $\text{dep}[v]$ を計算 (2) 頂点 $v$ の DFS が終わったら Union-Find で $v$ を親にマージし「訪問済み」にマーク (3) クエリ $(v, u)$ で $u$ が既に訪問済みなら $\text{lca}(v, u) = \text{ancestor}[\text{find}(u)]$

ヒント3(ほぼ答え)
def tarjan():
    # DFS 終了時に Union-Find でマージ
    # ancestor[find(v)] = v で代表頂点を管理
    stk = [(v, par, False)]
    while stk:
        v, par, leaving = stk.pop()
        if leaving:
            visited[v] = True
            for u, qi in queries[v]:
                if visited[u]:
                    lca_res[qi] = ancestor[find(u)]
            if par != -1:
                union(v, par)
                ancestor[find(par)] = par
        else:
            stk.append((v, par, True))
            for u, w in graph[v]:
                if u != par:
                    stk.append((u, v, False))

模範解答

import sys
input = sys.stdin.readline

def solve():
    N, Q = map(int, input().split())
    graph = [[] for _ in range(N + 1)]
    for _ in range(N - 1):
        u, v, w = map(int, input().split())
        graph[u].append((v, w))
        graph[v].append((u, w))

    qs = []
    queries = [[] for _ in range(N + 1)]
    for i in range(Q):
        u, v = map(int, input().split())
        qs.append((u, v))
        queries[u].append((v, i))
        if u != v:
            queries[v].append((u, i))

    parent = list(range(N + 1))
    ancestor = list(range(N + 1))

    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x

    dep = [0] * (N + 1)
    visited = [False] * (N + 1)
    lca_res = [-1] * Q

    stk = [(1, -1, False)]
    while stk:
        v, par, leaving = stk.pop()
        if leaving:
            visited[v] = True
            for u, qi in queries[v]:
                if visited[u]:
                    lca_res[qi] = ancestor[find(u)]
            if par != -1:
                pv, pp = find(v), find(par)
                if pv != pp:
                    parent[pv] = pp
                ancestor[find(par)] = par
        else:
            stk.append((v, par, True))
            for u, w in graph[v]:
                if u != par:
                    dep[u] = dep[v] + w
                    stk.append((u, v, False))

    out = []
    for i, (u, v) in enumerate(qs):
        l = lca_res[i]
        out.append(dep[u] + dep[v] - 2 * dep[l])
    print('\n'.join(map(str, out)))

solve()

Step-by-Step 解説

Step 1: 距離クエリと LCA の関係

項目意味
$\text{dep}[v]$根から $v$ への辺重みの和
$\text{lca}(u,v)$$u$ と $v$ の最近共通祖先
$\text{dist}(u,v)$$\text{dep}[u] + \text{dep}[v] - 2\cdot\text{dep}[\text{lca}]$

Step 2: Tarjan Offline LCA の核心

DFS で頂点 $v$ を「離れる」タイミングで処理する。Union-Find で $v$ を親にマージし、ancestor[find(par)] = par で「現在の代表 = LCA 候補」を記録する。

# v の DFS 完了後
union(v, parent_of_v)
ancestor[find(parent_of_v)] = parent_of_v  # 親が代表

Step 3: 計算量

操作計算量
DFS$O(N)$
Union-Find 操作$O(\alpha(N))$ per query
合計$O((N+Q)\alpha(N))$

よくあるミス

ミス原因正しい書き方
ancestor 配列の更新忘れUnion-Find の代表 ≠ LCAancestor[find(par)] = par
再帰 DFS でスタックオーバーフロー$N=2\times10^5$ では Python 再帰は危険反復 DFS で実装
自己クエリ (u=v) の処理漏れ片側だけクエリ登録if u != v: で両側登録

次のステップ

  • 発展問題: オンライン LCA(Binary Lifting $O(N \log N)$ 前処理・$O(\log N)$ クエリ)
  • 発展問題: HLD + SegTree によるパス加算・最大値クエリ
  • 参考: Tarjan (1979) "Applications of path compression on balanced trees"

自己評価

理解度: / /

自分の回答:

気づき・メモ: