FIG classic application - the minimum spanning tree (MST, Minimum-cost Spanning Tree) 05

Example 1 - primer minimum cost network set up a communication
primers Example 2 - information transmission problems in the network
STP to the bridge (switch) device (Spanning Tree Protocol) protocol

MSTI
Here Insert Picture Description
spanning a minimal connected subgraphs of the connectivity graph. Refers to so-called minimal if any increase in the side of a tree, a loop will occur; if one side removed, so will not communicate with FIG.
Not determined spanning tree root, commonly referred to as free tree. Select a vertex in a free tree root to do, it becomes a normal tree. Starting at the root, for each child vertex prescribed order from left to right, then it becomes an ordered tree.

Concept of the minimum spanning tree
Here Insert Picture Description
nature of the minimum spanning tree: Following the definition of the spanning tree, the spanning tree communication network n vertices with n vertices, n-1 edges.
Minimum spanning tree, to solve the following two questions:
(1) the right to select the smallest possible value of the side, but can not form a loop (ie, ring).
(2) selecting n vertices and the n-1 is connected to the right side of the network.

Minimum spanning tree algorithm

Minimum spanning tree algorithm normally used for greedy strategy, the classical algorithm Prim's algorithm and Krusal algorithm.
Both algorithms using greedy method has a similar way of thinking, that a "generation" will not produce a "safe side" circuit, the difference between the two algorithms only in different ways seeking safe side.
Here Insert Picture Description

  1. Minimum spanning tree algorithm 1 - Prim algorithm

The basic idea
Here Insert Picture Description
pseudocode Details:
Here Insert Picture Description
Here Insert Picture Description
set the start edge point u0 = A, and A is associated has (A, B) (A, C) and (A, D), wherein the minimum weight side is (A, C) .

1)Prim算法步骤 1,其中 # 表示无权值, 表示无路径。
u0=A,在候选边集表中列出 A 到其余各点(终点)的权值。
(1)u=A;
(2)候选边集表中找到最短边(u,v)=(A,C),v=C;
(3)在候选边集表中列出 v 到各终点的权值;
(4)比较 u 到终点与 v 到终点 x 的权值,取值小的那条边。
如终点 x=B,(A,B)=6>(C,B)=5,则选择边(C,B)替代(A,B),其他替换的还有(C,E)替换(A,E)与(C,F)替换(A,F),注意相等的边则不做调整。
在调整了整个候选边集后,确定(A,C)边加入。替换掉的边在图中有直接连线的则去掉,如替换的(A,B)线。

注:最底层单元格,由它判断起点到终点的边是否可以删去。
Here Insert Picture Description
2)prim 算法步骤 2,表格中 “ / ” 表示起点到终点的这条边已经确定,后面不在考虑。
(1)候选边集表中找到最短边(u,v)=(C,F),v=F。注意(A,C)不在考虑。
(2)在候选边集表中列出 v 到各终点的权值,调整的边为(F,D)替换(A,D)线,确定(C,F)边加入。
Here Insert Picture Description
3)Prim 算法步骤3
(1)候选边集表中找到最短边(u,v)=(F,D),v=D。
(2)在候选边集表中列出 v 到各终点的权值,确定(F,D)边加入。
Here Insert Picture Description
4)Prim算法步骤4,见图。
v=B;(C,B)边加入。
Here Insert Picture Description
5)Prim 算法步骤5,见图
v=E;(B,E)边加入。
Here Insert Picture Description

#include<stdio.h>
#define VERTEX_NUM 6                    //图的顶点数
#define INF 32767                       //INF 表示 ∞
typedef int InfoType;
struct set {
	int starNode[VERTEX_NUM];               //起点
	int endNode[VERTEX_NUM];                //终点
	int value[VERTEX_NUM];                  //权值
} edgeSet;                               	//候选边集

InfoType AdjMatrix[VERTEX_NUM][VERTEX_NUM];  //邻接矩阵
void DispMat(InfoType AdjMatrix[][VERTEX_NUM]);//输出邻接矩阵
void prim(InfoType AdjMatrix[][VERTEX_NUM],int v);
int main() {
	int A[VERTEX_NUM][VERTEX_NUM]= {
		{INF,6,1,5,INF,INF},
		{6,INF,5,INF,3,INF},
		{1,5,INF,5,6,4},
		{5,INF,5,INF,INF,2},
		{INF,3,6,INF,INF,6},
		{INF,INF,4,2,6,INF}
	};                         //初始化邻接矩阵

	printf("图的邻接矩阵:\n");
	DispMat(A);
	printf("\n");
	printf("prim 算法求解结果:\n");
	prim(A,0);
	printf("\n");
	return 0;
}
/*==============================================
函数功能:prim方法构造最小生成树
函数输入:邻接矩阵、起始顶点编号
函数输出:无
屏幕输出:邻接矩阵、最小生成树的各边
===============================================*/
//U---已加入最小生成树的点
void prim(int AdjMatrix[][VERTEX_NUM] ,int v) {
	int i,j,k;

	int Visited[VERTEX_NUM]= {0};               //访问数组,记录已加入 U 的结点

	int min;                                    //记录候选边集中的最小权值
	for(i=0; i<VERTEX_NUM; i++) {               //初始化候选边集
		if(i!=v) {
			edgeSet.starNode[i]=v;              //初始时 v 为起始点
			edgeSet.endNode[i]=i;               //终点赋值
			edgeSet.value[i]=AdjMatrix[v][i];   //赋权值
		}
	}
	//起始顶点 v 加入候选边集
	edgeSet.starNode[v]=v;
	edgeSet.endNode[v]=v;
	edgeSet.value[v]=INF;
	
	for(i=1;i<VERTEX_NUM;i++)
	{
		min=INF;
		//在候选边集中查找未加入 U 中且 value 最小的点 k
		for(j=0;j<VERTEX_NUM;j++)
		{
			if(edgeSet.value[j]<min && Visited[j]==0)
			{
				min=edgeSet.value[j];
				k=j;                    //k 记录 value 值最小的顶点 
			}
		 } 
		 Visited[k]=1;                  //k 加入 U 中
		 if(min!=INF)
		     printf("边(%c,%c)权为:%d\n",edgeSet.starNode[k]+'A',k+'A',min);
			 //由于顶点 k 的新加入而调整候选边集的 value 和 startNode
			for(j=0;j<VERTEX_NUM;j++)
			{   //若终点邻接边权值大于始点邻接边权值且i,j,k三点不重合 
				if(AdjMatrix[k][j]<edgeSet.value[j]&&j!=v)
				   {
				   	    edgeSet.value[j]=AdjMatrix[k][j];
				   	    edgeSet.starNode[j]=k;
				   }
			 } 
	}
}
/*==============================================
函数功能:打印邻接矩阵 
函数输入:邻接矩阵
函数输出:无
屏幕输出:邻接矩阵
===============================================*/
void DispMat(InfoType AdjMatrix[][VERTEX_NUM])
{
	int i,j;
	for(i=0;i<VERTEX_NUM;i++)
	{
		for(j=0;j<VERTEX_NUM;j++)
		    if(AdjMatrix[i][j]==INF)
		        printf("%3s"," ∞");
			else
			    printf("%3d",AdjMatrix[i][j]);
			printf("\n"); 
	}
}

在看prim函数第77、78行调整候选边集权值和初始点时,注意:若初始点与终点到终点邻接点权值小于或等于时(i->k<=j-<k),starNode不变,value不变。第57行for循环变量i,相当于找邻接边,所以找VERTEX_NUM-1次。

  1. 求最小生成树算法2——Kruskal 算法

解最小花费生成树问题的“克鲁斯卡尔”算法:
对无向连通赋权图 G = (V, E, W) ,求最小花费生成树 T :

  1. 对边集 E 按每个边的权值大小进行升序排列。
  2. 初始化最小花费生成树的边集 T 为空。
  3. 把图的每个顶点都初始化为树的根节点。
  4. 取出当前边集 E 中最小权值的边,假设边 e = (u, v) ,如果当前边连接的两个节点 u 和 节点 v 不在同一棵树中,则把两个节点所在的树合并在一起,成为同一棵树。同时从边集 E 中删除边 e 并且把边 e 加入到最小花费生成树的边集 T 中。
  5. 重复步骤 (4) ,直到边集 T 中的边数为 n - 1 。( n 为图 G 中节点的总个数 )
    Kruskal 算法也是基于“穷举法策略”的。通俗也称为“避环法”。从这个名字可以看出算法的主要思路是依次选取权值最小的边来组成树,但要避开会让树构成回路的边。这一点从步骤 (4) 中可以看出。
    现在主要问题将集中在以下两个问题上:
  6. 如何判断边 e 所连接的两个节点 u 和 v 不在同一棵树上?
  7. 如何把两个节点 u 和 v 所在的树合并为一棵树?
    解决第一个问题的办法是分别查找节点 u 和 v 的根节点,如果它们的根节点相同,则认为它们在同一棵树上。
    解决第二个问题的办法是把节点 u 和 v 的根节点分别作为父、子节点而连接在一起。这样就把两个分离的树组成了同一棵树。
    这里为了尽量降低合并后的树的深度,将为树中每个节点保存一个“秩”值,“秩”值代表的是该节点作为树的根节点时,该树的高度。初始时,每个节点都独立成为一棵树,因此它们的“秩”值都为 0 。
    在上述合并过程中我们将把“秩”值大的节点作为父节点,“秩”值小的节点作为子节点,如果两个节点的“秩”值相等,则任意让其中一个节点作为父节点,并同时让该节点的“秩”值加一,而子节点的“秩”值不变。

Here Insert Picture Description
设图G的生成树集合 T 中开始只有图的全部的顶点而没有边,每次选择图 G 的边集 E 中权值最小并且不产生循环的边加入 T 集合,直至覆盖全部结点。

Kruskal 算法描述:
设无向连通图网络 G =(V,E),
V为图的顶点集,E为图的边集,T为生成树集,N为顶点数集
(1)对边集 E 按每个边的权值大小进行升序排列。
(2)初始化最小生成树集 T=(V,Φ)——即只有顶点,没有边。
(3)取出当前边集 E 中最小权值的边,假设边 e=(u,v),如果当前边连接的两个结点 u 和结点 v 不在同一棵树中,则把两个结点所在的树合并在一起,成为同一棵树。同时从边集 E 中删除边 e 并且把边 e 加入到最小生成树集 T 中。
(4)重复步骤(3),直到边集 E 中的边数为 N-1 。
Here Insert Picture Description
Here Insert Picture Description

Kruskal 算法中数据结构的设计

每个子树是一个连通分量,即一个子集。在子树每一步的合并中就应该标出它们的根来,即子集中的每个结点都应该记录其根结点的域,若用连续的存储方式,则可以设计成有一个指向其根结点的静态链(下标);而根结点的双亲链中则可存储该树子集中的成员个数,为了和普通结点有所区别,可以把根结点的双亲域中的成员数设为负值。
初始时,每个结点均为根,则数组中的初值都是-1,表示当前的结点为根,以此根为树的子树中只有一个结点。当有边(A,C)加入时,A,C两个子树的根不同,故可以合并这两个子树(或子集),此时若选 A 为子树的根,则 C 变为树的普通结点,它的根的状态记录由原来的 -1 就要改为在下标为 0 的结点 A 。
Here Insert Picture Description
Here Insert Picture Description
下图给出了上图中 Kruskal 算法步骤对应的子树中各结点间的关系,其中 L 根表示结点数多的子集的根, S 根表示结点数少的子集的根。初始时每个结点都是一棵子树,它们的根即是自己。在步骤 4 中,加入边(C,F),在步骤 3 结束时的状态可以查到顶点 C 与 F 的双亲分别为 A 与 D,故边(C,F)可以加入,现在 C 亦可作为 D 的双亲,现在的问题是,F 的根应该选哪个结点做它的根?如果依然是 D 为根,则分量{A,C}与{D,F}没有变化;如果选 C 做根,由于 C 的根为 A,则 F 的根与 C 的根应该合并为 A,这样子树 {A,C} 与 {D,F} 由于边(C,F)的加入而合并为一棵子树,见上图中的第 3 步到第 4 步,故在步骤 4 里面,D 的双亲改为下标 0,A 的双亲域由于子树 {D,F} 的加入,增加了两个结点,值变为 -4 。
Here Insert Picture Description
树的合并问题与并查集

树如何合并的问题属于子集归并的并查集问题。并查集上的子集合并运算,即要合并两个元素所属的子集,首先要确定两个元素所属子集所对应的树的根结点,然后将其中一棵树的根结点链作另一个棵树的子树即可。为减免在合并过程中出现畸形树(近似单链树),通常将成员较少的子集对应的树作为成员较多的子集对应的树的子树。

并查集(Union-find-Sets)是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。集就是让每个元素构成一个单元素的集合,也就是按一定顺序将属于同一组的元素所在的集合合并。
并查集的主要操作有:
(1)初始化:把每个点所在集合初始化为其自身。
(2)查找:查找元素所在的集合,即根结点。
(3)合并:将两个元素所在的集合合并为一个集合。通常来说,合并之前应先判断两个元素是否属于同一集合,这可用查找操作来实现。

Kruskal 算法的程序实现

程序测试数据已按升序排列

1)数据结构描述
将结点数多的集合设为 L,结点数少的集合设为 S 。
(1)边集数组
权值按递增有序放在边集数组 EdgeSet[] 中。

边集数组结构
typedef  struct  			//边集数组单元结构
{   VexType  start_vex;		//起点
        VexType  end_vex;  	//终点
        InfoType  weight;  	//权值项可以根据需要设置
        int sigle;  //当前边是否加入标志,0为初值,1为加入
} EdgeStruct;

(2)双亲结点数组

记录各顶点的根,负值为子集合的结点个数
int  parent[VERTEX_NUM]; 

2)伪代码描述
Here Insert Picture Description
Here Insert Picture Description
Kruskal 算法程序

程序默认边集表中数据已按升序排列,过程大致是取一条边,查找根,合并过程,是否加入生成树中,过程结束标志是已经查找完所有边或者所有结点都已经连接;判断是否生成回路后才决定是否将边加入。

/*==================================================
函数功能:求图的最小生成树Kruskal算法
函数输入:图的边集、图的边数、结点数
函数输出:无
===================================================*/
void Kruskal(EdgeStruct EdgeSet[],int edge_num, int vertex_num ) {
	int  parent[VERTEX_NUM];	//记录各顶点的根,负值为本集合的结点个数
	int  i,k;
	int  num=0;
	int  v1Root,v2Root;
	int  LRoot, SRoot;			//LRoot:大集合的根;SRoot:小集合的根
	char LVertex,SVertex;
	
	for (i=0; i<vertex_num; i++)  parent[i]=-1;
	i=0;
	k=0;
	while ( k<edge_num && num<vertex_num ) { //边集全部加入或生成树的边集足够
		//查找start_vexd的根v1Root,注意边集元素升序 
		v1Root=(EdgeSet[k].start_vex-'A');
		while (parent[v1Root]>=0) v1Root=parent[v1Root];
		//查找end_vexd的根v2Root
		v2Root=(EdgeSet[k].end_vex-'A');
		while (parent[v2Root]>=0) v2Root=parent[v2Root];
		//将S集合合并到L集合
		if (parent[v1Root]<=parent[v2Root]) {
			LRoot=v1Root;
			SRoot=v2Root;
			LVertex= EdgeSet[k].start_vex;
			SVertex= EdgeSet[k].end_vex;
		} else {
			LRoot=v2Root;
			SRoot=v1Root;
			LVertex= EdgeSet[k].end_vex;
			SVertex= EdgeSet[k].start_vex;
		}
		printf("%c--%c ",EdgeSet[k].start_vex,EdgeSet[k].end_vex);
		printf("v1Root=%c  v2Root=%c\n",LRoot+'A',SRoot+'A');
		//start_vex与end_vex的根不同,则S集合归并到L集合中
		if (v1Root!=v2Root) {
			parent[LRoot]+=parent[SRoot]; //L子集与S子集成员数合并
			parent[SRoot]=LRoot; //S结点的根改为L的根
			EdgeSet[k].sigle=1;
			num++;
		}
		for (i=0; i<vertex_num; i++) printf("%4d",parent[i]);
		printf("\n");
		k++;
	}
}

程序测试:

#include<stdio.h>
#define VERTEX_NUM 6            //测试数据一的顶点数 
#define EDGE_NUM 10             //测试数据一的边的数目 
#define VERTEX_NUM 7            //测试数据二的顶点数 
#define EDGE_NUM 11             //测试数据二的边的数目 

typedef char VexType;
typedef int InfoType;

typedef struct {                //边集数组单元结构
	VexType start_vex;          //起点
	VexType end_vex;            //终点
	InfoType weight;            //权值项可以根据需要设置
	int sigle;
} EdgeStruct;
EdgeStruct EdgeSet[EDGE_NUM];   //边集数组

void Kruskal(EdgeStruct EdgeSet[],int edge_num,int vertex_num);
int main() {
	EdgeStruct EdgeSet[EDGE_NUM]
//==================测试数据一======================
	= {{'A','C',1,0},{'D','F',2,0},{'B','E',3,0},{'C','F',4,0},
		{'A','D',5,0},{'B','C',5,0},{'C','D',5,0},{'A','B',6,0},
		{'C','E',6,0},{'E','F',6,0}
	};
//==================测试数据一======================
//={{'A','D',5,0},{'C','E',5,0},{'D','F',6,0},{'A','B',7,0},
//{'B','E',7,0},{'B','C',8,0},{'F','E',8,0},{'D','B',9,0},
//{'E','G',9,0},{'F','G',11,0},{'D','E',15,0}};
	Kruskal(EdgeSet,EDGE_NUM,VERTEX_NUM);
	for(int i=0; i<EDGE_NUM; i++)
		if(EdgeSet[i].sigle==1)
			printf("%c--%c %d\n",EdgeSet[i].start_vex,EdgeSet[i].end_vex,
			       EdgeSet[i].weight);

	return 0;
}

小结

Prim 和 Kruskal 算法考虑问题的出发点都是使生成树上边的权值之和达到最小,则应使生成树中每一条边的权值尽可能的小。

Kruskal's algorithm on a fast efficient than Prim's algorithm, Kruskal just because of the weight by doing a sort, while Prim algorithm you need to do multiple sorts. Although the right to do Prim algorithm for each algorithm involves heavy side will not necessarily cover all edges connected graph.

Prim start from the point - for dense graph
Kruskal side from the start - for sparse graphs

Published 26 original articles · won praise 3 · Views 1463

Guess you like

Origin blog.csdn.net/herui7322/article/details/104394014