トポロジカルソートを Python で書きます。
以下の続きです。
トポロジカルソート
トポロジカルソート(英: topological sort)とは、グラフ理論において、有向非巡回グラフ(英: directed acyclic graph, DAG)の各ノードを順序付けして、どのノードもその出力辺の先のノードより前にくるように並べることである。有向非巡回グラフは必ずトポロジカルソートすることができる。
出典: フリー百科事典『ウィキペディア(Wikipedia)』
トポロジカルソートは、例えば、a, b, cというタスクがあって、a, b が終わってから c に取り掛かれるような時に、[a, b] -> [c] というような順序に要素を並び替えるソートです。
こういったタスクの依存関係はDAGを使って表すことができるので、DAGの問題として取り扱うことができます。
Kahn’s algorithm
Kahn (1962)[2] が発明したアルゴリズムは、トポロジカルソートされた結果になるようにノードを順に選択していくというものである。
出典: フリー百科事典『ウィキペディア(Wikipedia)』
のアルゴリズムで書きます。
幅優先探索のように行います。
「入力辺を持たないノード」を開始ノードにして、「入力辺を持たないノード」から隣接ノードへのエッジを削除します。
もし隣接ノードが削除したノード以外に入力エッジを持たない場合は、この隣接ノードは開始ノードに続くノードと判断できます。
def topological_sort_bfs(graph):
# トポロジカルソートした結果を蓄積する空リスト
topological_sorted_list = []
queue = collections.deque()
# 入力辺を持たないすべてのノードの集合
for vertex in graph:
indegree = vertex.get_indegree()
if indegree == 0:
queue.append(vertex)
# while S が空ではない do
while len(queue) > 0:
# S からノード n を削除する
current_vertex = queue.popleft()
# L に n を追加する
topological_sorted_list.append(current_vertex.get_vertex_id())
# for each n の出力辺 e とその先のノード m do
for neighbor in current_vertex.get_connections():
# 辺 e をグラフから削除する
neighbor.set_indegree(neighbor.get_indegree() - 1)
# if m がその他の入力辺を持っていなければ then
if neighbor.get_indegree() == 0:
# m を S に追加する
queue.append(neighbor)
if len(topological_sorted_list) != len(graph.get_vertices()):
print("Kahn's algorithm:", '閉路があります。DAGではありません。')
else:
print("Kahn's algorithm tological sorted list:", topological_sorted_list)
深さ優先探索トポロジカルソート
幅優先探索のように行えるということは、深さ優先探索のように行うこともできます。
トポロジカルソートの別のアルゴリズムは深さ優先探索をベースにしている。このアルゴリズムではグラフの各ノードについて、トポロジカルソートを始めてからすでに訪れたノードに到達するまで深さ優先探索を行う
出典: フリー百科事典『ウィキペディア(Wikipedia)』
深さ優先探索中に訪れたノードには全て 「一時的」というマークを付け、もしその後の深さ優先探索で再度 「一時的」 のマークの付いたノードに出会った場合は、その部分は閉路になっていると判断できます。
また、リストへの追加を先頭ではなく、append で後方に行っているため、リストを読みだすときは逆順で行っています。
def topological_sort_dfs(graph):
# L ← トポロジカルソートされた結果の入る空の連結リスト
topological_reverse_sorted_list = []
try:
# for each ノード n do
for current_vertex in graph:
# if n に permanent の印が付いていない then
if not(current_vertex.permanent):
topological_sort_dfs_visit(current_vertex, topological_reverse_sorted_list)
# 閉路を発見した場合
except GraphTopologicalError:
print('DFS algorithm:', '閉路があります。DAGではありません。')
else:
# リストは最後から読み出す
print("DFS algorithm tological sorted list:", topological_reverse_sorted_list[::-1])
def topological_sort_dfs_visit(vertex, topological_sorted_list):
if vertex.permanent:
return
# if n に「一時的」の印が付いている then
if vertex.temporary:
# 閉路があり DAG でないので中断
raise GraphTopologicalError(topological_sorted_list)
# n に「一時的」の印を付ける
vertex.temporary = True
# for each n の出力辺 e とその先のノード m do
for neighbor in vertex.get_connections():
topological_sort_dfs_visit(neighbor, topological_sorted_list)
vertex.temprary = False
# n に「恒久的」の印を付ける
vertex.permanent = True
# n を L に追加
topological_sorted_list.append(vertex.get_vertex_id())
テスト
以下の2つのグラフでトポロジカルソートを行います。
最初のグラフはDAGなので、[a, b, c, d, e] (または [a, c, b, d, e] )と出力してくれれば問題ありません。

次のグラフは、e から a へと巡回してしまっているので、「DAGではありません」と出力します。

import collections
class Vertex(object):
def __init__(self, id):
self.id = id
self.adjacent = {}
self.visited = False
# 入力辺のカウント
self.in_degree = 0
# DFSトポロジカルソートで使うマーク
self.permanent = False
self.temporary = False
def add_neighbor(self, neighbor, weight=0):
self.adjacent[neighbor] = weight
def get_neighbors(self):
return self.adjacent
def get_connections(self):
return self.adjacent.keys()
def get_vertex_id(self):
return self.id
def get_weight(self, neighbor):
return self.adjacent[neighbor]
def set_visited(self):
self.visited = True
def unset_visited(self):
self.visited = False
def set_indegree(self, num):
self.in_degree = num
def get_indegree(self):
return self.in_degree
class Graph(object):
def __init__(self):
self.vertex_dict = {}
self.num_vertex = 0
def __iter__(self):
return iter(self.vertex_dict.values())
def add_vertex(self, id):
self.num_vertex = self.num_vertex + 1
new_vertex = Vertex(id)
self.vertex_dict[id] = new_vertex
return new_vertex
def get_vertex(self, id):
if id in self.vertex_dict:
return self.vertex_dict[id]
else:
return None
def add_edge(self, frm, to, weight=0):
if frm not in self.vertex_dict:
self.add_vertex(frm)
if to not in self.vertex_dict:
self.add_vertex(to)
self.vertex_dict[frm].add_neighbor(self.vertex_dict[to], weight)
# 入力辺を増やす
self.vertex_dict[to].in_degree += 1
def get_vertices(self):
return self.vertex_dict.keys()
def get_edges(self):
edges = []
for v in self.vertex_dict.values():
for w in v.get_connections():
vid = v.get_vertex_id()
wid = w.get_vertex_id()
edges.append((vid, wid, v.get_weight(w)))
return edges
class GraphTopologicalError(Exception):
pass
def topological_sort_bfs(graph):
# トポロジカルソートした結果を蓄積する空リスト
topological_sorted_list = []
queue = collections.deque()
# 入力辺を持たないすべてのノードの集合
for vertex in graph:
indegree = vertex.get_indegree()
if indegree == 0:
queue.append(vertex)
# while S が空ではない do
while len(queue) > 0:
# S からノード n を削除する
current_vertex = queue.popleft()
# L に n を追加する
topological_sorted_list.append(current_vertex.get_vertex_id())
# for each n の出力辺 e とその先のノード m do
for neighbor in current_vertex.get_connections():
# 辺 e をグラフから削除する
neighbor.set_indegree(neighbor.get_indegree() - 1)
# if m がその他の入力辺を持っていなければ then
if neighbor.get_indegree() == 0:
# m を S に追加する
queue.append(neighbor)
if len(topological_sorted_list) != len(graph.get_vertices()):
print("Kahn's algorithm:", '閉路があります。DAGではありません。')
else:
print("Kahn's algorithm tological sorted list:", topological_sorted_list)
def topological_sort_dfs(graph):
# L ← トポロジカルソートされた結果の入る空の連結リスト
topological_reverse_sorted_list = []
try:
# for each ノード n do
for current_vertex in graph:
# if n に permanent の印が付いていない then
if not(current_vertex.permanent):
topological_sort_dfs_visit(current_vertex, topological_reverse_sorted_list)
# 閉路を発見した場合
except GraphTopologicalError:
print('DFS algorithm:', '閉路があります。DAGではありません。')
else:
# リストは最後から読み出す
print("DFS algorithm tological sorted list:", topological_reverse_sorted_list[::-1])
def topological_sort_dfs_visit(vertex, topological_sorted_list):
if vertex.permanent:
return
# if n に「一時的」の印が付いている then
if vertex.temporary:
# 閉路があり DAG でないので中断
raise GraphTopologicalError(topological_sorted_list)
# n に「一時的」の印を付ける
vertex.temporary = True
# for each n の出力辺 e とその先のノード m do
for neighbor in vertex.get_connections():
topological_sort_dfs_visit(neighbor, topological_sorted_list)
vertex.temprary = False
# n に「恒久的」の印を付ける
vertex.permanent = True
# n を L に追加
topological_sorted_list.append(vertex.get_vertex_id())
if __name__ == '__main__':
graph = Graph()
graph.add_vertex('a')
graph.add_vertex('b')
graph.add_vertex('c')
graph.add_vertex('d')
graph.add_vertex('e')
graph.add_edge('a', 'b', 1)
graph.add_edge('a', 'c', 1)
graph.add_edge('a', 'e', 1)
graph.add_edge('b', 'd', 1)
graph.add_edge('b', 'e', 1)
graph.add_edge('c', 'd', 1)
graph.add_edge('c', 'e', 1)
graph.add_edge('d', 'e', 1)
topological_sort_bfs(graph)
# Kahn's algorithm tological sorted list: ['a', 'b', 'c', 'd', 'e']
topological_sort_dfs(graph)
# DFS algorithm tological sorted list: ['a', 'c', 'b', 'd', 'e']
graph = Graph()
graph.add_vertex('a')
graph.add_vertex('b')
graph.add_vertex('c')
graph.add_vertex('d')
graph.add_vertex('e')
graph.add_edge('a', 'b', 1)
graph.add_edge('a', 'c', 1)
graph.add_edge('a', 'e', 1)
graph.add_edge('b', 'd', 1)
graph.add_edge('b', 'e', 1)
graph.add_edge('c', 'd', 1)
graph.add_edge('c', 'e', 1)
graph.add_edge('d', 'e', 1)
graph.add_edge('e', 'a', 1)
topological_sort_bfs(graph)
# Kahn's algorithm: 閉路があります。DAGではありません。
topological_sort_dfs(graph)
# DFS algorithm: 閉路があります。DAGではありません。
想定通りの結果を得ることができました。