C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++のSTLを活用したクラスカル法による最小全域木(MST)の実装


本記事では、C++のSTL(標準テンプレートライブラリ)を活用して、クラスカル法(Kruskal's Algorithm)により最小全域木(MST: Minimum Spanning Tree)を求めるプログラムについて詳しく解説します。

クラスカル法では、連結された無向の重み付きグラフが入力として与えられ、そのグラフから最小全域木を計算することを目的とします。

クラスカル法とは

クラスカル法は貪欲法(グリーディ法)に基づく代表的な最小全域木アルゴリズムです。以下の手順で動作します。

  1. グラフのすべての辺を重みの昇順にソートします。
  2. 重みの小さい辺から順に取り出し、その辺を採用してもサイクルが発生しない場合のみ全域木に追加します。
  3. サイクルの判定には、Union-Find(素集合データ構造 / Disjoint Set)を使用します。
  4. 採用した辺の数が「頂点数 − 1」に達した時点で、最小全域木の完成です。

この手法により、すべての頂点を最小のコストで連結できることが保証されます。

C++での実装例

以下のコードでは、グラフを表す構造体とDisjointSets(Union-Find)構造体を定義し、STLの vector および sort を利用してクラスカル法を実装しています。

#include<bits/stdc++.h>
using namespace std;
typedef pair<int, int> iPair;
// グラフを表す構造体
struct Graph{
    int V, E;
    vector< pair<int, iPair> > edges;
    Graph(int V, int E){
        this->V = V;
        this->E = E;
    }
    // 辺を追加する(重みw、頂点uとvを結ぶ)
    void addEdge(int u, int v, int w){
        edges.push_back({w, {u, v}});
    }
    int kruskalMST();
};
// Union-Find(素集合データ構造)
struct DisjointSets{
    int *parent, *rnk;
    int n;
    DisjointSets(int n){
        this->n = n;
        parent = new int[n+1];
        rnk = new int[n+1];
        for (int i = 0; i <= n; i++){
            rnk[i] = 0;
            parent[i] = i;
        }
    }
    // 属する集合の代表元を検索(経路圧縮あり)
    int find(int u){
        if (u != parent[u])
            parent[u] = find(parent[u]);
        return parent[u];
    }
    // 2つの集合をランクに基づいて統合
    void merge(int x, int y){
        x = find(x), y = find(y);
        if (rnk[x] > rnk[y])
            parent[y] = x;
        else
            parent[x] = y;
        if (rnk[x] == rnk[y])
            rnk[y]++;
    }
};
// クラスカル法によるMSTの計算
int Graph::kruskalMST(){
    int mst_wt = 0;
    // 辺を重みの昇順にソート
    sort(edges.begin(), edges.end());
    DisjointSets ds(V);
    vector< pair<int, iPair> >::iterator it;
    for (it=edges.begin(); it!=edges.end(); it++){
        int u = it->second.first;
        int v = it->second.second;
        int set_u = ds.find(u);
        int set_v = ds.find(v);
        // サイクルを形成しない場合のみ採用
        if (set_u != set_v){
            cout << u << " - " << v << endl;
            mst_wt += it->first;
            ds.merge(set_u, set_v);
        }
    }
    return mst_wt;
}
int main(){
    int V = 9, E = 14;
    Graph g(V, E);
    g.addEdge(0, 1, 4);
    g.addEdge(0, 7, 8);
    g.addEdge(1, 2, 8);
    g.addEdge(1, 7, 11);
    g.addEdge(2, 3, 7);
    g.addEdge(2, 8, 2);
    g.addEdge(2, 5, 4);
    g.addEdge(3, 4, 9);
    g.addEdge(3, 5, 14);
    g.addEdge(4, 5, 10);
    g.addEdge(5, 6, 2);
    g.addEdge(6, 7, 1);
    g.addEdge(6, 8, 6);
    g.addEdge(7, 8, 7);
    cout << "Edges of MST are \n";
    int mst_wt = g.kruskalMST();
    cout << "\nWeight of MST is " << mst_wt;
    return 0;
}

出力結果

Edges of MST are
6 - 7
2 - 8
5 - 6
0 - 1
2 - 5
2 - 3
0 - 7
3 - 4
Weight of MST is 37

コードの解説

  • Graph構造体: 頂点数V・辺数Eを保持し、各辺を (重み, (始点, 終点)) のペアとして vector に格納します。重みを先頭にすることで、sort を呼ぶだけで重み昇順に並び替えられます。
  • DisjointSets構造体: find 関数は経路圧縮を行い、merge 関数はランク(木の高さ)に基づいて効率的に集合を統合します。これにより、ほぼ一定時間でサイクル判定が可能になります。
  • kruskalMST関数: ソート済みの辺を順に走査し、両端点が異なる集合に属している(=サイクルにならない)場合のみ、その辺をMSTに採用して重みを加算します。

このアルゴリズムの計算量は、辺のソートが支配的となるため O(E log E) です。稠密なグラフよりも疎なグラフに対して特に有効な手法といえます。

  1. C++でツリーノードを削除する:合計値が0の部分木を除去するアルゴリズム

    問題概要根がノード0であるような木構造を考えます。この木には、次の情報が与えられています。ノードの総数:nodesi番目のノードの値:value[i]i番目のノードの親:parent[i]求めたいのは、「ノードの値の合計が0になる部分木」をすべて削除した後、木に残っているノードの個数です。たとえば、下図のような木を考えてみましょう。ノードは全部で7つありますが、出力は2になります。これは、値が0であるノード3を根とする部分木と、ノード2を根とする部分木(4 + (-2) + (-1) + (-1) = 0)が削除対象となり、最終的に残るのがノード0とノード1だけだからです。解法の考え方この問題

  2. C++で木の直径を求めるアルゴリズムを解説

    木の直径とは無向木(undirected tree)が与えられたとき、その直径を求めることを考えます。木の直径とは、木の中で最も長い経路に含まれる辺の数のことです。ここでは、木は辺のリストとして与えられます。edges[i] = [u, v] は、ノードuとノードvをつなぐ双方向の辺を表します。また、各ノードには {0, 1, ..., edges.length} の集合からラベルが割り当てられています。例として、次のような木を考えてみましょう。この場合、最も長い経路の長さは4となるため、出力は4になります。解法のアプローチ木の直径を効率的に求めるには、DFS(深さ優先探索)を2回実行するとい