プリム法
最終更新
プリム法は、最小全域木 (MST) - すべてのノードを閉路なしでつなぐ最も安い辺の集合 - を、開始ノードから一本の木を外側へ成長させることで構築します。各ステップで、木からその外側のノードへ交差するすべての辺を調べ、最も安いものを追加します。上の再生ボタンを押すと、常に新しいノードに到達する最小重みの辺を選びながら木が広がる様子を見られます。
常に最小の交差辺を選ぶため、各追加は安全です (何らかの MST の一部であることが保証されます)。各外側ノードへの最も安い辺をキーとする二分ヒープの優先度付きキューを使うと、プリム法は O(E log V) で動作します。これは、一本の連結した木を成長させる代わりにすべての辺をグローバルにソートするクラスカル法と対照的です。
時間計算量と空間計算量
| 実装 | 計算量 | 備考 |
|---|---|---|
| 二分ヒープ | O(E log V) | 交差辺の優先度付きキュー |
| 隣接行列 | O(V²) | より単純; 密なグラフに適する |
| 空間 | O(V + E) | 木への所属 + 優先度付きキュー |
| 最適な用途 | 密なグラフ | 単一の開始ノードから成長 |
ステップごとの説明
| ステップ | 何が起こるか |
|---|---|
| 1 | 任意の一つのノードで木を開始する。 |
| 2 | 木からその外側のノードへ交差するすべての辺を調べる。 |
| 3 | 重みが最小の交差辺を選ぶ。 |
| 4 | その辺と新しいノードを木に追加する。 |
| 5 | すべてのノードが木に入るまで繰り返す。 |
具体例
辺 A-B=1, A-C=3, B-C=2, B-D=4, C-D=5 を持つ4ノードのグラフの MST を、A から始めて構築する:
| ステップ | 木 | 交差辺 | 選ばれた辺 |
|---|---|---|---|
| 1 | {A} | A-B=1, A-C=3 | A-B (重み 1) |
| 2 | {A, B} | A-C=3, B-C=2, B-D=4 | B-C (重み 2) |
| 3 | {A, B, C} | B-D=4, C-D=5 | B-D (重み 4) |
| 4 | {A, B, C, D} | なし - すべてのノードが木に含まれる | 完了: MST の重み 1+2+4 = 7 |
プリム法を使うべき場面
| 使うべき場面 | 避けるべき場面 |
|---|---|
| 連結・無向・重み付きグラフの最小全域木が必要なとき。 | グラフが有向、または最短経路が必要なとき - 代わりにダイクストラ法やベルマン・フォード法を使う。 |
グラフが密 (E が V² に近い) なとき; O(V²) の行列形式は単純で高速。 | グラフが疎で、辺がすでにソート済みまたはソートしやすいとき - クラスカル法の方が単純なことが多い。 |
| 木を一つの領域から外側へ成長させたいとき (例: ネットワークの逐次的なレイアウト)。 | グラフが非連結なとき - プリム法は一つの連結成分しか張らない; 最小全域森が必要になる。 |
| すでに隣接構造と優先度付きキューが利用できるとき。 | グローバルな辺集合にわたって閉路を検出する必要があるとき - union-find (クラスカル法) の方が適する。 |
Prim's Algorithmのコード
Python, JavaScript, Java, C++, Cによるクリーンで実行可能なPrim's Algorithmの実装です。言語を選んでコードをコピーするか、Coddyプレイグラウンドに読み込んだ状態で開けます。
PythonでのPrim's Algorithmのコード
Python
1import heapq2
3
4def prim(graph, start):5 visited = {start}6 heap = [(w, start, v) for v, w in graph[start]]7 heapq.heapify(heap)8 mst, total = [], 09 while heap and len(visited) < len(graph):10 w, u, v = heapq.heappop(heap)11 if v in visited:12 continue13 visited.add(v)14 mst.append((u, v, w))15 total += w16 # Offer the new node's edges to the frontier17 for neighbor, weight in graph[v]:18 if neighbor not in visited:19 heapq.heappush(heap, (weight, v, neighbor))20 return mst, total21
22
23graph = {24 "A": [("B", 4), ("C", 1)],25 "B": [("A", 4), ("C", 3), ("D", 2)],26 "C": [("A", 1), ("B", 3), ("D", 5)],27 "D": [("B", 2), ("C", 5), ("E", 7)],28 "E": [("D", 7)],29}30
31mst, total = prim(graph, "A")32for u, v, w in mst:33 print(f"{u} - {v} (weight {w})")34print("Total MST weight:", total)JavaScriptでのPrim's Algorithmのコード
JavaScript
1const graph = {2 A: { B: 4, C: 2 },3 B: { A: 4, C: 1, D: 5 },4 C: { A: 2, B: 1, D: 8, E: 10 },5 D: { B: 5, C: 8, E: 2 },6 E: { C: 10, D: 2 },7};8
9function prim(start) {10 const inTree = new Set([start]);11 const mst = [];12 let total = 0;13 while (inTree.size < Object.keys(graph).length) {14 // JS has no built-in heap: scan all crossing edges for the cheapest15 let best = null;16 for (const u of inTree) {17 for (const [v, w] of Object.entries(graph[u])) {18 if (!inTree.has(v) && (best === null || w < best[2])) {19 best = [u, v, w];20 }21 }22 }23 const [u, v, w] = best;24 inTree.add(v);25 mst.push(`${u}-${v} (${w})`);26 total += w;27 }28 return { mst, total };29}30
31const { mst, total } = prim("A");32console.log("MST edges:", mst.join(", "));33console.log("Total weight:", total);JavaでのPrim's Algorithmのコード
Java
1import java.util.ArrayList;2import java.util.List;3import java.util.PriorityQueue;4
5public class Main {6 public static void main(String[] args) {7 int n = 6;8 List<List<int[]>> adj = new ArrayList<>();9 for (int i = 0; i < n; i++) adj.add(new ArrayList<>());10 int[][] edges = {11 {0, 1, 4}, {0, 2, 3}, {1, 2, 1}, {1, 3, 2},12 {2, 3, 4}, {3, 4, 2}, {4, 5, 6}, {2, 5, 7}13 };14 for (int[] e : edges) {15 adj.get(e[0]).add(new int[]{e[1], e[2]});16 adj.get(e[1]).add(new int[]{e[0], e[2]});17 }18
19 boolean[] inMst = new boolean[n];20 // Always grow the tree along the cheapest crossing edge21 PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);22 pq.add(new int[]{0, 0}); // {node, edge weight}23 int total = 0, taken = 0;24 while (!pq.isEmpty() && taken < n) {25 int[] cur = pq.poll();26 if (inMst[cur[0]]) continue;27 inMst[cur[0]] = true;28 total += cur[1];29 taken++;30 for (int[] edge : adj.get(cur[0])) {31 if (!inMst[edge[0]]) pq.add(edge);32 }33 }34 System.out.println("MST nodes reached: " + taken);35 System.out.println("MST total weight: " + total);36 }37}C++でのPrim's Algorithmのコード
C++
1#include <iostream>2#include <queue>3#include <vector>4
5int main() {6 // Undirected weighted graph: adj[u] = {(neighbor, weight), ...}7 std::vector<std::vector<std::pair<int, int>>> adj = {8 {{1, 2}, {3, 6}}, // 09 {{0, 2}, {2, 3}, {3, 8}, {4, 5}}, // 110 {{1, 3}, {4, 7}}, // 211 {{0, 6}, {1, 8}}, // 312 {{1, 5}, {2, 7}}, // 413 };14 int n = static_cast<int>(adj.size());15 std::vector<bool> inMST(n, false);16 using State = std::pair<int, int>; // (edge weight, node)17 std::priority_queue<State, std::vector<State>, std::greater<State>> pq;18 pq.push({0, 0});19 int total = 0, picked = 0;20 // Always grow the tree along the cheapest crossing edge21 while (!pq.empty() && picked < n) {22 auto [w, u] = pq.top();23 pq.pop();24 if (inMST[u]) continue;25 inMST[u] = true;26 total += w;27 ++picked;28 std::cout << "Add node " << u << " (edge weight " << w << ")\n";29 for (auto [v, weight] : adj[u]) {30 if (!inMST[v]) pq.push({weight, v});31 }32 }33 std::cout << "MST total weight: " << total << "\n";34 return 0;35}CでのPrim's Algorithmのコード
C
1#include <stdbool.h>2#include <stdio.h>3
4#define N 55#define INF 10000000006
7int main(void) {8 // Undirected weighted graph: w[u][v] = 0 means no edge9 int w[N][N] = {10 {0, 2, 0, 6, 0},11 {2, 0, 3, 8, 5},12 {0, 3, 0, 0, 7},13 {6, 8, 0, 0, 0},14 {0, 5, 7, 0, 0},15 };16 int key[N], parent[N];17 bool inMST[N] = {false};18 for (int v = 0; v < N; v++) {19 key[v] = INF;20 parent[v] = -1;21 }22 key[0] = 0;23 int total = 0;24 // O(V^2) scan for the cheapest crossing edge (no heap needed here)25 for (int iter = 0; iter < N; iter++) {26 int u = -1;27 for (int v = 0; v < N; v++) {28 if (!inMST[v] && (u == -1 || key[v] < key[u])) u = v;29 }30 inMST[u] = true;31 total += key[u];32 if (parent[u] != -1) {33 printf("Add edge %d - %d (weight %d)\n", parent[u], u, key[u]);34 }35 for (int v = 0; v < N; v++) {36 if (w[u][v] > 0 && !inMST[v] && w[u][v] < key[v]) {37 key[v] = w[u][v];38 parent[v] = u;39 }40 }41 }42 printf("MST total weight: %d\n", total);43 return 0;44}プリム法に関するよくある質問
プリム法の時間計算量はどのくらいですか?
二分ヒープの優先度付きキューを使うと、プリム法は
O(E log V) で動作します。各ステップで最小の交差辺を走査する、より単純な隣接行列版は O(V²) で、密なグラフではこちらの方が速いことがあります。どちらも O(V + E) の空間を使います。プリム法とクラスカル法の違いは何ですか?
どちらも貪欲法で最小全域木を求めます。プリム法は開始ノードから一本の連結した木を成長させ、木から出る最も安い辺を (優先度付きキューを使って) 繰り返し追加します。クラスカル法は重み順にソートしたすべての辺を検討し、閉路を作らない最も安い辺を (union-find を使って) 追加します。プリム法は密なグラフで、クラスカル法は疎なグラフで好まれることが多いです。
プリム法は常に最適な MST を見つけますか?
はい。各ステップで現在の木を交差する最も安い辺は追加しても安全です - それは何らかの最小全域木に属します (カット性質)。安全な辺を繰り返し追加すると、グラフが連結である限り、真の最小全域木が得られます。
クラスカル法ではなくプリム法をいつ使うべきですか?
グラフが密なときはプリム法を選びましょう。
O(V²) の行列形式はすべての E 本の辺をソートせずに済み、開始ノードから一本の木を成長させるからです。クラスカル法は、辺リストのソートが安価で union-find が閉路チェックを高速に保てる疎なグラフで真価を発揮します。どちらも正しい MST を生成するので、選択は主に辺の密度と、すでに持っているデータ構造次第です。開始ノードは得られる MST に影響しますか?
いいえ - 連結グラフの最小全域木の重みは一意なので、どのノードから始めても MST の総重みは同じです。具体的な辺が異なりうるのは、複数の辺が同じ重みを持ち、複数の MST が存在する場合だけです。それ以外の場合、プリム法は開始ノードに関係なくまったく同じ木に到達します。
プリム法は非連結グラフを扱えますか?
直接には扱えません。プリム法は一本の木を成長させるため、開始ノードを含む連結成分しか張らず、残りには触れません。各成分をカバーするには、未訪問のノードからプリム法を繰り返し実行し、一本の木ではなく最小全域森を生成することになります。