Day 056-Q2 — Offline Dynamic Reachability(DAG・辺削除分割統治)

2026-06-09 赤色 Master / Phase 8+ ★★★★★★★★★ DAG / 動的グラフ / 分割統治 / 推移閉包

問題

$N$ 頂点の DAG と $M$ 本の辺が与えられる。各辺 $e_k = (u_k, v_k)$ には有効期間 $[a_k, b_k)$ がある。$Q$ 個のクエリ $(t_i, s_i, g_i)$: タイムステップ $t_i$ において $s_i \to g_i$ へ到達可能か答えよ。

制約

パラメータ範囲
$N$$2 \le N \le 500$
$M$$1 \le M \le 3000$
$Q$$1 \le Q \le 3000$
$[a_k, b_k)$$0 \le a_k < b_k \le Q$
グラフ常に DAG(有向非巡回)

入出力例

入力例 1

4 3 4
1 2 0 2
2 3 1 4
3 4 2 3
0 1 3
1 1 3
2 1 4
3 1 4

出力例 1

No
Yes
Yes
No

$t=0$: 辺1→2のみ有効 → 1→3不可。$t=1$: 辺1→2と2→3有効 → 1→3可。 $t=2$: 辺2→3と3→4有効 → 1→4不可(1→2が切れている)。$t=3$: 全辺切れ → 不可。

概念図: タイムセグメント木と分割統治

タイムセグメント木 (Q=8) + 辺の区間配置 [0, 8) [0, 4) [4, 8) [0, 2) [2, 4) [4, 6) [6, 8) t=0 t=1 t=2 t=3 辺 e1=[0,2): [0,2) ノードに配置 辺 e2=[1,4): [1,2) + [2,4) の2ノードに分割配置 辺 e3=[2,3): [2,3)=[2,2) と t=2 葉に配置

ヒント(段階的開示)

ヒント1: 方向性
辺の有効区間をタイムセグメント木に配置し、DFS でセグメント木を下りながら隣接行列(bitset)を累積する。葉に到達したとき Warshall-Floyd で推移閉包を計算してクエリに答える。
ヒント2: アプローチ
  • セグメント木サイズ: $2Q$(葉の個数 $Q$)
  • 辺 $[a, b)$ を区間 $O(\log Q)$ ノードに配置
  • DFS: 各ノードで辺を隣接行列に追加(コピーして子に渡す)
  • 葉で Warshall-Floyd: $O(N^2)$($N \le 500$ では bitset で高速化)
ヒント3: コード骨格
def dfs(node, adj):
    local = adj[:]           # 親からコピー
    for u, v in seg[node]:
        local[u] |= (1 << v) # bitset に辺追加

    if node >= SIZE:         # 葉
        t = node - SIZE
        if t < Q:
            reach = warshall(local)
            for qi, s, g in leaf_queries[t]:
                ans[qi] = 'Yes' if (reach[s] >> g) & 1 else 'No'
        return

    dfs(node*2,   local)
    dfs(node*2+1, local)

模範解答 (Python)

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

def solve():
    N, M, Q = map(int, input().split())
    edges_raw = []
    for _ in range(M):
        u, v, a, b = map(int, input().split())
        edges_raw.append((u-1, v-1, a, b))
    queries_raw = []
    for i in range(Q):
        t, s, g = map(int, input().split())
        queries_raw.append((t, s-1, g-1))

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

    def add_edge(l, r, e):
        l += SIZE; r += SIZE
        while l < r:
            if l & 1: seg[l].append(e); l += 1
            if r & 1: r -= 1; seg[r].append(e)
            l >>= 1; r >>= 1

    for u, v, a, b in edges_raw:
        if a < b:
            add_edge(a, b, (u, v))

    leaf_queries = [[] for _ in range(SIZE)]
    for i, (t, s, g) in enumerate(queries_raw):
        leaf_queries[t].append((i, s, g))

    ans = ['No'] * Q

    def warshall(adj):
        reach = adj[:]
        for k in range(N):
            mk = reach[k]
            for i in range(N):
                if (reach[i] >> k) & 1:
                    reach[i] |= mk
        return reach

    def dfs(node, adj):
        local = adj[:]
        for u, v in seg[node]:
            local[u] |= (1 << v)
        if node >= SIZE:
            t = node - SIZE
            if t < Q and leaf_queries[t]:
                reach = warshall(local)
                for qi, s, g in leaf_queries[t]:
                    if (reach[s] >> g) & 1:
                        ans[qi] = 'Yes'
            return
        dfs(node * 2, local)
        dfs(node * 2 + 1, local)

    dfs(1, [0] * N)
    print('\n'.join(ans))

solve()

Step-by-Step 解説

Step 1: タイムセグメント木への辺配置

辺の有効区間 $[a, b)$ をセグメント木上の標準区間分解で $O(\log Q)$ ノードに配置する。

Step 2: DFS による辺の累積

セグメント木をDFSで下る際、現在のノードに配置された辺を隣接行列(Pythonの大整数ビット演算)にORで追加する。子に渡すときはコピーを取るので $O(N)$ のコピーが発生。

Step 3: Warshall-Floyd で推移閉包

$N \le 500$ の bitset 表現(1頂点 = 1整数)で $O(N^2)$ の推移閉包計算。Python の大整数ビット演算はワード並列で定数倍有利。

Step 4: クエリ処理

葉ノードで対応するクエリの $(s, g)$ について (reach[s] >> g) & 1 を確認。

計算量

処理計算量
辺のセグメント木配置$O(M \log Q)$
DFS(辺累積)$O(N \cdot M \log Q)$ (コピー込み)
Warshall-Floyd(全葉)$O(N^2 \cdot Q)$ (最悪)
全体$O(N^2 Q + M N \log Q)$

よくあるミス

ミス原因正しい書き方
adj の破壊的変更親ノードの辺が子に混入local = adj[:] でコピー
推移閉包の更新順$k$ が内側だと正しくないfor k ... for i の順
辺区間の半開区間$[a, b)$ の $b$ は含まないadd_edge(a, b, ...)($b$ を含まない)

次のステップ

  • 無向動的グラフの連結性(Offline Dynamic Connectivity + Undo DSU)
  • DAG 上の最長路の動的変化

自己評価

自分の回答:

気づき・メモ: