問題
$N$ 頂点からなる森(最初は辺なし)に対して $Q$ 個のクエリを処理せよ。1 u v: 辺追加(異なる成分保証)、2 u v: 辺削除(存在保証)、3 u v: 連結性判定。
制約
| パラメータ | 範囲 | 備考 |
|---|---|---|
| $N$ | $1 \le N \le 10^4$ | 頂点数 |
| $Q$ | $1 \le Q \le 10^4$ | クエリ数 |
入出力例
入力例1
5 6
1 0 1
1 1 2
3 0 2
2 1 2
3 0 2
3 1 4
出力例1
Yes
No
No
概念図: Euler ツアーを列として管理し reroot で回転
ヒント
ヒント1(方向性)
Union-Find は辺の削除に対応できない。動的に辺を追加・削除しながら連結性を判定するには木構造を平衡二分探索木で表現し、部分木の付け替えを $O(\log N)$ で行える構造が必要。
ヒント2(アプローチ)
木の Euler ツアー(各辺を行き帰り2回通る訪問順)を Splay木上の1本の列として表現する。頂点 $v$ を先頭にする「reroot」操作と列の split/merge があれば、辺の追加・削除・連結性判定がすべて $O(\log N)$ amortized で実現できる。
ヒント3(ほぼ答え)
def reroot(v):
splay(v)
L = v.left
if L:
L.parent = None
v.left = None
return merge(v, L)
模範解答
import sys
class Node:
__slots__ = ('key', 'parent', 'left', 'right')
def __init__(self, key):
self.key = key
self.parent = None
self.left = None
self.right = None
def rotate(x):
p = x.parent
g = p.parent
if p.left is x:
p.left = x.right
if x.right: x.right.parent = p
x.right = p
else:
p.right = x.left
if x.left: x.left.parent = p
x.left = p
p.parent = x
x.parent = g
if g:
if g.left is p: g.left = x
else: g.right = x
def splay(x):
while x.parent:
p = x.parent
g = p.parent
if g:
rotate(p if (g.left is p) == (p.left is x) else x)
rotate(x)
return x
def merge(a, b):
if a is None: return b
if b is None: return a
x = a
while x.right:
x = x.right
splay(x)
x.right = b
b.parent = x
return x
def reroot(v):
splay(v)
L = v.left
if L:
L.parent = None
v.left = None
return merge(v, L)
def connected(u, v):
splay(u)
cur = v
while cur.parent is not None:
cur = cur.parent
return cur is u
def link(u, v, tok):
u_root = reroot(u)
v_root = reroot(v)
e1 = Node(('e', tok, 0))
e2 = Node(('e', tok, 1))
t = merge(u_root, e1)
t = merge(t, v_root)
t = merge(t, e2)
return e1, e2
def cut(e1, e2):
splay(e1)
cur = e2
while cur.parent is not e1:
cur = cur.parent
if cur is e1.left:
e_before, e_after = e2, e1
else:
e_before, e_after = e1, e2
splay(e_before)
A, Rest = e_before.left, e_before.right
if A: A.parent = None
if Rest: Rest.parent = None
splay(e_after)
M, C = e_after.left, e_after.right
if M: M.parent = None
if C: C.parent = None
merge(A, C)
return M
def solve():
data = sys.stdin.read().split()
idx = 0
n = int(data[idx]); idx += 1
q = int(data[idx]); idx += 1
vnode = [Node(('v', i)) for i in range(n)]
edge_tok = {}
out = []
for _ in range(q):
typ = data[idx]; idx += 1
u = int(data[idx]); idx += 1
v = int(data[idx]); idx += 1
if typ == '1':
e1, e2 = link(vnode[u], vnode[v], (u, v))
edge_tok[(u, v)] = (e1, e2)
edge_tok[(v, u)] = (e1, e2)
elif typ == '2':
e1, e2 = edge_tok.pop((u, v))
edge_tok.pop((v, u), None)
cut(e1, e2)
else:
out.append("Yes" if connected(vnode[u], vnode[v]) else "No")
print('\n'.join(out))
solve()
計算量: Splay木ベースのため各操作 amortized $O(\log N)$。全体 $O((N+Q)\log N)$。
Step-by-Step 解説
Step 1: Euler ツアーを列として表現
頂点・辺トークンをノードオブジェクトとして直接保持することで、「頂点 $v$ の位置」を $O(1)$ でノード参照から取得できる。
Step 2: reroot(根の付け替え)
$v$ を splay して根にし、左部分木を切り離して merge(v以降, 前半) とすることで列を回転させる。
Step 3: link と cut
link は reroot 後に「$u$の列 → 辺トークン → $v$の列 → 辺トークン」の順に連結。cut は先に現れる辺トークンを splay して右部分木を切り離し、もう一方でさらに分割する。
Step 4: connected
$u$ を splay して根にし、$v$ から親をたどって到達するノードが $u$ 自身かどうかで判定する。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| cut で辺トークンの前後関係を固定と仮定 | reroot の繰り返しで順序は動的に変わる | splay 後に実際の前後関係を毎回判定 |
| Union-Find で代用しようとする | 辺削除に対応できない | Euler Tour Tree / Link-Cut Tree を使う |
| 頂点ノードと辺トークンを混同 | 辺トークンは列を分割するための印 | 2種類のノードを明確に区別 |
次のステップ
- 発展問題: 部分木の総和・最小値など集約情報を載せる
- 比較学習: Link-Cut Tree との使い分け(LCT はパス、ETT は部分木・連結性)
自己評価
理解度: / /
自分の回答:
気づき・メモ: