ダイクストラ法 Python & C++ (例)

⚡ スマートサマリー

ダイクストラ法は、負でない辺を持つ重み付きグラフにおいて、単一の始点頂点から他のすべての頂点への最短経路を計算します。この貪欲法は、 Google マップルーティング、OSPF IPルーティング、そして無数のネットワーク最短経路のユースケース。

  • 🎯 コアアイデア: ダイクストラ法は、最も近い未訪問の頂点を貪欲に拡張し、到達可能なすべてのノードが始点からの真の最短コストを持つまで、隣接ノードまでの距離を更新します。
  • 🔄 BFSとDFSとの比較: BFSとDFSは辺の重みを考慮せずに任意の経路を見つけるのに対し、ダイクストラ法は重み付き辺全体にわたる総コストを最小化する。
  • 🧭 ステップバイステップの例: 7つの頂点を持つ重み付きグラフを用いて、距離がどのように反復的に更新されるか、そして経路1-2-6-7がコスト7で勝利する様子を示します。
  • 💻 言語範囲: 両方 C++ (NAIST) と Python 実装例では、最小距離選択関数を用いた隣接行列バージョンを示しています。
  • ⚠️ 制限: ダイクストラ法は負のエッジ重みを持つグラフでは機能しません。なぜなら、確定したノードは再検討されないためです。負のエッジを持つグラフにはベルマン・フォード法を使用してください。
  • 📊 複雑: 単純な配列バージョンはO(V²)の時間と空間で動作しますが、優先度付きキューを使用すると、疎グラフの場合、時間はO(E log V)に短縮されます。

ダイクストラの最短経路アルゴリズム

最短経路または最短距離とは何ですか?

始点頂点から終点頂点までの経路のうち、コストが最小となる経路を最短経路または最短距離と呼びます。グラフ理論では、始点から終点まで複数の経路が存在する可能性があります。これらの経路の中で、コストが最小となる経路があれば、それを最短経路と呼びます。

ここでいう「コスト」とは、経路上のノード数、または各エッジのコストの合計を意味します。経路は1つまたは複数のエッジを持つことができます。2つの頂点間の接続を「エッジ」と呼びます。最短経路アルゴリズムには、ダイクストラ法やベルマン・フォード法など、さまざまな種類があります。

ここでは、ダイクストラ法について説明します。以下の重み付きグラフを見てみましょう。

無向重み付きグラフ

無向重み付きグラフ

  • 「重み付け」という用語は、あるノードから別のノードへ移動する際のコストを意味します。例えば、ノード1からノード2へ移動する場合、コスト(重み)は1です。
  • ノード1とノード2の間の経路をエッジと呼びます。
  • 「無方向」とは、あるノードから別のノードへ移動したり、前のノードに戻ったりできることを意味します。したがって、ノード1からノード7までのすべての経路を検索しようとすると、次のようになります。
ルートまたはパス費用
1-2-6-7(1+3+3) = 7
1-2-3-7(1+9+1) = 11
1-3-7(7 + 1)= 8
1-4-5-7(6+2+5) = 13

これら4つのルートのうち、最初のルートのコストは7であることがわかります。したがって、コストの面ではこれが最短ルートです。

最短経路

最短経路

ダイクストラのアルゴリズムの仕組み

ダイクストラ法は、有向グラフと無向グラフの両方において、最短距離を求めることができます。このアルゴリズムは、常に始点から最も近いノードを選択するため、「貪欲」なアルゴリズムと言えます。「貪欲」とは、複数の結果の中から最良の結果を選択することを意味します。

ここでは、他のすべての経路の中から最短経路を見つけようとしています。そのため、ダイクストラ法は単一の始点ノードからすべての最短経路を見つけます。結果として、それは 貪欲なアルゴリズム.

以下の「例」セクションでは、手順を追って説明します。手順は以下のとおりです。

ステップ1) 開始ノードのコストを0に初期化し、残りのノードのコストを無限大にする。
ステップ2) 配列またはリストを保持して trac訪問したノードのk個。
ステップ3) ノードのコストを最小コストに更新します。これは、現在のコストとパスのコストを比較することで実行できます(例のセクションで説明しています)。
ステップ4) すべてのノードを訪問するまで、ステップ3を続けます。

これらの手順をすべて完了すると、送信元から宛先までのコストが最小となるパスが見つかります。

ダイクストラとBFS、DFSの違い

ダイクストラ法とBFS-DFSの主な違いは、ダイクストラ法が最短経路探索アルゴリズムであるのに対し、BFSとDFSは一般的な経路探索アルゴリズムであるという点です。一般的に、BFSとDFSは経路探索時に辺のコストを考慮しません。そのため、これらのアルゴリズムは最短経路を保証することはできません。

BFSの動作原理を示す2Dグリッドデモンストレーション

2Dグリッドデモンストレーション BFS

アルゴスケッチBFSのデモンストレーション

このデモは、BFS がパスを検索するだけであることを示しています。 ただし、パスの重みは気にされません。 BFS (幅優先探索) は、あるノードから別のノードへの移動にかかるコストが 1 だけであると想定しています。

例としてグラフを見てみましょう。

2Dグリッドのデモンストレーション例グラフ

ここでは、BFSはレベル2でパスを見つけます。BFSはレベル順にグラフを走査します。したがって、次のように移動します。

ステップ1) ノード「1」から開始し、隣接するノード2、3、4をすべて訪問します。

ステップ2) ノード2、3、4をレベル1としてマークし、それらの隣接ノードを訪問します。目的地ノードに到達するまで、すべての隣接ノードの探索を続けます。

DFS に関しては、次のように 1 から 7 までのパスをトラバースします。

  • 1→2→3→7(オリジナルコスト10、DFSコスト3)
  • 1→2→6→7(オリジナルコスト7、DFSコスト3)
  • 1→3→7 (オリジナルコスト8、DFSコスト2)
  • 1→4→5→7(オリジナルコスト13、DFSコスト3)

ご覧のとおり、DFSはエッジの数に基づいてパスコストを計算します。DFSは以下の処理を行います。

  • DFS は、ソース (開始頂点) から宛先までのパスを見つけることができます。
  • 発見されたソースノードから宛先までのパスが最短パスであるかどうかは保証できません。

しかし、ダイクストラ法においては、辺の選択はコストに基づいて行われます。貪欲法であるため、最小コストの経路を選択します。

ダイクストラのアルゴリズムの例

ダイクストラのアルゴリズムは、コストまたは重みを使用してパスの総コストを計算します。

ダイクストラ法の例

ダイクストラのアルゴリズムの目標は、この総コストまたは重量を最小限に抑えることです。 上に示した例では、ノード 1 からノード 7 までの最適なパスを見つけて、すべてのコストを計算します。

ダイクストラ法では、重みを計算することで最短経路を見つけます。すべての経路を探索するわけではありません。ダイクストラ法を例を用いて説明しましょう。例えば、ノード1からノード7までの最短経路を見つけるように求められたとします。

このプロセスの手順は次のとおりです。

ステップ1) 開始ノードのコストを0に初期化します。 「インフ」 他のノードへ。これは、ソースとノードの間にパスが存在しないか、パスがまだ訪問されていないことを意味します。

ダイクストラ法の初期化

ステップ2) ノード1を選択すると、訪問済みとしてマークされます。次に、ノード1に隣接するすべてのノードを更新します。2、3、4はノード1の隣接ノードです。

コストを更新するときは、次の手順に従う必要があります。

ダイクストラ法の更新手順

上記の式を使用して、各ノードのコストを更新できます。たとえば、ノード1にいて、隣接するノード2、3、4のコストを更新する必要があるとします。更新後のコストは次のようになります。

ダイクストラ法(初回更新後)

ステップ3) ノード「2」の隣接ノードは6と3です。ノード「6」のコストは、無限大(現在の値)とノード2のコスト+2から6までのパスコストを比較することで更新されます。簡単に言うと、ノード「6」のコストは1+3、つまり4になります。

ダイクストラ法によるノード6の更新

ノード 3 はノード 2 の隣接ノードです。ただし、前の手順でそのコストを計算したところ、7 でした。ここで、パスが 1-2-3 の場合、ノード 3 のコストは 10 になります。パス 1-2- 3はコスト10、1~3はコスト7になります。

ステップ4) ノード3の隣接ノードは7です。そこで、ノード7の現在の値をパスコスト(7+1)または8と比較し、ノード7のコストを更新します。つまり、8です。したがって、ノード1からノード7へのパスは1→3→7となり、コストは8です。

ステップ5) ノード4については、隣接ノードのコストをそれに応じて更新します。したがって、ノード「5」のコストは8に更新されます。ステップ4と5の後、次のようになります。

ダイクストラ法のステップ4 5後

さて、パス1-3-7のコストは(以前の)8です。ノード「7」は、ノード「6」からノード「7」に到達できるため、訪問済みとしてマークされていません。パス「1-2-6」のコストは4でした。したがって、パス1-2-6-7のコストは7になります。

7 < 8 なので、始点頂点「1」から終点頂点「7」への最短経路は 1-2-6-7 となり、コストは 7 です。以前は 1-3-7 で、コストは 8 でした。したがって、最終的なグラフは次のようになります。

ダイクストラ法の最終グラフ

黒い線でマークされたエッジは 1 から 7 までの最短パスであり、コストは 7 になります。

ニックネーム Code ダイクストラ法

ダイクストラ法の擬似コードは以下のとおりです。

Dijkstra(G, S):
  for each vertex V in G
    distance[V] <- Infinity
    previous[V] <- NULL
    if V does not equal S, then,
      (priority queue) Q.push(V)
  distance[S] = 0
  While Q is not empty
    U <- Extract the MIN from Q
    For each unvisited adjacent V of U
      TotalDistance <- distance[U] + edge_cost(U, V)
      if TotalDistance is less than distance[V], then
        distance[V] <- TotalDistance
        previous[V] <- U
  return distance, previous

C++ ダイクストラ法の実装

ダイクストラのアルゴリズムを実装するには、 C++コードは以下のとおりです。

#include <bits/stdc++.h>
using namespace std;
#define size 7
int minimumDistance(int distance[], bool visited[]) {
  int min = INT_MAX;
  int min_index = INT_MAX;
  for (int i = 0; i < size; i++) {
    if (!visited[i] && distance[i] <= min) {
      min = distance[i];
      min_index = i;
    }
  }
  return min_index;
}
void printParentPath(int parent[], int i) {
  if (parent[i] == -1) {
    return;
  }
  printParentPath(parent, parent[i]);
  cout << i + 1 << " ";
}
void dijkstra(int graph[size][size], int source) {
  int distance[size];
  bool visited[size];
  int parent[size];
  for (int i = 0; i < size; i++) {
    parent[0] = -1;
    distance[i] = INT_MAX;
    visited[i] = false;
  }
  distance[source] = 0;
  for (int i = 0; i < size - 1; i++) {
    int U = minimumDistance(distance, visited);
    visited[U] = true;
    for (int j = 0; j < size; j++) {
      int curr_distance = distance[U] + graph[U][j];
      if (!visited[j] && graph[U][j] &&
          curr_distance < distance[j]) {
        parent[j] = U;
        distance[j] = curr_distance;
      }
    }
  }
  cout << "Vertex\t\tDistance\tPath" << endl;
  for (int i = 1; i < size; i++) {
    cout << source + 1 << "->" << i + 1 << "\t\t" << distance[i] << "\t\t"
         << source + 1 << " ";
    printParentPath(parent, i);
    cout << endl;
  }
}
int main() {
  int graph[size][size] = {{0, 1, 7, 6, 0, 0, 0}, {1, 0, 9, 0, 0, 3, 0},
                           {7, 9, 0, 0, 0, 0, 1}, {6, 0, 0, 0, 2, 0, 0},
                           {0, 0, 0, 2, 0, 0, 0}, {0, 3, 0, 0, 0, 0, 3},
                           {0, 0, 0, 0, 5, 3, 0}};
  dijkstra(graph, 0);
}

出力:

Vertex     Distance        Path

1->2           1             1 2
1->3           7             1 3
1->4           6             1 4
1->5           8             1 4 5
1->6           4             1 2 6
1->7           7             1 2 6 7

Python ダイクストラ法の実装

ダイクストラのアルゴリズムを実装するには、 Pythonコードは以下のとおりです。

num_of_vertex = 7
def minimumDistance(distance, visited):
    _min = 1e11
    min_index = 1e11
    for i in range(num_of_vertex):
        if not visited[i] and distance[i] <= _min:
            _min = distance[i]
            min_index = i
    return min_index

def printParentNode(parent, i):
    if parent[i] == -1:
        return
    printParentNode(parent, parent[i])
    print("{} ".format(i + 1), end="")

def dijkstra(graph, src):
    distance = list()
    visited = list()
    parent = list()
    for i in range(num_of_vertex):
        parent.append(-1)
        distance.append(1e11)
        visited.append(False)
    distance[src] = 0
    for i in range(num_of_vertex - 1):
        U = minimumDistance(distance, visited)
        visited[U] = True
        for j in range(num_of_vertex):
            curr_distance = distance[U] + graph[U][j]
            if not visited[j] and graph[U][j] and curr_distance < distance[j]:
                parent[j] = U
                distance[j] = curr_distance
    print("Vertex\t\tDistance\tPath")
    for i in range(num_of_vertex):
        print("{}->{}\t\t{}\t\t{} ".format(src + 1, i + 1, distance[i], src + 1), end="")
        printParentNode(parent, i)
        print("")

graph = [
    [0, 1, 7, 6, 0, 0, 0],
    [1, 0, 9, 0, 0, 3, 0],
    [7, 9, 0, 0, 0, 0, 1],
    [6, 0, 0, 0, 2, 0, 0],
    [0, 0, 0, 2, 0, 0, 0],
    [0, 3, 0, 0, 0, 0, 3],
    [0, 0, 0, 0, 5, 3, 0]
]
dijkstra(graph, 0)

出力:

Vertex     Distance        Path

1->1           0              1
1->2           1              1 2
1->3           7              1 3
1->4           6              1 4
1->5           8              1 4 5
1->6           4              1 2 6
1->7           7              1 2 6 7

このアルゴリズムは、始点ノードからの最短距離を計算していることがわかります。

ダイクストラアルゴリズムの応用

ダイクストラ法は幅広い用途があります。中でも、ネットワーク分野で広く利用されています。以下に、ダイクストラ法の実際の使用例をいくつか紹介します。

ダイクストラは Google マップ: 上記のコードスニペットの出力からもわかるように、このアルゴリズムは最短経路を見つけるための基盤となるものです。

ダイクストラアルゴリズムの応用 Google ゲレンデマップ

Google 単純なダイクストラアルゴリズムは使用しません。代わりに、改良版を使用します。目的地を選択すると、複数の経路が表示されます。 Google 地図。これらの経路のうち、いくつかはユーザー向けに整理されています。これらの経路は「時間」に基づいて選択されます。つまり、「時間」は最短経路のエッジコストとなります。

IPルーティングにおけるダイクストラ法: IPルーティング IPルーティングはネットワーク用語です。データパケットがさまざまな経路を経由して受信側に送信される方法を説明します。これらの経路は、ルーター、サーバー、その他の機器で構成されます。IPルーティングには、さまざまな種類のプロトコルがあります。

これらのプロトコルは、ルーターがデータを送信するための最短経路を見つけるのに役立ちます。プロトコルの1つに「OSPF(Open Shortest Path First)」があります。OSPFはダイクストラ法を使用します。ルーターは経路テーブルを保持し、各ルーターはそのテーブルを隣接ルーターと共有します。更新されたテーブルを受け取った後、ルーターはすべての経路を再度計算する必要があります。その際、ルーターはダイクストラ法を使用します。

ダイクストラアルゴリズムの限界

ダイクストラ法は、負のエッジを持つグラフにおいて最短経路を保証することはできません。ダイクストラ法は以下の原理に従います。

  • あるノードから別のノードへは XNUMX つの最短パスが取られます。
  • XNUMX つのノード間の最短パスが選択されると、再度計算されることはありません。

ここで、負のエッジを持つ XNUMX つの例に注目してください。

ダイクストラ法の限界:負のエッジ

左のグラフでは、 頂点は3つあります。ダイクストラ法は、グラフ上で次のように実行されます。

ステップ1) 開始頂点「1」はゼロに初期化されます。 他のノードは無限大になります。

ダイクストラ法の限界 ステップ1

ステップ2) ノード「1」を訪問済みとしてマークし、最短経路に含める。

ステップ3) 最短経路がまだ計算されていないため、始点ノード1からノード「2」および「3」までの距離は無限大に設定されます。したがって、無限大よりもコストの低い経路はすべて最短経路に追加されます(貪欲法)。

ステップ4) ソース頂点「1」から「2」までの距離を更新します。現在の重みは5(5<無限大)です。同様に、ノード「1」から「3」までの距離を重み3で更新します。

ダイクストラ法の限界 ステップ4

ステップ5) ここで、ノード「1」からの最短距離を調べてみると、エッジ1→2の最短距離は5であることがわかります。したがって、ノード「2」は訪問済みとしてマークされます。同様に、ノード「3」も最短距離が3であるため、訪問済みとしてマークされます。

しかし、よく見ると、コストがわずか2のパス1-3-2が存在します。ところが、ダイクストラ法では、ノード「1」からノード「2」までの最短距離は5と示されています。つまり、ダイクストラ法は最短距離を正しく計算できていません。その理由は、ダイクストラ法が貪欲アルゴリズムであるためです。そのため、一度訪問済みとマークされたノードは、より短いパスが存在する可能性があっても再検討されません。この問題は、エッジのコストが負の値、または重みが負の値の場合にのみ発生します。

ダイクストラ法は、このシナリオでは2つのノード間の最短経路を計算できません。そのため、このアルゴリズムにはいくつかの欠点があります。この負のエッジの問題を解決するために、「ベルマン・フォードアルゴリズム」と呼ばれる別のアルゴリズムが使用されます。このアルゴリズムは負のエッジにも対応できます。

ダイクストラのアルゴリズムの複雑さ

上記の実装では2つの「for」ループが使われています。これらのループは頂点の数だけ実行されます。したがって、時間計算量は O(V²)ここで、「O」という用語は、ダイクストラアルゴリズムの前提条件を示す表記法です。

グラフは「優先度キュー」を使用して格納できます。優先度キューはバイナリヒープデータ構造です。2D行列よりも効率的です。コストが最小のエッジは高い優先度を持ちます。すると、時間計算量は O(E log V)。 ここで、E はエッジの数、V は頂点の数です。

空間計算量は O(V²)、隣接行列を使用しているため(2D配列) 空間計算量は、隣接リストまたはキュー データ構造を使用して最適化できます。

よくあるご質問

ロボット工学、自律走行車、ゲームNPCにおけるAI経路計画エージェントは、重み付きグラフ上で最小コスト経路を見つけるためにダイクストラ法を使用します。強化学習環境も、報酬シェアのための最適な参照経路を計算するためにこのアルゴリズムに依存しています。ping と評価。

はい。GitHub CopilotやGPTのようなAIコーディングアシスタントは、ダイクストラ法を生成できます。 Python, C++または Javaこれには、ヒープを使用した優先度キューの派生版も含まれます。また、実際の最短経路を出力したり、隣接リストとして格納されたグラフに合わせてコードを適応させたりすることもできます。

最小ノードを見つけるために単純な配列を使用する場合、ダイクストラ法はO(V²)の時間で実行されます。バイナリヒープ優先度キューを使用するとO((V + E) log V)に短縮され、フィボナッチヒープを使用するとO(E + V log V)に達し、疎グラフに最適です。

ダイクストラ法では、現在の最小距離を選択するとすぐに頂点が確定されます。後から負の辺が追加されると、より長い経路が安くなる可能性がありますが、確定された頂点は二度と訪れられないため、アルゴリズムは誤った最短距離を報告します。

すべてのエッジの重みが非負の場合は、ダイクストラ法がO((V+E) log V)と高速なので、ダイクストラ法を選択してください。エッジが負になる場合や、負の重みのサイクルを検出する必要がある場合は、ベルマン・フォード法を選択してください。ただし、実行時間はO(V·E)とトレードオフの関係にあります。

Google Mapsは、A*やConを含む、ダイクストラ法の派生版や後継版を使用しています。trac道路ネットワークと実際の交通状況に合わせて調整された階層構造。累積コストを最小限に抑える貪欲な拡張という基本的な考え方は、依然としてダイクストラの中心的な貢献である。

A*アルゴリズムは、ダイクストラアルゴリズムに目標までの距離に関するヒューリスティックな推定値を追加することで拡張されており、適切なヒューリスティックが利用可能な場合は展開するノードの数を減らします。ダイクストラアルゴリズムはあらゆる方向に探索を行いますが、A*アルゴリズムは目標方向への探索を優先するため、実際にはより高速になります。

地図以外にも、ダイクストラ法はインターネット上のOSPFおよびIS-ISルーティングプロトコル、ネットワークトポロジーの最適化、電話のルーティング、ロボットの動作計画、ソーシャルネットワークにおける最短接続クエリ、航空会社のフライトコスト最小化などに活用されている。