数据结构(四)—— 图(4):最小生成树问题

本文介绍了数据结构中最小生成树的概念,包括其性质和存在条件。讲解了贪心算法,并详细阐述了Prim算法和Kruskal算法的原理及其实现过程,附带了两种算法的伪代码。最后,提供了两种算法在具体图例上的应用及测试效果。
摘要由CSDN通过智能技术生成

数据结构系列内容的学习目录 → \rightarrow 浙大版数据结构学习系列内容汇总

4. 最小生成树问题

4.1 什么是最小生成树

  一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。 最小生成树可以用kruskal(克鲁斯卡尔)算法或prim(普里姆)算法求出。

  最小生成树(Minimum Spanning Tree): ■ 是一棵树
                       ⋄ 无回路
                       ⋄ |V|个顶点一定有|V|-1条边
                      ■ 是生成树
                       ⋄ 包含全部顶点
                       ⋄ |V|-1条边都在图里
                      ■ 边的权重和最小

  向生成树中任加一条边,都一定构成回路,如下图所示,左上角第一幅图是完全图,其他三幅图均为生成图。

在这里插入图片描述
  结论: 最小生成树存在 ↔ \leftrightarrow 图连通。

4.2 贪心算法

  贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解 。

  ■ 什么是 “贪”:每一步都要最好的
  ■ 什么是 “好”:权重最小的边
  ■ 需要约束: ⋄ 只能用图里有的边
         ⋄ 只能正好用掉|V|-1条边
         ⋄ 不能有回路

4.2.1 Prim算法

  普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。

  Prim算法,让一棵小树长大。核心思想:贪心思想,找到临近最短的就更新。

  Prim算法的步骤

  • step1:在下图所示的图中,首先选择 v 1 v_{1} v1作为根节点;

在这里插入图片描述

  • step2:寻找与 v 1 v_{1} v1相关的边中权重最小的一条,即 v 1 v_{1} v1 v 4 v_{4} v4之间的边,并将其结点 v 4 v_{4} v4收录进来;

在这里插入图片描述

  • step3:寻找与 v 1 v_{1} v1 v 4 v_{4} v4相关的边中权重最小的一条,即 v 1 v_{1} v1 v 2 v_{2} v2之间的边,并将其结点 v 2 v_{2} v2收录进来;

在这里插入图片描述

  • step4:寻找与 v 1 v_{1} v1 v 4 v_{4} v4 v 2 v_{2} v2相关的边中权重最小的一条,即 v 4 v_{4} v4 v 3 v_{3} v3之间的边,并将其结点 v 3 v_{3} v3收录进来;

在这里插入图片描述

  • step5:寻找与 v 1 v_{1} v1 v 4 v_{4} v4 v 2 v_{2} v2 v 3 v_{3} v3相关的边中权重最小的一条,即 v 4 v_{4} v4 v 7 v_{7} v7之间的边,并将其结点 v 7 v_{7} v7收录进来(不能将 v 2 v_{2} v2 v 4 v_{4} v4 v 1 v_{1} v1 v 3 v_{3} v3之间的边收录进来,因为会构成一个回路);

在这里插入图片描述

  • step6:寻找与 v 1 v_{1} v1 v 4 v_{4} v4 v 2 v_{2} v2 v 3 v_{3} v3 v 7 v_{7} v7相关的边中权重最小的一条,即 v 7 v_{7} v7 v 6 v_{6} v6之间的边,并将其结点 v 6 v_{6} v6收录进来;

在这里插入图片描述

  • step7:寻找与 v 1 v_{1} v1 v 4 v_{4} v4 v 2 v_{2} v2 v 3 v_{3} v3 v 7 v_{7} v7 v 6 v_{6} v6相关的边中权重最小的一条,即 v 7 v_{7} v7 v 5 v_{5} v5之间的边,并将其结点 v 5 v_{5} v5收录进来。

在这里插入图片描述
  Prim算法的代码如下所示。

void Prim ()
{ 
    MST= {s};
    while (1){
        V = 未收录顶点中dist最小者;
        if (这样的V不存在)
            break;
        将V收录进MST: dist[v]= 0 ;
        for ( V的每个邻接点W )
            if ( dist[W]!=0 )
                if (E(V,W)<dist[W]){
                    dist[W]=E(V,W);
                    parent[W]=V;
                }
    }
    if(MST中收的顶点不到|V|)
        Error ("生成树不存在" );
}

  初始化: d i s t [ V ] = E ( s , V ) dist[V]=E_{(s,V)} dist[V]=E(s,V)或正无穷
       parent[s] =-1

  Prim算法的时间复杂度 T = O ( ∣ V ∣ 2 ) T=O(|V|^{2}) T=O(V2),对稠密图合算。

4.2.2 Kruskal算法

  克鲁斯卡尔算法(Kruskal算法)是求连通网的最小生成树的另一种方法。与普里姆算法不同,它的时间复杂度为 O ( e l o g e ) O(eloge) O(eloge)(e为网中的边数),所以,适合于求边稀疏的网的最小生成树 。

  Kruskal算法,将森林合并成树。核心思想:不停地找权重最小的边合在一起,但需要注意的是,不能构成回路。如何找最小权重所在的边,可以靠最小堆来解决;如何判断是否构成回路,可以用并查集看是否属于同一棵树。

  Kruskal算法的步骤

  • step1:首先将权重最小(即权重为1)的 v 1 v_{1} v1 v 4 v_{4} v4 v 6 v_{6} v6 v 7 v_{7} v7之间的边收录进来;

在这里插入图片描述

  • step2:接下来继续将权重最小(即权重为2)的 v 1 v_{1} v1 v 2 v_{2} v2 v 3 v_{3} v3 v 4 v_{4} v4之间的边收录进来;

在这里插入图片描述

  • step3:接下来继续将权重最小(即权重为4)的 v 4 v_{4} v4 v 7 v_{7} v7之间的边收录进来(权重为3的边不能被收录进来,因为会形成回路);

在这里插入图片描述

  • step4:接下来继续将权重最小(即权重为6)的 v 5 v_{5} v5 v 7 v_{7} v7之间的边收录进来(权重为5的边不能被收录进来,因为会形成回路),最小树生成完毕。

在这里插入图片描述
  Kruskal算法的代码如下所示。

void Kruskal (Graph G)
{
    MST = { };
    while ( MST中不到|V|-1条边 && E中还有边) {
        从E中取一条权重最小的边E<V,W>;  // 最小堆
        将E<V,W>从E中删除;
        if ( E<V,W>不在MST中构成回路 )  // 并查集
            将E<V,W>加入MST;
        else
            彻底无视 E<V,W>;
    }
    if ( MST 中不到|V|-1条边 )
        Error("生成树不存在");  //即图不连通
}

  Kruskal算法的时间复杂度 T = O ( ∣ E ∣ l o g ∣ E ∣ ) T=O(|E|log |E|) T=O(ElogE),对稀疏图合算。

4.3 最小树生成问题的实现

在这里插入图片描述

图1

4.3.1 Prim算法的实现

  对于图1所示的图,使用Prim算法从顶点 v 1 v_{1} v1开始生成最小生成树,代码如下所示。

#include<iostream>
using namespace std;
#include<queue>

#define INF 100000
#define MaxVertex 105
typedef int Vertex;
int G[MaxVertex][MaxVertex];
int parent[MaxVertex];   // 并查集 
int dist[MaxVertex]; // 距离 
int Nv;    // 结点 
int Ne;    // 边 
int sum;  // 权重和 
queue<Vertex> MST;  // 最小生成树

// 初始化图信息 
void BuildGraph() 
{
	Vertex v1, v2;
	int w;
	cin >> Nv >> Ne;
	for (int i = 1; i <= Nv; i++) 
	{
		for (int j = 1; j <= Nv; j++)
			G[i][j] = 0;  // 初始化图 
		dist[i] = INF;   // 初始化距离
		parent[i] = -1;  // 初始化并查集 
	}
	// 初始化点
	for (int i = 0; i < Ne; i++) 
	{
		cin >> v1 >> v2 >> w;
		G[v1][v2] = w;
		G[v2][v1] = w;
	}
}

// Prim算法前的初始化 
void IniPrim(Vertex s) 
{
	dist[s] = 0;
	MST.push(s);
	for (Vertex i = 1; i <= Nv; i++)
		if (G[s][i]) {
			dist[i] = G[s][i];
			parent[i] = s;
		}
}

// 查找未收录中dist最小的点 
Vertex FindMin()
{
	int min = INF;
	Vertex xb = -1;
	for (Vertex i = 1; i <= Nv; i++)
		if (dist[i] && dist[i] < min)
		{
			min = dist[i];
			xb = i;
		}
	return xb;
}

void Prim(Vertex s) 
{
	IniPrim(s);
	while (1) 
	{
		Vertex v = FindMin();
		if (v == -1)
			break;
		sum += dist[v];
		dist[v] = 0;
		MST.push(v);
		for (Vertex w = 1; w <= Nv; w++)
			if (G[v][w] && dist[w])
				if (G[v][w] < dist[w]) 
				{
					dist[w] = G[v][w];
					parent[w] = v;
				}
	}
}

void Print()
{
	cout << "被收录顺序:" << endl;
	while (!MST.empty())
	{
		cout << MST.front() << " ";
		MST.pop();
	}
	cout << endl;
	cout << "权重和为:" << endl;
	cout << sum << endl;
}

int main() 
{
	BuildGraph();
	Prim(1);
	Print();
	system("pause");
	return 0;
}

  图1所示的图使用Prim算法从顶点 v 1 v_{1} v1开始生成最小生成树的测试效果如下图所示。

在这里插入图片描述

4.3.2 Kruskal算法的实现

  对于图1所示的图,使用Kruskal算法开始生成最小生成树,代码如下所示。

#include<iostream>
using namespace std;
#include<string>
#include<queue>

#define INF 100000
#define MaxVertex 105
typedef int Vertex;
int G[MaxVertex][MaxVertex];
int parent[MaxVertex];   // 并查集最小生成树 
int Nv;    // 结点 
int Ne;    // 边 
int sum;  // 权重和 

struct Node {
	Vertex v1;
	Vertex v2;
	int weight; // 权重 
	// 重载运算符成最大堆 
	bool operator < (const Node &a) const
	{
		return weight > a.weight;
	}
};
queue<Node> MST;  // 最小生成树 
priority_queue<Node> q;  // 最小堆 

// 初始化图信息 
void BuildGraph() 
{
	Vertex v1, v2;
	int w;
	cin >> Nv >> Ne;
	for (int i = 1; i <= Nv; i++) 
	{
		for (int j = 1; j <= Nv; j++)
			G[i][j] = 0;  // 初始化图
		parent[i] = -1;
	}
	// 初始化点
	for (int i = 0; i < Ne; i++) 
	{
		cin >> v1 >> v2 >> w;
		struct Node tmpE;
		tmpE.v1 = v1;
		tmpE.v2 = v2;
		tmpE.weight = w;
		q.push(tmpE);
	}
}

//  路径压缩查找 
int Find(int x) 
{
	if (parent[x] < 0)
		return x;
	else
		return parent[x] = Find(parent[x]);
}

//  按秩归并 
void Union(int x1, int x2) 
{
	x1 = Find(x1);
	x2 = Find(x2);
	if (parent[x1] < parent[x2]) 
	{
		parent[x1] += parent[x2];
		parent[x2] = x1;
	}
	else 
	{
		parent[x2] += parent[x1];
		parent[x1] = x2;
	}
}

void Kruskal() 
{
	// 最小生成树的边不到 Nv-1 条且还有边 
	while (MST.size() != Nv - 1 && !q.empty()) 
	{
		Node E = q.top();  // 从最小堆取出一条权重最小的边
		q.pop(); // 出队这条边 
		if (Find(E.v1) != Find(E.v2)) 
		{  // 检测两条边是否在同一集合 
			sum += E.weight;
			Union(E.v1, E.v2);     // 并起来 
			MST.push(E);
		}
	}

}

void Print() 
{
	cout << "被收录顺序(每条边对应的顶点1 顶点2 权重):" << endl;
	while (!MST.empty())
	{
		Node a = MST.front();
		cout << a.v1 << " " << a.v2 << " " << a.weight << endl;
		MST.pop();
	}
	cout << "权重和为:" << endl;
	cout << sum << endl;
}


int main() 
{
	BuildGraph();
	Kruskal();
	Print();
	system("pause");
	return 0;
}

  图1所示的图使用Kruskal算法开始生成最小生成树的测试效果如下图所示。

在这里插入图片描述

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值