JavaScriptで実装するプリム法:最小全域木を求めるアルゴリズムの基本と実装例
プリム法(Prim's Algorithm)とは
プリム法は、重み付き無向グラフから最小全域木(MST: Minimum Spanning Tree)を求めるための貪欲法(グリーディアルゴリズム)です。グラフ内のすべての頂点を含み、かつ辺の重みの合計が最小になるような辺の部分集合(木)を見つけ出します。
アルゴリズムは、任意の開始頂点から木の構築を始め、1ステップごとに「木に属する頂点」と「木に属さない頂点」をつなぐ辺の中から、最もコスト(重み)の小さいものを1本追加していくことで動作します。
プリム法の動作の流れ
以下の図を使って、プリム法がどのように動作するのかを順番に見ていきましょう。
ステップ1:任意のノードをルートとして選ぶ
まず、任意のノードを全域木のルートノードとして選択します。ここでは例として、ノードSをルートノードに選びました。ルートはどのノードでも構いません。なぜなら、全域木にはグラフのすべてのノードが含まれ、グラフが連結である以上、必ず少なくとも1本の辺で残りの木とつながるからです。
ステップ2:出ていく辺を確認し、コストが最小のものを選ぶ
ルートノードSを選んだ後、Sから出る辺を確認すると、重み7の「S-A」と重み8の「S-C」の2本があります。ここでは、よりコストの小さい「S-A」を選択します。

次に、S-7-Aという木をひとまとまりのノードとして扱い、そこから出ていくすべての辺を調べます。そして、その中で最もコストの低い辺を選んで木に追加します。

このステップの後、S-7-A-3-Cという木が形成されました。再びこれを1つのノードとして扱い、すべての辺を確認します。ただし選ぶのは最小コストの辺だけです。この例では、他の辺のコスト(8、6、4など)より小さい「C-3-D」が新しい辺として選ばれます。

ノードDを全域木に追加した後、Dからは同じコスト2の2本の辺(D-2-TとD-2-B)が出ています。この場合、どちらを先に追加しても問題ありません。いずれにしても次のステップでコスト2の辺が最小となるため、ここでは両方の辺を含む全域木を示しています。

JavaScriptでの実装
それでは、このアルゴリズムを実際にコードで実装する方法を見ていきましょう。
primsMST() {
// MSTを格納するグラフを初期化
const MST = new Graph();
if (this.nodes.length === 0) {
return MST;
}
// 最初のノードを開始ノードとして選択
let s = this.nodes[0];
// 優先度キューと探索済みセットを作成
let edgeQueue = new PriorityQueue(this.nodes.length * this.nodes.length);
let explored = new Set();
explored.add(s);
MST.addNode(s);
// 開始ノードから出るすべての辺を、重みを優先度としてキューに追加
this.edges[s].forEach(edge => {
edgeQueue.enqueue([s, edge.node], edge.weight);
});
// 最小の辺を取り出して新しいグラフに追加
let currentMinEdge = edgeQueue.dequeue();
while (!edgeQueue.isEmpty()) {
// 未探索ノードにつながる辺が見つかるまで、辺を取り出し続ける
while (!edgeQueue.isEmpty() && explored.has(currentMinEdge.data[1])) {
currentMinEdge = edgeQueue.dequeue();
}
let nextNode = currentMinEdge.data[1];
// キューが空になって未探索ノードが得られない場合もあるため再確認
if (!explored.has(nextNode)) {
MST.addNode(nextNode);
MST.addEdge(currentMinEdge.data[0], nextNode, currentMinEdge.priority);
// 新しいノードから出るすべての辺をキューに追加
this.edges[nextNode].forEach(edge => {
edgeQueue.enqueue([nextNode, edge.node], edge.weight);
});
// このノードを探索済みとしてマーク
explored.add(nextNode);
s = nextNode;
}
}
return MST;
}テストコード
次のようなコードで、この実装をテストできます。
let g = new Graph();
g.addNode("A");
g.addNode("B");
g.addNode("C");
g.addNode("D");
g.addNode("E");
g.addNode("F");
g.addNode("G");
g.addEdge("A", "C", 100);
g.addEdge("A", "B", 3);
g.addEdge("A", "D", 4);
g.addEdge("C", "D", 3);
g.addEdge("D", "E", 8);
g.addEdge("E", "F", 10);
g.addEdge("B", "G", 9);
g.primsMST().display();出力結果
このコードを実行すると、次のような出力が得られます。
A->B, D B->A, G D->A, C, E C->D E->D, F G->B F->E
元のグラフ
/** * A * / | \ * C | B * \ | | * D G * | / * E * | * F */
MST適用後のグラフ
処理後のグラフは次のようになります。
/** * A * |\ * C | B * \ | | * D G * | * E * | * F * */
このように、最もコストの高い辺(重み100のA-C間など)を取り除くことで、全体の重みが最小となる全域木が完成しました。
計算量について
プリム法の計算量は、使用するデータ構造によって変わります。上記の実装のように優先度キュー(二分ヒープ)を使用した場合、計算量は O(E log V)(Eは辺の数、Vは頂点の数)となります。一方、隣接行列を使った単純な実装では O(V²) となり、辺の密度が高いグラフ(密なグラフ)ではこちらが有利になることもあります。
また、同じく最小全域木を求めるアルゴリズムとしてクラスカル法(Kruskal's algorithm)があります。プリム法は頂点を基準に木を成長させるのに対し、クラスカル法は辺を重みの昇順にソートして追加していく点が異なります。用途やグラフの性質に応じて使い分けるとよいでしょう。
-
JavaScriptで学ぶAVL木の回転操作|LL・RR・LR・RL回転を図解&コード付きで解説
AVL木は、ノードの挿入や削除によってバランスが崩れた際に、自己平衡性を保つために以下の4種類の回転(ローテーション)操作を実行します。 左回転(Left Rotation)右回転(Right Rotation)左右回転(Left-Right Rotation)右左回転(Right-Left Rotation) 最初の2つは「単回転」、後の2つは「二重回転」に分類されます。木が不平衡となるためには、少なくとも高さ2の木が必要です。ここではシンプルな木を例に、それぞれの回転操作を順番に解説していきます。 左回転(Left Rotation) あるノードの「右部分木のさらに右部分木」にノードを挿
-
JavaScriptで子ノードの数を取得する方法
JavaScriptでは、children.lengthプロパティを使うことで、要素が持つ子ノード(子要素)の数を簡単に取得できます。この記事では、実際のコード例を使って、その基本的な使い方を解説します。children.lengthとはchildrenは、ある要素の子要素(HTMLコレクション)を返すプロパティです。さらに.lengthを付けることで、子要素の総数を数値として取得できます。例えば、<ul>タグの中にある<li>要素の個数を知りたい場合などに便利です。サンプルコード<!DOCTYPE html> <html lang="ja&