最小生成树的三种算法及图的相关知识

欢迎访问https://blog.csdn.net/lxt_Lucia~~
 

宇宙第一小仙女\(^o^)/~~萌量爆表求带飞=≡Σ((( つ^o^)つ~dalao们点个关注呗~~

 

 

先介绍几个关于图的几个概念定义:

  • 连通图:在无向图中,若任意两个顶点vivj都有路径相通,则称该无向图为连通图。
  • 强连通图:在有向图中,若任意两个顶点vivj都有路径相通,则称该有向图为强连通图。
  • 连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
  • 生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
  • 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。这里写图片描述

 

emmmmmm...... 今天师哥讲的时候,说最小生成树除了Kruskal和Prim还有一种,Sollin(Boruvka),但是很麻烦很不常用。

三种算法如下:

 

1.Kruskal算法(相对最好)

此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
1. 把图中的所有边按代价从小到大排序;
2. 把图中的n个顶点看成独立的n棵树组成的森林;
3. 按权值从小到大选择边,所选的边连接的两个顶点ui,vi

,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
4. 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。

这里写图片描述

初始情况,一个连通图,定义针对边的数据结构,包括起点,终点,边长度:

typedef struct _node{

    intval;   //长度

    intstart; //边的起点

    intend;   //边的终点

}Node;

 

举个栗子好了,例图如下:

 

代码实现:

#include<iostream>  
#define N 7  
using namespace std;  
typedef struct _node{  
    intval;  
    intstart;  
    intend;  
}Node;  
Node V[N];  
intcmp(constvoid *a, constvoid *b)  
{  
    return(*(Node *)a).val - (*(Node*)b).val;  
}  
intedge[N][3] = {  { 0,1,3},  
                    {0,4,1},   
                    {1,2,5},   
                    {1,4,4},  
                    {2,3,2},   
                    {2,4,6},   
                    {3,4,7}  
                    };  
   
intfather[N] = { 0, };  
intcap[N] = {0,};  
   
voidmake_set()              //初始化集合,让所有的点都各成一个集合,每个集合都只包含自己  
{  
    for(inti = 0; i < N; i++)  
    {  
        father[i] = i;  
        cap[i] = 1;  
    }  
}  
   
intfind_set(intx)              //判断一个点属于哪个集合,点如果都有着共同的祖先结点,就可以说他们属于一个集合  
{  
    if(x != father[x])  
     {                               
        father[x] = find_set(father[x]);  
    }      
    returnfather[x];  
}                                   
   
voidUnion(intx, inty)         //将x,y合并到同一个集合  
{  
    x = find_set(x);  
    y = find_set(y);  
    if(x == y)  
        return;  
    if(cap[x] < cap[y])  
        father[x] = find_set(y);  
    else  
    {  
        if(cap[x] == cap[y])  
            cap[x]++;  
        father[y] = find_set(x);  
    }  
}  
   
intKruskal(intn)  
{  
    intsum = 0;  
    make_set();  
    for(inti = 0; i < N; i++)//将边的顺序按从小到大取出来  
    {  
        if(find_set(V[i].start) != find_set(V[i].end))     //如果改变的两个顶点还不在一个集合中,就并到一个集合里,生成树的长度加上这条边的长度  
        {  
            Union(V[i].start, V[i].end);  //合并两个顶点到一个集合  
            sum += V[i].val;  
        }  
    }  
    returnsum;  
}  
intmain()  
{  
    for(inti = 0; i < N; i++)   //初始化边的数据,在实际应用中可根据具体情况转换并且读取数据,这边只是测试用例  
    {  
        V[i].start = edge[i][0];  
        V[i].end = edge[i][1];  
        V[i].val = edge[i][2];  
    }  
    qsort(V, N, sizeof(V[0]), cmp);  
    cout << Kruskal(0)<<endl;

Kruskal 模板

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n, m,sum;
struct node
{
    int start,end,power;//start为起始点,end为终止点,power为权值
} edge[5050];
int pre[5050];
 
int cmp(node a, node b)
{
    return a.power<b.power;//按照权值排序
}
 
int find(int x)//并查集找祖先
{
    if(x!=pre[x])
    {
        pre[x]=find(pre[x]);
    }
    return pre[x];
}
 
void merge(int x,int y,int n)//并查集合并函数,n是用来记录最短路中应该加入哪个点
{
    int fx=find(x);
    int fy=find(y);
    if(fx!=fy)
    {
        pre[fx]=fy;
        sum+=edge[n].power;
    }
}
int main()
{
    while(~scanf("%d", &n), n)//n是点数
    {
        sum=0;
        m=n*(n-1)/2;//m是边数,可以输入
        int i;
        int start,end,power;
        for(i=1; i<=m; i++)
        {
            scanf("%d %d %d", &start, &end, &power);
            edge[i].start=start,edge[i].end=end,edge[i].power=power;
        }
        for(i=1; i<=m; i++)
        {
            pre[i]=i;
        }//并查集初始化
        sort(edge+1, edge+m+1,cmp);
        for(i=1; i <= m; i++)
        {
            merge(edge[i].start,edge[i].end,i);
        }
        printf("%d\n",sum);
    }
    return 0;
}

 

2.Prim算法(基于贪心)

此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。

  • 图的所有顶点集合为V

;初始令集合u={s},v=Vu;在两个集合u,v能够组成的边中,选择一条代价最小的边(u0,v0),加入到最小生成树中,并把v0

  • 并入到集合u中。
  • 重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。

由于不断向集合u中加点,所以最小代价边必须同步更新;需要建立一个辅助数组closedge,用来维护集合v中每个顶点与集合u中最小代价边信息,:

struct
{
  char vertexData   //表示u中顶点信息
  UINT lowestcost   //最小代价
}closedge[vexCounts]
  • 1
  • 2
  • 3
  • 4
  • 5

这里写图片描述

typedef struct _node{

    intval;   //长度

    intstart; //边的起点

    intend;   //边的终点

}Node;

 

下面我们也举个栗子叭:

 

例图如下:

针对上面的图,代码如下:

#include<iostream>  
#define INF 10000  
using namespace std;  
constint N = 6;  
bool visit[N];  
intdist[N] = { 0, };  
intgraph[N][N] = { {INF,7,4,INF,INF,INF},  //INF代表两点之间不可达  
                    {7,INF,6,2,INF,4},  
                    {4,6,INF,INF,9,8},  
                    {INF,2,INF,INF,INF,7},  
                    {INF,INF,9,INF,INF,1},  
                    {INF,4,8,7,1,INF}  
                  };  
intprim(intcur)  
{  
    intindex = cur;  
    intsum = 0;  
    inti = 0;  
    intj = 0;  
    cout << index << " ";  
    memset(visit,false, sizeof(visit));  
    visit[cur] = true;  
    for(i = 0; i < N; i++)  
        dist[i] = graph[cur][i];//初始化,每个与a邻接的点的距离存入dist  
    for(i = 1; i < N; i++)  
    {  
        intminor = INF;  
        for(j = 0; j < N; j++)  
        {  
            if(!visit[j] && dist[j] < minor)          //找到未访问的点中,距离当前最小生成树距离最小的点  
            {  
                minor = dist[j];  
                index = j;  
            }  
        }  
        visit[index] = true;  
        cout << index << " ";  
        sum += minor;  
        for(j = 0; j < N; j++)  
        {  
            if(!visit[j] && dist[j]>graph[index][j])      //执行更新,如果点距离当前点的距离更近,就更新dist  
            {  
                dist[j] = graph[index][j];  
            }  
        }  
    }  
    cout << endl;  
    returnsum;               //返回最小生成树的总路径值  
}  
intmain()  
{  
    cout << prim(0) << endl;//从顶点a开始  
    return0;  
}

 

师哥讲的模板,看起来好像更熟悉一些:

 

#include<stdio.h>
#include<string.h>
#include <iostream>
#include <bits/stdc++.h>
#define IO ios::sync_with_stdio(false);\
    cin.tie(0);\
    cout.tie(0);
#define MAX 0x3f3f3f3f
using namespace std;
int logo[1010];//用来标记0和1  表示这个点是否被选择过
int map1[1010][1010];//邻接矩阵用来存储图的信息
int dis[1010];//记录任意一点到这个点的最近距离
int n;//点个数
int prim()
{
    int i,j,now;
    int sum=0;
    /*初始化*/
    for(i=1; i<=n; i++)
    {
        dis[i]=MAX;
        logo[i]=0;
    }
    /*选定1为起始点,初始化*/
    for(i=1; i<=n; i++)
    {
        dis[i]=map1[1][i];
    }
    dis[1]=0;
    logo[1]=1;
    /*循环找最小边,循环n-1次*/
    for(i=1; i<n; i++)
    {
        now=MAX;
        int min1=MAX;
        for(j=1; j<=n; j++)
        {
            if(logo[j]==0&&dis[j]<min1)
            {
                now=j;
                min1=dis[j];
            }
        }
        if(now==MAX)
            break;//防止不成图
        logo[now]=1;
        sum+=min1;
        for(j=1; j<=n; j++)//添入新点后更新最小距离
        {
            if(logo[j]==0&&dis[j]>map1[now][j])
                dis[j]=map1[now][j];
        }
    }
    if(i<n)
        printf("?\n");
    else
        printf("%d\n",sum);
}
int main()
{
    while(scanf("%d",&n),n)//n是点数
    {
        int m=n*(n-1)/2;//m是边数
        memset(map1,0x3f3f3f3f,sizeof(map1));//map是邻接矩阵存储图的信息
        for(int i=0; i<m; i++)
        {
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            if(c<map1[a][b])//防止重边
                map1[a][b]=map1[b][a]=c;
        }
        prim();
    }
}

HINT:

如果是想求最短路的话,只需要将被调中更新数组部分的两行代码改为:

 if(logo[j]==0&&dis[j]>map1[now][j]+dis[now])
                dis[j]=map1[now][j]+dis[now];

 

且此时min1和sum就没有用了,可以删掉。另外还需注意:其中表示循环的变量,有好几个都是从1开始的,如果纯粹用来计数,从0开始不会有影响,但是其他的不能改,在其他题目中,要参考其题意再决定。

 

Prim模板

#include<stdio.h>
#include<string.h>
#include <iostream>
#include <bits/stdc++.h>
#define MAX 0x3f3f3f3f
using namespace std;
int logo[1010];//用来标记0和1  表示这个点是否被选择过
int map1[1010][1010];//邻接矩阵用来存储图的信息
int dis[1010];//记录任意一点到这个点的最近距离
int n;//点个数
int prim()
{
    int i,j,now;
    int sum=0;
    /*初始化*/
    for(i=1; i<=n; i++)
    {
        dis[i]=MAX;
        logo[i]=0;
    }
    /*选定1为起始点,初始化*/
    for(i=1; i<=n; i++)
    {
        dis[i]=map1[1][i];
    }
    dis[1]=0;
    logo[1]=1;
    /*循环找最小边,循环n-1次*/
    for(i=1; i<n; i++)
    {
        now=MAX;
        int min1=MAX;
        for(j=1; j<=n; j++)
        {
            if(logo[j]==0&&dis[j]<min1)
            {
                now=j;
                min1=dis[j];
            }
        }
        if(now==MAX)
            break;//防止不成图
        logo[now]=1;
        sum+=min1;
        for(j=1; j<=n; j++)//添入新点后更新最小距离
        {
            if(logo[j]==0&&dis[j]>map1[now][j])
                dis[j]=map1[now][j];
        }
    }
    if(i<n)
        printf("?\n");
    else
        printf("%d\n",sum);
}
int main()
{
    while(scanf("%d",&n),n)//n是点数
    {
        int m=n*(n-1)/2;//m是边数
        memset(map1,0x3f3f3f3f,sizeof(map1));//map是邻接矩阵存储图的信息
        for(int i=0; i<m; i++)
        {
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            if(c<map1[a][b])//防止重边
                map1[a][b]=map1[b][a]=c;
        }
        prim();
    }
}

 

3.Sollin(Boruvka)算法

 

Sollin(Brouvka)算法虽然是最小生成树最古老的一个算法之一,其实是前面介绍两种算法的综合,每次迭代同时扩展多课子树,直到得到最小生成树T。

Sollin(Boruvka)算法步骤

1.用定点数组记录每个子树(一开始是单个定点)的最近邻居。(类似Prim算法)

2.对于每一条边进行处理(类似Kruskal算法)

如果这条边连成的两个顶点同属于一个集合,则不处理,否则检测这条边连接的两个子树,如果是连接这两个子树的最小边,则更新(合并)

由于每次循环迭代时,每棵树都会合并成一棵较大的子树,因此每次循环迭代都会使子树的数量至少减少一半,或者说第i次迭代每个分量大小至少为。所以,循环迭代的总次数为O(logn)。每次循环迭代所需要的计算时间:对于第2步,每次检查所有边O(m),去更新每个连通分量的最小弧;对于第3步,合并个子树。所以总的复杂度为O(E*logV)。

        代码实现如下:

    typedef struct{int v;int w;double wt;}Edge;  
    typeder struct{int V;int E;double **adj}Graph;  
    /*nn存储每个分量的最邻近,a存储尚未删除且还没在MST中的边 
    *h用于访问要检查的下一条边 
    *N用于存放下一步所保存的边 
    *每一步都对应着检查剩余的边,连接不同分量的顶点的边被保留在下一步中 
    *最后每一步将每个分量与它最邻近的分量合并,并将最近邻边添加到MST中 
    */  
    Edge nn[maxE],a[maxE];  
    void Boruvka(Graph g,Edge mst[])  
    {  
    int h,i,j,k,v,w,N;  
    Edge e;  
    int E=GRAPHedges(a,G);  
    for(UFinit(G->V);E!=0;E=N)  
    {  
         for(k=0;k<G->V;k++)  
               nn[k]=Edge(G->V,G->V,maxWT);  
         for(h=0,N=0;h<E;h++)  
         {  
              i=find(a[h].v);j=find(a[h].w);  
              if(i==h) continue;  
              if(a[h].wt<nn[i].wt)nn[i]=a[h];  
              if(a[h].wt<nn[j].wt)nn[j]=a[h];  
              a[N++]=a[h];  
          }  
          for(k=0;k<G->V;k++)  
          {  
               e=nn[k];v=e.v;w=e.w;  
               if(v!=G->V&&!UFfind(v,w))  
               {  
                    UFunion(v,w);mst[k]=e;  
                 }  
            }  
      
    }  

 

 

参考资料有:

http://www.cnblogs.com/aiguona/p/7223625.html(师哥博客)

https://blog.csdn.net/gettogetto/article/details/53216951

https://blog.csdn.net/luoshixian099/article/details/5190817

http://dsqiu.iteye.com/blog/1689178

  • 43
    点赞
  • 158
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值