最小生成树-Prim算法

生成树

生成树是连通图的最小连通子图。所谓最小是指:若在树中任意增加一条边,则将出现一个回路;若去掉一条边,将会使之变成非连通图。按照该定义,n个顶点的连通网络的生成树有n个顶点,n-1条边

可知用不同的遍历图的方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树。



最小生成树

生成树各边的权值总和称为生成树的权,权最小的生成树称为最小生成树

最小生成树的概念可以应用到许多实际问题中。例如:以尽可能低的总造价建设城市间的通讯网络,以把10个城市联系在一起。在这10个城市中,任意两个城市间都可以建造通讯线路,通讯线路的造价依据城市间的距离不同而有不同的造价,可以构造一个通讯线路造价网络,在网络中,每个顶点表示城市,顶点之间的边表示城市之间可构造通讯线路,每条边的权值表示该条通讯线路的造价,要想使总的造价最低,实际上就是寻找该网络的最小生成树。

常见的构造最小生成树的方法有Prim算法和Kruskal算法。

下面介绍Prim算法



Prim算法

Prim算法通常以邻接矩阵作为储存结构。

它的基本思想是以顶点为主导地位,从起始顶点出发,通过选择当前可用的最小权值边把顶点加入到生成树当中来

1.从连通网络N={V,E}中的某一顶点U0出发,选择与它关联的具有最小权值的边(U0,V),将其顶点加入到生成树的顶点集合U中。

2.以后每一步从一个顶点在U中,而另一个顶点不在U中的各条边中选择权值最小的边(U,V),把它的顶点加入到集合U中。如此继续下去,直到网络中的所有顶点都加入到生成树顶点集合U中为止。 


可行性证明:

设prim生成的树为G0

假设存在Gmin使得cost(Gmin)<cost(G0)

则在Gmin中存在(u,v)不属于G0

将(u,v)加入G0中可得一个环,且(u,v)不是该环的最长边

这与prim每次生成最短边矛盾

故假设不成立,得证.



下面看具体数据和模拟过程


现在有一个存储结构为邻接矩阵的G,有9个顶点,它的arc二维数组如上图所示。

于是Prim算法的代码如下,其中INFINITY为权值极大值,不妨设为0xfffffff,MAXVEX为顶点个数最大值,此处大于等于9即可。

void MinSpanTree_Prim(MGraph G)
{
	int min,i,j,k;
	int adjvex[MAXVEX];//保存相关顶点下标以记录路径 
	int lowcost[MAXVEX];//保存相关顶点间边的权值
	lowcost[0]=0;//初始化第一个权值为0,即v0加入生成树
	//lowcost的值为0,在这里就是此下标的顶点已经加入生成树
	adjvex[0]=0;//初始化到第一个顶点的下标路径为0
	for(i=1;i<G.numVertexes;++i){//循环除下标为0外的全部顶点 
		lowcost[i]=G.arc[0][i];//将V0顶点与之有边的权值存入数组
		adjvex[i]=0;//初始化都为V0的下标路径 
	}
	for(i=1;i<G.numVertexes;++i){
		min=INFINITY;//初始化最小权值为+inf,通常设置为上限数字,如0x3f3f3f3f		
		
		j=0;k=0;
		while(j<G.numVertexes)//循环全部顶点 
		{
			if(lowcost[j]!=0&&lowcost[j]<min){//如果权值不为零,且权值小于min 
				min=lowcost[j];//则让当前权值成为最小值 
				k=j;//将当前最小值的下标存入k 
			}
			j++;
		}
		printf("(%d,%d)",adjvex[k],k);//打印当前顶点边中权值最小边 
		lowcost[k]=0;//将当前顶点的权值设置为0,表示此顶点已经完成任务
		for(j=1;j<G.numVertexes;++j){//循环所有顶点 
			if(lowcost[j]!=0&&G.arc[k][j]<lowcost[j]){
				//若k能到的点未完成任务并且此边小于对应的lowcost
				lowcost[j]=G.arc[k][j];//将较小权值存入lowcost
				adjvex[j]=k;//将下标为k的顶点存入adjvex 
			}
		} 
	}
}


1.程序开始运行,我们由第4~5行,创建了两个一维数组lowcostadjvex,长度都为顶点个数9。他们的作用后面会体现。

 

2.第6~8行分别给这两个数组的第一个下标位赋值为0,adjvex[0]=0意思就是现在从顶点V0开始(事实上,最小生成树从哪个顶点开始计算都无所谓,我们假定从V0开始),lowcost[0]=0就表示V0已经被纳入到最小生成树中,之后凡是lowcost数组中的值被设置为0就是表示此下标的顶点被纳入最小生成树

 

3.第9~12行表示我们读取邻接矩阵的第一行数据。将数值赋值给lowcost数组,所以此时lowcost数组值为{0,10,inf,inf,inf,11,inf,inf,inf},而adjvex则全部为0。此时,已经完成了整个初始化的工作,准备开始生成。

 

4.第13~35行,整个循环过程就是构造最小生成树的过程。

 

5.第14~16行,将min设置为了一个极大值0x3fffffff,它的目的是为了之后找到一定范围内的最小权值。j是用来做顶点下标循环的变量,k是用来存储最小权值的顶点下标。

 

6. 第17~24行,循环中不断修改min为当前lowcost数组中最小值,并用k保留此最小值的顶点下标。经过循环后,min=10,k=1。第19行if判断的lowcost[j]!=0表示已经是生成树的顶点不参与最小权值的查找

 

7.第25行,因k=1,adjvex[1]=0,所以打印结果为(0,1),表示V0至V1边为最小生成树的第一条边。如下图所示:

 

8.第26行,此时因k=1,我们将lowcost[k]=0就是说顶点v1纳入到最小生成树中。此时lowcost数组值为{0,0,inf,inf,inf,11,inf,inf,inf}。

 

9.第27~33行,j循环由1至8,因k=1,查找邻接矩阵的第v1行的各个权值,与lowcost的对应值比较,若更小则修改lowcost值,并将k值存入adjvex数组中。因第v1行有18、16、12均比0x3fffffff小,所以最终lowcost数组的值为:{0,0,18,inf,inf,11,16,inf,12}。adjvex数组的值为:{0,0,1,0,0,0,1,0,1}。这里第30行if判断的lowcost[j]!=0也说明v0和v1已经是生成树的顶点不参与最小权值的对比了

 

10.再次循环,由第14行到25行,此时min=11,k=5,adjvex[5]=0。因此打印结构为(0,5)。表示v0至v5边为最小生成树的第二条边,如下图所示:

 

11.接下来执行到33行,lowcost数组的值为:{0,0,18,inf,26,0,16,inf,12}。adjvex数组的值为:{0,0,1,0,5,0,1,0,1}。

 

12.之后经过类似的模拟,如下图:

 


总结

       使用邻接矩阵作为存储结构的Prim算法的时间复杂度为O(V2),如果使用二叉堆与邻接表表示的话,Prim算法的时间复杂度可缩减为O(E log V),其中E为连通图的边数,V为顶点数。

      如果使用较复杂的斐波那契堆,则可将运行时间进一步缩短为O(E + V log V),这在连通图足够密集时(边较多,即E满足Ω(V log V))可较显著地提高运行速度。


 

例:POJ - 2395

求minispan_tree中longest edge

vector中存每个点所连边的序号 通过边集数组索引

//求minispan_tree中longest edge
//O(N^2)
//vector中存每个点所连边的序号 通过边集数组索引
#include<iostream>
#include<vector>
#include<cstdio>
using namespace std;


struct Edge{
    int from,to,dist;
    Edge(int u,int v,int w):from(u),to(v),dist(w){}
};


const int maxn=10010;
const int inf=0x3f3f3f3f;
int ans;


vector<Edge> edges;


vector<int> G[maxn];


int n,m;


void addEdge(int u,int v,int w){
    edges.push_back(Edge(u,v,w));
    edges.push_back(Edge(v,u,w));
    int size=edges.size();
    G[u].push_back(size-2);
    G[v].push_back(size-1);
}


int adjvex[maxn];//最小生成树上每个点的前驱
int lowcost[maxn];//前驱到这个点的距离,即目前最小生成树中到该点的最短距离
bool v[maxn];//是否在生成树中的标记


void Prim()
{
    for(int i=1;i<=n;++i) lowcost[i]=inf;
    lowcost[1]=0;
    for(int i=1;i<=n;++i) adjvex[i]=0,v[i]=0;
    adjvex[1]=1;
    for(int i=1;i<=n;++i){
        //for(int i=1;i<=n;++i) cout<<lowcost[i]<<' ';
        int minn=inf,mark=-1;
        for(int j=1;j<=n;++j){
            if(!v[j]&&lowcost[j]<minn){
                minn=lowcost[j];
                mark=j;
            }
        }
        if(mark==-1) break;
        //printf("(i:%d,%d,%d)\n",i,adjvex[mark],mark);
        ans=max(minn,ans);
        v[mark]=1;
        for(int j=0;j<G[mark].size();++j){//松弛
            if(!v[edges[G[mark][j]].to]&&edges[G[mark][j]].dist<lowcost[edges[G[mark][j]].to]){
                lowcost[edges[G[mark][j]].to]=edges[G[mark][j]].dist;
                adjvex[edges[G[mark][j]].to]=mark;
            }
        }
    }
}


int main()
{
    //freopen("read.in","r",stdin);
    scanf("%d %d",&n,&m);
    int tu,tv,tw;
    for(int i=0;i<m;++i){
        scanf("%d %d %d",&tu,&tv,&tw);
        addEdge(tu,tv,tw);
    }
    ans=-inf;
    Prim();
    printf("%d\n",ans);
    return 0;
}


vector中直接存每个边的信息(常用pair)first表示所连点的编号 second表示距离

//求minispan_tree中longest edge
//O(N^2)
//vector中直接存每个边的信息(常用pair)first表示所连点的编号 second表示距离
#include<iostream>
#include<vector>
#include<cstdio>
using namespace std;
#define mp make_pair


const int maxn=10010;
const int inf=0x3f3f3f3f;
int ans;


vector<pair<int,int> > G[maxn];


int n,m;
void addEdge(int u,int v,int w){
    G[u].push_back(mp(v,w));
    G[v].push_back(mp(u,w));
}


int adjvex[maxn];//最小生成树上每个点的前驱
int lowcost[maxn];//前驱到这个点的距离
int v[maxn];//是否在生成树中的标记


void Prim()
{
    for(int i=1;i<=n;++i) lowcost[i]=inf;
    lowcost[1]=0;
    for(int i=1;i<=n;++i) adjvex[i]=0,v[i]=0;
    adjvex[1]=1;
    for(int i=1;i<=n;++i){
        //for(int i=1;i<=n;++i) cout<<lowcost[i]<<' ';
        int minn=inf,mark=-1;
        for(int j=1;j<=n;++j){
            if(!v[j]&&lowcost[j]<minn){
                minn=lowcost[j];
                mark=j;
            }
        }
        if(mark==-1) break;
        //printf("(i:%d,%d,%d)\n",i,adjvex[mark],mark);
        ans=max(minn,ans);
        v[mark]=1;
        for(int j=0;j<G[mark].size();++j){
            if(lowcost[G[mark][j].first]!=0&&G[mark][j].second<lowcost[G[mark][j].first]){
                lowcost[G[mark][j].first]=G[mark][j].second;
                adjvex[G[mark][j].first]=mark;
            }
        }
    }
}


int main()
{
    //freopen("read.in","r",stdin);
    scanf("%d %d",&n,&m);
    int tu,tv,tw;
    for(int i=0;i<m;++i){
        scanf("%d %d %d",&tu,&tv,&tw);
        addEdge(tu,tv,tw);
    }
    ans=-inf;
    Prim();
    printf("%d\n",ans);
    return 0;
}

  • 13
    点赞
  • 66
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值