数据结构与算法最小生成树算法

Posted 区块链and语义研究实验室

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法最小生成树算法相关的知识,希望对你有一定的参考价值。

【数据结构与算法】最小生成树算法


最小生成树

 树是图论的重要内容,最小生成树(Minimum Spanning Tree,MST)是图论中的经典问题,在计算机科学技术,特别是数据结构中有广泛的应用。关于加权图的应用,常常需要我们求解一颗无向生成树,使其边的总权值最小。这样的一棵生成树称作最小生成树。

例如:要在n个城市之间铺设光缆,主要目标是要使这n个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同;另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。


首先了解的几个概念:


连通图:在无向图中,若任意两个顶点vi与vj都有路径相通,则称该无向图为连通图。

强连通图:在有向图中,若任意两个顶点vi与vj都有路径相通,则称该有向图为强连通图。

连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。

生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。

最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。

最小生成树仅针对加权连通无向图。对于一副加权连通无向图,其生成树是它的一棵含有其所有顶点的无环连通子图。


最小生成树的一些性质:


①具有V个顶点的加权连通无向图,其最小生成树包含V个顶点,V-1条边。


②设图G=<V,R>是一个带权的连通图,即连通网,集合U是顶点集V的一个非空子集。构建生成树时还需要一条边连通顶点集合U和V-U。如果(u,v)∈R,其中,u∈U,v∈V-U,且边(u,v)是具有最小权值的一条边,那么一定存在一棵包含边(u,v)的最小生成树。


构造最小生成树有多种算法。本文将主要介绍Kruskal和Prim两种算法,这两种算法都属于贪心算法,利用的就是上述性质。


Kruskal算法

Kruskal算法使用的贪心准则是从剩下的边中选择不会产生环路且具有最小权值的边加入到生成树的边集中。

基本思想:

先构造一个只含有n个顶点的子图G,然后从权值最小的边开始,若它的添加使G中产生回路,则在G上加入该边,依次按照权值递增的次序,选择合适的边进行添加,如此重复,直至加完n-1条边为止。因此Kruskal算法也称为“加边法”,即初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。

算法过程:

①把图中的所有边按代价从小到大排序;

②把图中的n个顶点看成独立的n棵树组成的森林;

③按权值从小到大选择边,所选的边连接的两个顶点ui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树;

④重复③,直到所有顶点都在一颗树内或者有n-1条边为止。

演示示例:

【数据结构与算法】最小生成树算法


step 1:
最小权值边为顶点 7、8 形成的边;

【数据结构与算法】最小生成树算法


step 2:
最小权值边为顶点 3、9 形成的边;

【数据结构与算法】最小生成树算法


step 3:
最小权值边为顶点 6、7 形成的边;

【数据结构与算法】最小生成树算法


step 4:
最小权值边为顶点 3、6 形成的边;

【数据结构与算法】最小生成树算法


step 5:
最小权值边为顶点 1、2 形成的边;

【数据结构与算法】最小生成树算法


step 6:
最小权值边为顶点 3、4 形成的边;

【数据结构与算法】最小生成树算法


step 7:
最小权值边为顶点 1、8 形成的边;

【数据结构与算法】最小生成树算法


step 8:
最小权值边为顶点 4、5 形成的边;

【数据结构与算法】最小生成树算法

最小生成树的权值之和为 37。

伪代码描述:

1,初始化:U=V;TE={ };

2,循环直到T中的连通分量个数为1

(1)在E中寻找最短边(u,v);

(2)如果顶点u、v位于T的两个不同连通分量,则:

     ①将边(u,v)并入TE;

     ②将这两个连通分量合并为一个;

     ③E=E-{(u,v)};

时间复杂度:

Kruskal算法为了提高每次贪心选择时查找最短边的效率,可以先将图G中的边按代价从小到达排序,则这个操作的时间复杂度为O(elog2e),其中e为无向连通网中边的个数。对于两个顶点是否属于同一个连通分量,可以用并查集的操作将其时间性能提高到O(n),所以Kruskal算法的时间性能是O(elog2e)


Prim算法

Prim算法的过程是只存在一个子图,不断选择顶点加入到该子图中,即通过对子图进行扩张,直到形成最终的最小生成树。


基本思想:

从连通网络N={v,E}中的某一顶点u0出发,选择与它关联的具有最小权值的边(u0,v),将其顶点加入到生成树的顶点集合U中。以后每一步从一个顶点在U中,而另一个顶点不在U中的各条边中选择权值最小的边(u,v),把它的顶点加入到集合V中,这意味着(u,v)也加入到生成树的边集合中,如此继续下去,直到网络中的所有顶点都加入到生成树顶点集合U中为止。因此Prim算法可以称为“加点法”,即每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。

算法过程:

①图的所有顶点集合为V;初始令集合u={s},v=V−u;

②在两个集合u,v能够组成的边中,选择一条代价最小的边(u0,v0),加入到最小生成树中,并把v0并入到集合u中;

③重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。

演示示例:

【数据结构与算法】最小生成树算法


这里不妨以顶点 5 作为子图中的第一个顶点。


step 1:
距离子图的最近顶点为 4;

【数据结构与算法】最小生成树算法

step 2:

距离子图的最近顶点为 3;

【数据结构与算法】最小生成树算法

step 3:

距离子图的最近顶点为 9;

【数据结构与算法】最小生成树算法

step 4:

距离子图的最近顶点为 6;

【数据结构与算法】最小生成树算法

step 5:

距离子图的最近顶点为 7;

【数据结构与算法】最小生成树算法

step 6:

距离子图的最近顶点为 8;

【数据结构与算法】最小生成树算法

step 7:

距离子图的最近顶点为 2;

【数据结构与算法】最小生成树算法

step 8:

距离子图的最近顶点为 1;

【数据结构与算法】最小生成树算法

最小生成树的权值之和为 37。


伪代码描述:

1,初始化两个辅助数组lowcost和adjvex;

2,U={u0};输出顶点u0;   //将顶点u0加入生成树中

3,重复执行下列操作n−1次

(1)在lowcost中选择最短边,取adjvex中对应的顶点序号k;

(2)输出顶点k和对应的权值;

(3)U=U+k;

(4)调整数组lowcost和adjvex;

时间复杂度:

分析Prim算法,设连通网中有n个顶点,则第一个进行初始化的循环语句需要执行n-1次,第二个循环共执行n-1次,内嵌两个循环,其一是在长度为n的数组中求最小值,需要执行n-1次,其二是调整辅助数组,需要执行n-1次,所以,Prim算法的时间复杂度为O(n2)。



完整代码

#include <iostream>

#include <vector>

#include <queue>

#include <algorithm>

using namespace std;

#define INFINITE 0xFFFFFFFF   

#define VertexData unsigned int  //顶点数据

#define UINT  unsigned int

#define vexCounts 6  //顶点数量

char vextex[] = { 'A', 'B', 'C', 'D', 'E', 'F' };

struct node 

{

    VertexData data;

    unsigned int lowestcost;

}closedge[vexCounts]; //Prim算法中的辅助信息

typedef struct 

{

    VertexData u;

    VertexData v;

    unsigned int cost;  //边的代价

}Arc;  //原始图的边信息

void AdjMatrix(unsigned int adjMat[][vexCounts])  //邻接矩阵表示法

{

    for (int i = 0; i < vexCounts; i++)   //初始化邻接矩阵

        for (int j = 0; j < vexCounts; j++)

        {

            adjMat[i][j] = INFINITE;

        }

    adjMat[0][1] = 6; adjMat[0][2] = 1; adjMat[0][3] = 5;

    adjMat[1][0] = 6; adjMat[1][2] = 5; adjMat[1][4] = 3;

    adjMat[2][0] = 1; adjMat[2][1] = 5; adjMat[2][3] = 5; adjMat[2][4] = 6; adjMat[2][5] = 4;

    adjMat[3][0] = 5; adjMat[3][2] = 5; adjMat[3][5] = 2;

    adjMat[4][1] = 3; adjMat[4][2] = 6; adjMat[4][5] = 6;

    adjMat[5][2] = 4; adjMat[5][3] = 2; adjMat[5][4] = 6;

}

int Minmum(struct node * closedge)  //返回最小代价边

{

    unsigned int min = INFINITE;

    int index = -1;

    for (int i = 0; i < vexCounts;i++)

    {

        if (closedge[i].lowestcost < min && closedge[i].lowestcost !=0)

        {

            min = closedge[i].lowestcost;

            index = i;

        }

    }

    return index;

}

void MiniSpanTree_Prim(unsigned int adjMat[][vexCounts], VertexData s)

{

    for (int i = 0; i < vexCounts;i++)

    {

        closedge[i].lowestcost = INFINITE;

    }      

    closedge[s].data = s;      //从顶点s开始

    closedge[s].lowestcost = 0;

    for (int i = 0; i < vexCounts;i++)  //初始化辅助数组

    {

        if (i != s)

        {

            closedge[i].data = s;

            closedge[i].lowestcost = adjMat[s][i];

        }

    }

    for (int e = 1; e <= vexCounts -1; e++)  //n-1条边时退出

    {

        int k = Minmum(closedge);  //选择最小代价边

        cout << vextex[closedge[k].data] << "--" << vextex[k] << endl;//加入到最小生成树

        closedge[k].lowestcost = 0; //代价置为0

        for (int i = 0; i < vexCounts;i++)  //更新v中顶点最小代价边信息

        {

            if ( adjMat[k][i] < closedge[i].lowestcost)

            {

                closedge[i].data = k;

                closedge[i].lowestcost = adjMat[k][i];

            }

        }

    }

}

void ReadArc(unsigned int  adjMat[][vexCounts],vector<Arc> &vertexArc) //保存图的边代价信息

{

    Arc * temp = NULL;

    for (unsigned int i = 0; i < vexCounts;i++)

    {

        for (unsigned int j = 0; j < i; j++)

        {

            if (adjMat[i][j]!=INFINITE)

            {

                temp = new Arc;

                temp->u = i;

                temp->v = j;

                temp->cost = adjMat[i][j];

                vertexArc.push_back(*temp);

            }

        }

    }

}

bool compare(Arc  A, Arc  B)

{

    return A.cost < B.cost ? true : false;

}

bool FindTree(VertexData u, VertexData v,vector<vector<VertexData> > &Tree)

{

    unsigned int index_u = INFINITE;

    unsigned int index_v = INFINITE;

    for (unsigned int i = 0; i < Tree.size();i++)  //检查u,v分别属于哪颗树

    {

        if (find(Tree[i].begin(), Tree[i].end(), u) != Tree[i].end())

            index_u = i;

        if (find(Tree[i].begin(), Tree[i].end(), v) != Tree[i].end())

            index_v = i;

    }


    if (index_u != index_v)   //u,v不在一颗树上,合并两颗树

    {

        for (unsigned int i = 0; i < Tree[index_v].size();i++)

        {

            Tree[index_u].push_back(Tree[index_v][i]);

        }

        Tree[index_v].clear();

        return true;

    }

    return false;

}

void MiniSpanTree_Kruskal(unsigned int adjMat[][vexCounts])

{

    vector<Arc> vertexArc;

    ReadArc(adjMat, vertexArc);//读取边信息

    sort(vertexArc.begin(), vertexArc.end(), compare);//边按从小到大排序

    vector<vector<VertexData> > Tree(vexCounts); //6棵独立树

    for (unsigned int i = 0; i < vexCounts; i++)

    {

        Tree[i].push_back(i);  //初始化6棵独立树的信息

    }

    for (unsigned int i = 0; i < vertexArc.size(); i++)//依次从小到大取最小代价边

    {

        VertexData u = vertexArc[i].u;  

        VertexData v = vertexArc[i].v;

        if (FindTree(u, v, Tree))//检查此边的两个顶点是否在一颗树内

        {

            cout << vextex[u] << "---" << vextex[v] << endl;//把此边加入到最小生成树中

        }   

    }

}


int main()

{

    unsigned int  adjMat[vexCounts][vexCounts] = { 0 };

    AdjMatrix(adjMat);   //邻接矩阵

    cout << "Prim :" << endl;

    MiniSpanTree_Prim(adjMat,0); //Prim算法,从顶点0开始.

    cout << "-------------" << endl << "Kruskal:" << endl;

    MiniSpanTree_Kruskal(adjMat);//Kruskal算法

    return 0;

}



FF409

与你分享学术科研


以上是关于数据结构与算法最小生成树算法的主要内容,如果未能解决你的问题,请参考以下文章

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

图论算法零基础最小生成树学习与总结

最小生成树-Prim算法与Kruskal算法

数据结构与算法—并查集Kruskal算法求最小生成树

最小生成树

图解:如何实现最小生成树(Prim算法与Kruskal算法)