Day 159-Q1 — 2026-09-24 — Pruned Landmark Labeling(枝刈りランドマークラベリング・2-hop ラベルによる最短距離クエリ)

2026-09-24 赤色 Master (Phase 8+) ★★★★★★★★★ 実ネットワークの構造(次数の偏り・小さい直径)を利用して、全点対最短距離を「頂点ごとの小さなラベル」に圧縮する索引法

問題

$N$ 頂点 $M$ 辺の無向・単純・重みなしグラフと $Q$ 個の質問 $(u_i, v_i)$ が与えられる。各質問について $u_i$ から $v_i$ への最短距離(辺数)を出力せよ。到達不能なら $-1$ を出力せよ。

入力形式

N M Q
a_1 b_1
...
a_M b_M
u_1 v_1
...
u_Q v_Q

制約

#条件
1$2 \le N \le 5 \times 10^4$, $0 \le M \le 10^5$, $1 \le Q \le 2 \times 10^5$
2グラフは 優先的選択(Barabási–Albert 型、各新頂点が既存頂点 2 個へ次数比例確率で接続) で生成される。すなわち次数分布が裾の重い「実ネットワーク型」である(この前提が無いと本問は解けない——「Step 5」参照)
3$1 \le a_i, b_i, u_i, v_i \le N$、$a_i \ne b_i$、多重辺なし

入出力例

入力例1

7 6 4
1 2
2 3
3 4
2 5
5 6
6 4
1 4
3 6
1 7
5 5

出力例1

3
2
-1
0

($1\to2\to3\to4$ で 3。$3\to2\to5\to6$ または $3\to4\to6$ で 2。頂点 7 は孤立なので $-1$。$5\to5$ は 0)

この問題は Akiba, Iwata, Yoshida「Fast Exact Shortest-Path Distance Queries on Large Networks by Pruned Landmark Labeling」(SIGMOD 2013、https://doi.org/10.1145/2463676.2465315 、著者実装 https://github.com/iwiwi/pruned-landmark-labeling 、2026-09-24 参照)の主結果をそのまま問題化したものである。原論文の主張は「実ネットワーク(SNS・Web・道路以外)では、次数順に BFS を行い既存ラベルで答えられる頂点を枝刈りするだけで、ラベル総数が $N$ に対して小さく収まり、クエリは $\mu$s 単位で答えられる」。

概念図:次数順の枝刈り BFS と 2-hop ラベル

入力例1: ハブ 2(次数 3)を最初のランドマークにする 枝刈り例: 6 の BFS は 4 で d(6,4)=1 ≤ 既存ラベル? No → 追加 1 2 3 4 5 6 7 順位 0(ハブ) ラベル L(v) = {順位: 距離} L(1) = {2:1, 1:0} L(2) = {2:0} L(3) = {2:1, 3:0} L(4) = {2:2, 3:1, 4:0} L(5) = {2:1, 4:2, 5:0} L(6) = {2:2, 3:2, 4:1, 5:1, 6:0} L(7) = {7:0} (順位は次数降順で振った通し番号。 図では頂点番号で表記) query(1,4) = min over 共通ランドマーク: 順位 2 → 1 + 2 = 3 query(1,7): 共通ランドマークなし → -1。ラベルは「最短路上で最高順位の頂点」を必ず含む

ヒント(クリックで段階的に開示)

ヒント1(方向性)

$Q$ 回 BFS すると $O(Q(N+M))=3\times10^{10}$ で不可能。全点対 $N^2$ の表も $2.5\times10^9$ 要素で不可能。「各頂点 $v$ に、少数の代表点 $w$ と $d(v,w)$ の一覧(ラベル)を持たせ、$d(u,v)=\min_w d(u,w)+d(w,v)$ で答える」という 2-hop 被覆(Cohen ら 2003)の形を狙う。問題は「どの $w$ を、どの頂点のラベルに入れるか」。

ヒント2(アプローチ)

代表点を 次数の大きい順 に $v_1, v_2, \dots$ と並べ、その順に BFS する。$v_k$ からの BFS で頂点 $u$ に距離 $d$ で到達したとき、既に持っているラベルだけで $d(v_k,u)\le d$ が分かるなら、$u$ にラベルを追加せず、$u$ の先も探索しない(枝刈り)。こうしても「任意の $(s,t)$ の最短路上の、順位が最も高い頂点」が両者のラベルに必ず残るので正しさが保たれる。

ヒント3(誘導)
order = sorted(range(n), key=lambda v: -len(adj[v]))   # 次数降順 = ランドマーク順
labels = [dict() for _ in range(n)]                    # labels[u] = {ランドマーク順位: 距離}
for k, s in enumerate(order):
    BFS from s:
        u に距離 d で到達
        if query(s, u) <= d:  # 既存ラベルだけで答えられる
            continue          # ← 枝刈り: u にラベルを足さない・u から先へ進まない
        labels[u][k] = d
        push neighbors
# query(u, v) = min over common keys w of labels[u][w] + labels[v][w]

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys
from collections import deque


def main():
    data = sys.stdin.buffer.read().split()
    n, m, q = int(data[0]), int(data[1]), int(data[2])
    adj = [[] for _ in range(n)]
    pos = 3
    for _ in range(m):
        a = int(data[pos]) - 1; b = int(data[pos + 1]) - 1; pos += 2
        adj[a].append(b); adj[b].append(a)

    # 1. ランドマーク順序: 次数降順(原論文の既定)
    order = sorted(range(n), key=lambda v: -len(adj[v]))

    # 2. 枝刈り BFS でラベル構築
    labels = [dict() for _ in range(n)]   # labels[u] = {landmark_rank: dist}
    INF = 1 << 30
    dist = [INF] * n
    for k, s in enumerate(order):
        ls = labels[s]
        dist[s] = 0
        dq = deque([s]); visited = [s]
        while dq:
            u = dq.popleft(); d = dist[u]
            lu = labels[u]
            best = INF                      # query(s, u) を既存ラベルだけで計算
            for w, dw in lu.items():
                dv = ls.get(w)
                if dv is not None and dv + dw < best:
                    best = dv + dw
            if best <= d:
                continue                    # 枝刈り
            lu[k] = d
            for v in adj[u]:
                if dist[v] == INF:
                    dist[v] = d + 1; dq.append(v); visited.append(v)
        for v in visited:                   # dist を O(訪問数) で初期化し直す
            dist[v] = INF

    # 3. クエリ: 共通ランドマーク上の最小値
    out = []
    for _ in range(q):
        u = int(data[pos]) - 1; v = int(data[pos + 1]) - 1; pos += 2
        lu, lv = labels[u], labels[v]
        if len(lu) > len(lv):
            lu, lv = lv, lu
        best = -1
        for w, du in lu.items():
            dv = lv.get(w)
            if dv is not None:
                d = du + dv
                if best < 0 or d < best:
                    best = d
        out.append(best)
    sys.stdout.write("\n".join(map(str, out)) + "\n")


main()

計算量: 構築は「枝刈りされずに訪問した (ランドマーク, 頂点) 対の数」$\times$ ラベル参照 $= O(\sum_u |L(u)| \cdot \bar{L})$ で、$|L(u)|$ がグラフ構造で決まる(実測 Step 5)。クエリは $O(|L(u)|+|L(v)|)$。空間 $O(\sum_u |L(u)|)$。

Step-by-Step 解説

12-hop 被覆という枠組み

$d(u,v)=\min_{w\in L(u)\cap L(v)} d(u,w)+d(w,v)$ が全対で成り立つラベル集合 $\{L(u)\}$ を 2-hop 被覆 と呼ぶ(Cohen, Halperin, Kaplan, Zwick 2003)。任意の $(u,v)$ について「$u$–$v$ のある最短路上の頂点 $w$ が両方のラベルに入っている」なら、上の $\min$ は $d(u,v)$ 以下にも以上にもならず、ちょうど等しい。最小の 2-hop 被覆を求めるのは NP 困難だが、貪欲に「良さそうな順序」で作れば実ネットワークでは十分小さい、というのが本手法の立場。

2枝刈り BFS の正しさ

ランドマーク順序を $v_1 \succ v_2 \succ \cdots$ とする。主張: 任意の $(s,t)$ と、$s$–$t$ 間のある最短路上で順位最高の頂点 $w=v_k$ について、$v_k$ の BFS は $s$ にも $t$ にも枝刈りされずに到達し、$(k, d(v_k,s))\in L(s)$、$(k,d(v_k,t))\in L(t)$ が入る。 理由($s$ 側): $v_k$ の BFS が $s$ で枝刈りされるには、既存ラベル($v_1..v_{k-1}$ のみ)で $d(v_k,s)$ 以下の距離が説明できる必要がある。すると $v_k$–$s$ の最短路上に順位 $<k$ の頂点があることになり、それは $s$–$t$ の最短路($v_k$ を経由するもの)にも乗る。これは $v_k$ が最高順位という仮定に反する。$t$ 側も同様。よってクエリは正しい距離を返す。

3なぜ「次数降順」か

枝刈りが効くのは「多くの最短路が通る頂点」を先に処理したとき。実ネットワークでは次数の大きいハブがそれに当たる(原論文 §4.3 は次数順・近接中心性順などを比較し、次数順が簡単かつ十分と報告)。順序を変えても正しさは変わらず、変わるのはラベルサイズだけである。

4実装上の要点
  • dist を毎回 [INF]*n で作り直すと $O(N^2)$。訪問した頂点だけ戻す
  • ラベルは「順位 → 距離」の辞書。順位 $k$ 以前しか入らないので、原論文のように順位順の配列で持ち、クエリを 2 本のポインタで走査すると更に速い(Python では辞書の方が簡潔で十分だったので本解では辞書)
  • クエリ時は小さいラベルを走査し、大きい方を get で引く
5構造依存性——本手法が効かないグラフ(実測)
グラフ平均ラベル数最大構築時間
優先的選択 $N=10^4$, $k=2$37.21142.26 s
優先的選択 $N=5\times10^4$, $k=2$80.729859.0 s
ランダム $N=5000$, $M=10^4$192.140513.0 s
ランダム $N=10^4$, $M=2\times10^4$342.9658106 s

ランダムグラフ(Erdős–Rényi 型)はハブが無く、直径が $\log N$ 程度で「どの頂点も似たような役割」なので枝刈りが効かず、ラベルが $N$ に比例して増える。道路網(格子)は分離数 $\Theta(\sqrt N)$ から平均ラベル数も $\Theta(\sqrt N)$ 級になると導出される(導出のみ。本セッションでは $100\times100$ 格子の構築が 20 分以内に終わらず、実測値は得られなかった——それ自体が「効かない」証拠でもある。原論文も道路網には別手法——縮約階層 Day118 Q3 など——を勧めている)。制約に「優先的選択で生成」と書いたのはこのためで、手法の有効範囲を制約として明示しないと嘘になる。

迷った他案と採らなかった理由

案計算量採否理由
クエリごとに BFS$O(Q(N+M))$比較用$N=5\times10^4$ で 1 クエリ 40.5 ms(実測)。$Q=2\times10^5$ で 2 時間超
全点対 BFS で表を作る$O(N(N+M))$、空間 $N^2$不採用空間 $2.5\times10^9$ 要素。時間も $N$ 回の BFS で 30 分超(40 ms × 5 万)
ランドマーク近似(少数の代表点から距離の上界のみ)前処理 $O(k(N+M))$不採用上界しか出ず厳密解にならない。ALT(Day144 Q2)のように「探索の下界」に使う道具であって、距離そのものの答えではない
縮約階層(CH, Day118 Q3)前処理は重い、クエリ $O(\text{探索空間})$不採用道路網向け。次数の偏った SNS 型グラフではショートカット辺が爆発する(原論文 Table 4 で CH と比較し PLL が桁違いに速い)
PLL(本解)構築 $O(\sumL\cdot \bar L)$、クエリ $O(L(u)+L(v))$採用厳密・簡潔(BFS に「既存ラベルで答えられるなら打ち切り」を足すだけ)。有効範囲を制約に明記した
PLL + ビット並列 BFS(原論文 §4.2)構築を定数倍高速化扱わない上位 64 頂点をまとめて BFS する最適化。Python では多倍長整数で書けるが本質でないので範囲外

よくあるミス

ミス原因正しい書き方
枝刈り判定を「$<$」にする等しい場合も既存ラベルで答えられるので追加不要if best <= d: continue(< だと正しさは保たれるがラベルが余分に増える)
枝刈りした頂点の隣接も探索する「ラベルを足さないだけ」と誤解枝刈りされた頂点から先は 探索自体を打ち切る。そうしないと $O(N(N+M))$ に退化する
クエリで共通ランドマークが無いとき 0 を返す未初期化の best共通が無ければ非連結なので $-1$
ランドマーク順序を頂点番号順にする正しさは保たれるがラベルが巨大化次数降順(ハブ優先)にする
dist を毎回 [INF]*n で初期化$O(N^2)$訪問リストを持って戻す

次のステップ

  • 発展1: 重み付きグラフへ拡張する(BFS を Dijkstra に置き換えるだけで枝刈り条件は同じ。原論文 §5.1)
  • 発展2: 辺の追加に対する動的更新(Akiba, Iwata, Yoshida WWW 2014「Dynamic and Historical Shortest-Path Distance Queries」)——追加された辺 $(a,b)$ の両端から「短くなった頂点だけ」を再 BFS する
  • 扱わなかった範囲: ビット並列 BFS(上位 64 ランドマークを同時処理する最適化)、辺削除への対応(削除は再構築が必要で、原論文でも未解決とされる)、有向グラフ(前向き・後ろ向きの 2 種類のラベルが要る)

検証ログ(本セッションで実行した数値)

  • 入力例1: 出力 [3, 2, -1, 0]。構築されたラベル(順位: 距離)は頂点 2(次数 3、順位 0)が全連結頂点のラベルに入り、頂点 7 は自分だけ {6: 0}
  • ランダム小グラフ($N\le30$、300 個)の 全点対 100,166 組を BFS と比較 → 不一致 0 件(平均ラベル数 3.56)
  • 優先的選択 $N=10^4$: 構築 2.26 s、平均ラベル 37.2、最大 114。$10^5$ クエリ 0.311 s(3.1 µs/クエリ)。同じ 200 クエリを BFS で解くと 3.5 ms/クエリ。200 件で結果一致
  • 優先的選択 $N=5\times10^4$: 構築 59.0 s、平均ラベル 80.7、最大 298。$10^5$ クエリ 0.738 s(7.4 µs/クエリ)vs BFS 40.5 ms/クエリ(5,500 倍)。200 件で一致
  • ランダムグラフ $N=5000, M=10^4$: 平均ラベル 192.1、構築 13.0 s。$N=10^4, M=2\times10^4$: 平均ラベル 342.9、構築 106 s($N$ に比例してラベルが増える=手法が効かない構造)
  • 出所ラベル: 上記はすべて実測(Python 3.14、本ファイルの模範解答と同じ構築・クエリ関数)。「原論文 Table 4」の数値は本セッションでは再現していない(引用のみ)
  • 検証スクリプト: スクラッチパッドの q1.py

自己評価

自分の回答

気づき・メモ