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

1. 图的最小生成树

生成树的定义:如果连通图G的一个子图是一棵包含G的所有顶点的树,则该子图称为G的生成树。 
生成树是连通图的包含图中的所有顶点的极小连通子图。它并不唯一,从不同的顶点出发进行遍历,可以得到不同的生成树。

其中,权值最小的树就是最小生成树。

关于最小生成树最经典的应用模型就是城市通信线路网最小造价的问题:网络G表示n个城市之间的通信线路(其中顶点表示城市,边表示两个城市之间的通信线路,边上的权值表示线路的长度或造价),通过求该网络的最小生成树找到求解通信线路总造价最小的最佳方案。

求图的最小生成树主要有两种经典算法:

  1. 普里姆(Prim)算法 
    时间复杂度为O(n2),适合于求边稠密的最小生成树。
  2. 克鲁斯卡尔(Kruskal)算法

2. 普里姆(Prim)算法

2.1 算法思想

取图中任意一个顶点V作为生成树的根,之后若要往生成树上添加顶点W,则在顶点V和W之间必定存在一条边。并且该边的权值在所有连通顶点V和W之间的边中取值最小。

2.2 算法实现

  1. 输入:一个加权连通图,其中顶点集合为V,边集合为E;
  2. 初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {}为空;
  3. 重复下列操作,直到Vnew = V: 
    a.在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一); 
    b.将v加入集合Vnew中,将<u, v>边加入集合Enew中;

  4. 输出:使用集合Vnew和Enew来描述所得到的最小生成树。

    完整实现的示意图如下: 
    这里写图片描述

代码实现为:

int edge[1005][1005];//用邻接矩阵表示的图
int book[1005];//已确定的节点集合
int dis[1005];//最短路径
int num=0;//节点的总个数

int prim(int s) {
    int pos, min;//pos记录每次确定下来要加入集合book的那个节点,min表示当前这轮确定的最短路径
    int MST = 0;//累加最短路径,表示最短路径和

    book[s] = 1;//首先,将节点s放入集合book
    pos = s;

    for (int i = 1; i <= num; i++) //初始化dis
        dis[i] = edge[pos][i];

    //执行n-1次
    for (int i = 2; i <= num; i++) {
        min = INT_MAX;

        for (int j = 1; j <= num; j++) {
            if (book[j]==0 && min > dis[j]) {
                min = dis[j];
                pos = j;
            }
        }
        book[pos] = 1;//确定选择出离当前节点最近的那个节点
        MST += min;//加上这条最短路径

        for (int j = 1; j <= num; j++) //尝试更新dis
        if (book[j]==0 && dis[j] > edge[pos][j])//更新条件:j点未访问,加入新点后已访问集合到j的距离变小了
            dis[j] = edge[pos][j];
    }
    return MST;
}

int main() {
    int n;
    int i,j;
    while(cin>>n) {
        for(int i=1;i<=n;i++) {
            book[i]=0;
            dis[i]=0;
            for(int j=1;j<=n;j++) {
                edge[i][j]=0;
            }
        }

        num=n;
        for(i=1;i<=n;i++)//输入邻接矩阵(图)
            for(j=1;j<=n;j++)
                cin>>edge[i][j];
        int ans = prim(1);
        cout << ans << endl;    

    }

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

可以发现,prim算法的结构和dj算法很相似,都是每次确定一个节点,然后更新dis数组,只不过更新的时候略有不同。

3. 克鲁斯克尔(Kruskal)算法

3.1 算法思想

有n个顶点的最小生成树含有n-1条边。 
按权值递增次序选择合适的边来构造最小生成树。先把所有的顶点包括在生成树中,将图中的边按权值递增的顺序依次选取,要保证选取的边不使生成树中产生回路,将选取的边加到生成树中,直至有n-1条边为止。

3.2 算法实现

  1. 记Graph中有v个顶点,e条边;
  2. 新建图Graphnew,Graphnew中拥有原图中的v个顶点,但没有边;
  3. 将原图Graph中所有e条边按权值从小到大排序;
  4. 循环:从权值最小的边开始,判断并添加每条边,直至添加了n-1条边:
 if 这条边连接的两个节点于图Graphnew中不在同一个连通分量中(即不构成回路)
    添加这条边到图Graphnew中
  • 1
  • 2

这里写图片描述

代码实现为:

typedef struct  
{  
   int begin;  
   int end;  
   int weight;  

}Edge;  //定义边集结构  

void sort_edges(Edge& edge,Edge& edge2)   //交换大小,为后面排序所用  
{  
    Edge edge_temp;  
    edge_temp.begin = edge.begin;  
    edge_temp.end = edge.end;  
    edge_temp.weight = edge.weight;  
    edge.begin = edge2.begin;  
    edge.end = edge2.end;  
    edge.weight = edge2.weight;  
    edge2.begin = edge_temp.begin;  
    edge2.end =  edge_temp.end;  
    edge2.weight = edge_temp.weight;  
}  
void turn_to_edges(MGraph &G,Edge* edges)   //讲邻接矩阵转换成边集数组  
{  
  int k=0;  
  for(int i=0;i<G.vexnum;++i)  
  {  
       for(int j=0;j<G.vexnum;++j)  
       {  
           if( (G.arcs[i][j] != MAX) && (i<j))  
           {   
              edges[k].begin = i;  
              edges[k].end=j;  
              edges[k].weight=G.arcs[i][j];  
              k++;  
           }  
       }  
  }  
  k=G.edgenum-1;  
  for(int i=0;i<G.edgenum;++i)    //讲转换的边集数组按从小到大的顺序排列  
  {  
     int weight=0,index=0;  
     for(int j= 0;j<k;++j)  
     {  
           if(edges[j].weight>weight)  
           {  
              weight = edges[j].weight;  
              index = j;   
           }  
     }  
     sort_edges(edges[index],edges[k]);  
     --k;  
  }  

}  
int find(int* parent,int num)    //查找已添加进树中的顶点最后尾部索引  
{  
   while(parent[num])  
     num = parent[num];  

    return num;  
}  
void minTree_kruskal(MGraph &G)  
{  
   int *parent = (int*)malloc(sizeof(int)*G.vexnum);  
   Edge *edges = (Edge*)malloc(sizeof(Edge)*G.edgenum);  
   turn_to_edges(G,edges);  
   for(int i=0;i<G.vexnum;++i)  
      parent[i]=0;  

   for(int i=0;i<G.edgenum;++i)  
   {  
       int n = find(parent,edges[i].begin);  
       int m = find(parent,edges[i].end);  
       if(m!=n)     //若相等,则添加此边会形成环  
       {  
         parent[n]=m;  
         printf("(%d,%d) %d",edges[i].begin,edges[i].end,edges[i].weight);  
       }  
   }  

} 这是克鲁斯科尔算法的伪代码

猜你喜欢

转载自blog.csdn.net/zhao2018/article/details/80778860
今日推荐