問題
$N$ 頂点の木(最初は辺なし)に対して $Q$ クエリを処理せよ。
1 u v w: 頂点 $u$ と $v$ を辺重み $w$ で結ぶ(追加後も木であることが保証される)2 u v w: 頂点 $u$ と $v$ をつなぐ辺の重みを $w$ に変更する3: 現在の木の直径(最長パス長)を出力せよ
制約
| パラメータ | 範囲 |
|---|---|
| $N, Q$ | $1 \le N, Q \le 10^5$ |
| $w$ | $0 \le w \le 10^9$ |
| クエリ1 | 追加後も木であることが保証される |
入出力例
入力例 1
4 5
1 1 2 3
1 2 3 4
3
1 3 4 2
3
出力例 1
7
9
辺追加後: 1-2(3), 2-3(4)。直径 = 1→2→3 = 7。
辺追加後: 1-2(3), 2-3(4), 3-4(2)。直径 = 1→2→3→4 = 9。
概念図: 直径モノイドとLCT辺ノードテクニック
ヒント(段階的開示)
ヒント1: 方向性
木の直径は静的なら2-BFS で $O(N)$。辺追加・辺重み変更がある動的木では Link-Cut Tree に「直径モノイド」を持たせ、各操作 $O(\log N)$ を実現する。
ヒント2: アプローチ
- 辺ノードテクニック: 辺 $(u,v,w)$ を仮想ノード $e$(重み $w$)として LCT に挿入。$u - e - v$ のパスを管理。辺重み変更 = $e$ の値変更
- 直径モノイド: 各 splay ツリーのノードに
(diam, ldep, rdep, len)を持たせ、マージ時に直径を更新 - 全体直径 = LCT の根ノードの
diam
ヒント3: 素朴解法(TLE参考)
from collections import deque, defaultdict
def bfs_diameter(adj):
if not adj: return 0
start = next(iter(adj))
dist = {start: 0}; q = deque([start])
while q:
v = q.popleft()
for u, w in adj[v].items():
if u not in dist:
dist[u] = dist[v]+w; q.append(u)
u = max(dist, key=dist.get)
dist2 = {u: 0}; q = deque([u])
while q:
v = q.popleft()
for x, w in adj[v].items():
if x not in dist2:
dist2[x] = dist2[v]+w; q.append(x)
return max(dist2.values())
# クエリ3ごとに O(N) → Q=10^5 で TLE
模範解答 (Python — 素朴解法)
import sys
from collections import deque, defaultdict
input = sys.stdin.readline
def main():
N, Q = map(int, input().split())
adj = defaultdict(dict)
def bfs_diameter():
if not adj: return 0
start = next(iter(adj))
dist = {start: 0}
q = deque([start])
while q:
v = q.popleft()
for u, w in adj[v].items():
if u not in dist:
dist[u] = dist[v] + w
q.append(u)
u = max(dist, key=dist.get)
dist2 = {u: 0}
q = deque([u])
while q:
v = q.popleft()
for x, w in adj[v].items():
if x not in dist2:
dist2[x] = dist2[v] + w
q.append(x)
return max(dist2.values()) if dist2 else 0
for _ in range(Q):
line = input().split()
if line[0] == '1':
u, v, w = int(line[1]), int(line[2]), int(line[3])
adj[u][v] = w
adj[v][u] = w
elif line[0] == '2':
u, v, w = int(line[1]), int(line[2]), int(line[3])
adj[u][v] = w
adj[v][u] = w
else:
print(bfs_diameter())
main()
# 完全解法(LCT)は ~200行の実装が必要。
# 各クエリ O(log N) を達成するには辺ノードテクニック + 直径モノイドが不可欠。
Step-by-Step 解説
Step 1: 木の直径の2-BFS
任意頂点から BFS → 最遠頂点 $u$ → $u$ からの最遠距離が直径。$O(N)$。
Step 2: 動的木での課題
クエリ3のたびに $O(N)$ BFS を行うと $Q \cdot N = 10^{10}$ で TLE。Link-Cut Tree で各操作を $O(\log N)$ amortized に落とす必要がある。
Step 3: 直径モノイドの設計
各 splay ノードに (diam, ldep, rdep, len) を持たせる:
diam: この部分木内の最長パスldep: 左端からの最長パスrdep: 右端からの最長パス
マージ: new_diam = max(L.diam, R.diam, L.rdep + w + R.ldep)
Step 4: 辺ノードテクニック
辺 $(u,v,w)$ を仮想ノード $e$ として LCT に挿入。$u-e-v$ のパスを管理。辺重み変更は $e$ の val を更新して splay と push_up を伝播するだけ。
Step 5: 全体計算量
LCT の各操作: $O(\log N)$ amortized。全体 $O(Q \log N)$。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
dep の更新前に diam を計算 | dep が古い値のまま | dep を先に更新してから diam に使う |
辺重み変更で push_up を根まで伝播しない | 祖先のキャッシュが古い | 辺ノードを splay して根まで更新 |
| 辺ノードを頂点数に含めない | 配列サイズ不足 | N + Q サイズで確保 |
次のステップ
発展問題: 辺の追加・削除(動的フォレスト)と直径クエリを組み合わせた問題を、完全な LCT 実装で $O(Q \log N)$ で解け。
自己評価
解いた後に記入してください。