【数据结构】图的最小生成树

news/2024/9/30 10:34:30 标签: 数据结构, c++, 图论



快乐的流畅:个人主页


个人专栏:《C游记》《进击的C++》《Linux迷航》

远方有一堆篝火,在为久候之人燃烧!

文章目录

  • 引言
  • 一、最小生成树的概念
  • 二、Kruskal算法
    • 2.1 思想
    • 2.2 实现
  • 三、Prim算法
    • 3.1 思想
    • 3.2 实现
  • 四、Kruskal和Prim的对比

引言

前置知识:【数据结构】并查集
前置知识:【数据结构】图的概念和存储结构

一、最小生成树的概念

最小生成树(Minimum Spanning Tree, MST)图论中的一个重要概念,指的是在一个连通的无向图中,选择一部分边,使得这些边连接所有顶点且边权值之和最小,同时生成的子图仍是一个树结构(没有环)。


按照定义,若连通网络由n个顶点组成,则其生成树必含n个顶点,n-1条边。因此,构造最小生成树有3个准则:

  1. 只能使用该网络的边来构造最小生成树
  2. 只能使用恰好n-1条边来连接n个顶点
  3. 选用的这n-1条边不能构成回路


下面讲解两种经典的最小生成树算法——kruskal算法和prim算法,它们都采用了贪心策略来进行实现。

前置声明:

template<class W>
struct Edge
{
	int _srci;
	int _dsti;
	W _w;

	Edge(int srci, int dsti, const W& w)
		: _srci(srci)
		, _dsti(dsti)
		, _w(w)
	{}

	bool operator>(const Edge& e) const
	{
		return _w > e._w;
	}
};

template<class V, class W, W W_MAX = INT_MAX, bool Direction = false>
class Graph
{
	typedef Edge<W> Edge;
	typedef Graph<V, W, W_MAX, Direction> Self;
	//...
}

二、Kruskal算法

2.1 思想

Kruskal算法采用全局贪心的策略:

  1. 每次选取图中权值最小的边。
  2. 每次选取完后,判断是否构成回路。若构成,则舍弃这条边。

具体图示如下:

2.2 实现

思路:

  1. 采用优先级队列(小堆),将所有边存入,以便每次选取全图权值最小的边。
  2. 采用并查集,存储已选取的顶点。每次选取边时,判断两侧顶点是否在并查集中,以此判断是否构成回路。
W Kruskal(Self& minTree)
{
	minTree._vertexs = _vertexs;
	minTree._indexMap = _indexMap;
	int n = _edges.size();
	minTree._edges.resize(n, vector<W>(n, W_MAX));

	priority_queue<Edge, vector<Edge>, greater<Edge>> minHeap;
	for (int i = 0; i < n; ++i)
	{
		for (int j = 0; j < n; ++j)
		{
			if (i < j && _edges[i][j] != W_MAX)//无向图,防止重复记录路径
			{
				minHeap.push(Edge(i, j, _edges[i][j]));
			}
		}
	}

	UnionFindSet<V> ufs(n);
	W total = W();
	int count = 0;
	while (!minHeap.empty())
	{
		Edge top = minHeap.top();
		minHeap.pop();

		int srci = top._srci, dsti = top._dsti;
		W w = top._w;
		if (!ufs.InSet(srci, dsti))
		{
			minTree._AddEdge(srci, dsti, w);
			ufs.Union(srci, dsti);
			total += w;
			++count;

			if (count == n - 1)
			{
				break;
			}

			cout << _vertexs[srci] << "--" << _vertexs[dsti] << ":" << w << endl;
		}
		else
		{
			cout << _vertexs[srci] << "--" << _vertexs[dsti] << ":" << w << "[构成环]" << endl;
		}
	}

	if (count == n - 1)
	{
		return total;
	}
	else
	{
		return W();
	}
}

细节:

  1. 输入一个空图,输出最小生成树的总权值,并将空图变为最小生成树。
  2. count记录已选边数,若达到n-1,则可直接跳出循环,提高效率。

三、Prim算法

3.1 思想

Prim算法采用局部贪心的策略:

  1. 将已被选择的点看作一个顶点集合,初始状态只有起点在集合中。
  2. 每次在集合周围查找,找到消耗权值最小即可抵达的点,并将其纳入集合。

具体图示如下:

3.2 实现

思路:

  1. 采用优先级队列(小堆),将起始点周围的路径存入,以便每次选取集合周围权值最小的边。
  2. 集合用bool数组表示,每次只有当目标点不在集合中,才将其纳入集合。
  3. 每次将新点纳入集合后,再将新点周围的路径存入优先级队列,依次迭代。
W Prim(Self& minTree, const V& src)
{
	minTree._vertexs = _vertexs;
	minTree._indexMap = _indexMap;
	int n = _edges.size();
	minTree._edges.resize(n, vector<W>(n, W_MAX));

	//起始点周围的路径入堆
	priority_queue<Edge, vector<Edge>, greater<Edge>> minHeap;
	int srci = GetIndex(src);
	for (int i = 0; i < n; ++i)
	{
		if (_edges[srci][i] != W_MAX)
		{
			minHeap.push(Edge(srci, i, _edges[srci][i]));
		}
	}

	vector<bool> S(n, false);
	S[srci] = true;

	W total = W();
	int count = 0;
	while (!minHeap.empty())
	{
		Edge top = minHeap.top();
		minHeap.pop();

		int srci = top._srci, dsti = top._dsti;
		W w = top._w;
		if (!S[dsti])
		{
			minTree._AddEdge(srci, dsti, w);
			S[dsti] = true;
			total += w;
			++count;

			if (count == n - 1)
			{
				break;
			}

			for (int i = 0; i < n; ++i)
			{
				if (!S[i] && _edges[dsti][i] != W_MAX)//无向图,防止重复记录路径
				{
					minHeap.push(Edge(dsti, i, _edges[dsti][i]));
				}
			}

			cout << _vertexs[srci] << "--" << _vertexs[dsti] << ":" << w << endl;
		}
		else
		{
			cout << _vertexs[srci] << "--" << _vertexs[dsti] << ":" << w << "[构成环]" << endl;
		}
	}

	if (count == n - 1)
	{
		return total;
	}
	else
	{
		return W();
	}
}

细节:

  1. 输入一个空图和一个起始点,输出最小生成树的总权值,并将空图变为最小生成树。
  2. count记录已选边数,若达到n-1,则可直接跳出循环,提高效率。

四、Kruskal和Prim的对比

Kruskal 算法Prim 算法
算法类型贪心算法贪心算法
适用图类型稀疏图,特别适合边权值分布较广的图稠密图,特别适合顶点多边多的图
基本思想从边的角度出发,按权值从小到大选择边从顶点出发,每次扩展最小权值的边
时间复杂度O(E log V)O(E log V)
典型应用网络设计问题,边多且分散的图电网、网络设计问题,稠密的图
贪心选择标准每次选择权值最小且不形成环的边每次选择最小的连接权值,扩展已加入的顶点集合

  • Kruskal:适用于稀疏图,因为其从边的角度出发,边的数量相对少时效率更高。
  • Prim:适用于稠密图,因为它每次从顶点出发,逐渐扩展树,对于稠密图(边多的图)效率更高

真诚点赞,手有余香


http://www.niftyadmin.cn/n/5684910.html

相关文章

【C++单调队列】1438. 绝对差不超过限制的最长连续子数组|1672

本文时间知识点 C队列、双向队列 LeetCode1438. 绝对差不超过限制的最长连续子数组 给你一个整数数组 nums &#xff0c;和一个表示限制的整数 limit&#xff0c;请你返回最长连续子数组的长度&#xff0c;该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit 。 如…

Linux中安装ffmpeg

Linux中安装ffmpeg 一、下载二、安装三、测试 一、下载 先到这里下载ffmpeg。 二、安装 先将上传到服务器的某一目录&#xff0c;我这里是&#xff1a; /usr/local/ffmpeg 然后解压&#xff0c;解压命令如下&#xff1a; tar -xvf “你的安装包名称”我的是&#xff1a; ta…

二叉搜索树(c++版)

前言 在前面我们介绍过二叉树这个数据结构&#xff0c;今天我们更进一步来介绍二叉树的一种在实现中运用的场景——二叉搜索树。二叉搜索树顾名思义其在“搜索”这个场景下有不俗的表现&#xff0c;之所以会这样是因为它在二叉树的基础上添加了一些属性。下面我们就来简单的介…

矩阵奇异值

一、ATA 任给一个矩阵A&#xff0c;都有&#xff1a; ATA 为一个对称矩阵 例子&#xff1a;A为一个mn的矩阵&#xff0c;A的转置为一个nm的矩阵 对称矩阵的重要性质如下&#xff1a; ① 对称矩阵的特征值全为实数&#xff08;实数特征根&#xff09; ② 任意一个n阶对称矩阵…

ubuntu切换源方式记录(清华源、中科大源、阿里源)

文章目录 前言一、中科大源二、清华源三、阿里源 前言 记录ubunut切换各个源的方式。 备注&#xff1a;更换源之后使用sudo apt-get update更新索引。 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、中科大源 地址&#xff1a;https://mirrors.u…

RK3588主板PCB设计学习(一)

DCDC电路可以直接参考数据手册&#xff1a; 电源输出3A&#xff0c;回流GND也应该是3A&#xff0c;回流路径和输出路径的电流是一致的&#xff0c;不要输出路径布线很粗&#xff0c;GND回流路径很细&#xff0c;并且应该保证回流面积最小&#xff1a; 这一点讲的很到位&#xf…

第十四周学习周报

目录 摘要Abstract1. LSTM的代码实现2. 序列到序列模型3. 梯度与方向导数总结 摘要 在上周的学习基础之上&#xff0c;本周学习的内容有LSTM的代码实现&#xff0c;通过对代码的学习进一步加深了对LSTM的理解。为了切入到transformer的学习&#xff0c;本文通过对一些应用例子…

SQL中基本SELECT语句及常见关键字的使用(内连接,左/右连接)

这里写目录标题 SQL中基本SELECT语句的使用SQL语法简介DDL、DML、DCLSEECT SELECT常用关键词group by分组having筛选limit限定条数UION和UION ALL合并SQL执行顺序 联表查询多表查询示例特殊用法&#xff1a;笛卡尔积&#xff08;交叉连接&#xff09;等值连接vs非等值连接自连接…