Day 088-Q4 — ビットセット高速化 到達可能性判定($O(N^3/64)$)

2026-07-11 赤色 Master / Phase 8+ ★★★★★★★★★ Warshall型DP・big int OR

問題

$N$ 頂点 $M$ 辺の有向グラフが与えられる。各頂点 $i$ から到達可能な頂点集合を、長さ $N$ の $01$ 文字列($j$ 文字目が到達可能なら 1)で出力せよ($i$ 自身も含む)。

制約

パラメータ範囲備考
$N$$1 \le N \le 2000$頂点数
$M$$0 \le M \le N^2$辺数

入出力例

入力例1

4 3
0 1
1 2
2 3

出力例1

1111
0111
0011
0001

概念図: 大整数の OR 演算で $N$ ビット分を一括処理

reach[i] |= reach[k] (i が k に到達できるとき) reach[i] 0 1 1 0 1 0 0 1 ... reach[k] 1 0 1 1 0 0 1 0 ... OR結果 1 1 1 1 1 0 1 1 ... Python の巨大整数は内部で64bit単位のリム配列 → 1回の | 演算が O(N/64) ワード処理

ヒント

ヒント1(方向性)

到達可能性を頂点対ごとに真偽値で Floyd-Warshall 型に計算すると $O(N^3)$ の真偽値演算が必要で $N=2000$ では遅い。各演算を64ビット単位でまとめられないか考える。

ヒント2(アプローチ)

各頂点の到達可能集合を巨大整数(ビットセット)で表現する。Python の任意精度整数は論理和 | を1回で全ビットまとめて計算できるため、内側演算 reach[i] |= reach[k] が実質 $O(N/64)$ 相当になる。

ヒント3(ほぼ答え)
reach = [1 << i for i in range(n)]
for u, v in edges:
    reach[u] |= 1 << v
for k in range(n):
    kb = 1 << k
    for i in range(n):
        if reach[i] & kb:
            reach[i] |= reach[k]

模範解答

import sys

def solve():
    data = sys.stdin.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    m = int(data[idx]); idx += 1

    reach = [1 << i for i in range(n)]
    for _ in range(m):
        u = int(data[idx]); idx += 1
        v = int(data[idx]); idx += 1
        reach[u] |= 1 << v

    for k in range(n):
        kb = 1 << k
        rk = reach[k]
        for i in range(n):
            if reach[i] & kb:
                reach[i] |= rk

    out = []
    for i in range(n):
        s = bin(reach[i])[2:].zfill(n)[::-1]
        out.append(s)
    print('\n'.join(out))

solve()

計算量: 外側2重ループ $O(N^2)$ 回だが、各 |= が $O(N/64)$ ワード処理のため実質 $O(N^3/64)$ 相当。

Step-by-Step 解説

Step 1: ビットセットとしての初期化

各頂点 $i$ の到達可能集合を整数 reach[i] で表現し、自分自身のビットと直接の隣接辺を立てる。

Step 2: Warshall型DPの立式

頂点 $k$ を経由してよいときの到達可能性を $k=0,\dots,N-1$ の順に確定させる。$i$ から $k$ に到達できるなら $k$ の到達可能集合をまるごと $i$ に合成する。

Step 3: 整数単位で並列化

方式1回の合成コスト
真偽値ごとに更新$O(N)$($N$回のbool演算)
big int の |=$O(N/64)$(ワード単位)

Step 4: 出力形式への変換

bin() はビット0が文字列末尾に来るため zfill 後に [::-1] で反転し頂点番号順に対応させる。

よくあるミス

ミス原因正しい書き方
3重ループで真偽値ごとに更新ビットセットの利点を活かせず $O(N^3)$内側は reach[i] |= reach[k] のみ
reach[i] & kb の判定を忘れる到達できない場合でも情報を誤って取り込む到達可能性を確認してから合成
bin() の桁順序をそのまま出力ビット0が末尾に来るため逆順になるzfill(n)[::-1] で反転

次のステップ

  • 発展問題: 無向グラフの連結成分ビットセット高速化(Union-Findとの比較)
  • 応用: LCS の bitset 高速化版($O(NM/64)$)

自己評価

理解度: / /

自分の回答:

気づき・メモ: