最小生成树 Prim Kruskal算法

本文深入探讨了最小生成树的概念,介绍了Prim和Kruskal两种算法的原理与实现过程,通过实例展示了如何构建最小生成树。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最小生成树

  • 最小生成树定义

    • 是一棵树
      • 无回路
      • |V|个顶点一定有|V|-1条边
    • 是生成树
      • 包含全部顶点
      • |V|-1条边都在图内
    • 最小
      • 边的权重和最小

  总的来说,最小生成树覆盖图中所有顶点以及|V|-1条边。


  • Prim和Kruskal

    • 这两种算法均属于贪心算法
    • 贪心算法:
      • “贪”:每一步都要最好的
      • “好”:权重最小的边
    • 但是在这两种算法中需要有约束:
      • 只能用图中存在的边
      • 只能正好用掉|V|-1条边
      • 不能有回路

  • Prim算法思路

  与dijkstra算法类似,都需要对每一个顶点保存一个距离值dist[V]和parent[V],以及一个visit指标,标记是否已经过改点。parent[V]则表示该结点的父结点。不同点为,dist[V]的定义不同: dijkstra算法里dist[V]的定义是源点S到各个点之间的最短距离,而prim算法里的dist[V]则是顶点V到已有的最小生成树MST的最短距离。

  下面我们对下面这幅图求其最小生成树:

这里写图片描述

  假设我们从顶点v1开始,所以我们可以发现(v1,v3)边的权重最小,所以第一个输出的边就是:v1—v3=1
这里写图片描述

  然后,我们要从v1和v3作为起点的边中寻找权重最小的边,首先了(v1,v3)已经访问过了,所以我们从其他边中寻找,发现(v3,v6)这条边最小,所以输出边就是:v3—-v6=4
这里写图片描述

  然后,我们要从v1、v3、v6这三个点相关联的边中寻找一条权重最小的边,我们可以发现边(v6,v4)权重最小,所以输出边就是:v6—-v4=2.
这里写图片描述

  然后,我们就从v1、v3、v6、v4这四个顶点相关联的边中寻找权重最小的边,发现边(v3,v2)的权重最小,所以输出边:v3—–v2=5
这里写图片描述

  然后,我们就从v1、v3、v6、v4,v2这2五个顶点相关联的边中寻找权重最小的边,发现边(v2,v5)的权重最小,所以输出边:v2—–v5=3
这里写图片描述

  最后,我们发现六个点都已经加入到集合U了,我们的最小生成树建立完成。


  • 伪代码

    • dist[V]–>顶点V到最小生成树MST的最短距离
    • parent[W] = V–>顶点W的父结点为V
    • collected[W]–>用来判断W是否录入MST中,collected[W] = true即为录入,collected[W] = false即为未录入;每次只录入当前collected为false的点,可直接避免在MST中形成回路
    • 与Dijkstra算法非常像
int Prim(MGraph Graph, LGraph MST)
{
   MST = {s};
   while (1){
   	V = 未收录顶点中dist最小者;
   	if ( 这样的V不存在 )
   		break;
   	将V收录进MST中;
   	TotalWeight += dist[V];
   	dist[V] = 0;
   	for ( V的每个邻接点W )
   		if (collected[W] == false && dist[V] + E<V, W> < dist[W]){
   			dist[W] = dist[V] + E<V, W>;
   			parent[W] = V;
   		} 
   }
   if (MST中收录的顶点数 < |V|)
   	ERROR("生成的树不存在"); 
   return TotalWeight;
}
****************************************/

  • 详细代码片段

  /**** 用邻接矩阵存储图的Prinm算法 *******/ 
#define MaxvertexNum N //(N为顶点的最大个数)
#define INFINITY 65533
#defint ERROR -1
typedef int WeightType;
typedef int Vertex;

Vertex FindMinDist(MGraph Graph, int *dist)
{
	Vertex V, MinV;
	int MinDist = INFINITY;
	for (V = 0; V < Graph->Nv; V++){
		if (dist[V] != 0 && dist[V] < MinDist){
			MinDist = dist[V];
			MinV = V;
		}
	}
	if (MinDist < INFINITY)
		return MinV;
	else return ERROR;
}

int Prinm(MGraph Graph, LGraph MST)
{//将最小生成树保存为邻接表存储的图MST,返回最小权重和TotalWeight
	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];
		parent[V] = 0;//暂且定义所有顶点的父节点均为初始结点0 
	}
	Totalweight = 0;//初始化权重和
	Vcount = 0;//初始化收录的顶点数
	
	//创建MST并初始化,即建立含有Graph->Nv个顶点但边为0的空MST,用邻接表存储
	MST = CreateLGraph(Graph->Nv);
	E = (Edge)malloc(sizeof (struct ENode));//建立空的边结点
	 
	//将初始顶点录入MST中
	dist[0] = 0;
	parent[0] = -1;//表示当前最小生成树的树根是初始顶点0 
	Vcount ++;
	while (1){
		V = FindMinDist(Graph, dist);//V = 未收录顶点中dist最小者
		if (V == ERROR)
			break;
		//将V及相应的边<parent[V], V>收录进MST
		E->V1 = parent[V];
		E->V2 = V;
		E->Weight = dist[V];
		InsertEdge(MST, E);
		TotalWeight += dist[V];
		Vcount ++;//收录顶点数+1
		dist[V] = 0;
		for (W = 0; W < Graph->Nv; W++){
			if (dist[W] != 0 && Graph->G[V][W] < INFINITY)
				if (dist[V] + Graph-G[V][W] < dist[W]){
					dist[W] = dist[V] + Graph->G[V][W];
					parent[W] = V;
				}
		}
	}//while结束
	if (Vcount < Graph->Nv)//MST中收录的顶点个数小于|V|个
		TotalWeight = ERROR;
	
	return TotalWeight;	//算法执行完毕,返回最小权值和或者错误标记 
}

  • Kruskal算法思路

    • 简单来说,就是将一个个单独的森林合并成一棵树。
    • 其思想就是直接了当的贪心,每次都将权值最短的边收进来:可以将每个顶点都看成一个森林,然后将权值最短的边的顶点连接起来,在不构成回路的情况下,将这些森林合并成一棵树。
    • 存在三个问题:
    1. 如何选择一条权重最小的边;因为权值是不变的,所以可以事先按照权值升序排列,这项工作可以采用效率较高的排序算法,时间复杂度为O(|E|log|E|);也可以使用最小堆先将图中的边存储起来,每次选择堆顶元素并删除重新调整为最小堆,时间复杂度为O(|E|log|E|)。
    2. 判断新加入一条边后会不会在MST中构成回路:这项工作可以使用并查集
    3. 如何合并两个树:使用并查集中的Union。
    • 下面我们对这幅图求最小生成树:

    在这里插入图片描述

    1.选取一条最小边<V1, V3>,权重为1
    2.选取一条最小边<V4, V6>,权重为2
    3.选取一条最小边<V2, V5>,权重为3
    4.选取一条最小边<V3, V6>,权重为4
    5.不能选取最小边<V3, V4>,权重为5,会构成回路
    6.不能选取最小边<V1, V4>,权重为5,会构成回路
    7.选取一条最小边<V2, V3>,权重为5
    8.选取一条最小边<V5, V6>,权重为6
    9.录入顶点数 = |V| 且录入边的数量 = |V| - 1,结束
    

  • 伪代码

int Kruskal(Lgraph Graph, LGraph MST)
{
   MST = 包含所有顶点但没有边的图;  //邻接表存储;
   while (MST中收集的边 < Graph->Nv && 原图的边集E非空){
   	从E中选择一条权重最小的边E<V, W>;  //利用最小堆完成
   	从E中删除边E<V, W>;
   	if (E<V, W>不在MST中构成回路) //利用并查集完成
   		将E<V, W>加入MST;
   	else
   		彻底无视E<V, W>; 
   }
   if (MST中边的个数 < Graph->Nv - 1)
   	ERROR("生成树不存在");
   else 
   	return 最小权重和; 
}

  • 详细代码片段

  由于该算法使用了较多的结构,在函数传参方面有点复杂,我在写代码片段的过程中边写边画思维导图,发现对理清思路有奇效,不至于产生混乱,但速度还是挺慢,还是基础不牢靠。下面贴一下我在敲代码过程中画的思维导图,主要作用是 理清每种结构参数相互之间的关系,有利于不同结构之间参数的传递

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 代码与上面导图一起食用效果更佳哦
/************** 最小堆的定义 ************
/*1.最小堆中存放的结点是原始图的边结点 
/*2.若原始图是无向图,则需要避免重复存边结点
/*3.在找出权重最小的边后,需要从最小堆中将该边结点删除
*****************************************/
struct ENode {
	Vertex V1, V2;
	WeightType Weight;	
};
typedef ENode *Edge;

//建堆
struct EHNode {
	Edge *Data;//存储边结点的数组 
	int size;
};
typedef struct EHNode *EHeap
typedef EHeap MinEHeap;

MinEHeap BuildEHeap(MGraph Graph, MinEHeap EH )
{//建立存放边结点的最小堆,存储的包括边权重和边的两个顶点 
	//需要先将边按顺序存入堆,然后调整为最小堆
	Vertex V, W;
	for (V = 0; V < Graph->Nv; V++){
		for (W = 0; W < Graph->Nv; W++){
			if (V < W){//避免重复录入无向图的边 
				EH->Data[size].V1 = V;
				EH->Data[size].V2 = W;
				EH->Data[size].Weight = Graph->G[V][W];
				size ++;
			}
		}
	}//边结点按顺序存入堆,结束
	//调整为最小堆,从最后一个父节点开始,到根结点0
	for (int i = EH->size /2; i > 0; i-- )
		PercDown(EH, i); 
	return EH;
	
}

void PercDown(MinEHeap EH, int p)
{//往下过滤,将EHeap中以EHeap->Data[p]为根的子堆调整为最小堆 
	int Parent, Child;
	Edge X = EH->Data[p];
	for (Parent = p; Parent*2 <= EH->size ; Parent = Child){
		Childe = Parent * 2;
		if ((Child != EH->size ) && (EH->Data[Child].Weight > EH->Data[Child + 1].Weight))
			Child ++;
		if (X.Weight > EH->Data[Child].Weight )
			//下滤
			EH->Data[Parent] = EH->Data[Child];
		else break;
	}
	EH->Data[Parent] = X;//Parent即为X插入的位置 
}

Edge DeleteMin(MinEHeap EH)
//堆的删除与堆的调整非常类似 
{
	Edge MinE = EH->Data[1];
	//用最小堆中最后一个元素从根结点开始往上过滤下层结点
	Edge X = EH->Data[EH->size --];//注意当前堆的规模要减小
	EH->Data[1] = X;
	PercDown(EH, 1);//从根结点开始往上过滤下层结点 
	
	return MinE;
 } 

/**********************并查集*******************************
/* 1.E<V, W>是从最小堆中选出来的权重最小边结点 
/* 2.E<V, W>是否在MST中构成回路:利用并查集Find进行判断,然后用Union将不构成回路的两个定点结合 
/* 3.将E<V, W>加入MST中:InsertEdge
************************************************************/
typedef Vertex SetType[MaxVertexNum];//假设集合元素下标从0开始 

void InitiakizeVset(int N, SetType Vset)
//初始化顶点集合 
{
	for (int i = 0; i < N; i++)
		Vset[i] = -1;
}

bool CheckCycle(SetType Vset, Edge E)
//判断E<V, W>是否在MST中构成回路 
{
	Vertex Root_1, Root_2;
	Root_1 = Find(Vset, E->V1 );
	Root_2 = Find(Vset, E->V2 );
	if (Root_1 == Root_2)
		return false;
	else{ 
		Union(Vset, Root_1, Root_2);
		return true; 
	}
} 

Vertex Find(SetType Vset, Vertex V)
//寻找顶点V的父节点 
{
	if (Vset[V] < 0)
		return V;
	else
		return Find(Vset, Vset[V]);//路径压缩 
 } 

void Union(SetType Vset, Vertex Root_1, Vertex Root_2)
//这里默认Root_1和Root_2是不同集合的根结点
//保证小集合并入大集合 
{
	if (Vset[Root_1] < Vset[Root_2]){//集合1比集合2大 
		Vset[Root_1] += Vset[Root_2];
		Vset[Root_2] = Root_1;
	}else {//集合2比集合1大 
		Vset[Root_2] += Vset[Root_1];
		Vset[Root_1] = Root_2;
	}
} 
 
//主程序框架 
int Kruskal(MGraph Graph, LGraph MST)
//将最小生成树保存为邻接表存储的图MST中,并返回最小权重和 
{
	WeightType TotalWeight;
	Edge MinE = (Edge)malloc(sizeof (struct ENode));
	MinEHeap EH = (MinEHeap)malloc(sizeof (struct EHNode));
	EH->Data = (Edge)malloc(sizeof (struct ENode) * Graph->Ne);
	EH->size = 1; 
	int cnt = 0;
	SetType Vset = (Vertex)malloc(sizeof (Vertex) * Graph->Nv);
	InitiakizeVset(Graph->Nv, Vset);
	EH = BuildEHeap(Graph, EH );//针对原始图中的每条边,建最小堆
	while ((cnt < Graph->Nv - 1) && (!IsEmpty(EH)) ){
		MinE = DeleteMin(EH);
		if ( CheckCycle(Vset, MinE) ){
			InsertEdge(MST, MinE);
			cnt ++;
			TotalWeight += MinE->Weight ;
		}
	}
	if (cnt < Graph->Nv - 1)
		TotalWeight = -1;//设置错误标记,表示最小生成树不存在
	return TotalWeight; 
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xuuyann

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值