YAZONG 我的开源

(数据结构)第八讲-图(8.1最小生成树问题)

  , ,
0 评论0 浏览

简介(最小生成树<->图连通)

eg:

村村通例子,把分散的村庄以最少的成本连接起来,做成连通图,有最少的边,每个边对应一条路。

此例子可以归纳为一个最小生成树的问题。

image.png

最小生成树存在,那么图一定是连通的,图连通的,那么最小生成树一定存在。

二者是充分必要条件。

而最小生成树包括三个概念:

树。

生成树。

最小生成树。

image.png

首先一棵树是连通的,一定没有回路。

有|V|个顶点的话,正好有|V|-1条边,不能多也不能少。

image.png

生成树:包含了全部的顶点,图中所有的顶点一定都要在这棵树中,|V|-1条边也肯定都在图中。

image.png

除第一张图外,其余三张图都是第一张图的子图,也就是说生成树不是唯一的,往其余三张图中加任何一条边都会构成回路,在树中构成回路是不可以的。

image.png

村庄之间有很多种修路方法,如何使这些边的权重和最小。

上述就是最小生成树的问题,解决其问题,可以归结为贪心算法。

贪心算法

image.png

解决问题是一步步来的,但在解决问题过程中每一步都要眼前最好的,这就是”贪”。

image.png

在不同问题中,”好”有不同的定义。

在最小生成树的问题中,所谓好就是每次都要一条权重最小边,这就叫”贪心”。

并不是把每条边按照权重排序,收购V-1条边就停止,没这么简单,看”下述约束”。

image.png

原图中没有的边是不能用的。

有|V|个顶点的话,正好有|V|-1条边,不能多也不能少。

当每条边收到集合中时,不能在现有树中构成回路,也要避免负值圈的现象。

8.1.1 贪心算法-Prim算法 (无向图-稠密图容易实现)

image.png

Prim算法的思想:从一个根结点开始,让一颗小数慢慢长大。

这里的前提条件:如果图比较稠密(完全图)。

image.png

1:首先在图中随意选一个V1结点作为根结点。

2:然后要找跟V1直接相关的三条边中找一条最小的,这条最小的边是要找跟这棵树有关系的往外能长出去的最小边。

这里把V4收录进来,这样树就有了两个顶点V1和V4。

3:这棵树继续往外长。此时以V1和V4为一棵树,找距离这棵树最小的边(而不是距离这棵树中某个结点的最小边的顶点),此时可以选择的有V1-V2和V4-V3,先选这个V1-V2,此时,V2被收录进来,现在树中包括V1、V2、V4。

要注意V6-V7不与这棵树直接相连,不能收录。

4:收录V3进树,但V4-V2这条边是不能收录的,因为构成了V1-V2-V4的回路,V1-V3同理构成回路。

5:继续找最小的边。V4-V7。收录V7进树。

6:V7-V6。收录V6进树。

7:V7-V5。收录V5进树。

此时,全部7个顶点都收录进树中,六条边构成最小生成树。

8.1.1 Prim算法过程与Dijkstra算法差异(稠密图:O(|V|^2))

image.png

image.png

image.png

image.png

8.1.1 Prim算法源码(邻接表存储、收录边、结点、无回路计算)


#include<stdio.h>
/*
图-最小生成树问题-贪心算法-Prime算法-稠密图容易实现-邻接表存储-收录边和结点-与Dijkstra算法差异
*/
int main(void){
	return 0;
}
/* 返回未被收录顶点中dist最小者 */
Vertex FindMinDist(MGraph Graph, WeightType dist[]){

	Vertex MinV, V;
	WeightType MinDist = INFINITY;

	for(V = 0; V < Graph->Nv; V++){
	
		if(dist[V] != 0 && dist[V] < MinDist){
			/* 若V未被收录,且dist[V]更小 */
			MinDist = dist[V];/* 更新最小距离 */
			MinV = V;/* 更新对应顶点 */
		}
	
	}
	/* 若找到最小dist */
	if(MinDist < INFINITY){
		/* 返回对应的顶点下标 */
		return MinV;
	}
	/* 若这样的顶点不存在,返回-1作为标记 */
	else{
		return ERROR;
	}
}

/* 将最小生成树保存为邻接表存储的图MST,返回最小权重和 */
int Prim(MGraph Graph, LGraph MST){

	WeightType dist[MaxVertexNum], TotalWeight;
	Vertex parent[MaxVertexNum], V, W;
	int VCount;

	Edge E;

	/* 初始化。默认初始点下标是0 */
	for(V = 0; V < Graph->Nv; V++){
		/* 这里假设若V到W没有直接的边,则Graph->G[V][W]定义为INFINITY */
		dist[V] = Graph->G[0][V];
		/* 暂且定义所有顶点的父结点都是初始点0 */
		parent[V] = 0;
	}

	/* 初始化权重和     */
	TotalWeight = 0;
	/* 初始化收录的顶点数 */
	VCount = 0;
	/* 创建包含所有顶点但没有边的图。注意用邻接表版本 */
	MST = CreateGraph(Graph->Nv);
	/* 建立空的边结点 */
	E = (Edge)malloc(sizeof(struct ENode));

	/* 将初始点0收录进MST */
	dist[0] = 0;
	VCount++;
	/* 当前树根是0 */
	parent[0] = -1;

	while(1){
		/* 返回未被收录顶点中dist最小者 */
		V = FindMinDist(Graph, dist);
		/* V = 未被收录顶点中dist最小者 */
		if(V == ERROR){/* 若这样的V不存在 */
			break;/* 算法结束 */
		}
		/* 将V及相应的边<parent[V], V>收录进MST */
		E->V1 = parent[V];
		E->V2 = V;
		E->Weight = dist[V];
		InsertEdge(MST, E);
		TotalWeight += dist[V];
		dist[V] = 0;
		VCount++;
	}

	for(W = 0; W < Graph->Nv; W++){ /* 对图中的每个顶点W */
	
		if(dist[W] != 0 && Graph->G[V][W] < INFINITY){
			/* 若W是V的邻接点并且未被收录 */
			if(Graph->G[V][W] < dist[W]){
				/* 若收录V使得dist[W]变小 */
				dist[W] = Graph->G[V][W];/* 更新dist[W] */
				parent[W] = V;/* 更新树 */
			}
		}
	
	}

	/* MST中收的顶点不到|V|个 */
	if(VCount < Graph->Nv){
		TotalWeight = ERROR;
	}
	/* 算法执行完毕,返回最小权重和或错误标记 */
	return TotalWeight;

}

/* 初始化一个有VertexNum个顶点但没有边的图 */
LGraph CreateGraph(int VertexNum){
	Vertex V;
	LGraph Graph;
	/* 建立图 */
	Graph = (LGraph)malloc(sizeof(struct GNode));
	Graph->Nv = VertexNum;
	Graph->Ne = 0;
    /* 初始化邻接表头指针 */
    /* 注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) */
    for(V = 0; V < Graph->Nv; V++){
    	Graph->G[V].FirstEdge = NULL;
	}
	return Graph;
}

8.1.2贪心算法-Kruskal算法 (无向图-稀疏图容易实现)

image.png

这里的前提条件:如果图比较稀疏,有很多节点,但边条数比较少,差不多跟顶点数是同一数量级的,这种情况下更好的算法是Kruskal算法。

Kruskal算法的基本思想是将森林合并成树,比Prim更好理解。

什么叫做”将森林合并成树”?
认为每个顶点都是一棵树,通过不断把边收录进来,把两棵树合并成了一棵树,就逐渐把所有的结点合并成一棵树,这样就生成了最小生成树。

非常直截了当的贪心算法,每次就直接找权重最小的边把它收录进树中。

image.png

1:V1-V4和V6-V7的边权重最小,先收录V1-V4的边权重1。

2:收录V6-V7的边权重1。

3:收录V3-V4的边权重2。

4:收录V1-V2的边权重2。

(V2-V4的边权重3是可不收录的,因为V1-V2-V4构成了回路。V1-V3也不能收录。)

5:收录V4-V7的边权重4。

6:收录V5-V7的边权重6。

(V3-V6的边权重5是可不收录的,因为V3-V4-V6构成了回路。V4-V5和V2-V5也不能收录。)

此时,全部6条边都收录进树中,六条边构成最小生成树。

8.1.2 Kruskal算法过程(最小堆、并查集、稀疏图:O(|E|Log|E|))

image.png

image.png

image.png

image.png

8.1.2 Kruskal算法源码(邻接表存储、收录边、有回路计算)

#include<stdio.h>
/*
图-最小生成树问题-贪心算法-Kruskal算法-稀疏图容易实现-邻接表存储-收录边-最小堆和并查集处理边
*/
int main(void){
	return 0;
}

/*--------------------顶点并查集定义--------------------*/

typedef Vertex ElementType;/* 默认元素可以用非负整数表示 */
typedef Vertex SetName;/* 默认用根结点的下标作为集合名称 */
typedef ElementType SetType[MaxVertexNum];/* 假设集合元素下标从0开始 */

/* 初始化并查集 */
void InitializeVSet(SetType S, int N){
	ElementType X;
	for(X = 0; X < N; X++){
		S[X] = -1;
	}
}

/* 这里默认Root1和Root2是不同集合的根结点 */
void Union(){
	/* 保证小集合并入大集合 */
	if(S[Root2] < S[Root1]){/* 如果集合2比较大 */
		S[Root2] += S[Root1];/* 集合1并入集合2  */
		S[Root1] = Root2;
	}
	else{/* 如果集合1比较大 */
		S[Root1] += S[Root2];/* 集合2并入集合1  */
		S[Root2] = S[Root1];
	}
}

/* 默认集合元素全部初始化为-1 */
SetName Find(SetType S, ElementType X){
	if(S[X] < 0){
		/* 找到集合的根 */
		return X;
	}else{
		/* 路径压缩 */
		return S[X] = Find(S, S[X]);
	}
}

/* 检查连接V1和V2的边是否在现有的最小生成树子集中构成回路 */
bool CheckCycle(SetType VSet, Vertex V1, Vertex V2){
	Vertex Root1, Root2;
	/* 得到V1所属的连通集名称 */
	Root1 = Find(VSet, V1);
	/* 得到V2所属的连通集名称 */
	Root2 = Find(VSet, V2);
	/* 若V1和V2已经连通,则该边不能要 */
	if(Root1 == Root2){
		return false;
	}
	/* 否则该边可以被收集,同时将V1和V2并入同一连通集 */
	else{
		Union(VSet, Root1, Root2);
		return true;
	}
}

/*--------------------并查集定义结束--------------------*/

/*--------------------边的最小堆定义--------------------*/
/* 将图的边存入数组ESet,并且初始化为最小堆 */
void InitializeESet(LGraph Graph, Edge ESet){
	Vertex V;
	PtrToAdjVNode W;
	int ECount;
	/* 将图的边存入数组ESet */
	ECount = 0;
	for(V = 0; V < Graph->Nv; V++){
		for(W = Graph->G[V].FirstEdge; W; W = W->Next){
			/* 避免重复录入无向图的边,只收V1<V2的边 */
			if(V < W->AdjV){
				ESet[ECount].V1 = V;
				ESet[ECount].V2 = W->AdjV;
				ESet[ECount].Weight = W->Weight;
			}
		}
	}
	/* 初始化为最小堆 */
	for(ECount = Graph->Ne/2; ECount >= 0; ECount--){
		PercDown(ESet, ECount, Graph->Ne);
	}

}

/* 改编代码4.24的PercDown( MaxHeap H, int p )    */
/* 将N个元素的边数组中以ESet[p]为根的子堆调整为关于Weight的最小堆 */
void PercDown(Edge ESet, int p, int N){

	int Parent, Child;
	struct ENode X;

	/* 取出根结点存放的值 */
	X = ESet[p];
	for(Parent = p; (Parent * 2 + 1) < N; Parent = Child){
		Child = Parent * 2 + 1;
		if((Child != N - 1) && (ESet[Child].Weight > ESet[Child + 1].Weight)){
			/* Child指向左右子结点的较小者 */
			Child++;
		}
		if(X.Weight <= ESet[Child].Weight){
			/* 找到了合适位置 */
			break;
		}else{
			/* 下滤X */
			ESet[Parent] = ESet[Child];
		}
	}
	ESet[Parent] = X;
}

/* 给定当前堆的大小CurrentSize,将当前最小边位置弹出并调整堆 */
int GetEdge(Edge ESet, int CurrentSize){
	/* 将最小边与当前堆的最后一个位置的边交换 */
	Swap(&ESet[0], &ESet[CurrentSize - 1]);
	/* 将剩下的边继续调整成最小堆 */
	PercDown(ESet, 0, CurrentSize - 1);
	/* 返回最小边所在位置 */
	return CurrentSize - 1;
}

/*--------------------最小堆定义结束--------------------*/

/* 
将最小生成树保存为邻接表存储的图MST,返回最小权重和 
*/
int Kruskal(LGraph Graph, LGraph MST){

	WeighType TotalWeight;
	int ECount, NextEdge;
	/* 顶点数组 */
	SetType VSet;
	/* 边数组 */
	Edge ESet;
	/* 初始化顶点并查集 */
	InitializeVSet(VSet, Graph->Nv);
	ESet = (Edge)malloc(sizeof(struct ENode) * Graph->Ne);
	/* 初始化边的最小堆 */
	InitializeESet(Graph, ESet);
	/* 创建包含所有顶点但没有边的图。注意用邻接表版本 */
	MST = CreateGraph(Graph->Nv);
	/* 初始化权重和     */
	TotalWeight = 0;
	/* 初始化收录的边数 */
	ECount = 0;
	/* 原始边集的规模 */
	NextEdge = Graph->Ne;
	/* 当收集的边不足以构成树时 */
	while(ECount < Graph->Nv - 1){
		/* 从边集中得到最小边的位置 */
		NextEdge = GetEdge(ESet, NextEdge);
		/* 边集已空 */
		if(NextEdge < 0){
			break;
		}
		/* 如果该边的加入不构成回路,即两端结点不属于同一连通集 */
		if(CheckCycle(VSet, ESet[NextEdge].V1, ESet[NextEdge].V2) == true){
			/* 将该边插入MST */
			InsertEdge(MST, ESet + NextEdge);
			/* 累计权重 */
			TotalWeight += ESet[NextEdge].Weight;
			/* 生成树中边数加1 */
			ECount++;
		}
	}
	if(ECount < Graph->Nv - 1){
		/* 设置错误标记,表示生成树不存在 */
		TotalWeight = -1;
	}

	return TotalWeight;
}

/* 初始化一个有VertexNum个顶点但没有边的图 */
LGraph CreateGraph(int VertexNum){
	Vertex V;
	LGraph Graph;
	/* 建立图 */
	Graph = (LGraph)malloc(sizeof(struct GNode));
	Graph->Nv = VertexNum;
	Graph->Ne = 0;
    /* 初始化邻接表头指针 */
    /* 注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) */
    for(V = 0; V < Graph->Nv; V++){
    	Graph->G[V].FirstEdge = NULL;
	}
	return Graph;
}

标题:(数据结构)第八讲-图(8.1最小生成树问题)
作者:yazong
地址:https://blog.llyweb.com/articles/2023/10/22/1697964360912.html