Day 049-Q3 — Offline Borůvka + 分割統治(動的グラフ上の最小全域木)

2026-06-02 赤色 Master / Phase 8+ ★★★★★★★★★ Offline Dynamic MST / Kruskal / 辺存在区間管理

問題

$N$ 頂点のグラフに対して $Q$ 個の更新クエリが与えられる。各クエリは以下のいずれか:

  • add u v w: 辺 $(u, v, w)$ を追加する
  • remove id: 辺 $id$ を削除する($id$ は追加された順番の 1-indexed)
  • query: 現在のグラフの最小全域木の重みを出力する(連結でない場合は -1

制約

$2 \le N \le 300$
$1 \le Q \le 3000$
$1 \le w \le 10^9$
追加する辺の総数 $\le 3000$
時間制限: 4秒

入出力例

入力例 1

4 8
add 1 2 3
add 2 3 5
add 3 4 2
query
remove 2
add 1 4 4
query
remove 1

出力例 1

10
9

クエリ1: 辺{1-2:3, 2-3:5, 3-4:2}でMST=3+5+2=10。クエリ2: 辺{1-2:3, 3-4:2, 1-4:4}でMST=3+2+4=9

概念図: 辺の存在区間と時刻軸管理

時刻軸と辺の存在区間 t=0 add(1-2) t=1 add(2-3) t=2 add(3-4) t=3 query t=4 rm(2) t=5 add(1-4) t=6 query 辺1 (1-2, w=3): t=[0, Q-1] 削除なし 辺2 (2-3, w=5): t=[1, 3] 辺3 (3-4, w=2): t=[2, Q-1] 辺4 (1-4, w=4): t=[5, Q-1] query t=3 active: 辺1,2,3 → MST=10 query t=6 active: 辺1,3,4 → MST=9

ヒント(段階的開示)

ヒント1: 方向性
辺の存在期間を時刻軸区間として管理します。各 add で辺の開始時刻を記録し、remove で区間を確定させます。各 query 時刻で有効な辺集合に Kruskal 法を適用します。
ヒント2: アプローチ
  • 各辺の存在区間 $[s, e]$ を記録(削除なし辺は $[s, Q-1]$)
  • query 時刻 $t$ に対し、$s \le t \le e$ を満たす辺の集合を取得
  • Kruskal 法で MST を計算(連結でなければ $-1$)
  • $N \le 300$ なので Kruskal は $O(M \log M)$ で高速
ヒント3: 辺区間管理の実装骨格
add_time = {}  # edge_id -> (time, u, v, w)
edge_intervals = []  # (u, v, w, start, end)
edge_count = 0

for t, op in enumerate(ops):
    if op[0] == 'add':
        edge_count += 1
        add_time[edge_count] = (t, op[1], op[2], op[3])
    elif op[0] == 'remove':
        eid = op[1]
        s, u, v, w = add_time.pop(eid)
        edge_intervals.append((u, v, w, s, t - 1))
    # 残存辺を最後にループ後処理
for eid, (s, u, v, w) in add_time.items():
    edge_intervals.append((u, v, w, s, Q - 1))

模範解答 (Python)

import sys
from sys import stdin

def solve():
    data = stdin.read().split()
    idx = 0
    N, Q = int(data[idx]), int(data[idx+1]); idx += 2

    ops = []
    for _ in range(Q):
        op = data[idx]; idx += 1
        if op == 'add':
            u, v, w = int(data[idx]), int(data[idx+1]), int(data[idx+2])
            idx += 3
            ops.append(('add', u, v, w))
        elif op == 'remove':
            eid = int(data[idx]); idx += 1
            ops.append(('remove', eid))
        else:
            ops.append(('query',))

    edges = []
    add_time = {}
    edge_count = 0
    query_times = []

    for t, op in enumerate(ops):
        if op[0] == 'add':
            edge_count += 1
            add_time[edge_count] = (t, op[1], op[2], op[3])
        elif op[0] == 'remove':
            eid = op[1]
            s, u, v, w = add_time.pop(eid)
            edges.append((u, v, w, s, t - 1))
        else:
            query_times.append(t)

    for eid, (s, u, v, w) in add_time.items():
        edges.append((u, v, w, s, Q - 1))

    def kruskal(n, edge_list):
        parent = list(range(n + 1))
        rank = [0] * (n + 1)

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

        def union(x, y):
            px, py = find(x), find(y)
            if px == py:
                return False
            if rank[px] < rank[py]:
                px, py = py, px
            parent[py] = px
            if rank[px] == rank[py]:
                rank[px] += 1
            return True

        edge_list_sorted = sorted(edge_list, key=lambda e: e[2])
        total = 0
        cnt = 0
        for u, v, w, *_ in edge_list_sorted:
            if union(u, v):
                total += w
                cnt += 1
        return total if cnt == n - 1 else -1

    results = []
    for qt in query_times:
        active = [(u, v, w, s, e) for (u, v, w, s, e) in edges if s <= qt <= e]
        results.append(kruskal(N, active))

    print('\n'.join(map(str, results)))

solve()

Step-by-Step 解説

1辺の存在区間管理
各辺が add されてから remove されるまでの時刻区間 $[s, e]$ を記録します。削除されなかった辺は $[s, Q-1]$。
2クエリ時刻の特定
query 操作が発生する時刻 $t$ を列挙し、その時刻 $t$ に有効な辺($s \le t \le e$)を集めます。
3Kruskal 法の適用
有効辺の集合を重みでソートし、Union-Find で MST を構築。辺数が $N-1$ に達しない場合は連結でないので $-1$。
4計算量の分析
辺の総数 $M \le 3000$、クエリ数 $K \le Q$。各クエリで $O(M \log M)$ の Kruskal を実行するので全体 $O(QM \log M)$。
5大規模入力への対応
$N, Q$ が大きい場合は Offline Dynamic MST(時刻軸セグメント木 + Undo DSU + 重みのランキング)を使います(Day035 Q5)。

計算量

辺の区間管理: $O(Q)$
各クエリの Kruskal: $O(M \log M + M \alpha(N))$
全体: $O(Q \cdot M \log M)$
空間: $O(M + N)$

よくあるミス

ミス原因正しい書き方
remove 後の辺の端点を正しく閉じないend を t-1 でなく t にするedges.append((u,v,w,s,t-1))
削除されなかった辺を無視add_time が空にならないループ後に残存 add_time を処理
union-find の再利用kruskal 内で parent を再初期化しないparent = list(range(n+1)) を毎回作成
query の時刻を ops インデックスと混同query_times に何を入れるかops のインデックス(時刻)を記録

次のステップ

  • 発展問題: $N, Q \le 10^5$ での Offline Dynamic MST(時刻軸セグメント木 + Undo DSU)
  • 関連: Day022 Q3(動的最小全域木)、Day035 Q5(Offline Dynamic MSF)
  • 応用: 辺重みを動的に変更しながら MST を維持するクエリ

自己評価