前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【数据结构与算法】最小生成树算法实现:Prim && Kruskal

【数据结构与算法】最小生成树算法实现:Prim && Kruskal

作者头像
利刃大大
发布于 2025-03-16 12:47:43
发布于 2025-03-16 12:47:43
3500
代码可运行
举报
文章被收录于专栏:csdn文章搬运csdn文章搬运
运行总次数:0
代码可运行

Ⅰ. 最小生成树

​ 连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不再连通;反之,在其中引入任何一条新边,都会形成一条回路。

若连通图由 n 个顶点组成,则其生成树必含 n 个顶点和 n-1 条边。因此构造最小生成树的准则有三条:

  1. 只能使用图中的边来构造最小生成树
  2. 只能使用恰好 n-1 条边来连接图中的 n 个顶点
  3. 选用的 n-1 条边不能构成回路

构造最小生成树的方法:Kruskal 算法和 Prim 算法。这两个算法都采用了逐步求解的贪心策略。

​ 贪心算法:是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的的选择,而是某种意义上的局部最优解。贪心算法不是对所有的问题都能得到整体最优解(也就是说这两种算法并不是万能的)

🔴 并且 最小生成树是不唯一的

在这里插入图片描述
在这里插入图片描述

Ⅱ. Kruskal算法

​ 任给一个有 n 个顶点的连通网络 N={V,E},首先构造一个由这 n 个顶点组成、不含任何边的图 G={V,NULL},其中每个顶点自成一个连通分量,其次不断从 E 中取出权值最小的一条边 ( 若有多条任取其一 ) ,若该边的两个顶点来自不同的连通分量,则将此边加入到 G 中。

​ 如此重复,直到所有顶点在同一个连通分量上为止。

​ 核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。

​ 具体实现的时候,由于考虑到每次都要选最小的一条边,那这里就用 优先级队列,也就是一个堆,来进行存放,并且是一个小堆每次堆顶是最小的边一般来说我们操作的一般是无向图,所以只需要将矩阵中的上三角行列式中的边入队列即可~

​ 除此之外,还要 判断连接起来的边会不会形成环路,这个时候我们就可以用 并查集 来判断,每次将选择的边对应的邻接顶点加入到并查集中,然后每次新增边的时候 判断一下新增的边引入的邻接顶点是否已经在并查集中,是的话说明形成回路了,则不选这条边~ (🦅 并查集的具体实现翻笔记查找 )

​ 另外我们还需要单独 弄个结构体包装一下边的属性

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 为下面一些算法如Kruskal算法做准备的
struct Edge
{
    size_t _srci;
    size_t _dsti;
    W _weight;

    Edge(size_t srci, size_t dsti, const W& weight)
        :_srci(srci)
        , _dsti(dsti)
        , _weight(weight)
    {}

    // 下面要比较边的大小,所以要重载一下比较
    bool operator>(const Edge& e) const
    {
        return _weight > e._weight;
    }
};

Kruskal 算法如下所示

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 下面Kruskal算法接收的参数需要用到默认构造函数
Graph() = default;

typedef Graph<V, W, MAX_W, Direction> Self;

// Kruskal算法
// 下面的minTree是接收的一般是未初始化的图,所以我们要有默认的构造函数
W Kruskal(Self& minTree)
{
    // 初始化一下最小生成树的模板
    size_t n = _vertexs.size();
    minTree._vIndexMap = _vIndexMap;
    minTree._vertexs = _vertexs;
    minTree._matrix.resize(n);
    for (size_t i = 0; i < n; ++i)
    {
        // 默认这些顶点都是不相通的
        minTree._matrix[i].resize(n, MAX_W);
    }

    // 由于我们要从小到大排序,所以得传仿函数过去
    priority_queue<Edge, vector<Edge>, greater<Edge>> pq;

    // 将矩阵中的边入队列
    for (size_t i = 0; i < n; ++i)
    {
        for (size_t j = 0; j < n; ++j)
        {
            // 由于是无向图,我们只需要将上三角行列式中的边加入即可
            if (i < j && _matrix[i][j] != MAX_W)
            {
                pq.push(Edge(i, j, _matrix[i][j]));
            }
        }
    }

    // 贪心算法,从最小的边开始选
    int size = 0;    // 选出n-1条边,所以用size来计数
    W sum = W();     // 最后结束的时候返回最小生成树的总权值
    UnionFindSet ufs(n);    // 并查集

    while (!pq.empty())
    {
        Edge min = pq.top();
        pq.pop();

        // 判断是否成环
        if (!ufs.IsInSameSet(min._srci, min._dsti))
        {
            cout << minTree._vertexs[min._srci] << "->" <<minTree._vertexs[min._dsti] << ":" << min._weight << endl;

            // 将边加上去(注意这里调用的是_AddEdge,接收的参数是下标而不是顶点的版本)
            minTree._AddEdge(min._srci, min._dsti, min._weight);

            // 再将两个点算入并查集
            ufs.Union(min._srci, min._dsti);

            sum += min._weight;
            ++size;
        }
        else
        {
            cout << "构成环: ";
            cout << minTree._vertexs[min._srci] << "->" <<minTree._vertexs[min._dsti] << ":" << min._weight << endl;
        }
    }

    // 若成功生成最小生成树则返回sum,否则返回默认值
    if (size == n - 1)
        return sum;
    else
        return W();
}

​ 我们还要对 AddEdge 函数修改一下,因为我们在 Kruskal 算法中传过去的是下标,但是我们之前的 AddEdge 接收的是顶点,所以我们稍微修改一下~

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// 下面函数的子函数,这个接收的参数是下标,这是为了给下面的算法用的
void _AddEdge(size_t srci, size_t dsti, const W& w)
{
    _matrix[srci][dsti] = w;
    // 判断是否为无向图,是的话矩阵要对称赋值
    if (Direction == false)
    {
        _matrix[dsti][srci] = w;
    }
}

// 链接边的函数,接收的参数是顶点
void AddEdge(const V& src, const V& dst, const W& w)
{
    size_t srci = GetVertexIndex(src);
    size_t dsti = GetVertexIndex(dst);

    _AddEdge(srci, dsti, w);
}

​ 下面实现中的测试样例如下图所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void TestGraphMinTree()
{
    const char* str = "abcdefghi";
    Graph<char, int> g(str, strlen(str));
    g.AddEdge('a', 'b', 4);
    g.AddEdge('a', 'h', 8);
    g.AddEdge('a', 'h', 9);
    g.AddEdge('b', 'c', 8);
    g.AddEdge('b', 'h', 11);
    g.AddEdge('c', 'i', 2);
    g.AddEdge('c', 'f', 4);
    g.AddEdge('c', 'd', 7);
    g.AddEdge('d', 'f', 14);
    g.AddEdge('d', 'e', 9);
    g.AddEdge('e', 'f', 10);
    g.AddEdge('f', 'g', 2);
    g.AddEdge('g', 'h', 1);
    g.AddEdge('g', 'i', 6);
    g.AddEdge('h', 'i', 7);

    Graph<char, int> kminTree;
    cout << "Kruskal:" << g.Kruskal(kminTree) << endl;
    kminTree.Print();
}

// 运行结果----------------------------------------------------------------------
g->h:1
c->i:2
f->g:2
c->f:4
a->b:4
构成环: g->i:6
构成环: h->i:7
c->d:7
b->c:8
构成环: a->h:9
d->e:9
构成环: e->f:10
构成环: b->h:11
构成环: d->f:14
Kruskal:37
[0]->a
[1]->b
[2]->c
[3]->d
[4]->e
[5]->f
[6]->g
[7]->h
[8]->i

     0   1   2   3   4   5   6   7   8
0    *   4   *   *   *   *   *   *   *
1    4   *   8   *   *   *   *   *   *
2    *   8   *   7   *   4   *   *   2
3    *   *   7   *   9   *   *   *   *
4    *   *   *   9   *   *   *   *   *
5    *   *   4   *   *   *   2   *   *
6    *   *   *   *   *   2   *   1   *
7    *   *   *   *   *   *   1   *   *
8    *   *   2   *   *   *   *   *   *

Ⅲ. Prim算法

​ 除了 Kruskal 算法以外,普里姆 Prim 算法也是常用的最小生成树算法。虽然在效率上差不多。但是贪心的方式和 Kruskal 完全不同。

prim 算法的核心信仰是:从已知顶点集合,扩散寻找最短距离的边。它的实现方式和 Dijkstra算法相似但稍微有所区别,Dijkstra 是求单源最短路径。而每计算一个点需要对这个点从新更新距离。而 prim 甚至不用更新距离。直接找已知点的邻边最小加入即可!

Prim 算法原理:

  1. 以某一个点开始,寻找当前该点可以访问的所有的边
  2. 在已经寻找的边中发现最小边,这个边必须有一个点还没有访问过,将还没有访问的点加入我们的集合,记录添加的边
  3. 寻找当前集合可以访问的所有边,重复步骤 2 的过程,直到没有新的点可以加入
  4. 此时由所有边构成的树即为最小生成树

​ 在实现过程中,考虑到每次要挑选已有点的边的最小权值问题,所以我们还是得用 优先级队列,效率会比较高一些!但是要注意的是,我们 不能一开始就将所有的边入队列,因为 Prim 算法是通过顶点来找边的,只能找已经确认的点的邻接边,所以我们只能边走边将邻接边入队列

​ 除此之外,我们还 需要判断一下加入的边会不会构成环,考虑到 Prim 算法与 Kruskal 算法不同的点也在于 Prim 算法是 以顶点为对象的,所以我们时时刻刻都知道哪些点是已经确定的,所以我们可以用一个 vector<bool> vState 来标记这些顶点true 表示这些顶点在这个集合,false 表示这些顶点不在!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// Prim算法
// minTree是接收的未初始化的图,所以我们要有默认的构造函数
// src是首个顶点
W Prim(Self& minTree, const V& src)
{
    size_t srci = GetVertexIndex(src);

    // 初始化图,与Kruskal算法一样
    size_t n = _vertexs.size();
    minTree._vertexs = _vertexs;
    minTree._vIndexMap = _vIndexMap;
    minTree._matrix.resize(n);
    for (size_t i = 0; i < n; ++i)
        minTree._matrix[i].resize(n, MAX_W);

    // 使用vector来存储顶点的确认情况:是否在生成树中已经确认(默认是不确认)
    // true表示确认存在
    // false表示还不确认存在
    vector<bool> vState(n, false);
    vState[srci] = true;    // 注意将首个顶点设置为true

    // 初始化优先级队列(与Kruskal算法一样)
    // 先将与首个顶点邻接的边入队列
    priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
    for (size_t i = 0; i < n; ++i)
    {
        if (_matrix[srci][i] != MAX_W)
            pq.push(Edge(srci, i, _matrix[srci][i]));
    }

    size_t size = 0;
    W sum = W();
    cout << "Prim开始选边" << endl;
    while (!pq.empty())
    {
        Edge min = pq.top();
        pq.pop();

        // 判断一下是否成环
        // 若这条边的目标节点下标是已经确认的,说明是成环的,反之不成环
        if (vState[min._dsti] == true)
        {
            cout << "构成环:";
            cout << minTree._vertexs[min._srci] << "->" <<minTree._vertexs[min._dsti] << ":" << min._weight << endl;
        }
        else
        {
            cout << minTree._vertexs[min._srci] << "->" <<minTree._vertexs[min._dsti] << ":" << min._weight << endl;

            // 将边添加到生成树中去	
            minTree._AddEdge(min._srci, min._dsti, min._weight);
            vState[min._dsti] = true;  // 将目的顶点设为确认
            sum += min._weight;
            size++;

            if (size == n - 1)
                break;

            // 将目的顶点的邻接边入队列,继续循环
            for (size_t i = 0; i < n; ++i)
            {
                // 若它的邻接边是存在的且该目的顶点还未被确认,则入队列
                if (_matrix[min._dsti][i] != MAX_W && vState[i] == false)
                    pq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
            }
        }
    }

    // 若成功生成最小生成树则返回sum,否则返回默认值
    if (size == n - 1)
        return sum;
    else
        return W();
}

​ 下面实现中的测试样例如下图所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void TestGraphMinTree()
{
    const char* str = "abcdefghi";
    Graph<char, int> g(str, strlen(str));
    g.AddEdge('a', 'b', 4);
    g.AddEdge('a', 'h', 8);
    //g.AddEdge('a', 'h', 9);
    g.AddEdge('b', 'c', 8);
    g.AddEdge('b', 'h', 11);
    g.AddEdge('c', 'i', 2);
    g.AddEdge('c', 'f', 4);
    g.AddEdge('c', 'd', 7);
    g.AddEdge('d', 'f', 14);
    g.AddEdge('d', 'e', 9);
    g.AddEdge('e', 'f', 10);
    g.AddEdge('f', 'g', 2);
    g.AddEdge('g', 'h', 1);
    g.AddEdge('g', 'i', 6);
    g.AddEdge('h', 'i', 7);

    Graph<char, int> pminTree;
    cout << "Prim:" << g.Prim(pminTree, 'a') << endl << endl;
    pminTree.Print();
}

// 运行结果
Prim开始选边
a->b:4
a->h:8
h->g:1
g->f:2
f->c:4
c->i:2
构成环:g->i:6
c->d:7
构成环:h->i:7
构成环:b->c:8
d->e:9
Prim:37
    
[0]->a
[1]->b
[2]->c
[3]->d
[4]->e
[5]->f
[6]->g
[7]->h
[8]->i

     0   1   2   3   4   5   6   7   8
0    *   4   *   *   *   *   *   8   *
1    4   *   *   *   *   *   *   *   *
2    *   *   *   7   *   4   *   *   2
3    *   *   7   *   9   *   *   *   *
4    *   *   *   9   *   *   *   *   *
5    *   *   4   *   *   *   2   *   *
6    *   *   *   *   *   2   *   1   *
7    8   *   *   *   *   *   1   *   *
8    *   *   2   *   *   *   *   *   *

Ⅳ. 两种最小生成树算法的区别

​ 两种算法其实在效率是差不多的,只不过实现的方式是不一样的,具体问题具体分析!

🔴 总的来说,Prim 算法是 以顶点为对象,挑选与点相连的最短边来构成最小生成树。而 Kruskal 算法是 以边为对象,不断地加入新的不构成环路的最短边来构成最小生成树。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【c++高阶DS】最小生成树
连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路,且这些边的权值之和最小
用户11029103
2024/12/28
1670
【c++高阶DS】最小生成树
图详解第三篇:最小生成树(Kruskal算法+Prim算法)
连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。
YIN_尹
2024/01/23
3K0
图详解第三篇:最小生成树(Kruskal算法+Prim算法)
图的基本概念以及DFS与BFS算法
顶点和边:图中结点称为顶点,第 i 个顶点记作 vi。两个顶点 vi 和 vj 相关联称作顶点 vi 和顶点 vj 之间有一条边,图中的第 k 条边记作 ek,ek = (vi,vj) 或 <vi,vj>。
利刃大大
2023/04/12
6340
图的基本概念以及DFS与BFS算法
最小生成树算法(下)——Kruskal(克鲁斯卡尔)算法
在我的上一篇文章最小生成树算法(上)——Prim(普里姆)算法 主要讲解对于稠密图较为合适的Prim算法。那么在接下里这片文章中我主要讲解对于稀疏图较为合适的Kruskal算法。
AI那点小事
2020/04/20
1.3K0
最小生成树算法(下)——Kruskal(克鲁斯卡尔)算法
文心一言 VS 讯飞星火 VS chatgpt (337)-- 算法导论23.1 6题
假设图 G 的每个切割都包含一条横跨该切割的唯一轻量级边(即最小权重的边)。我们需要证明 G 存在一棵唯一的最小生成树。
福大大架构师每日一题
2024/09/06
760
文心一言 VS 讯飞星火 VS chatgpt (337)-- 算法导论23.1 6题
C++ Prim和 Kruskal 求最小生成树算法
生成树:在图中找一棵包含图中的所有节点的树,生成树是含有图中所有顶点的无环连通子图。所有可能的生成树中,权重和最小的那棵生成树就叫最小生成树。在无向加权图中计算最小生成树,使用最小生成树算法的现实场景中,图的边权重一般代表成本、距离这样的标量。
一枚大果壳
2023/11/07
2870
C++ Prim和 Kruskal 求最小生成树算法
【c++高阶DS】图
比如:某公司今年校招全国总共招生10人,西安招4人,成都招3人,武汉招3人,10个人来自不同的学校,起先互不相识,每个学生都是一个独立的小团体,现给这些学生进行编号:{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; 给以下数组用来存储该小集体,数组中的数字代表:该小集体中具有成员的个数
用户11029103
2024/12/25
820
【c++高阶DS】图
【算法与图】通向高效解决方案的钥匙
BFS(广度优先搜索)是一种图的遍历算法,用于从一个起始节点出发,逐层访问图中的所有节点。其基本流程如下:
用户11305458
2024/10/09
3980
【算法与图】通向高效解决方案的钥匙
图论基础,如何快速上手图论?
前面我们学过了一些基本的数据结构,像顺序表,链表,栈,队列,树等...其中最有难度的就属树的部分了,而图论的与树也是有关联的,在后续我们经常可以看到一些图类似树,但是又不是树,他们的区别在哪?二叉树是父亲节点和孩子的关联,是从上到下的,而图没有父亲节点和孩子节点,他主要使用节点描述各个事件之间的关系;
啊QQQQQ
2025/01/20
780
图论基础,如何快速上手图论?
DS高阶:图论算法经典应用
连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。
小陈在拼命
2024/05/06
1120
DS高阶:图论算法经典应用
【数据结构】图论基础
图(Graph)是离散数学中的一种重要数据结构,用于描述对象(称为顶点,或节点)之间的关系(称为边)。图可以表示各种事物之间的连接关系,比如网络中的路由器、社交网络中的用户、城市之间的道路等。
用户11305458
2024/10/09
1530
【数据结构】图论基础
DS高阶:图论基础知识
        图是比线性表和树更为复杂且抽象的结,和以往所学结构不同的是图是一种表示型的结构,也就是说他更关注的是元素与元素之间的关系。下面进入正题。
小陈在拼命
2024/05/04
870
DS高阶:图论基础知识
加权无向图----Prim算法实现最小生成树
上一篇:加权无向图的实现 加权无向图----Kruskal算法实现最小生成树 图的生成树是它的一棵含有其所有顶点的无环连通子图,加权图的最小生成树(MST)是它的一棵权值最小的生成树。 切分:图的一种切分是将图的所有顶点分为两个非空且不重合的两个集合。横切边是一条连接两个属于不同集合的顶点的边。 切分定理:在一幅加权图中,给定任意的切分,它横切边中权重最小者必然属于图的最小生成树。 切分定理是解决最小生成树问题的所有算法的基础。  Prim算法能够得到任意加权连通无向图的最小生成树。 数据结构设计: 采用一
SuperHeroes
2018/05/30
1.6K0
【高阶数据结构】秘法(二)——图(一):图的基本概念和存储结构
图是一种非线性的数据结构:G=(V,E),它由节点(也称为顶点)和连接这些节点的边组成。图可以用来表示现实世界中的各种关系,如社交网络、交通网络、电路网络等。
GG Bond1
2024/09/05
1440
【高阶数据结构】秘法(二)——图(一):图的基本概念和存储结构
最小生成树算法实现与分析:Prim 算法,Kruskal 算法;
连通图:无向图G中,若从顶点i到顶点j有路径相连,则称i,j是连通的;如果G是有向图,那么连接i和j的路径中所有的边都必须同向;如果图中任意两点之间都是连通的,那么图被称作连通图。
西湖醋鱼
2020/12/30
1.4K0
最小生成树算法实现与分析:Prim 算法,Kruskal 算法;
数据结构01-最小生成树-Prim算法
给定一个带权的无向连通图,能够连通该图的全部顶点且不产生回路的子图即为该图的生成树;
yangjiao
2021/03/04
5620
文心一言 VS 讯飞星火 VS chatgpt (335)-- 算法导论23.1 4题
为了提供一个例子,其中边集合 {(u,v): 存在一个切割(S,V-S),使得(u,v)是横跨该切割的一条轻量级边} 不形成一棵最小生成树,我们可以考虑一个具有特殊结构的图。
福大大架构师每日一题
2024/08/30
1070
文心一言 VS 讯飞星火 VS chatgpt (335)-- 算法导论23.1 4题
图论-最小生成树kruskal算法(Java)
和prim算法以顶点为出发点不同,kruskal算法以边为中心,将所有边以小到大排序,遍历边,如果当前边的两个顶点有一个没有访问过,则记录该边,直到记录的边到达顶点数-1时,即所有顶点都可以相连,为最小生成树 实现代码:
aruba
2021/12/06
8080
图论-最小生成树kruskal算法(Java)
最小生成树——Prim算法与Kruskal算法
最小生成树概念:连通图: 在一个无向图中,任意两个顶点之间都是可达的(有路径连通),则成该无向图为连通图。生成树: 一个连通图的生成树是一个极小的连通子图,它含有图中的全部顶点,但只有构成一棵树的n-1条边。也就是说,无向图中连通n个顶点n-1条边就叫做生成树。最小生成树: 构造连通图的最小代价生成树称为最小生成树,也就是说,所有的边加权后和最小的树。
mindtechnist
2024/08/08
970
最小生成树——Prim算法与Kruskal算法
加权无向图----Kruskal算法实现最小生成树
上一篇:加权无向图的实现 加权无向图----Prim算法实现最小生成树 数据结构: 用一条优先队列将边按照权重从小到大排序 用union-find数据结构来识别会形成环的边 用一条队列来保存最小生成树的所有边 Kruskal算法的计算一个含V个顶点和E条边的连通加权无向图的最小生成树所需空间与E成正比,所需时间与ElogE成正比(最坏情况)。 方法:将边都添加进最小优先权队列中,每次从中取出最小的边,检查会不会与已经选出的边构成环(使用union-find算法),如果构成环,则弃掉这条边,否则将这条边加入最
SuperHeroes
2018/05/30
1.1K0
推荐阅读
相关推荐
【c++高阶DS】最小生成树
更多 >
领券
社区富文本编辑器全新改版!诚邀体验~
全新交互,全新视觉,新增快捷键、悬浮工具栏、高亮块等功能并同时优化现有功能,全面提升创作效率和体验
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文