最小生成树(Kruskal和Prim算法)

文章和资源同步更新至微信公众号:算法工程师之路
8月份会在公众号开启每日算法题,值得期待哦~

上一篇文章,我们讲了图的创建和遍历,其中遍历的算法主要有BFS(广度优先算法)和DFS(深度优先算法)两种,并且DFS算法对很多问题都有很好的启示!而今天我们要说一个非常实用的算法——最小生成树的建立!这是图论中一个经典问题,可以使用Kruskal和Prim两种算法来进行实现!

什么是最小生成树

在给定一张无向图,如果在它的子图中,任意两个顶点都是互相连通,并且是一个树结构,那么这棵树叫做生成树。当连接顶点之间的图有权重时,权重之和最小的树结构为最小生成树!

怎么创建图结构呢?图的创建与遍历

在实际中,这种算法的应用非常广泛,比如我们需要在n个城市铺设电缆,则需要n-1条通信线路,那么我们如何铺设可以使得电缆最短呢?最小生成树就是为了解决这个问题而诞生的!
在这里插入图片描述
如上图所示,一幅两两相连的图中,找到一个子图,连接到所有的节点,并且连接边的权重最小(也就是说边的数量也是最小的,这也保证了其是树结构).

Kruskal算法(克鲁斯卡算法)

Kruskal算法是一种贪心算法,我们将图中的每个edge按照权重大小进行排序,每次从边集中取出权重最小且两个顶点都不在同一个集合的边加入生成树中!**注意:如果这两个顶点都在同一集合内,说明已经通过其他边相连,因此如果将这个边添加到生成树中,那么就会形成环!**这样反复做,直到所有的节点都连接成功!

在这个算法中,最重要的一环就是判断两个节点在不在同一个集合内,很多人想,那你直接用一个set来保存不就好了?这是思路显然不行,因为假设A和其他三个节点相连,同属一个集合,而B和另外三个节点相连,同属一个集合,那么我们将A和B取并集时,是将这两组数据合并一起的,如果使用set,那么当数据量大时还需要遍历,复杂度就会很高,因此使用并查集就会效率快很多了

什么是并查集?并查集实现与详解!!!

  1. 对所有节点遍历建立并查集,按照边的权重建立最小堆
  2. 取出最小堆堆顶数据,并判断两端节点是否在同一集合
  3. 如不在,则将这两个节点添加到同一集合,接着将边加入生成边,如在,则不进行操作,为无效边
  4. 重复上面的操作,直到所有的边都检查完
unordered_set<Edge, EdgeHash, EdgeEqual> kruskalMST(Graph graph){
    auto cmp = [](const Edge& a, const Edge& b){
        return a.weight > b.weight;    // 最小堆
    };
    vector<Node*> list;
    for(auto ite: graph.nodes){
        list.push_back(ite.second);
    }
    UnionFindSet unionFind(list);   // 建立并查集
    priority_queue<Edge, vector<Edge>, decltype(cmp)> smallQueue(cmp);
    for(auto edge: graph.edges){
        smallQueue.push(*edge);
    }
    // 构造选中的输出边集
    unordered_set<Edge, EdgeHash, EdgeEqual> result;
    while(!smallQueue.empty()){
        Edge edge = smallQueue.top();
        
        smallQueue.pop();
        if(!unionFind.isSameSet(edge.from, edge.to)){  // 判断是否为一个环,如果一个边的两端节点为一个集合,那么必为一个闭合环
            result.insert(edge);
            unionFind.Union(edge.from, edge.to);
        }
    }
    return result;
}

Prim算法(普里姆算法)

Prim算法是另一种贪心算法,和Kruskal算法的贪心策略不同,Kruskal算法主要对边进行操作,而Prim算法则是对节点进行操作,每次遍历添加一个点,这时候我们就不需要使用并查集了。具体步骤为:

  1. 建立边set用来存放结果,建立节点set用来存放节点同时用于标记是否被访问过,建立边的最小堆
  2. 开始遍历所有节点,如果没有访问,则添加到节点set,然后将其相连的边入堆。
  3. 从堆中取最小的边,然后判断to节点是否被访问过,如果没有,将这个边加入生成树(我们想要的边),并标记该节点访问。
  4. 然后将to节点所相连的边添加到最小堆中,不然这个网络就不会向外扩展了(这个步骤是必须的)。
  5. 循环上面的操作,直到所有的节点遍历完。

注意:对于单连通,从一个节点出发就可以直接找到完整的最小生成树,因此最外的for循环也可以为一个顺序结构,但多连通,就需要从不同的节点出发了!!!

unordered_set<Edge, EdgeHash, EdgeEqual> primMST(Graph graph){
    // 装边的最小堆
    auto cmp = [](const Edge& e1, const Edge& e2){
        return e1.weight > e2.weight;
    };
    priority_queue<Edge, vector<Edge>, decltype(cmp)> smallQueue(cmp);
    // 判断节点是否访问过
    unordered_set<Node, NodeHash, NodeEqual> node_set;
    unordered_set<Edge, EdgeHash, EdgeEqual> result;
    for(auto ite: graph.nodes){
        if(node_set.find(*ite.second) == node_set.end()){
            // 如果没有访问,将其标记为访问过,并把它对应的边放入最小堆
            node_set.insert(*ite.second);
            for(Edge* edge: ite.second->edges){
                smallQueue.push(*edge);
            }
            // 在当前这个图中寻找最小生成树
            while(!smallQueue.empty()){
                // 从堆中取出一个最小权重边,并取出对应节点
                Edge help_edge = smallQueue.top();
                smallQueue.pop();
                
                Node edge_to = *(help_edge.to);
                // 然后判断这个节点是否被访问过,如果没有则将这个边加入边集
                if(node_set.find(edge_to) == node_set.end()){
                    result.insert(help_edge);
                    node_set.insert(edge_to);   // 标记edge_to也已经访问过了
                    for(Edge *newEdge: edge_to.edges){
                        smallQueue.push(*newEdge);
                    }
                }
            }
        }
    }
    return result;
}

那么对于这两种算法,我们应该用哪种呢?记得某个人说过这句话,算法没有优劣,每个算法都有它的使用场景,在对的场合使用对的算法,这才是算法工程师应该做的事情!

由于Kruskal算法是对边进行操作,先取出边,然后判断边的两个节点,这样的话,如果一个图结构非常的稠密,那么Kruskal算法就比较慢了,而Prim算法只是对节点进行遍历,并使用set进行标记,因此会相对于Kruskal算法,在稠密图方面好很多,因此Kruskal算法常用于稀疏图,而Prim算法常用于稠密图!

资源分享

以上完整代码文件(C++版),文件名为:最小生成树(Kruskal算法和Prim算法),请关注我的个人公众号 (算法工程师之路),回复"左神算法基础CPP"即可获得,并实时更新!希望大家多多支持哦~

公众号简介:分享算法工程师必备技能,谈谈那些有深度有意思的算法,主要范围:C++数据结构与算法/深度学习(CV),立志成为Offer收割机!坚持分享算法题目和解题思路(Day By Day)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值