クラスカル法を用いて、重み付き無向グラフの最小全域木を求めます。
以下の記事の続きです。
プリム法は、ある頂点を選び、その頂点と繋がる辺の中で最小のものを選ぶことで、結果的に最小全域木を得ることができるアルゴリズムです。
クラスカル法は、閉路を作らないように最小の辺を選んでいくことで、最終的に最小全域木を得ることができるアルゴリズムです。
プリム法もクラスカル法も貪欲法ですが、数学的に最小全域木を得ると証明されています。
閉路を作らないようにするために、素集合データ集合を使います。
クラスカル法
クラスカル法(英: Kruskal’s algorithm)は、グラフ理論において重み付き連結グラフの最小全域木を求める最適化問題のアルゴリズムである。
出典: フリー百科事典『ウィキペディア(Wikipedia)』
以下のようなアルゴリズムになります。
- グラフ内の各頂点を個別の木とする森F(素集合データ構造)を作成
- グラフ内のすべての辺を含む集合Sを作成
- Sは空ではなく、Fは全域木ではない時
- Sから最小の重みを持つ辺を削除
- 削除された辺が2つの異なる木を接続する場合、森Fに追加し、2つの木を1つの木に結合 (素集合データ構造の Union & Find を用いる)
- アルゴリズムの終了時、森はグラフの最小領域木となる
プリム法の時と同じ以下のグラフの探索を行います。
探索の結果、以下の緑線の最小全域木が求まり、重さの総和は39になります。
import heapq import sys class Vertex(object): def __init__(self, id): self.id = id self.adjacent = {} self.distance = sys.maxsize self.visited = False self.previous = None def __lt__(self, other): return self.distance < other.distance 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_distance(self, distance): self.distance = distance def get_distance(self): return self.distance def set_previous(self, previous_vertex): self.previous = previous_vertex def get_previous(self): return self.previous def set_visited(self): self.visited= True class Graph(object): def __init__(self): self.vertex_dict = {} self.num_vertex = 0 def __iter__(self): return iter(self.vertex_dict.values()) def __len__(self): return self.num_vertex 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].add_neighbor(self.vertex_dict[frm], weight) 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 def prim(graph, source): source.set_distance(0) priority_queue = [(vertex.get_distance(), vertex) for vertex in graph] heapq.heapify(priority_queue) while len(priority_queue): nearest_vrtex = heapq.heappop(priority_queue) current = nearest_vrtex[1] current.set_visited() for next in current.adjacent: if next.visited: continue new_distance = current.get_weight(next) if next.get_distance() > new_distance: next.set_distance(new_distance) next.set_previous(current) heapq.heapify(priority_queue) print('プリム法') total_distance = 0 for vertex in graph.vertex_dict.values(): if vertex.get_previous(): print(f'辺 {vertex.get_vertex_id()}{vertex.previous.get_vertex_id()} --> {vertex.get_distance()}') total_distance += vertex.get_distance() print(f'総距離 {total_distance}') # 素集合データ構造 parent = dict() rank = dict() def make_disjoint_set(vertex): parent[vertex] = vertex rank[vertex] = 0 def find(vertex): if parent[vertex] != vertex: parent[vertex] = find(parent[vertex]) return parent[vertex] def union(vertex1, vertex2): root1 = find(vertex1) root2 = find(vertex2) if root1 != root2: if rank[root1] > rank[root2]: parent[root2] = root1 else: parent[root1] = root2 if rank[root1] == rank[root2]: rank[root2] += 1 def kruskal(graph): edges = [] for vertex in graph: disjoint_set = make_disjoint_set(vertex) for neighbor in vertex.get_connections(): edges.append((vertex.get_weight(neighbor), vertex, neighbor)) edges.sort() # 最小全域木は、リストに保存して出力を簡略化。 minimum_spanning_tree = [] for edge in edges: weight, vertex1, vertex2 = edge if find(vertex1) != find(vertex2): union(vertex1, vertex2) minimum_spanning_tree.append((weight, vertex1.get_vertex_id(), vertex2.get_vertex_id())) print('クルスカル法') distance = 0 for weight, vertex1, vertex2 in minimum_spanning_tree: print(f'辺 {vertex1}{vertex2} --> {weight}') distance += weight print(f'総距離 {distance}') 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_vertex('f') graph.add_vertex('g') graph.add_edge('a', 'b', 7) graph.add_edge('a', 'd', 5) graph.add_edge('b', 'c', 8) graph.add_edge('b', 'd', 9) graph.add_edge('b', 'e', 7) graph.add_edge('c', 'e', 5) graph.add_edge('d', 'e', 15) graph.add_edge('d', 'f', 6) graph.add_edge('e', 'f', 8) graph.add_edge('e', 'g', 9) graph.add_edge('f', 'g', 11) source = graph.get_vertex('d') prim(graph, source) # プリム法 # 辺 ad --> 5 # 辺 ba --> 7 # 辺 ce --> 5 # 辺 eb --> 7 # 辺 fd --> 6 # 辺 ge --> 9 # 総距離 39 kruskal(graph) # クルスカル法 # 辺 da --> 5 # 辺 ce --> 5 # 辺 df --> 6 # 辺 ab --> 7 # 辺 be --> 7 # 辺 eg --> 9 # 総距離 39
想定通りの結果を得ることができました。