最小生成树和其算法

Aki 发布于 2022-11-17 342 次阅读


最小生成树算法适用于无向图。最小生成树算法旨在求解无向图中所有结点的最小生成树,即所有结点的最小连通子图。最小生成树算法一般不适用于有向图。

在无向图中,一条边的两个端点都是可以互相到达的,所以我们可以将一条边看作是两个结点之间的一条双向边。而在有向图中,一条边的两个端点之间的连通情况并不一定是可以互相到达的。所以,最小生成树算法不适用于有向图。

树、

如果一个无向连通图G中不存在回路,则称图G是一颗树。

生成树(可以带权也可以不带权)、

一个连通图的生成树是一个极小的连通子图,它包含图中全部的 n 个顶点,但只有构成一棵树的n-1条边。

生成树的属性:

  • 一个连通图可以有多个生成树;
  • 一个连通图的所有生成树都包含相同的顶点个数和边数;
  • 生成树当中不存在环;
  • 移除生成树中的任意一条边都会导致图的不连通, 生成树的边最少特性;
  • 在生成树中添加一条边会构成环。
  • 对于包含n个顶点的连通图,生成树包含n个顶点和n-1条边;
  • 对于包含n个顶点的无向完全图最多包含 n^(n-2) 颗生成树。
  • 如果在边中加上权值,那么权值最小的生成树即为最小生成树,权值最大的生成树为最大生成树。

最小生成树(带权连通图)、

所谓最小生成树,就是在一个具有N个顶点的带权连通图G中,如果存在某个子图G',其包含了图G中的所有顶点和一部分边,且不形成回路,并且子图G'的各边权值之和最小,则称G'为图G的最小生成树。
      由定义我们可得知最小生成树的三个性质:
      • 最小生成树不能有回路。
      • 最小生成树可能是一个,也可能是多个。
      • 最小生成树边的个数等于顶点的个数减一。

两种最小生成树的算法,分别为克鲁斯卡尔算法(Kruskal Algorithm)和普利姆算法(Prim Algorithm)。

克鲁斯卡尔算法(Kruskal Algorithm)

参考视频 :超丝滑的最小生成树演示--克鲁斯卡尔算法!

克鲁斯卡尔算法的核心思想是:在带权连通图中,不断地在边集合中找到权值最小的边,如果该边满足得到最小生成树的条件,就将其构造,直到最后一条边得到一颗最小生成树。

克鲁斯卡尔算法的执行步骤:

  • 第一步:在带权连通图中,将边的权值排序按小到大排序。
  • 第二步:按权值从小到大依次取出边,并且判断是否需要选择这条边(此时图中的边已按权值从小到大排好序)。判断的依据是是否出现环路,如果不出现环路,那么就选择加入这条边,出现环路就抛弃掉。
  • 第三步:循环第二步,直到图中所有的顶点都在同一个连通分量中,即得到最小生成树。

普利姆算法(Prim Algorithm)

参考视频:【算法】最小生成树

普利姆算法的核心思想是:

  • 一个加权连通图,其中顶点集合为V,边集合为E,初始化 Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空
  • 重复下列操作,直到 Vnew = V
  • 在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一),将v加入集合Vnew中,将<u, v>边加入集合Enew中。
  • 使用集合Vnew和Enew来描述所得到的最小生成树。
#include<iostream>     
#include<cassert>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;


//格式化print函数
template<class...Args>
inline void print(const Args&... args) noexcept
{
        ((cout << args), ...);
}


//并查集
class Union_Find_Disjoint_Sets
{
public:

        //并查集初始化操作,默认初始时所有的结点的根节点就是自身
        Union_Find_Disjoint_Sets(size_t n)
        {
                _set.resize(n + 1, 0);
                for (size_t i = 0; i < n + 1; ++i)
                {
                        _set[i] = i;
                }
        }


        ~Union_Find_Disjoint_Sets()noexcept {}


        //把两个结点i和j合并成为同一个结点
        void merge(size_t i, size_t j)
        {
                size_t a = _find_root(i);    //先找到i的根节点,得到根节点的编号
                size_t b = _find_root(j);    //然后在找到j的根节点,得到根节点的编号
                _set[a] = b;                        //连接i和j
                _set[i] = b;                         //路径压缩
        }

        //检测根节点i和j是不是在同一个关系中,也就是怕段它们是否有相同的根节点
        bool test(size_t i, size_t j)
        {
                size_t a = _find_root(i);
                size_t b = _find_root(j);
                return a == b;
        }


protected:

        //找一个结点的根结点,得到根节点的编号
        size_t _find_root(size_t x)
        {
                assert(x >= 0 && x < _set.size());
                if (_set[x] == x)     //这个情况是根节点就是自身
                {
                        return _set[x];
                }
                return _set[x] = _find_root(_set[x]);   //这个情况是根节点不是自身,那就继续递归,同时做一个路径压缩操作
        }

private:

        //并查集数组
        vector<size_t> _set;
};


//边结构体
struct edge
{
        edge(int b,int e,int w) : begin(b),end(e),weight(w) {}

        int  begin;  //边的起始顶点
        int  end;    //边的结束顶点
        int  weight;  //权值
};

//顶点结构体
struct vertex
{
        vertex(int n) : number(n),is_visited(false) {}
        int number;   //顶点编号
        bool is_visited;  //标志顶点是否被访问过
};


//图的邻接表存储结构
struct graph
{
        vector<vertex*> V;  //存储图的所有顶点
        vector<edge*> E;   //存储图的所有边
};

//两个仿函数用于权值大小比较
struct compare1
{
        bool operator()(edge* l,edge* r) const
        {
                return l->weight < r->weight;
        }
};

struct compare2
{
        bool operator()(edge* l,edge* r) noexcept
        {
                return l->weight > r->weight;
        }
};



//Prim algorithm
void Prim(graph& g) noexcept
{
        vector<vertex*> v_new;
        v_new.push_back(g.V[5]);
        sort(g.E.begin(),g.E.end(),compare1());
        vector<edge*> e_new;
        while(v_new.size() != g.V.size())
        {
                for(auto i : g.E)
                {
                        auto iter1 = find_if(v_new.begin(),v_new.end(),[&](auto x){return x->number == i->begin;});
                        auto iter2 = find_if(g.V.begin(),g.V.end(),[&](auto x){return x->number == i->end;});
                        auto iter3 = find_if(v_new.begin(),v_new.end(),[&](auto x){return x->number == i->end;});
                        if(iter1 != v_new.end() && iter2 != g.V.end() && iter3 == v_new.end())
                        {
                                e_new.push_back(i);
                                v_new.push_back(*iter2);
                                break;
                        }
                        //特别注意!!!
                        //由于无向图的一条边是没有方向的,一条边可以当作两条边!!!
                        //即起始顶点和结束顶点是相反的!!!
                        //这里要进行两次判断,不然会得不到正确答案!!!
                        //也可以在构造图的边时构造出两条结束顶点和起始顶点相反的边,这样这里就不需要判断两次了!!!
                        iter1 = find_if(v_new.begin(),v_new.end(),[&](auto x){return x->number == i->end;});
                        iter2 = find_if(g.V.begin(),g.V.end(),[&](auto x){return x->number == i->begin;});
                        iter3 = find_if(v_new.begin(),v_new.end(),[&](auto x){return x->number == i->begin;});
                        if(iter1 != v_new.end() && iter2 != g.V.end() && iter3 == v_new.end())
                        {
                                e_new.push_back(i);
                                v_new.push_back(*iter2);
                                break;
                        }
                }
        }
        cout << "Prim algorithm :" << endl;
        for(auto c: e_new)
        {
                print("边开始 : ",c->begin," 边结束 : ",c->end," 权值 : ",c->weight,"\n");
        }
}


//Kruskal algorithm
void Kruskal(graph& g) noexcept
{
        priority_queue<edge*,vector<edge*>,compare2> E;
        for(auto c : g.E)
        {
                E.push(c);
        }
        vector<edge*> new_edge;
        Union_Find_Disjoint_Sets set(g.V.size());
        while(!E.empty())
        {
                auto tmp = E.top();
                E.pop();
                if(!set.test(tmp->begin,tmp->end))
                {
                        set.merge(tmp->begin,tmp->end);
                        new_edge.push_back(tmp);
                }
        }
        cout << "Kruskal algorithm :" << endl;
        for(auto c : new_edge)
        {
                print("边开始 : ",c->begin," 边结束 : ",c->end," 权值 : ",c->weight,"\n");
        }
}


int main()
{

        graph g;

        //构造顶点
        for(int i = 1;i < 8;++i)
        {
                g.V.push_back(new vertex(i));
        }

        //构造边
        g.E.push_back(new edge(1,2,23));
        g.E.push_back(new edge(1,7,36));
        g.E.push_back(new edge(1,6,28));
        g.E.push_back(new edge(2,7,1));
        g.E.push_back(new edge(2,3,20));
        g.E.push_back(new edge(3,7,4));
        g.E.push_back(new edge(3,4,15));
        g.E.push_back(new edge(4,7,9));
        g.E.push_back(new edge(4,5,3));
        g.E.push_back(new edge(5,7,16));
        g.E.push_back(new edge(5,6,17));
        g.E.push_back(new edge(6,7,25));


        Prim(g);
        cout << endl;
        Kruskal(g);

        //销毁所有顶点和边
        for(auto c : g.V)
        {
                delete c;
        }
        for(auto c : g.E)
        {
                delete c;
        }

        return 0;
}