DAG
DAG とは Directed acyclic graph 有向非巡回グラフの略で、閉路のない有向グラフのことです。
有向非巡回グラフ、有向非循環グラフ、有向無閉路グラフ(ゆうこうひじゅんかいグラフ、英: Directed acyclic graph, DAG)とは、グラフ理論における閉路のない有向グラフのことである。有向グラフは頂点と有向辺(方向を示す矢印付きの辺)からなり、辺は頂点同士をつなぐが、ある頂点 \(v\) から出発し、辺をたどり、頂点 \(v\) に戻ってこないのが有向非巡回グラフである[1][2][3]。
出典: フリー百科事典『ウィキペディア(Wikipedia)』
DAG であれば、トポロジカルソートを使うことで、ダイクストラ法やベルマンフォード法より速く、\( O(V + E)\)で最短経路を求めることができます。
トポロジカルソート
トポロジカルソート(英: topological sort)とは、グラフ理論において、有向非巡回グラフ(英: directed acyclic graph, DAG)の各ノードを順序付けして、どのノードもその出力辺の先のノードより前にくるように並べることである。有向非巡回グラフは必ずトポロジカルソートすることができる。
出典: フリー百科事典『ウィキペディア(Wikipedia)』
DAGでなので、「入力辺を持たないノード」は開始ノードになります。
「入力辺を持たないノード」から隣接ノードへのエッジを削除します。
もし隣接ノードが他に入力エッジを持たない場合は、この隣接ノードは開始ノードに続くノードと判断できます。
トポロジカルソートの計算量は\(O(V+E)\)です。
以下のグラフ (a) をトポロジカルソートしてみます。
import collections import sys class Node(object): def __init__(self, name): self.name = name self.indegree = 0 self.adjacency_list = [] self.predecessor = None class ShortestPathDAG(object): def __init__(self, nodes): self.nodes = nodes self.topological_sorted_list = [] def topological_sort(self): queue = collections.deque() for node in self.nodes: if node.indegree == 0: queue.append(node) while queue: current_node = queue.popleft() self.topological_sorted_list.append(current_node) for neighbor, weight in current_node.adjacency_list: neighbor.indegree -= 1 if neighbor.indegree == 0: queue.append(neighbor) if len(self.nodes) != len(self.topological_sorted_list): raise Exception('Not DAG') if __name__ == '__main__': r = Node('r') s = Node('s') t = Node('t') x = Node('x') y = Node('y') z = Node('z') r.indegree = 0 r.adjacency_list.extend([(t, 3), (s, -1)]) s.indegree = 1 s.adjacency_list.extend([(t, 2), (x, 6)]) t.indegree = 2 t.adjacency_list.extend([(x, 7), (y, 4)]) x.indegree = 2 x.adjacency_list.extend([(y, 5), (z, 1)]) y.indegree = 2 y.adjacency_list.extend([(z, -2)]) z.indegree = 2 z.adjacency_list.extend([]) nodes = [r, s, t, x, y, z] shortest_path_dag = ShortestPathDAG(nodes) shortest_path_dag.topological_sort() # r s t x y z for node in shortest_path_dag.topological_sorted_list: print(node.name, end=' ')
DAGの最短経路
トポロジカルソート後の最短経路の計算は簡単に行えます。
開始地点の最短距離を0、その他を無限大に設定し、トポロジカルソート順に最短距離の更新を行うだけです。
計算量は\(O(V+E)\)です。
以下のグラフ(b) を探索します。
import collections import sys class Node(object): def __init__(self, name): self.name = name self.indegree = 0 self.adjacency_list = [] self.min_distance = sys.maxsize self.predecessor = None class ShortestPathDAG(object): def __init__(self, start_node, nodes): self.start_node = start_node self.nodes = nodes self.topological_sorted_list = [] def _topological_sort(self): queue = collections.deque() for node in self.nodes: if node.indegree == 0: queue.append(node) while queue: current_node = queue.popleft() self.topological_sorted_list.append(current_node) for neighbor, _ in current_node.adjacency_list: neighbor.indegree -= 1 if neighbor.indegree == 0: queue.append(neighbor) if len(self.nodes) != len(self.topological_sorted_list): raise Exception('Not DAG') def calculate_shoretest_path(self): self._topological_sort() self.start_node.min_distance = 0 for node in self.topological_sorted_list: for neighbor, weight in node.adjacency_list: temp_distance = node.min_distance + weight if temp_distance < neighbor.min_distance: neighbor.min_distance = temp_distance neighbor.predecessor = node def show_shotest_path(self, goal): current_node = goal order_list = [] while current_node is not None: order_list.append(current_node.name) current_node = current_node.predecessor print(f'from {self.start_node.name} to {goal.name} distance {goal.min_distance}', order_list[::-1]) if __name__ == '__main__': r = Node('r') s = Node('s') t = Node('t') x = Node('x') y = Node('y') z = Node('z') r.indegree = 0 r.adjacency_list.extend([(s, 5), (t, 3)]) s.indegree = 1 s.adjacency_list.extend([(t, 2), (x, 6)]) t.indegree = 2 t.adjacency_list.extend([(x, 7), (y, 4), (z, 2)]) x.indegree = 2 x.adjacency_list.extend([(y, -1), (z, 1)]) y.indegree = 2 y.adjacency_list.extend([(z, -2)]) z.indegree = 3 z.adjacency_list.extend([]) nodes = [r, s, t, x, y, z] shortest_path_dag = ShortestPathDAG(s, nodes) shortest_path_dag.calculate_shoretest_path() # from s to r distance 9223372036854775807 ['r'] shortest_path_dag.show_shotest_path(r) # from s to s distance 0 ['s'] shortest_path_dag.show_shotest_path(s) # from s to t distance 2 ['s', 't'] shortest_path_dag.show_shotest_path(t) # from s to x distance 6 ['s', 'x'] shortest_path_dag.show_shotest_path(x) # from s to y distance 5 ['s', 'x', 'y'] shortest_path_dag.show_shotest_path(y) # from s to z distance 3 ['s', 'x', 'y', 'z'] shortest_path_dag.show_shotest_path(z)
想定通りの結果になりました。
最長経路問題
最長経路問題はNP完全問題です。
最短経路とは逆の問題で、最長単純道問題もある。最短経路の場合は、最短経路の部分問題もやはり最短経路であるが、最長単純道の場合、部分構造最適性が成立しておらず、貪欲法などで解くことが出来ない。辺の重みなしであっても、NP完全問題である。
出典: フリー百科事典『ウィキペディア(Wikipedia)』
しかし、DAGであればトポロジカルソートを使う最短経路とほぼ同様な考え方で解くことができます。