問題
$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(方向性)
$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 解説
$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 困難だが、貪欲に「良さそうな順序」で作れば実ネットワークでは十分小さい、というのが本手法の立場。
ランドマーク順序を $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$ 側も同様。よってクエリは正しい距離を返す。
枝刈りが効くのは「多くの最短路が通る頂点」を先に処理したとき。実ネットワークでは次数の大きいハブがそれに当たる(原論文 §4.3 は次数順・近接中心性順などを比較し、次数順が簡単かつ十分と報告)。順序を変えても正しさは変わらず、変わるのはラベルサイズだけである。
distを毎回[INF]*nで作り直すと $O(N^2)$。訪問した頂点だけ戻す- ラベルは「順位 → 距離」の辞書。順位 $k$ 以前しか入らないので、原論文のように順位順の配列で持ち、クエリを 2 本のポインタで走査すると更に速い(Python では辞書の方が簡潔で十分だったので本解では辞書)
- クエリ時は小さいラベルを走査し、大きい方を
getで引く
| グラフ | 平均ラベル数 | 最大 | 構築時間 |
|---|---|---|---|
| 優先的選択 $N=10^4$, $k=2$ | 37.2 | 114 | 2.26 s |
| 優先的選択 $N=5\times10^4$, $k=2$ | 80.7 | 298 | 59.0 s |
| ランダム $N=5000$, $M=10^4$ | 192.1 | 405 | 13.0 s |
| ランダム $N=10^4$, $M=2\times10^4$ | 342.9 | 658 | 106 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(\sum | L | \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