Day 029-Q1 — Offline LCT(リンク・カット木のオフライン辺削除)

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

問題

$N$ 頂点 $M$ 辺の無向グラフに対し、$Q$ 個のクエリ(ADD u v / DEL u v / QUERY u v)を処理せよ。クエリはオフライン処理可(先読み可)。

制約

$1 \le N \le 10^5$
$0 \le M \le 10^5$
$1 \le Q \le 10^5$
DEL は存在する辺のみ
自己ループ・多重辺なし

入出力例

入力例 1

4 1
1 2
6
QUERY 1 2
ADD 2 3
ADD 3 4
QUERY 1 4
DEL 1 2
QUERY 1 4

出力例 1

Yes
Yes
No

ヒント (段階的開示)

ヒント1: 方向性
各辺の「存在期間 $[l,r)$」をセグメント木の区間に貼り付ける。
ヒント2: アプローチ
セグメント木上を DFS し、ノード訪問時に辺を Undo DSU で union、バックトラックで undo。計算量 $O((Q+M)\log^2 Q)$。
ヒント3: 誘導
Undo DSU は union by rank のみ(パス圧縮なし)。union 操作を history スタックに積み、undo で巻き戻し。

模範解答 (Python)

import sys
from sys import stdin
input = stdin.readline
sys.setrecursionlimit(500000)

def solve():
    N, M = map(int, input().split())
    initial_edges = [tuple(map(int, input().split())) for _ in range(M)]
    Q_cnt = int(input())
    queries = []
    for _ in range(Q_cnt):
        parts = input().split()
        queries.append((parts[0], int(parts[1]), int(parts[2])))

    edge_live = {}
    for u, v in initial_edges:
        edge_live[(min(u,v), max(u,v))] = 0

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

    def add_to_seg(l, r, edge, 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(edge)
            return
        mid = (lo + hi) // 2
        add_to_seg(l, r, edge, 2*node, lo, mid)
        add_to_seg(l, r, edge, 2*node+1, mid, hi)

    for i, (typ, u, v) in enumerate(queries):
        key = (min(u,v), max(u,v))
        if typ == 'ADD':
            edge_live[key] = i + 1
        elif typ == 'DEL':
            start = edge_live.pop(key)
            add_to_seg(start, i + 1, key)
    for key, start in edge_live.items():
        add_to_seg(start, Q_cnt + 1, key)

    parent = list(range(N + 1))
    rank = [0] * (N + 1)
    history = []

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

    def union(x, y):
        rx, ry = find(x), find(y)
        if rx == ry:
            history.append(None); return
        if rank[rx] < rank[ry]:
            rx, ry = ry, rx
        history.append((rx, parent[rx], rank[rx], ry, parent[ry], rank[ry]))
        parent[ry] = rx
        if rank[rx] == rank[ry]:
            rank[rx] += 1

    def undo():
        h = history.pop()
        if h is None: return
        rx, prx, rrx, ry, pry, rry = h
        parent[rx] = prx; rank[rx] = rrx
        parent[ry] = pry; rank[ry] = rry

    results = []

    def dfs(node, lo, hi):
        cnt = len(seg[node])
        for (u, v) in seg[node]:
            union(u, v)
        if lo + 1 == hi:
            if lo < Q_cnt:
                typ, u, v = queries[lo]
                if typ == 'QUERY':
                    results.append('Yes' if find(u) == find(v) else 'No')
        else:
            mid = (lo + hi) // 2
            dfs(2*node, lo, mid)
            dfs(2*node+1, mid, hi)
        for _ in range(cnt):
            undo()

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

solve()

Step-by-Step 解説

1各辺の存在区間を計算
ADD で開始、DEL で終了。残った辺は $[start, Q)$。
2セグメント木に辺を配置
各辺の区間 $[l,r)$ を $O(\log Q)$ ノードに分散して貼る。
3Undo DSU
union by rank のみ、操作履歴をスタックに積む。
4DFS 走査
ノード訪問で union、葉で QUERY 判定、バックトラックで undo。
5結果出力
DFS 中に集めた QUERY 結果を順番に出力。

よくあるミス

ミス原因正しい書き方
パス圧縮を使うundo できなくなるunion by rank のみ
ADD/DEL の対応ミスキーが不一致(min(u,v), max(u,v)) で正規化
セグメント木サイズ不足末尾辺で範囲外seg_size >= Q+1
再帰上限Pythonデフォルト 1000sys.setrecursionlimit

次のステップ

  • 辺重み付き Undo Weighted DSU

自己評価

自分の回答

気づき・メモ