基于Kruskal和Prime算法的最小生成树算法

本文介绍了Prim算法、Kruskal算法和Boruvka算法,用于在图中寻找最小生成树,分析了它们的时间复杂度和空间复杂度,以及适用场景。Prim适合稠密图,Kruskal适合稀疏图,Boruvka是一种并行算法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

常见的最小生成树算法包括:

  1. Prim算法:基于顶点的贪婪算法,从一个初始顶点开始,逐步选择与当前生成树相连的权值最小的边所连接的顶点,直到所有顶点都被包含在生成树中。

  2. Kruskal算法:基于边的贪婪算法,首先将所有边按权值从小到大排序,然后逐步选择权值最小的边,若加入该边不构成环,则将其加入生成树中,直到生成树包含了所有顶点。

  3. Boruvka算法:一种并行算法,通过每次选择每个连通分量的最小边来构建生成树,直到只剩下一个连通分量为止。

这些算法都是用来寻找一个图中的最小生成树,即连接所有顶点且总权值最小的树。

  • Prim算法:

    • 时间复杂度: Prim算法的时间复杂度通常是O(n^2),其中n是顶点的数量。这是因为算法需要遍历所有的边来找到最小权重的边,而在一个稠密图中,边的数量接近于n^2。如果使用优先队列(如二叉堆)来优化,时间复杂度可以降低到O(m log n),其中m是边的数量。
    • 空间复杂度: Prim算法的空间复杂度主要取决于存储图的数据结构和并查集的大小。如果使用邻接矩阵存储图,则空间复杂度为O(n^2);如果使用邻接表,则为O(n + m)。并查集的空间复杂度为O(n)。
  • Kruskal算法:

    • 时间复杂度: Kruskal算法的时间复杂度为O(m log m),因为它需要对所有的边进行排序,然后遍历这些边来构建最小生成树。这里的m是边的数量,排序通常使用快速排序或归并排序,它们的时间复杂度都是O(m log m)。
    • 空间复杂度: Kruskal算法的空间复杂度同样取决于图的表示方法和并查集的大小。如果使用邻接矩阵,空间复杂度为O(n^2);使用邻接表则为O(n + m)。并查集的空间复杂度为O(n)。

综上所述,Prim算法更适合求解稠密图的最小生成树问题,而Kruskal算法更适合求解稀疏图的最小生成树问题。

基于Kruskal算法的最小生成树算法:

Kruskal算法是一种贪心算法,用于求解最小生成树问题。它的基本步骤如下:

  1. 将所有边按照权值从小到大排序。
  2. 初始化一个空的森林,用于存储最小生成树的边。
  3. 遍历排序后的边,对于每一条边,检查它连接的两个顶点是否属于同一个连通分量。如果不属于,则将这条边加入森林中,并将两个顶点所在的连通分量合并。
  4. 当森林中的边数等于顶点数减一时,停止遍历。此时得到的森林就是最小生成树。

    另外注意一件事,所谓的并查集查找,可以通俗地将其看作从某个结点开始一路查找到根,而这个根节点就是当下节点所在树的集合的代表。
    因此,在这个最小生成树里,每次找到的边要通过查看点是否与已经纳入到正在构成的最小树里(即查看当前点的根与最小树的根是否一致)来避免形成环。
  5. 算法的时间复杂度为:O(ElogE)或者O(ElogV),其中E代表图中的边的数目,V代表图中的顶点数目。对图中的边按照非降序排列需要O(ElogE)的时间。排序后遍历所有的边并判断添加边是否构成环,判断添加一条边是否构成环最坏情况下需要O(logV),关于这个复杂度等到景禹给你们谈并查集的时候再分析;因此,总的时间复杂度为O(ElogE + ElogV),其中E的值最大为V(V-1)/2,因此O(logV) 等于 O(logE)。因此,总的时间复杂度为O(ElogE) 或者O(ElogV)。

    参考:算法学习笔记(1) : 并查集 - 知乎 (zhihu.com)
  6. Kruskal算法的基本思想是维护一个森林,查询两个结点是否在同一棵树中,并连接两棵树。在实际的算法过程中,我们需要对边集进行排序,复杂度O ( mlogm ) O(m\log m)O(mlog m),并使用O ( mlog ⁡n ) O(m \log n)O(mlogn)并查集维护集合。总复杂度O ( m log ⁡ m ) O(m \log m)O(mlogm)。因此对于稠密图,算法性能就会变差。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

//边,u、v代表边的起点和重点,w代表权重
struct Edge {
    int u, v, w;
    bool operator<(const Edge& other) const {
        return w < other.w;
    }
};

class UnionFind {
public:
    UnionFind(int n) {
        parent.resize(n);
        rank.resize(n, 0);
        for (int i = 0; i < n; ++i) {
            parent[i] = i;
        }
    }

    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    bool unite(int x, int y) {
        int root_x = find(x);
        int root_y = find(y);
        if (root_x == root_y) {
            return false;
        }
        if (rank[root_x] < rank[root_y]) {
            parent[root_x] = root_y;
        } else {
            parent[root_y] = root_x;
            if (rank[root_x] == rank[root_y]) {
                ++rank[root_x];
            }
        }
        return true;
    }

private:
    vector<int> parent;
    vector<int> rank;
};

vector<Edge> kruskal(const vector<Edge>& edges, int n) {
    sort(edges.begin(), edges.end());
    UnionFind uf(n);
    vector<Edge> mst;
    for (const Edge& e : edges) {
        if (uf.unite(e.u, e.v)) {
            mst.push_back(e);
        }
    }
    return mst;
}

int main() {
    int n, m;
    cin >> n >> m;
    vector<Edge> edges(m);
    for (int i = 0; i < m; ++i) {
        cin >> edges[i].u >> edges[i].v >> edges[i].w;
        --edges[i].u;
        --edges[i].v;
    }
    vector<Edge> mst = kruskal(edges, n);
    cout << "Minimum Spanning Tree:" << endl;
    for (const Edge& e : mst) {
        cout << e.u + 1 << " - " << e.v + 1 << " : " << e.w << endl;
    }
    return 0;
}

基于Prim算法的最小生成树算法:

        普里姆算法在找最小生成树时,将顶点分为两类,一类是在查找的过程中已经包含在生成树中的顶点(假设为 A 类),剩下的为另一类(假设为 B 类)。 

        对于给定的连通网,起始状态全部顶点都归为 B 类。在找最小生成树时,选定任意一个顶点作为起始点,并将之从 B 类移至 A 类;然后找出 B 类中到 A 类中的顶点之间权值最小的顶点,将之从 B 类移至 A 类,如此重复,直到 B 类中没有顶点为止。所走过的顶点和边就是该连通图的最小生成树。

该算法的时间复杂度是O(n^2)

图解:什么是最小生成树? - 知乎 (zhihu.com)

        Prim算法的基本思想是每次需要寻找距离最小的一个结点(与Dijkstra’s Algorithm相似),以及新的边来更新其它结点的距离。在寻找距离最小点的过程中,可以暴力查找,也可以采用堆维护进行优化。在使用二叉堆优化的加持下,复杂度O ( ( n + m ) log ⁡ n ) O((n + m)\log n)O((n+m)logn)。相比于K r u s k a l KruskalKruskal,P r i m PrimPrim更适用于稠密图

#include <iostream>
#include <vector>
#include <climits>

using namespace std;

const int V = 5; // 顶点个数

int minKey(int key[], bool mstSet[]) {
    int min = INT_MAX, min_index;

    for (int v = 0; v < V; v++) {
        if (mstSet[v] == false && key[v] < min) {
            min = key[v];
            min_index = v;
        }
    }

    return min_index;
}

void printMST(vector<int> parent, vector<vector<int>> graph) {
    cout << "边\t权值" << endl;
    for (int i = 1; i < V; i++) {
        cout << parent[i] << " - " << i << "\t" << graph[i][parent[i]] << endl;
    }
}

void primMST(vector<vector<int>> graph) {
    vector<int> parent(V); // 存储最小生成树的父节点
    vector<int> key(V); // 存储每个顶点到最小生成树的最小权值
    bool mstSet[V]; // 记录顶点是否已经加入最小生成树

    for (int i = 0; i < V; i++) {
        key[i] = INT_MAX;
        mstSet[i] = false;
    }

    key[0] = 0; // 将第一个顶点作为起始点
    parent[0] = -1; // 第一个顶点没有父节点

    for (int count = 0; count < V - 1; count++) {
        int u = minKey(key, mstSet);
        mstSet[u] = true;

        for (int v = 0; v < V; v++) {
            if (graph[u][v] && mstSet[v] == false && graph[u][v] < key[v]) {
                parent[v] = u;
                key[v] = graph[u][v];
            }
        }
    }

    printMST(parent, graph);
}

int main() {
    vector<vector<int>> graph = {
        {0, 2, 0, 6, 0},
        {2, 0, 3, 8, 5},
        {0, 3, 0, 0, 7},
        {6, 8, 0, 0, 9},
        {0, 5, 7, 9, 0}
    };

    primMST(graph);

    return 0;
}

基于Boruvka算法的最小生成树算法:

        一种并行算法,通过每次选择每个连通分量的最小边来构建生成树,直到只剩下一个连通分量为止。

        对于BoruvkaBoruvka算法,一个比较笼统的表述是,一个多路增广版本的 Kruskal。它的思想是一开始所有点看做独立子集,每次遍历边找到两个集合(连通块)之间连接的最短边,不断扩大集合(连通块)直到所有点合并为一个集合(连通块)。

        在并查集算法中,初始状态下我们将每个点视为一个独立的点集,并不断地合并集合。在B r o u v k a BrouvkaBrouvka算法中,我们在一开始将所有点视为独立子集,每次我们找到两个集合(即为连通块)之间的最短边,然后扩展连通块进行合并。基本思路是:生成树中所有顶点必然是连通的,所以两个不相交集必须连接起来才能构成生成树,而且所选择的连接边的权重必须最小,才能得到最小生成树。

        首先将所有点视为各自独立的集合,初始化一个空的M S T MSTMST;
当子集个数大于1 11的时候,对各个子集和执行以下操作:

  1. 找到与当前集合有边的集合,选出权值最小的边;
  2. 如果该权值最小的边不在M S T MSTMST中;

#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
using namespace std;

// 定义边的结构体
struct Edge {
    int src, dest, weight;
};

// 定义并查集的结构体
struct Subset {
    int parent, rank;
};

// 查找顶点的根节点
int find(Subset subsets[], int i) {
    if (subsets[i].parent != i)
        subsets[i].parent = find(subsets, subsets[i].parent);
    return subsets[i].parent;
}

// 合并两个子集
void Union(Subset subsets[], int x, int y) {
    int xroot = find(subsets, x);
    int yroot = find(subsets, y);

    if (subsets[xroot].rank < subsets[yroot].rank)
        subsets[xroot].parent = yroot;
    else if (subsets[xroot].rank > subsets[yroot].rank)
        subsets[yroot].parent = xroot;
    else {
        subsets[yroot].parent = xroot;
        subsets[xroot].rank++;
    }
}

// Boruvka算法实现最小生成树
void boruvkaMST(vector<Edge>& edges, int V) {
    vector<Edge> result; // 存储最小生成树的边
    vector<Subset> subsets(V); // 并查集数组

    // 初始化并查集
    for (int v = 0; v < V; ++v) {
        subsets[v].parent = v;
        subsets[v].rank = 0;
    }

    // 迭代直到所有顶点都在同一个连通分量中
    while (result.size() < V - 1) {
        int minWeight = INT_MAX;
        int minIndex = -1;

        // 遍历所有的边,找到权重最小的边
        for (int i = 0; i < edges.size(); ++i) {
            int set1 = find(subsets, edges[i].src);
            int set2 = find(subsets, edges[i].dest);

            if (set1 != set2) {
                if (edges[i].weight < minWeight) {
                    minWeight = edges[i].weight;
                    minIndex = i;
                }
            }
        }

        // 将找到的边加入结果中,并合并两个顶点所在的集合
        result.push_back(edges[minIndex]);
        Union(subsets, edges[minIndex].src, edges[minIndex].dest);
    }

    // 输出最小生成树的边
    cout << "最小生成树的边为:" << endl;
    for (const auto& edge : result) {
        cout << edge.src << " -- " << edge.dest << " == " << edge.weight << endl;
    }
}

int main() {
    int V = 4; // 顶点数
    vector<Edge> edges = {
        {0, 1, 10},
        {0, 2, 6},
        {0, 3, 5},
        {1, 3, 15},
        {2, 3, 4}
    }; // 边的列表,每个元素包含源顶点、目标顶点和权重

    boruvkaMST(edges, V);

    return 0;
}

运行结果如下:
 

最小生成树的边为:
2 -- 3 == 4
0 -- 3 == 5
0 -- 1 == 10

Boruvka算法是一种基于并查集的贪心算法,用于求解最小生成树问题。算法的基本思想是每次从图中选择一条权重最小的边,将其加入到最小生成树中,并将这条边的两个顶点所在的集合合并。重复这个过程,直到所有顶点都在同一个连通分量中,此时得到的最小生成树就是最终结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值