图论中最小生成树构造算法之Prim算法和Kruskal算法

时间:2021-09-06 11:43:08

图是 由顶点的有穷非空集合和点之间边的集合构成:

G={V,E},V是顶点集合,E是顶点之间边的集合。根基顶点之间边有无方向性可分为:有向图和无向图:

图论中最小生成树构造算法之Prim算法和Kruskal算法

在图中,当对边赋予有意义数值时候,成为网图。

对于无向图:若任意两点之间有路径,则该图连通图;非连通图极大连通子图为连通分量;

对于有向图:任意两点之间,有方向路径,则该图的强连通图;非强连通图的极大连通子图为强连通分量。

具有N个顶点的连通图G的生成树:包含G的全部顶点的极小连通子图:即这样的图缺少一边则不能构成连通,多一条边则两点会形成第二个路径。n个顶点构成的生成树有且仅有n-1条边;非连通图每个连通分量可以得到一棵生成树,这些树构成森林。

设G={V,E}是一个无向连通图,其生成树各边权值之和称为该生成树代价。所以生成树代价最小那个称为最小生成树,因此下面需要求得最小生成树:引入Prim和Kruskal算法

一.Prim算法

其主要思想:

将G={V,E}的最小生成树为T={U,TE},其中U是顶点集,TE是边集,接下来会把V分成U和V-U两部分

1.初始化:U={u0},TE={ },在随后的算法执行中,这个形态会不断的发生变化,直到得到最小生成树为止。

2.迭代:在所有u∈U,v∈V-U的边(u,v)∈E中,找一条权最小的边(u0,v0),将此边加进集合TE中,并将此边的非U中顶点加入U中。此步骤的功能是在边集E中找一条边,要求这条边满足以下条件:首先边的两个顶点要分别在顶点集合U和V-U中,其次边的权要最小。找到这条边以后,把这条边放到边集TE中,并把这条边上不在U中的那个顶点加入到U中。这一步骤在算法中应执行多次,每执行一次,集合TE和U都将发生变化,分别增加一条边和一个顶点,因此,TE和U是两个动态的集合。

3.如果U=V,则算法结束;否则重复步骤2。可以把本步骤看成循环终止条件。我们可以算出当U=V时,步骤2共执行了n-1次(设n为图中顶点的数目),TE中也增加了n-1条边,这n-1条边就是需要求出的最小生成树的边。

举例:

图论中最小生成树构造算法之Prim算法和Kruskal算法

如图所示过程,可以看到U和TE是一直动态变化,直到U=V,经历了n-1次变化,TE中也由空增加了n-1个边,Pre表示候选最短边集(U到V-U集合),next表示入选的即将加入TE的最短边

为了表示候选最短集,设置两个一维数组lowcost[n],adj[n],前者表示V-U中各顶点与U中顶点最短边的权值,lowcost[v]=0表示在V-U中顶点已加入到U中,adj[n]用来表示这样的边的在U中的顶点:如vi属于V-U,vk属于U则:

lowcost[i]=w;

adj[i]=k;

初始时U={u0},则lowcost[0]=0,表示顶点u0已经加入到U。其他情况下lowcost[i]都表示V-U中顶点vi到U中顶点边的权值。不断选取权值最小的边(vi,vk),每选取一次就将lowcost[i]置零,表示将vi加入U。故有:

lowcost[i]=min{weight(vi,vk),lowcost[i]|vi属于V-U}

adj[i]有两种情况:

1:weight(vi,vk)<lowcost[i]  adj[i]=k

2:否则还是原值

代码流程:

1、初始化两个数字lowcost和adj;

2、输出顶点u0,将其加入到U

3、重复执行n-1次:

    在lowcost[i]中选取最短边,并取得adj[i]对应顶点序号k

   输出顶点k和对应权值

    将k加入到U

   调整lowcost和adj

由上面可以得到Prim算法构造最小生成树参数变化图:

图论中最小生成树构造算法之Prim算法和Kruskal算法

Prim算法时间由于不断读取任意两点之间边的权值,因此执行时采用邻接矩阵存储,由此可以知道时间复杂度O(n^2),故适应于稠密网生成最小生成树。

代码:

int visi[M]={0};
int f1=0;


void prim(grap&g,int v){
int lowcost[M];
int u[M];
int i,j,k;
Te te[M]={{0,0}};
int min;
for(i=0;i<g.v;i++){lowcost[i]=INF;u[i]=-1;}
visi[v]=1;
int d=0;
u[d]=v;
for(k=0;k<g.v-1;k++){
    min=INF;
for(i=0;i<=d;i++){
for(j=0;j<g.v;j++)
{ 
    if(g.edge[u[i]][j]!=0&&visi[j]==0)
    {if(min>g.edge[u[i]][j]){min=g.edge[u[i]][j];
    te[k].x=u[i];te[k].y=j;}
    }
}
}
visi[te[k].y]=1;
d++;
u[d]=te[k].y;

}

if(f1==0){
cout<<"已经选了的顶点以及边分别:"<<endl;f1=1;}
for(i=0;i<=d;i++){cout<<char(u[i]+'A')<<",";
}
cout<<endl;
for(i=0;i<k;i++)cout<<'('<<char(te[i].x+'A')<<','<<char(te[i].y+'A')<<')'<<endl;
}

本代码将一集i访问的节点记visii[i]=1,并建立一个含有两个节点的结构体类型数组以便存储并输出最后构成的边。由于有三重循环故时间复杂度为O(n^2).

二、Kruskal算法

其主要思想:

1、设无向连通网G={V,E},其最小生成树T={U,TE},其中U=V,TE={ },U是顶点集,TE是边集;;

2、迭代,T中各顶点各自形成一个连通分量,按照权值大小对边进行排序,主要研究边:当两个点属于T的;两个不同连通分量时候,将此边归入TE,同时把两个连通分量连接为一个连通分量;当两个顶点属于同一个两个分量时候,忽略此边;

3、当连通分量为1时候,产生了最小生成树

具体如下:

图论中最小生成树构造算法之Prim算法和Kruskal算法

由于Kruskal算法主要对边进行操作,故采用边集数组作为存储结构。

代码流程:

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

2、循环直到T连通分量为1

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

     如果两点位于T中不同连通分量:

        将边(u,v)并入TE

        这两个连通分量合并

Kruskal算法主要时间花在排序上,故总体时间复杂度取自排序算法:例如当采用直接插入排序时,时间复杂度O(e^2),若采用堆排序则为O(eloge),e为边数。故此发适应于稀疏图。

代码:

struct edge{
int u;
int v;
int w;};
void insert_sort(edge e[],int n){
int i,j;
edge t;
for(i=1;i<n;i++)
{t=e[i];
    j=i-1;
 while(j>=0&&t.w<e[j].w){e[j+1]=e[j];
            j--;}
 e[j+1]=t;
}
}
void kruscal(grap&g){
edge e[M*M]={{-1,-1,INF}};
int vset[M];
int i,j,k;
int t;
int m1,m2;
int s1,s2;
k=0;
for(i=0;i<g.v;i++)
    for(j=0;j<g.v;j++)
    {if(g.edge[i][j]!=0&&g.edge[i][j]!=INF)
      {e[k].u=i;e[k].v=j;e[k].w=g.edge[i][j];k++;}}
insert_sort(e,g.e);
for(i=0;i<g.v;i++)vset[i]=i;
t=1;
k=0;
while(t<g.v)//一共n-1条边
{m1=e[k].u;m2=e[k].v;
 s1=vset[m1];s2=vset[m2];
 if(s1!=s2){
 t++;
 cout<<'('<<char(m1+'A')<<','<<char(m2+'A')<<')'<<" 权重:"<<e[k].w<<endl;
 for(i=0;i<g.v;i++)if(vset[i]==s2)vset[i]=s1;
 }
 k++;
}
}