Day 023-Q5 — オフライン動的連結性 (Segment Tree on Time + Undo DSU)

2026-05-06 赤色 Master / Phase 8+ ★★★★★★★★★ 分割統治 / Undo DSU

問題

$N$ 頂点グラフに $Q$ クエリ:

  • add u v — 辺を追加
  • del u v — 辺を削除(存在保証)
  • query u v — 連結か答える(Yes/No)

制約

$1 \le N \le 3 \times 10^5$
$1 \le Q \le 3 \times 10^5$
多重辺あり可能

入出力例

入力例 1

4 6
add 1 2
add 2 3
query 1 3
del 2 3
query 1 3
query 3 4

出力例 1

Yes
No
No

ヒント (段階的開示)

ヒント1: 方向性
オフライン処理なら「Offline Dynamic Connectivity (分割統治 + Undo DSU)」で $O(Q \log Q \cdot \alpha(N))$ で解ける。
ヒント2: アプローチ
各辺を「存在する時間区間 $[l, r)$」に変換。時間軸セグメント木の各ノードに辺を登録。DFS 中に Union/Undo を行う。
ヒント3: 誘導
Path Compression を使わず Union by Rank のみ。変更を履歴に push し、再帰の戻りで pop & 復元。

模範解答 (Python)

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

def solve():
    N, Q = map(int, input().split())
    queries = []
    for _ in range(Q):
        parts = input().split()
        t, u, v = parts[0], int(parts[1]), int(parts[2])
        u -= 1; v -= 1
        if u > v:
            u, v = v, u
        queries.append((t, u, v))

    edge_last_add = {}
    edge_intervals = defaultdict(list)
    for i, (t, u, v) in enumerate(queries):
        key = (u, v)
        if t == 'add':
            edge_last_add.setdefault(key, []).append(i)
        elif t == 'del':
            l = edge_last_add[key].pop()
            edge_intervals[key].append((l, i))
    for key, stack in edge_last_add.items():
        for l in stack:
            edge_intervals[key].append((l, Q))

    class UndoDSU:
        def __init__(self, n):
            self.parent = list(range(n))
            self.rank = [0] * n
            self.history = []
        def find(self, x):
            while self.parent[x] != x:
                x = self.parent[x]
            return x
        def union(self, x, y):
            x, y = self.find(x), self.find(y)
            if x == y:
                self.history.append(None)
                return
            if self.rank[x] < self.rank[y]:
                x, y = y, x
            self.history.append((y, self.parent[y], x, self.rank[x]))
            self.parent[y] = x
            if self.rank[x] == self.rank[y]:
                self.rank[x] += 1
        def undo(self):
            op = self.history.pop()
            if op is None:
                return
            y, old_parent_y, x, old_rank_x = op
            self.parent[y] = old_parent_y
            self.rank[x] = old_rank_x
        def connected(self, x, y):
            return self.find(x) == self.find(y)

    seg_size = 1
    while seg_size < Q:
        seg_size <<= 1
    seg = [[] for _ in range(2 * seg_size)]

    def seg_add(l, r, val, node=1, lo=0, hi=None):
        if hi is None:
            hi = seg_size
        if r <= lo or hi <= l:
            return
        if l <= lo and hi <= r:
            seg[node].append(val)
            return
        mid = (lo + hi) // 2
        seg_add(l, r, val, 2*node, lo, mid)
        seg_add(l, r, val, 2*node+1, mid, hi)

    for key, intervals in edge_intervals.items():
        u, v = key
        for l, r in intervals:
            seg_add(l, r, (u, v))

    dsu = UndoDSU(N)
    results = []
    def dfs(node, lo, hi):
        edges_added = len(seg[node])
        for u, v in seg[node]:
            dsu.union(u, v)
        if hi - lo == 1:
            if lo < Q:
                t, u, v = queries[lo]
                if t == 'query':
                    results.append('Yes' if dsu.connected(u, v) else 'No')
        else:
            mid = (lo + hi) // 2
            dfs(2*node, lo, mid)
            dfs(2*node+1, mid, hi)
        for _ in range(edges_added):
            dsu.undo()

    sys.setrecursionlimit(1000000)
    dfs(1, 0, seg_size)
    print('\n'.join(results))

solve()

Step-by-Step 解説

1辺の時間区間化
各辺 $(u,v)$ を「存在する時間区間 $[l, r)$」に変換。最後まで削除されない辺は $[l, Q)$。
2Undo 可能 DSU
Path Compression を使わず Union by Rank のみ。変更操作をスタックに記録し、Undo で逆順復元。高さ $O(\log N)$。
3セグ木に辺登録
各区間 $[l, r)$ をセグ木の $O(\log Q)$ ノードに分割して格納。
4DFS で Union/Undo
ノード入 → 辺を Union、葉でクエリ処理、ノード出 → Undo。計算量 $O(Q \log Q \cdot \log N)$。

よくあるミス

ミス原因正しい書き方
Path Compression を使うUndo 不可能になるUnion by Rank のみ
Undo 回数を間違える辺数を忘れるedges_added = len(seg[node])
多重辺の処理同辺の複数追加edge_last_add[key] をリスト管理

次のステップ

  • 辺の重みあり + 連結成分の最小辺重み
  • Link-Cut Tree によるオンライン版
  • ETT との比較

自己評価

自分の回答

気づき・メモ