体系 › 探索と最適化
·無料·論文·10分で読めます

グラフアルゴリズムを1から — 最短経路とその応用

乗換案内からベクトル検索まで、世界の裏側は「点と線」で動いている。BFS・ダイクストラ法・A*を前提知識ゼロから積み上げ、最後はLLM時代の検索を支えるHNSWまで一本の道でつなぐ。

要点

  • グラフは点(頂点)と線(辺)のつながり方だけを抜き出した構造で、最短経路はその最も基本的な問いである。
  • 辺の本数が最小の経路はBFS、辺の重みの合計が最小の経路はダイクストラ法で解く。
  • ダイクストラ法は優先度付きキューで暫定最小の頂点を確定し、負の重みがあると正しく動かず、計算量はO((V+E)log⁡V)O((V+E)\log V)。
  • A*はf(n)=g(n)+h(n)f(n)=g(n)+h(n)で探索をゴールへ絞り、hhが過大評価しなければ最短を保証し、h(n)=0h(n)=0でダイクストラ法に一致する。
  • HNSWは近いベクトル同士をつないだ階層グラフを貪欲に歩く近似最近傍探索で、候補数`efSearch`が再現率と遅延を左右する。
対象textタスクalgorithms

Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs

一次資料 — この記事の根拠

この解説の公開 2026-08-13

Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphsarXiv:1603.09320論文ページ·PDF

世界は「点と線」でできている

駅の路線図を思い浮かべてください。駅が点で、駅と駅を結ぶ線路が線。地図の縮尺も駅間の実距離も無視して、「どこと どこが つながっているか」だけを描いた図です。この「点と線のつながり方だけを抜き出した構造」を、数学ではグラフと呼びます。点は頂点(ノード)、線は辺(エッジ)です。

グラフの守備範囲は路線図にとどまりません。SNSのフォロー関係(人が頂点、フォローが辺)、Webページとリンク、道路網、分子の原子と結合、そしてニューラルネットの層のつながりまで、「何かと何かの関係」はほぼすべてグラフで書けます。関係に向きがあるもの(フォローは一方通行)は有向グラフ、なければ無向グラフ。辺に「所要時間」や「距離」のような数値が付いていれば重み付きグラフと呼びます。

グラフに対する問いで最も基本かつ最頻出なのが、この記事の主題である最短経路です。「AからBまで、いちばん安く行くにはどう進むか」。カーナビも乗換案内もゲームの敵キャラの追跡も、そして後半で見るベクトル検索も、突き詰めればこの問いを解いています。

同じ「最短」でも2種類ある

最短経路には実は2つの顔があります。

前者を解くのがBFS、後者を解くのがダイクストラ法です。この区別を最初に押さえておくと、この先の道具の使い分けで迷いません。

BFS: 波紋のように広がる探索

幅優先探索(Breadth-First Search、BFS)は、池に石を落としたときの波紋のイメージです。出発点から「1歩で行ける頂点」を全部見て、次に「2歩で行ける頂点」を全部見て——と、近い順に輪を広げていきます。

実装の道具はキュー(先入れ先出しの待ち行列)ひとつ。出発点をキューに入れ、取り出しては「その隣でまだ見ていない頂点」を印を付けてキューの末尾に足す。これを繰り返すだけです。波紋は近い順に広がるので、ある頂点に最初に到達した瞬間の歩数が、そのままその頂点への最短ホップ数になります。後からもっと短い道が見つかることはありません。

コストは、頂点数を VV、辺数を EE とすると O(V+E)O(V + E)。各頂点と各辺を高々1回ずつ触るだけの、いわば線形時間です。一方、「経路を全部列挙して最短を選ぶ」素朴な方法は、経路の数が組合せ的に爆発するため指数時間になります。この差がどれほど桁違いかは、下の図で nn を動かすと体感できます。

FIG 1経路の総当たりは指数 O(2ⁿ) 側、BFSは線形側。n を増やすと両者の差が「速い遅い」ではなく「終わる終わらない」の差になる

ダイクストラ法: 「確定」を安い順に積み上げる

辺に重みが付くと、BFSは壊れます。「2本の辺で合計10分」の道より「3本の辺で合計6分」の道が速いことがあるのに、BFSは辺の本数しか見ないからです。重み付きの世界では「近い順に輪を広げる」の「近い」を、歩数ではなく累計コストで測り直す必要があります。

そこでダイクストラ法(Dijkstra's algorithm、1956年にエドガー・ダイクストラが考案)。イメージは、出発点から水を流し、低い土地から順に水没していく様子です。アルゴリズムは「暫定の距離が最小の頂点」を1つずつ取り出し、その頂点への最短距離を確定させます。取り出す道具はキューではなく優先度付きキュー(常に最小値から取り出せる待ち行列)。頂点 uu を確定させたら、uu の隣の各頂点 vv に対して次の更新を試みます。

d(v)←min⁡(d(v), d(u)+w(u,v))d(v) \leftarrow \min\bigl(d(v),\ d(u) + w(u,v)\bigr)
(1)

言い換えると「vv までの今知っている最短記録 d(v)d(v) と、確定したばかりの uu を経由して vv に入る新ルートの長さ d(u)+w(u,v)d(u)+w(u,v) を比べて、短いほうを記録し直す」だけです。w(u,v)w(u,v) は辺 u→vu\to v の重み。この1行の更新を緩和(relaxation)と呼びます。

なぜ「暫定最小の頂点」は確定してよいのでしょうか。すべての辺の重みが0以上なら、その頂点へ後からどんな迂回路で来ても、いま持っている暫定距離より短くなりようがないからです。これが成り立たない負の重みがあるグラフではダイクストラ法は正しく動きません(その場合はベルマン–フォード法という別の道具を使います)。二分ヒープの優先度付きキューを使ったときの計算量は O((V+E)log⁡V)O((V+E)\log V) で、数百万頂点でも実用になります。

A*: ゴールの方角を知っている探索

ダイクストラ法には少し無駄があります。東京から大阪への経路を探すのに、律儀に仙台方面まで水を流すのです。出発点から全方向に輪を広げる設計だからです。

A</strong>*(エースター)は、ここに「ゴールはだいたいあっちだ」という勘を持ち込みます。各頂点 nn の有望さを次のスコアで測り、優先度付きキューからスコア最小の頂点を取り出します。

f(n)=g(n)+h(n)f(n) = g(n) + h(n)
(2)

言い換えると「nn の評価 f(n)f(n) は、出発点から nn まで実際に歩いた距離 g(n)g(n) と、nn からゴールまでの残り距離の見積もり h(n)h(n) の和」です。hh はヒューリスティック関数と呼ばれ、たとえば地図なら「ゴールまでの直線距離」がよく使われます。道路は曲がりくねっていても直線より短くはならないので、これは実際の残距離を決して過大評価しない見積もりです。

この「過大評価しない」という性質(許容性)が成り立つ限り、Aは最短経路を見つけることが保証されます。しかも探索はゴール方向に絞られ、ダイクストラ法より調べる頂点がずっと少なくて済みます。実は h(n)=0h(n)=0 とおくとAはダイクストラ法そのものに戻ります。つまりダイクストラ法は「勘ゼロのA*」であり、2つは別物ではなく地続きです。

コードで書くダイクストラ法

Pythonの heapq(二分ヒープ)を使うと本体は10行ほどです。

import heapq

def dijkstra(graph, start):          # graph[u] = [(v, w), ...]
    dist = {start: 0}
    pq = [(0, start)]                # (暫定距離, 頂点)
    while pq:
        d, u = heapq.heappop(pq)     # 暫定最小の頂点を確定
        if d > dist.get(u, float("inf")):
            continue                 # 古い記録は読み捨て
        for v, w in graph[u]:
            if d + w < dist.get(v, float("inf")):
                dist[v] = d + w      # 緩和
                heapq.heappush(pq, (d + w, v))
    return dist

A*にするには、pushするキーを d + w から d + w + h(v) に変えるだけ。骨格は共通です。

そしてベクトル検索へ — HNSWはグラフ探索の応用

ここからが本題の応用です。RAGやレコメンドでは、文書や商品を埋め込みベクトルにして「クエリに最も近いベクトルを探す」処理(最近傍探索)が心臓部になります。愚直にやるなら全ベクトルとの距離を総当たりで測ればよいのですが、データが数千万本になると1クエリごとの全件走査は割に合いません。

そこで登場するのが HNSW(Hierarchical Navigable Small World、一次資料の論文で提案)です。発想はこうです。あらかじめ、互いに近いベクトル同士を辺でつないだグラフを作っておく。検索時は適当な入口の頂点から出発し、「いまいる頂点の隣接頂点のうち、クエリに最も近いものへ移る」を繰り返す。どの隣に移ってもクエリに近づけなくなったら、そこが答えの候補です。グラフの上を「クエリへの近さ」という見積もりに導かれて歩く——A*で見た発想の親戚だと分かります。

HNSWの「H」は階層です。上の層ほど頂点を間引いた粗いグラフを重ね、検索は最上層の長距離ジャンプで大まかに近づき、下の層に降りて細かく詰めます。高速道路で都市まで飛び、一般道で住所まで詰めるイメージで、スキップリストというデータ構造と同じ発想です。この構造により、実用上は全件走査よりはるかに少ない距離計算で近傍が見つかります。ただし貪欲に歩くぶん、真の最近傍を取り逃がすことがある近似手法です。取り逃しにくくする仕掛けが、1本道ではなく上位 ef 件の候補を持ち歩くビーム探索で、この ef が後述する実務の主役になります。

「近い」を測る物差しは内積・コサイン類似度・ユークリッド距離のいずれかです。物差しの感覚は下の図で確かめられます。

FIG 2ベクトルを回して内積と余弦の変化を見る。HNSWはこの「近さ」を頼りにグラフを歩き、これ以上近づけない場所で止まる

現場ではこう使う

RAG・検索基盤の担当者がインデックスを選ぶとき。 Faiss・pgvector・Qdrant・Milvus・OpenSearchなど主要なベクトルDBはほぼすべてHNSWを実装しています。触るパラメータは3つ: 各頂点から張る辺の本数 M(メモリと精度のトレードオフ)、構築時の候補数 efConstruction(インデックス品質)、検索時の候補数 efSearch(再現率と遅延のトレードオフ)。チューニングは「efSearch を上げて再現率がどこで頭打ちになるか」を実データで測るのが定石です。

落とし穴1: 再現率の低さを距離関数のせいにする。 「RAGが明らかに関連する文書を取ってこない」とき、埋め込みモデルを疑う前に efSearch(ライブラリによっては ef)が既定値のままでないかを見る。近似検索の取り逃しは仕様であり、候補数を増やせば消えることが多い問題です。

落とし穴2: 距離メトリクスの不一致。 コサイン類似度のつもりでベクトルを正規化せずに内積インデックスへ入れると、ノルムの大きいベクトルばかり返ってきます。インデックス作成時と検索時でメトリクス設定が一致しているかは、障害調査の最初の確認項目です。

落とし穴3: 負の重みにダイクストラ法。 経路コストに「割引」や「報酬」を混ぜた途端、前提が崩れて答えが黙って間違います。エラーは出ません。負の辺が入る設計ならベルマン–フォード法系に切り替えます。

ゲーム・地図系の担当者なら、Aのヒューリスティックが許容的か(過大評価していないか)のレビューが必須です。面接でも「hh が過大評価するとAはどうなるか」(答え: 速くなるが最短性が壊れる)は定番の問いです。

まとめ

  • グラフは「点と線」で関係を表す万能の言語。最短経路はその最重要問題
  • BFSは辺の本数の最短をキューで、ダイクストラ法は重み付きの最短を優先度付きキューと緩和で解く
  • A*は「残距離の過大評価しない見積もり」で探索をゴールへ絞る。h=0h=0 でダイクストラ法に一致
  • HNSWは「近いもの同士をつないだグラフを、近さに導かれて貪欲に歩く」近似最近傍探索。LLM時代の検索の土台にも、最短経路と同じグラフ探索の骨格が生きている

計算量の読み方は計算量の基礎、キューやヒープそのものはデータ構造ツアー、HNSWが組み込まれる検索システム全体はRAGの基礎で扱っています。

🎬 この内容の解説動画(Manim・字幕つき)

参考文献

  1. Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs. arXiv:1603.09320論文ページ·PDF

本記事は上記論文の本文にもとづいて執筆しています。数値・主張は原典を優先してください。

運営: TechAthletes · 編集方針 · 出典の付け方 · 訂正の記録

コメント

コメントにはログインが必要です