グラフアルゴリズム
グラフは、オブジェクト間のペアの関係をモデル化するために使用される数学的構造です。この文脈でのグラフは、エッジ(リンクまたは線とも呼ばれる)で接続された頂点(ノードまたはポイントとも呼ばれる)で構成されます。グラフアルゴリズムは、グラフに関連する問題を解決するために設計された数学的手続きです。
グラフアルゴリズムの重要性
グラフは、コンピュータサイエンス、生物学、社会科学を含む多くの分野で基本的に重要です。ネットワークフロー、最短経路の発見、スパニングツリーの作成などの問題を解決するのに役立ちます。基本的および高度なグラフアルゴリズムを探ってみましょう。
基本的な定義
アルゴリズムに入る前に、グラフに関連するいくつかの基本的な用語を理解することが重要です。
- 頂点(またはノード): グラフの中でエッジが交わる点。
- エッジ(またはリンク): 2つの頂点間の接続。
- 隣接性: 2つの頂点がエッジで直接接続されている場合、隣接していると言います。
- パス: 頂点のシーケンスを接続するエッジのシーケンス。
- サイクル: 同じ頂点で始まり、終わるパス。
- 連結グラフ: 全ての頂点のペア間にパスが存在するグラフ。
グラフのタイプ
- 有向グラフ(ダイグラフ): エッジに方向があるグラフ。各エッジは頂点の順序対。
- 無向グラフ: エッジに方向がないグラフ。各エッジは頂点の無順序対。
- 重み付きグラフ: 各エッジに数値(重み)が関連付けられているグラフ。
- 重みなしグラフ: エッジに重みがないグラフ。
- 完全グラフ: 各頂点のペア間にちょうど1つのエッジが存在するグラフ。
グラフの表現
グラフは主に2つの方法で表現できます:
隣接行列
隣接行列は2次元配列で、各要素は頂点のペア間にエッジがあるかどうかを示します。
a = [ [0, 1, 0, 0], [1, 0, 1, 1], [0, 1, 0, 1], [0, 1, 1, 0] ]
この行列は、4つの頂点とペア (0,1)、(1,2)、(1,3)、(2,3) 間のエッジを持つグラフを示しています。
隣接リスト
隣接リストはグラフを頂点のリストとして表現し、各頂点にはその隣接頂点のリストがあります。
{ 0: [1], 1: [0, 2, 3], 2: [1, 3], 3: [1, 2] }
このリストでは、頂点0は頂点1と接続されており、頂点1は頂点0、2、3と接続されています。
グラフ探索アルゴリズム
グラフ探索とは、グラフのすべての頂点を訪問するプロセスを指します。グラフを探索する主な方法は2つあります:
幅優先探索(BFS)
BFSは、現在の深さにある隣接ノードを探索してから、次の深さにあるノードに移動する方法です。
def bfs(graph, start): visited = set() queue = [start] while queue: vertex = queue.pop(0) if vertex not in visited: visited.add(vertex) queue.extend(set(graph[vertex]) - visited) return visited
上記のコードでは、頂点 start
から始めてグラフを探索します。BFSメソッドは、次に訪問する頂点を追跡するためのキューを使用します。
BFSの実例
上記のSVGは、0から3まで番号付けされた頂点を持つ単純なグラフを示しています。頂点0から始めて、BFSはこの順序でノードを訪問します:0、1、3、2。
深さ優先探索(DFS)
DFSは、深さを優先して探索し、可能な限り遠くまで進んでからバックトラックする方法です。
def dfs(graph, start, visited=None): if visited is None: visited = set() visited.add(start) for next in graph[start]: if next not in visited: dfs(graph, next, visited) return visited
DFSコードでは、メソッドが再帰的に呼び出され、関数呼び出しを通じてパスを追跡するためにスタックが使用されます。
DFSの実例
同じグラフに対して、DFSはこの順序でノードを訪問します:0、1、2、3。
最短経路アルゴリズム
グラフアルゴリズムの目標は、多くの場合ノード間の最短経路を見つけることです。ここでは、よく知られた2つの最短経路アルゴリズムを紹介します:
ダイクストラ法
ダイクストラ法は、重み付きグラフにおいて初期頂点から他のすべての頂点への最短経路を見つけます。
import heapq def dijkstra(graph, start): queue = [] heapq.heappush(queue, (0, start)) distances = {node: float('infinity') for node in graph} distances[start] = 0 while queue: current_distance, current_node = heapq.heappop(queue) if current_distance > distances[current_node]: continue for neighbor, weight in graph[current_node].items(): distance = current_distance + weight if distance < distances[neighbor]: distances[neighbor] = distance heapq.heappush(queue, (distance, neighbor)) return distances
このアルゴリズムは、最小累積重みのエッジを見つけるためにキューを維持します。最小距離のエッジを効率よく取得するために、優先度キュー(最小ヒープ)を使用します。
ベルマン–フォード法
ベルマン-フォード法は、単一のソース頂点から他のすべての頂点への最短経路を計算します。負の重みを持つグラフも処理できます。
def bellman_ford(graph, start): distance = {i: float('inf') for i in graph} distance[start] = 0 for _ in range(len(graph) - 1): for vertex in graph: for neighbor, weight in graph[vertex].items(): if distance[vertex] + weight < distance[neighbor]: distance[neighbor] = distance[vertex] + weight for vertex in graph: for neighbor, weight in graph[vertex].items(): if distance[vertex] + weight < distance[neighbor]: raise ValueError("グラフには負の重みサイクルが含まれています") return distance
ダイクストラ法とは異なり、ベルマン-フォード法は負の重みを持つエッジを含むグラフに対しても適しています。最適なパスが見つかったかを確認するために、各エッジを繰り返しチェックします。
最小全域木(MST)アルゴリズム
連結された無向グラフの最小全域木は、サイクルなしで全頂点を接続し、可能な限り最小のエッジの重み合計を持つエッジの部分集合です。
クラスカル法
クラスカル法は、サイクルを形成しないエッジのみを追加しながら、最短のものからエッジを1つずつ追加してMSTを構築します。
def kruskal(graph): parent = {} def find(n): while parent[n] != n: n = parent[n] return n def union(n1, n2): root1 = find(n1) root2 = find(n2) parent[root2] = root1 graph.sort(key=lambda x: x[2]) for n1, n2, _ in graph: parent[n1] = n1 parent[n2] = n2 mst = [] for n1, n2, weight in graph: if find(n1) != find(n2): union(n1, n2) mst.append((n1, n2, weight)) return mst
このアルゴリズムは、まずすべてのエッジを重さ順にソートします。サイクルが形成されないように連結コンポーネントを追跡するために、ユニオン-ファインドデータ構造を使用します。
プリム法
プリム法は、任意の頂点から始め、選択された頂点からまだ選択されていない頂点への最小重みのエッジを続けて追加することによりMSTを構築します。
import heapq def prim(graph, start): mst = [] visited = set() min_edges = [(0, start, None)] while min_edges: weight, vertex, frm = heapq.heappop(min_edges) if vertex in visited: continue visited.add(vertex) if frm is not None: mst.append((frm, vertex, weight)) for next_vertex, next_weight in graph[vertex].items(): if next_vertex not in visited: heapq.heappush(min_edges, (next_weight, next_vertex, vertex)) return mst
プリム法は優先キューを使用して、現在のフリンジで最もコストの低いノードを常に拡張します。
結論
グラフアルゴリズムは、ノードとエッジのネットワークに関連する幅広い問題を解決する手段を提供します。最短経路を見つけること、最適な接続を求めること、ノードをクロールすること、これらのアルゴリズムは複雑な問題を効率的に解決するのに不可欠です。基本的な定義、表現、BFS、DFS、ダイクストラ、ベルマンフォード、クラスカル、プリムなどの主要なアルゴリズムを理解することは、現実世界のアプリケーションでグラフアルゴリズムを活用するために重要です。