数据结构之图论算法(三)——构造图的最小生成树

构造图的最小生成树

最小生成树:

在这里插入图片描述

构造最小生成树的方法:

构造最小生成树的方法多数利用了最小生成树的MST性质

MST性质:设N =(V,E)是一个连通网,U是顶点集V上的一个非空子集,若边(u,v)是一条最小权值的边,其中u∈U,v∈U-V,则比存在一棵包含边(u,v)的最小生成树。
方法一:普里姆(Prim)算法
算法思想:

设N=(V,{E})是连通网,TE是N上最小生成树中边的集合

  1. 初始令U = {u₀},(u₀ ∈V),TE=Ф;
  2. 在所有u∈U,v∈V-U的边(u,v)∈E中,找到一条代价最小的边(u₀,v₀);
  3. 将(u₀,v₀)并入集合TE,同时v₀并入集合U中;
  4. 重复上述操作直至U==V为止,则T = (V,{TE})为N的最小生成树。
举例:

在这里插入图片描述

closedge数组:

附设一个辅助数组closedge,用以记录从V-U中的各个顶点到U的具有最小代价的边。

  • 对于每一个顶点vi∈V-U,相应的分量为closedge[i-1],它应该包括两个域:

    1. lowcost储存该边上的权,即
  •  closedge[i-1].lowcost = min{ cost ( u , vi ) | u ∈ U }
    
  •  (网采用邻接矩阵表示法)
    
    1. Adjvex存储该边依附在U中的顶点
  •  U中每增加一个顶点,只要考虑该新增顶点到vi这条边上的权值会不会更小即可。
    
closedge数组的组成部分:-
lowcostAdjvex
储存该边上的权存储该边依附在U中的顶点

closedge数组的应用举例:
在这里插入图片描述

在这里插入图片描述
(答案不唯一)

每次加入一个点,就看该点与其他点的关系,如果有比当前closedge数组中的权值更小的,就更新lowcost和adjvex,同时去掉原来的那条线。

算法实现:(图的组成结构参照数据结构之图论算法(一)图的存储结构及其构建算法中的邻接表表示)

//记录从顶点集U到V-U的代价最小的边的辅助closedge数组定义:
struct closedgetype{
	VertexType adjvex;	//存储最小权值边所连接的另一个点的地址
	VRType lowcost;		//存储边的最小权值
}closedge[MAX_VERTEX_NUM];

int GetWeight(Graph G, int start, int end){//start为起点地址,end为终点地址,该函数用于求两点间的距离;
	if(start == end) return 0;		//如果起点和终点为同一个,就返回0
	adjVert *p = G.v[start].firstarc;	//邻接表的探索的管用方法,先用p去指向邻接顶点相连的第一条边
	while(p){
		if(end == p->adjvert) return p->edgeInfo;	//如果end==p所指的边连接的下一顶点,就说明这两点存在边,权值大小就是这条边的值
		p = p->next;	//如果不相等,就看下一条与start相连的边
	}
	//如果p==NULL了还没有return,说明start与end之间没有直接相连的边,则返回无穷大;
	return INFINITY;
}

int MinCostVert(closedgetype closedge[], int sum){
	int i, minvert = 0;
	int min = INFINITY;	//初始化
	for(i = 0; i < sum; i++){
		if(closedge[i].lowcost < min){
			min = closedge[i].lowcost;
			minvert = closedge[i].adjvex;
		}
	}
	return minvert;
}

void MiniSpanTree_PRIM(Graph G, int u){
	//用Prim算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边;
	int k = u-1;	//找到点u对应的顶点地址;
	int sumcost = 0;	//用于求总代价
	int j, i;
	//初始化:U = {V1},只有一个点
	for(j = 0 ; j < G.n; j++){
		closedge[j].adjvex = k;	//表示每一个点如果都与u点相连的话,最小代价为多少
		closedge[j].lowcost = GetWeight(G, k, j);	//从u(起点地址)到j(终点地址)
	}
	for(i = 1; i < G.n; i++){
		//从当前closedge中找到最小的邻接结点的地址k;
		k = MinCostVert(closedge, G.n);
		sumcost += closedge[k].lowcost;
		//把k点并入集合U中:
		closedge[k].lowcost = 0;
		//重新选择最小边:
		for(j = 0;j < G.n; j++){
			if(GetWeight(G, i, j) < closedge[j].lowcost){
				closedge[j].adjvex = i;
				closedge[j].lowcost = GetWeight(G,i,j);
			}
		}
	}
	printf("%d\n", sumcost);	//输出最小代价
}

如果图的构成是邻接矩阵的话,则代码可参考如下:

int MinCostVert(closedgetype closedge[], int sum){
	int i, minvert = 0;
	int min = INFINITY;	//初始化
	for(i = 0; i < sum; i++){
		if(closedge[i].lowcost < min){
			min = closedge[i].lowcost;
			minvert = closedge[i].adjvex;
		}
	}
	return minvert;
}

void MiniSpanTree_PRIM(Graph G, int u){
	//用Prim算法从第u个顶点出发构造网G的最小生成树T,输出T的各条边;
	int k = u-1;	//找到点u对应的顶点地址;
	int sumcost = 0;	//用于求总代价
	int j, i;
	//初始化:U = {V1},只有一个点
	for(j = 0 ; j < G.n; j++){
		if(j != k){
			closedge[j].adjvex = k;	//表示每一个点如果都与u点相连的话,最小代价为多少
			closedge[j].lowcost = G.arcs[k][j].adj;
		}
	}
	closedge[k].lowcost = 0;
	for(i = 1; i < G.n; i++){
		//从当前closedge中找到最小的邻接结点的地址k;
		k = MinCostVert(closedge, G.n);
		sumcost += closedge[k].lowcost;
		//把k点并入集合U中:
		closedge[k].lowcost = 0;
		//重新选择最小边:
		for(j = 0;j < G.n; j++){
			if(G.arcs[k][j].adj < closedge[j].lowcost){
				closedge[j].adjvex = i;
				closedge[j].lowcost = G.arcs[k][j].adj;
			}
		}
	}
	printf("%d\n", sumcost);	//输出最小代价
}


大体不变,主要是把GetWeight函数换成了二维数组可以表示的值。

方法二:克鲁斯卡尔(Kruscal)算法
算法思想:

设连通网N =(V,{E})

  • 令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),图中每个顶点自成一个连通分量
  • 在E中选择代价最小的边若该边依附的顶点落在T中不同的连通分量上,则将此边加入T中,否则舍去该边而选择下一条代价最小的边,依次类推,直到T中所有顶点都在同一联通分量上为止。

用伪代码可表示为:

把所有边排序,记第i小的边为e[i](1<=i<m)//m为总边数;
初始化MST为空;
初始化连通分量,让每个点自成一个独立的连通分量;
for(int i = 0; i < m; i++){
	if(e[i].u 和 e[i].v 不在同一个连通分量里){
			把边e[i]并入MST;
			合并e[i].u 和 e[i].v 所在的连通分量;
	}
}

在上述的伪代码中,关键在于连通分量的查询与合并

  1. 需要知道任意两个点是否在一个连通分量中;
  2. 还需要合并这两个连通分量
使用并查集(Union-Find set)

如同Prim算法要借助closedge数组一样,Kruskal算法需要使用并查集。

可以把每个连通分量看成一个集合,该集合包含了连通分量中的所有点,这些点两两相通,而具体的连通方式无关紧要,类似于集合中的元素没有先后顺序之分,只有“属于”和“不属于”之分。

图的所有连通分量可以用若干个不相交的集合表示,而集合又通过树来表示。

例如:包含点{1,2,3,4,5,6}的图有3个连通分量{{1,2}, {3,4,5}, {6}},那么就需要用3棵树来表示。规定每棵树的根节点是这棵树所对应的集合的代表元

代表元存储在 p[ v ] 数组(parent)中,即把结点 vi 的父节点存储在 p[ i ] 中,因此可以写出“查找x所在树的根节点”的递归程序:(其中若 vi 没有父节点了,则p[i] = vi 本身)
(以邻接表为例)

int Findroot(int x){
		return p[x] == x ? x : Findroot(p[x]);	//如果x没有父节点了,即p[x] == x,就返回x;
												//否则继续递归寻找根节点
}

为了提高效率,就将遍历过的结点都改成树根的子节点;因此上述代码改进为:

int Findroot(int x){
		return p[x] == x ? x : p[x] = Findroot(p[x]);	
}

这样,Kruscal算法的完整代码如下:

int CountArcs(Graph G){
	int sum = 0;
	for(int i = 0; i <G.n; i++){
		adjVert *p = G.v[i].firstarc;
		while(p){
			sum++;
			p = p->next;
		}
	}
	return sum;
}

void setvalue_rank(Graph G,int *r, int *w, int *u, int *v){
	int e = 0;
	for(int i = 0; i < G.n; i++){
		adjVert *arcp = G.v[i].firstarc;
		while(arcp){
			w[e] = arcp->edgeInfo;
			u[e] = i;
			v[e] = arcp->adjvert;
			arcp = arcp->next;
			e++;
		}
	}

	for(int i = 0; i <= e-1; i++){
		for(int j = i+1; j<= e; j++){
			if(w[i] > w[j]){
				r[i] = j;
				r[j] = i;
			}
		}
	}
}

int Findroot(int x, int *p){
		return p[x] == x ? x : p[x] = Findroot(p[x], p);	
}

//第i条边的两个端点分别储存在u[i],v[i]中,其权值储存在w[i]中;
int Kruscal(Graph G){
	int m = CountArcs(G), ans = 0;
	int p[MAX_VERTEX_NUM], *w, *r, *u, *v;
	int i, j, e;
	w = (int*)malloc(m * sizeof(int));r = (int*)malloc(m * sizeof(int));
	u = (int*)malloc(m * sizeof(int));v = (int*)malloc(m * sizeof(int));
	//初始化:
	for(i = 0; i < m; i++) r[i] = i;
	for(i = 0; i < G.n;i++) p[i] = i;
	//赋值并排序
	setvalue_rank(G,r,w,u,v);
	for(i = 0; i < m; i++){
		e = r[i]; int x = Findroot(u[e], p); int y = Findroot(v[e],p);
		if(x != y){ans+=w[e]; p[x] = y;}
	}
	return ans;
}
  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值