其它高阶数据结构②_图(概念+存储+遍历+最小生成树)

news2024/12/28 3:54:47

目录

1. 图的概念

2. 图的存储结构

2.1 邻接矩阵(后面算法所用)

2.2 邻接表

3. 图的遍历

3.1 BFS广度优先遍历

3.2 DFS深度优先遍历

4. 最小生成树

4.1 Kruskal算法

4.2 Prim算法

本篇完。


1. 图的概念

图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中:

  • 顶点集合V = {x|x属于某个数据对象集}是有穷非空集合;
  • E = {(x,y)|x,y属于V}或者E = {<x, y>|x,y属于V && Path(x, y)}是顶点间关系的有穷集合,也叫做边的集合。
  • (x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即Path(x, y)是有方向的。
  • 顶点和边:图中结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi,vj)或<vi,vj>。
  • 有向图和无向图:在有向图中,顶点对<x, y>是有序的,顶点对<x,y>称为顶点x到顶点y的一条边(弧),<x, y>和<y, x>是两条不同的边,比如下图G3和G4为有向图。在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边,比如下图G1和G2为无向图。注意:无向边(x, y)等于有向边<x, y>和<y, x>。

  • 完全图:在有n个顶点的无向图中,若有n * (n-1)/2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图,比如上图G1;在n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图,比如下图G4。

  • 邻接顶点:在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依附于顶点u和v;在有向图G中,若<u, v>是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶点u,并称边<u, v>与顶点u和顶点v相关联。
  • 顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点的度等于该顶点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。注意:对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)。
  • 路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径。
  • 路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和。

带权图示例:

简单路径与回路:

  • 若路径上的各个顶点均不相同,则称这样的路径为简单路径。
  • 若路径上第一个顶点与最后一个顶点相同,则称这样的路径为回路或环。

如下图:

子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。

  • 连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任 意一 对顶点都是连通的,则称此图为连通图。
  • 强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj 到 vi的路径,则称此图是强连通图。
  • 生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点 和n - 1条边。
  • 最小生成树:一个图的生成树中,总权值最小的生成树。

图常见的表示场景如下:

  • 交通网络:图中的每个顶点表示一个地点,图中的边表示这两个地点之间是否有直接相连的公路,边的权值可以是这两个地点之间的距离、高铁时间等。
  • 网络设备拓扑:图中的每个顶点表示网络中的一个设备,图中的边表示这两个设备之间是否可以互传数据,边的权值可以是这两个设备之间传输数据所需的时间、丢包的概率等。
  • 社交网络:图中的每个顶点表示一个人,图中的边表示这两个人是否互相认识,边的权值可以是这两个人之间的亲密度、共同好友个数等。

关于有向图和无向图:

  • 交通网络对应的图可以是有向图,也可以是无向图,无向图对应就是双向车道,有向图对应就是单向车道。
  • 网络设备拓扑对应的图通常是无向图,两个设备之间有边表示这两个设备之间可以互相收发数据。
  • 社交网络对应的图可以是有向图,也可以是无向图,无向图通常表示一些强社交关系,比如QQ、微信等(一定互为好友),有向图通常表示一些弱社交关系,比如微博、抖音(不一定互相关注)。

图的其他相关作用:

  • 在交通网络中,根据最短路径算法计算两个地点之间的最短路径,根据最小生成树算法得到将各个地点连通起来所需的最小成本。
  • 在社交网络中,根据广度优先搜索得到两个人之间的共同好友进行好友推荐,根据入边表和出边表得知有哪些粉丝以及关注了哪些博主。

图与树的联系与区别:

  • 树是一种有向无环且连通的图(空树除外),但图并不一定是树。
  • 有 n 个结点的树必须有 n − 1条边,而图中边的数量不取决于顶点的数量。
  • 树通常用于存储数据,并快速查找目标数据,而图通常用于表示某种场景。

2. 图的存储结构

图由顶点和边组成,存储图本质就是将图中的顶点和边存储起来。

本篇博客的各种关于图的算法都用图的领接矩阵实现。

2.1 邻接矩阵(后面算法所用)

邻接矩阵存储图的方式如下:

  • 用一个数组存储顶点集合,顶点所在位置的下标作为该顶点的编号(所给顶点可能不是整型)。
  • 用一个二维数组 matrix 存储边的集合,其中 matrix[i][j]表示编号为 i 和 j 的两个顶点之间的关系。

注意事项:

  • 对于不带权的图,两个顶点之间要么相连,要么不相连,可以用0和1表示,matrix [ i ] [ j ] 为1表示编号为 i 和 j 的两个顶点相连为0表示不相连。
  • 对于带权的图,连接两个顶点的边会带有一个权值,可以用这个权值来设置对应matrix [ i ] [ j ]的值,如果两个顶点不相连,则使用不会出现的权值进行设置即可(图中为无穷大)。
  • 对于无向图来说,顶点 i 和顶点 j 相连,那么顶点 j 就和顶点 i 相连,因此无向图对应的邻接矩阵是一个对称矩阵,即matrix [ i ] [ j ] 的值等于matrix [ i ] [ j ]的值。
  • 在邻接矩阵中,第 i 行元素中有效权值的个数就是编号为 i 的顶点的出度,第 i 列元素中有效元素的个数就是编号为 i 的顶点的入度。

邻接矩阵的优点:

  • 邻接矩阵适合存储稠密图,因为存储稠密图和稀疏图时所开辟的二维数组大小是相同的,因此图中的边越多,邻接矩阵的优势就越明显。
  • 邻接矩阵能够O(1)地判断两个顶点是否相连,并获得相连边的权值。

邻接矩阵的缺点:

  • 邻接矩阵不适合查找一个顶点连接出去的所有边,需要遍历矩阵中对应的一行,该过程的时间复杂度是O ( N )  ,其中 N 表示的是顶点的个数。

邻接矩阵的实现:

#pragma once

#include <vector>
#include <iostream>
#include <string>
#include <queue>
#include <map>
#include <set>
#include <functional>
using namespace std;

namespace Matrix // 临接矩阵
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph //Vertex顶点,Weight权值,MAX_W不存在边的标识值 ,Direction有向无向
	{
	private:
		vector<V> _vertexs; // 顶点集合
		map<V, size_t> _vIndexMap; // 顶点映射下标
		vector<vector<W>> _matrix; // 存储边集合的矩阵(领接矩阵)

	public:
		struct Edge
		{
			V _srci;
			V _dsti;
			W _w;

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

			bool operator<(const Edge& eg) const
			{
				return _w < eg._w;
			}

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

		typedef Graph<V, W, MAX_W, Direction> Self;

		Graph() = default;

		Graph(const V* vertexs, int n)
			: _vertexs(vertexs, vertexs + n) // 设置顶点集合
			, _matrix(n, vector<int>(n, MAX_W)) // 开辟二维数组空间,MAX_W作为不存在边的标识值
		{
			for (int i = 0; i < n; i++) // 建立顶点与下标的映射关系
			{
				_vIndexMap[vertexs[i]] = i;
			}
		}

		size_t GetVertexIndex(const V& v) // 获取顶点对应的下标
		{
			auto ret = _vIndexMap.find(v);
			if (ret != _vIndexMap.end()) // 顶点存在
			{
				return ret->second;
			}
			else
			{
				throw invalid_argument("不存在的顶点");
				return -1;
			}
		}

		void _AddEdge(int srci, int dsti, const W& weight)
		{
			_matrix[srci][dsti] = weight; // 设置邻接矩阵中对应的值
			if (Direction == false) // 无向图
			{
				_matrix[dsti][srci] = weight; // 添加从目标顶点到源顶点的边
			}
		}
		void AddEdge(const V& src, const V& dst, const W& w) // 添加边
		{
			size_t srci = GetVertexIndex(src); // 获取源顶点和目标顶点的下标
			size_t dsti = GetVertexIndex(dst);

			_AddEdge(srci, dsti, w);
		}

		void Print()
		{
			for (size_t i = 0; i < _vertexs.size(); ++i) // 打印顶点和下标映射关系
			{
				cout << _vertexs[i] << "-" << i << " ";
			}
			cout << endl << endl;

			cout << "  ";
			for (size_t i = 0; i < _vertexs.size(); ++i)
			{
				cout << i << " ";
			}
			cout << endl;

			for (size_t i = 0; i < _matrix.size(); ++i) // 打印矩阵
			{
				cout << i << " ";
				for (size_t j = 0; j < _matrix[i].size(); ++j)
				{
					if (_matrix[i][j] != MAX_W)
						cout << _matrix[i][j] << " ";
					else
						cout << "#" << " ";
				}
				cout << endl;
			}
			cout << endl;

			for (size_t i = 0; i < _matrix.size(); ++i) // 打印所有的边
			{
				for (size_t j = 0; j < _matrix[i].size(); ++j)
				{
					if (i < j && _matrix[i][j] != MAX_W)
					{
						cout << _vertexs[i] << "-" << _vertexs[j] << ":" << _matrix[i][j] << endl;
					}
				}
			}
		}
	};

	void TestGraph()
	{
		Graph<char, int, INT_MAX, true> g("0123", 4);
		g.AddEdge('0', '1', 1);
		g.AddEdge('0', '3', 4);
		g.AddEdge('1', '3', 2);
		g.AddEdge('1', '2', 9);
		g.AddEdge('2', '3', 8);
		g.AddEdge('2', '1', 5);
		g.AddEdge('2', '0', 3);
		g.AddEdge('3', '2', 6);

		g.Print();
	}
}


2.2 邻接表

邻接表(类似哈希桶):使用数组存储顶点的集合,使用链表存储顶点的关系(边)。

邻接表存储图的方式如下:

  1. 用一个数组存储顶点集合,顶点所在的位置的下标作为该顶点的编号(所给顶点可能不是整型)。
  2. 用一个出边表存储从各个顶点连接出去的边,出边表中下标为 i 的位置存储的是从编号为 i 的顶点连接出去的边。
  3. 用一个入边表存储连接到各个顶点的边,入边表中下标为 i 的位置存储的是连接到编号为 i 的顶点的边。

如下图:

  • 出边表和入边表类似于哈希桶,其中每个位置存储的都是一个链表,出边表中下标为 i 的位置的链表中存储的都是从编号为 i 的顶点连接出去的边,入边表中下标为 i 的位置的链表中存储的都是连接到编号为 i 的顶点的边。
  • 在邻接表中,出边表中下标为 i 的位置的链表中元素的个数就是编号为 i 的顶点的出度,入边表中下标为 i 的的位置的链表中元素的个数就是编号为 i 的顶点的入度。
  • 在实现邻接表时,一般只需要用一个出边表来存储从各个顶点连接出去的边即可,因为大多数情况下都是需要从一个顶点出发找与其相连的其他顶点,所以一般不需要存储入边表。

邻接表的优点:

  • 邻接表适合存储稀疏图,因为邻接表存储图时开辟的空间大小取决于边的数量,图中边的数量越少,邻接表存储边时所需的内存空间就越少。
  • 邻接表适合查找一个顶点连接出去的所有边,出边表中下标为 i 的位置的链表中存储的就是从顶点 i 连接出去的所有边。

邻接表的缺点:

  • 邻接表不适合确定两个顶点是否相连,需要遍历出边表中源顶点对应位置的链表,该过程的时间复杂度是O(E),其中 E 表示从源顶点连接出去的边的数量。

邻接表的实现:

#pragma once

#include <vector>
#include <iostream>
#include <string>
#include <queue>
#include <map>
#include <set>
#include <functional>
using namespace std;

namespace LinkTable // 临接表
{
	template<class W>
	struct LinkEdge
	{
		int _srcIndex;
		int _dstIndex;
		W _w;
		LinkEdge<W>* _next;

		LinkEdge(const W& w)
			: _srcIndex(-1)
			, _dstIndex(-1)
			, _w(w)
			, _next(nullptr)
		{}
	};

	template<class V, class W, bool Direction = false>
	class Graph // 构造函数
	{
		typedef LinkEdge<W> Edge;

	private:
		map<string, int> _vIndexMap;
		vector<V> _vertexs;			 // 顶点集合
		vector<Edge*> _linkTable;    // 边的集合的临接表

	public:
		Graph(const V* vertexs, size_t n) // 构造函数
		{
			_vertexs.reserve(n); // 设置顶点集合
			for (size_t i = 0; i < n; ++i)
			{
				_vertexs.push_back(vertexs[i]);
				_vIndexMap[vertexs[i]] = i; // 建立顶点与下标的映射关系
			}
			_linkTable.resize(n, nullptr); // 开辟邻接表的空间
		}

		size_t GetVertexIndex(const V& v) // 获取顶点对应的下标
		{
			auto ret = _vIndexMap.find(v);
			if (ret != _vIndexMap.end()) // 顶点存在
			{
				return ret->second;
			}
			else
			{
				throw invalid_argument("不存在的顶点");
				return -1;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w) // 添加边
		{
			size_t srcindex = GetVertexIndex(src);
			size_t dstindex = GetVertexIndex(dst);

			Edge* sd_edge = new Edge(w); // 添加从源顶点到目标顶点的边
			sd_edge->_srcIndex = srcindex;
			sd_edge->_dstIndex = dstindex;
			sd_edge->_next = _linkTable[srcindex];
			_linkTable[srcindex] = sd_edge;

			if (Direction == false) // 1 0 无向图
			{
				Edge* ds_edge = new Edge(w); // 添加从目标顶点到源顶点的边
				ds_edge->_srcIndex = dstindex;
				ds_edge->_dstIndex = srcindex;
				ds_edge->_next = _linkTable[dstindex];
				_linkTable[dstindex] = ds_edge;
			}
		}

		void print() // 打印顶点集合和邻接表
		{
			int n = _vertexs.size();
			for (int i = 0; i < n; i++) // 打印顶点集合
			{
				cout << "[" << i << "]->" << _vertexs[i] << " ";
			}
			cout << endl << endl;

			for (int i = 0; i < n; i++) // 打印邻接表
			{
				Edge* cur = _linkTable[i];
				cout << "[" << i << ":" << _vertexs[i] << "]->";
				while (cur)
				{
					cout << "[" << cur->_dstIndex << ":" << _vertexs[cur->_dstIndex] << ":" << cur->_w << "]->";
					cur = cur->_next;
				}
				cout << "nullptr" << endl;
			}
		}
	};

	void TestGraph()
	{
		string a[] = { "张三", "李四", "王五", "赵六" };
		Graph<string, int> g1(a, 4);
		g1.AddEdge("张三", "李四", 100);
		g1.AddEdge("张三", "王五", 200);
		g1.AddEdge("王五", "赵六", 30);
		g1.print();
	}
}


3. 图的遍历

图的遍历指的是遍历图中的顶点,主要有BFS广度优先遍历和DFS深度优先遍历两种方式。

3.1 BFS广度优先遍历

        广度优先遍历又称BFS,其遍历过程类似于二叉树的层序遍历,从起始顶点开始一层一层向外进行遍历。

广度优先遍历的实现:

  • 广度优先遍历需要借助一个队列和一个标记数组,利用队列先进先出的特点实现一层一层向外遍历,利用标记数组来记录各个顶点是否被访问过。
  • 刚开始时将起始顶点入队列,并将起始顶点标记为访问过,然后不断从队列中取出顶点进行访问,并判断该顶点是否有邻接顶点,如果有邻接顶点并且该邻接顶点没有被访问过,则将该邻接顶点入队列,并在入队列后立即将该邻接顶点标记为访问过。

代码如下:

namespace Matrix // 临接矩阵
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph //Vertex顶点,Weight权值,MAX_W不存在边的标识值 ,Direction有向无向
	{
	private:
		vector<V> _vertexs; // 顶点集合
		map<V, size_t> _vIndexMap; // 顶点映射下标
		vector<vector<W>> _matrix; // 存储边集合的矩阵(领接矩阵)

	public:
		void BFS(const V& src)
		{
			size_t srci = GetVertexIndex(src);
			queue<int> q; // 队列和标记数组
			vector<bool> visited(_vertexs.size(), false);

			q.push(srci);
			visited[srci] = true;
			int levelSize = 1;

			size_t n = _vertexs.size();
			while (!q.empty())
			{
				for (int i = 0; i < levelSize; ++i) // 一层一层出
				{
					int front = q.front();
					q.pop();
					cout << front << ":" << _vertexs[front] << " ";
					for (size_t i = 0; i < n; ++i) // 把front顶点的邻接顶点入队列
					{
						if (_matrix[front][i] != MAX_W)
						{
							if (visited[i] == false)
							{
								q.push(i);
								visited[i] = true;
							}
						}
					}
				}
				cout << endl;
				levelSize = q.size();
			}
			cout << endl;
		}
    }

	void TestGraphBFS()
	{
		string a[] = { "张三", "李四", "王五", "赵六", "周七" };
		Graph<string, int> g1(a, sizeof(a) / sizeof(string));
		g1.AddEdge("张三", "李四", 100);
		g1.AddEdge("张三", "王五", 200);
		g1.AddEdge("王五", "赵六", 30);
		g1.AddEdge("王五", "周七", 30);

		g1.BFS("张三");
	}
}
  • 为了防止顶点被重复加入队列导致死循环,因此需要一个标记数组,当一个顶点被访问过后就不应该再将其加入队列了。
  • 如果当一个顶点从队列中取出访问时才再将其标记为访问过,也可能会存在顶点被重复加入队列的情况,比如当图中的顶点B出队列时,顶点C作为顶点B的邻接顶点并且还没有被访问过(顶点C还在队列中),此时顶点C就会再次被加入队列,因此最好在一个顶点被入队列时就将其标记为访问过。
  • 如果所给图不是一个连通图,那么从一个顶点开始进行广度优先遍历,无法遍历完图中的所有顶点,这时可以遍历标记数组,查看哪些顶点还没有被访问过,对于没有被访问过的顶点,则从该顶点处继续进行广度优先遍历,直到图中所有的顶点都被访问过。


3.2 DFS深度优先遍历

       深度优先遍历又称DFS,其遍历过程类似于二叉树的先序遍历,从起始顶点开始不断对顶点进行深入遍历。​​​​​

深度优先遍历的实现:

  • 深度优先遍历可以通过递归实现,同时也需要借助一个标记数组来记录各个顶点是否被访问过。
  • 从起始顶点处开始进行递归遍历,在遍历过程中先对当前顶点进行访问,并将其标记为访问过,然后判断该顶点是否有邻接顶点,如果有邻接顶点并且该邻接顶点没有被访问过,则递归遍历该邻接顶点。

代码如下:

namespace Matrix // 临接矩阵
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph //Vertex顶点,Weight权值,MAX_W不存在边的标识值 ,Direction有向无向
	{
	private:
		vector<V> _vertexs; // 顶点集合
		map<V, size_t> _vIndexMap; // 顶点映射下标
		vector<vector<W>> _matrix; // 存储边集合的矩阵(领接矩阵)

	public:
		void _DFS(size_t srci, vector<bool>& visited)
		{
			cout << srci << ":" << _vertexs[srci] << endl;  // 访问
			visited[srci] = true; 
			for (size_t i = 0; i < _vertexs.size(); ++i) // 找一个srci相邻的没有访问过的点,深度遍历
			{
				if (_matrix[srci][i] != MAX_W && visited[i] == false) // 邻接顶点,且没有被访问过
				{
					_DFS(i, visited);
				}
			}
		}
		void DFS(const V& src)
		{
			size_t srci = GetVertexIndex(src); // 起始顶点的下标
			vector<bool> visited(_vertexs.size(), false);
			_DFS(srci, visited);
		}
    }

	void TestGraphDFS()
	{
		string a[] = { "张三", "李四", "王五", "赵六", "周七" };
		Graph<string, int> g1(a, sizeof(a) / sizeof(string));
		g1.AddEdge("张三", "李四", 100);
		g1.AddEdge("张三", "王五", 200);
		g1.AddEdge("王五", "赵六", 30);
		g1.AddEdge("王五", "周七", 30);

		g1.DFS("张三");
	}
}
  • 如果所给图不是一个连通图,那么从一个顶点开始进行深度优先遍历,无法遍历完图中的所有顶点,这时可以遍历标记数组,查看哪些顶点还没有被访问过,对于没有被访问过的顶点,则从该顶点处继续进行深度优先遍历,直到图中所有的顶点都被访问过。


4. 最小生成树

最小生成树的概念:

  • 一个连通图的最小连通子图称为该图的生成树,若连通图由 n 个顶点组成,则其生成树必含 n 个顶点和 n − 1 条边,最小生成树指的是一个图的生成树中,总权值最小的生成树。
  • 连通图中的每一棵生成树都是原图的一个极大无环子图,从其中删去任何一条边,生成树就不再连通,在其中引入任何一条新边,都会形成一条回路。

注意事项:

  • 对于各个顶点来说,除了第一个顶点之外,其他每个顶点想要连接到图中,至少需要一条边使其连接进来,所以由 n 个顶点的连通图的生成树有 n 个顶点和 n − 1 条边。
  • 对于生成树来说,图中的每个顶点已经连通了,如果再引入一条新边,那么必然会使得被新边相连的两个顶点之间存在一条直接路径和一条间接路径,即形成回路。
  • 最小生成树是图的生成树中总权值最小的生成树,生成树是图的最小连通子图,而连通图是无向图的概念,有向图对应的是强连通图,所以最小生成树算法的处理对象都是无向图。

构成最小生成树的准则:

  1. 只能使用图中的边来构造最小生成树。
  2. 只能使用恰好 n − 1 条边来连接图中的 n 个顶点。
  3. 选用的 n − 1 条边不能构成回路。

构造最小生成树的算法有Kruskal算法和Prim算法,这两个算法都采用了逐步求解的贪心策略。

Kruskal算法是全局最优的策略,而Prim算法是局部最优的策略。


4.1 Kruskal算法

Kruskal算法(克鲁斯卡尔算法)的基本思想如下:

  • 构造一个含 n 个顶点、不含任何边的图作为最小生成树,对原图中的各个边按权值进行排序。
  • 每次从原图中选出一条最小权值的边,将其加入到最小生成树中,如果加入这条边会使得最小生成树中构成回路,则重新选择一条边。
  • 按照上述规则不断选边,当选出 n − 1 条合法的边时,则说明最小生成树构造完毕,如果无法选出 n − 1 条合法的边,则说明原图不存在最小生成树。

动图演示:(注意到选7,不选6,就是不能构成环)

再贴个动图:

以下是《算法导论》里对Kruskal算法的步骤图解:

代码实现:

namespace Matrix // 临接矩阵
{

	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph //Vertex顶点,Weight权值,MAX_W不存在边的标识值 ,Direction有向无向
	{
	private:
		vector<V> _vertexs; // 顶点集合,顶点所在位置的下标作为该顶点的编号
		map<V, size_t> _vIndexMap; // 顶点映射下标
		vector<vector<W>> _matrix; // 存储边集合的矩阵,_matrix[i][j]表示编号为i和j的两个顶点之间的关系

	public:
		struct Edge
		{
			V _srci;
			V _dsti;
			W _w;
			Edge(const V& srci, const V& dsti, const W& w)
				:_srci(srci)
				, _dsti(dsti)
				, _w(w)
			{}
			bool operator<(const Edge& eg) const
			{
				return _w < eg._w;
			}
			bool operator>(const Edge& eg) const
			{
				return _w > eg._w;
			}
		};
		void _AddEdge(int srci, int dsti, const W& weight)
		{
			_matrix[srci][dsti] = weight; // 设置邻接矩阵中对应的值
			if (Direction == false) // 无向图
			{
				_matrix[dsti][srci] = weight; // 添加从目标顶点到源顶点的边
			}
		}
		void AddEdge(const V& src, const V& dst, const W& w) // 添加边
		{
			size_t srci = GetVertexIndex(src); // 获取源顶点和目标顶点的下标
			size_t dsti = GetVertexIndex(dst);

			_AddEdge(srci, dsti, weight);
		}

		typedef Graph<V, W, MAX_W, Direction> Self;
		W Kruskal(Self& minTree) // 克鲁斯卡尔算法
		{
			int n = _vertexs.size(); // 二维数组空间
			minTree._vertexs = _vertexs; // 设置最小生成树的顶点集合
			minTree._vIndexMap = _vIndexMap; // 设置最小生成树顶点与下标的映射
			minTree._matrix.resize(n, vector<W>(n, MAX_W)); // 开辟最小生成树的二维数组空间

			priority_queue<Edge, vector<Edge>, greater<Edge>> pq; // 优先级队列(小堆)
			for (size_t i = 0; i < n; ++i) // 将所有边添加到优先级队列
			{								// 最小生成树算法的处理对象都是无向图
				for (size_t j = 0; j < i; ++j) // 只遍历矩阵的一半,避免重复添加相同的边
				{
					if (_matrix[i][j] != MAX_W)
					{
						pq.push(Edge(i, j, _matrix[i][j]));
					}
				}
			}

			UnionFindSet ufs(n); // n个顶点的并查集
			size_t i = 1; // 已选边的数量,贪心策略,从最小的边开始选
			W total = W(); // 总权值
			while (!pq.empty() && i < n)
			{
				Edge min = pq.top(); // 从优先级队列中获取一个权值最小的边
				pq.pop();
				// 边不在一个集合,说明不会构成环,则添加到最小生成树
				if (ufs.InSet(min._srci, min._dsti))
				{
					// cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":"
					// << _matrix[min._srci][min._dsti] << endl;
					minTree._AddEdge(min._srci, min._dsti, min._w); // 在最小生成树中添加边
					total += min._w;

					ufs.Union(min._srci, min._dsti);
					++i;
					cout << "选边: " << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
				}
				else // 边的源顶点和目标顶点在同一个集合,加入这条边会构成环
				{
					cout << "成环: " << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
				}

			}
			if (i == n)
			{
				cout << "构建最小生成树成功" << endl;
				return total;
			}
			else
			{
				cout << "无法构成最小生成树" << endl;
				return W();
			}
		}
    }
}
  • 在获取图的最小生成树时,会以无参的方式定义一个最小生成树对象,然后用原图对象调用上述Kruskal函数,通过输出型参数的方式获取原图的最小生成树,由于我们定义了一个带参的构造函数,使得编译器不再生成默认构造函数,因此需要通过default关键字强制生成Graph类的默认构造函数。
  • 一条边包含两个顶点和边的权值,可以定义一个Edge结构体来描述一条边,结构体内包含边的源顶点和目标顶点的下标以及边的权值,在使用优先级队列构造小堆结构时,需要存储的对象之间能够支持 > 运算符操作,因此需要对Edge结构体的 > 运算符进行重载,将其重载为边的权值的比较。
  • 当选出的边不会构成回路时,需要将这条边插入到最小生成树对应的图中,此时已经知道了这条边的源顶点和目标顶点对应的下标,可以在Graph类中新增一个_AddEdge子函数,该函数支持通过源顶点和目标顶点的下标向图中插入边,而Graph类中原有的AddEdge函数可以复用这个_AddEdge子函数。
  • 最小生成树不一定是唯一的,特别是当原图中存在很多权值相等的边的时候,比如对于动图中的图来说,将最小生成树中的 b c 边换成 a h 边也是一棵最小生成树。
  • 上述代码中通过优先级队列构造小堆来依次获取权值最小的边,也可以通过其他排序算法按权值对边进行排序,然后按权值从小到大依次遍历各个边进行选边操作。
  • 上述代码中使用的并查集UnionFindSet类,在本专栏上一篇博客中已经讲解过了。

4.2 Prim算法

        Prim算法是一种解决连通图最小生成树问题的贪心算法。最小生成树是原图的一个子图,包含了图中所有的顶点,并且是一棵树,使得所有边的权重之和最小。Prim算法通过逐步选择顶点来构建最小生成树。

        Prim算法的核心思想是从一个初始顶点开始,每次选择一个与当前最小生成树相邻的顶点,将权重最小的边加入最小生成树,直到所有顶点都被包含在最小生成树中。在整个过程中,保持当前最小生成树的所有顶点都是连通的。Prim算法的每一步都选择当前最小生成树与其余顶点之间的最小权重边,逐步构建最小生成树。这个过程保证了每一步都是局部最优的,最终得到的最小生成树是全局最优的。

Prim算法动图:

  • Prim算法中的边选择:
  1. 在Prim算法的执行过程中,每一步都是从已经选择的顶点集合到未选择的顶点中选择一条最小权重的边。这条边将一个未选择的顶点连接到已经选择的顶点集合中。
  2. 由于每一步都是通过选择连接已有树和未选择顶点的最小权重边,新加入的边不会形成环。这是因为Prim算法每次都是选择最小权重的边,而非随机选择。
  •  无需专门考虑成环问题:
  1. 由于Prim算法每一步的选择都确保了不会形成环,所以无需在算法实现中专门考虑成环问题。
  2. 成环问题通常涉及到判断当前考虑的边是否会形成环,需要使用一些额外的数据结构(例如并查集)来判断。Kruskal算法就需要在每一步中判断加入的边是否形成环,因此需要处理成环问题。
  • 简化实现:
  1. 由于Prim算法在每一步的选择中已经考虑了不形成环这一点,实现上更加简单。Prim算法通常可以通过优先队列(最小堆)等数据结构来高效地选择最小权重的边。
  2. 相对而言,Kruskal算法需要额外的成环判断,通常涉及更复杂的数据结构和算法。

以下是《算法导论》里对Prim算法的介绍:

代码:

namespace Matrix // 临接矩阵
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph //Vertex顶点,Weight权值,MAX_W不存在边的标识值 ,Direction有向无向
	{
	private:
		vector<V> _vertexs; // 顶点集合,顶点所在位置的下标作为该顶点的编号
		map<V, size_t> _vIndexMap; // 顶点映射下标
		vector<vector<W>> _matrix; // 存储边集合的矩阵,_matrix[i][j]表示编号为i和j的两个顶点之间的关系

	public:
		struct Edge
		{
			V _srci;
			V _dsti;
			W _w;
			Edge(const V& srci, const V& dsti, const W& w)
				:_srci(srci)
				, _dsti(dsti)
				, _w(w)
			{}
			bool operator<(const Edge& eg) const
			{
				return _w < eg._w;
			}
			bool operator>(const Edge& eg) const
			{
				return _w > eg._w;
			}
		};
		void _AddEdge(int srci, int dsti, const W& weight)
		{
			_matrix[srci][dsti] = weight; // 设置邻接矩阵中对应的值
			if (Direction == false) // 无向图
			{
				_matrix[dsti][srci] = weight; // 添加从目标顶点到源顶点的边
			}
		}
		void AddEdge(const V& src, const V& dst, const W& w) // 添加边
		{
			size_t srci = GetVertexIndex(src); // 获取源顶点和目标顶点的下标
			size_t dsti = GetVertexIndex(dst);

			_AddEdge(srci, dsti, weight);
		}

		typedef Graph<V, W, MAX_W, Direction> Self;
		W Prim(Self& minTree, const W& src) // 普里姆算法
		{
			size_t srci = GetVertexIndex(src);
			size_t n = _vertexs.size();
			minTree._vertexs = _vertexs; // 设置最小生成树的顶点集合
			minTree._vIndexMap = _vIndexMap; // 设置最小生成树顶点与下标的映射
			minTree._matrix.resize(n, vector<W>(n, MAX_W)); // 开辟最小生成树的二维数组空间

			vector<bool> X(n, false); // 已选边
			vector<bool> Y(n, true); // 未选边
			X[srci] = true;
			Y[srci] = false;

			// 从X->Y集合中连接的边里面选出最小的边
			priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
			for (size_t i = 0; i < n; ++i) // 先把srci连接的边添加到队列中
			{
				if (_matrix[srci][i] != MAX_W)
				{
					minq.push(Edge(srci, i, _matrix[srci][i]));
				}
			}

			cout << "Prim开始选边" << endl;
			size_t size = 0; // 已选边数量
			W totalW = W(); // 最小生成树的总权值
			while (!minq.empty())
			{
				Edge min = minq.top();
				minq.pop();

				if (X[min._dsti]) // 最小边的目标点也在X集合,则构成环
				{
					//cout << "构成环:";
					//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
				}
				else
				{
					minTree._AddEdge(min._srci, min._dsti, min._w);
					//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
					X[min._dsti] = true;
					Y[min._dsti] = false;
					++size;
					totalW += min._w;
					if (size == n - 1)
						break;

					for (size_t i = 0; i < n; ++i)
					{
						if (_matrix[min._dsti][i] != MAX_W && Y[i])
						{
							minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
						}
					}
				}
			}
			if (size == n - 1)
			{
				cout << "构建最小生成树成功" << endl;
				return totalW;
			}
			else
			{
				cout << "无法构成最小生成树" << endl;
				return W();
			}
		}
    }
	void TestGraphMinTree()
	{
		const char str[] = "abcdefghi";
		Graph<char, int> g(str, strlen(str));
		g.AddEdge('a', 'b', 4);
		g.AddEdge('a', 'h', 8);
		//g.AddEdge('a', 'h', 9);
		g.AddEdge('b', 'c', 8);
		g.AddEdge('b', 'h', 11);
		g.AddEdge('c', 'i', 2);
		g.AddEdge('c', 'f', 4);
		g.AddEdge('c', 'd', 7);
		g.AddEdge('d', 'f', 14);
		g.AddEdge('d', 'e', 9);
		g.AddEdge('e', 'f', 10);
		g.AddEdge('f', 'g', 2);
		g.AddEdge('g', 'h', 1);
		g.AddEdge('g', 'i', 6);
		g.AddEdge('h', 'i', 7);

		Graph<char, int> kminTree;
		cout << "Kruskal:" << g.Kruskal(kminTree) << endl;
		kminTree.Print();
		cout << endl << endl;

		Graph<char, int> pminTree;
		cout << "Prim:" << g.Prim(pminTree, 'a') << endl;
		pminTree.Print();
		cout << endl;

		for (size_t i = 0; i < strlen(str); ++i)
		{
			cout << "Prim:" << g.Prim(pminTree, str[i]) << endl;
		}
	}
}


本篇完。

下一篇是其它高阶数据结构③_图的最短路径(三种)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1679119.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI 绘画神器 Fooocus 图生图:图像放大或变化、图像提示、图像重绘或扩充、反推提示词、生成参数提取、所需模型下载

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里&#xff0c;订阅后可阅读专栏内所有文章。 大家好&#xff0c;我是水滴~~ 本文讲述 Fooocus 的图生图功能&#xff0c;主要内容包括&#xff1a;图像放大或变化、图像提示、图像重绘或扩充、反推…

没有疯狂内卷的日本智能机市场,小屏与设计仍旧是主流

如果聊起国内的智能机市场&#xff0c;我想大多数人的印象就是疯狂内卷。卷影像、卷屏幕、卷快充、卷性能……客观地说&#xff0c;国内的3C产品还是很有质价比的。不过在没有如此内卷的日本市场&#xff0c;各种小屏手机仍旧是主流。 除了苹果外&#xff0c;日本本土品牌的夏普…

SDL系列(四)—— 事件机制

事件循环 大多数多媒体程序依靠 事件系统 来处理输入。 SDL 为处理输入事件提供了灵活的 API 。 本质上&#xff0c; SDL 将来自设备&#xff08;如键盘&#xff0c;鼠标或控制器&#xff09;的输入记录为 事件 &#xff0c;将它们存储在 “ 事件队列 ”中。 您可以将此…

3D轻量化引擎HOOPS Communicator在PLM全生命周期管理中的应用

在当今快速发展的工业领域&#xff0c;产品生命周期管理&#xff08;PLM&#xff09;是确保产品从设计到市场再到最终退役过程中信息一致性和流程效率的关键。 随着3D模型的日益复杂化和数据量的不断增长&#xff0c;传统的PLM系统面临着数据管理和渲染效率的挑战。HOOPS Comm…

pandas dataframe 的几种过滤数据的方法

pandas dataframe简介 Pandas是一个用于数据科学的开源Python库。这个库在整个数据科学行业被广泛使用。它是一个快速和非常强大的python工具来执行数据分析。Pandas为我们提供了读取、过滤、检查、操作、分析和绘制数据的命令。它使用内置函数加载以各种文件格式存储的数据&a…

圆片/圆盘测厚设备 HW01-SG系列单点测厚仪

关键字:圆片测厚仪圆盘测厚仪, 圆形测厚仪, 单点测厚仪, 汽车工件测厚仪, 产品简介&#xff1a; 测厚仪采用上下两个对射的激光位移传感器测量圆盘状物体边缘的厚度。圆盘放置在由步进电机驱动的托盘上&#xff0c;点按测量按钮托盘旋转一周&#xff0c;可测量被测物整个圆周上…

三:哈希map day6 哈希表理论基础 ● 242.有效的字母异位词 ● 349. 两个数组的交集 ● 202. 快乐数● 1. 两数之和

今日任务 ● 哈希表理论基础 ● 242.有效的字母异位词 ● 349. 两个数组的交集 ● 202. 快乐数 ● 1. 两数之和 今日任务 242.有效的字母异位词 题目 大型概括&#xff1a;map 的使用 1.题目描述-->思路&#xff1a; 给出两个string s,t; …

开放式运动耳机哪款好用?五款高性能值得信赖产品推荐

身为户外运动的达人&#xff0c;我发现开放式运动耳机简直是咱们运动时的最佳拍档&#xff0c;不管是跑步还是健身&#xff0c;开放式运动耳机最为舒适&#xff0c;它的妙处就在于不用塞进耳朵&#xff0c;这样既安全又卫生&#xff0c;户外动起来更放心。但市面上好坏参半&…

SDL系列(一)—— 小白入门

SDL &#xff08; Simple DirectMedia Layer &#xff09; 是一套开放源代码的 跨平台多媒体开发库 &#xff0c;使用 C 语 言写成。 SDL 提供了数种控制图像、声音、输出入的函数&#xff0c;让开发者只要用相同或是相似的 代码就可以开发出 跨多个平台&#xff08; Linu…

线上3D博物馆搭建简单吗?有何优势?有哪些应用场景?

随着科技的飞速发展&#xff0c;传统的博物馆参观方式正在经历一场前所未有的变革&#xff0c;在科技的“加持”下&#xff0c;不少博物馆凭借强大的技术、创意和美学实践&#xff0c;频频“出圈”&#xff0c;线上3D博物馆逐渐崛起&#xff0c;这不仅丰富了人们的文化体验&…

量子计算机接入欧洲最快超算!芬兰加快混合架构算法开发

内容来源&#xff1a;量子前哨&#xff08;ID&#xff1a;Qforepost&#xff09; 文丨浪味仙 排版丨沛贤 深度好文&#xff1a;1900字丨7分钟阅读 摘要&#xff1a;芬兰技术研究中心&#xff08;VTT&#xff09;与 CSC 展开合作&#xff0c;基于量子计算机超算架构进行算法开…

年度更新!统信UOS服务器版V20(1070)超越期待

不负广大客户期待&#xff01; 统信UOS服务器版V20&#xff08;1070&#xff09;年度首更 功能更强大、性能更卓越、生态更丰富 助您畅享安全、便捷、高效的产品和服务 新平台&#xff0c;新生态 统信UOS服务器版始终坚持进行生态适配&#xff0c;目前已支持超过百万种兼容…

学习Nginx(五):虚拟主机配置

核心功能 在演示虚拟主机配置之前&#xff0c;来看一下Nginx配置的核心功能说明。 了解配置更多功能&#xff0c;请查看官方说明&#xff1a; http://nginx.org/en/docs/ngx_core_module.html [rootRockyLinux9 conf]# cat nginx.conf # 核心功能&#xff0c;全局配置 # 设置启…

TikTok Shop认知课 打通TK小店全流程

资料 001-先导课.mp4 002-如何用思维导图工具做课程笔记.mp4 003-TTS入驻模式.mp4 004-如何获取店铺.mp4 005-TTS店铺注册全流程,mp4 006-店铺整体运营思路.mp4 007-运营的几个误区.mp4 008-新店起店准备工作,mp4 009-规店铺风控注意事项,mp4 010-店铺基础设置之店铺…

怎么3d立面有些模型不能删除是什么原因怎么解决?---模大狮模型网

在进行3D建模和设计过程中&#xff0c;有时会遇到一些模型无法删除的情况&#xff0c;这可能会导致设计流程受阻&#xff0c;影响工作效率。本文将介绍在3D立面中遇到无法删除模型的原因以及解决方法&#xff0c;帮助您顺利解决这一问题&#xff0c;提高设计效率。 一、模型未正…

第二届视觉语音识别挑战赛 CNVSRC 2024 启动

由 NCMMSC 2024 组委会发起&#xff0c;清华大学、北京邮电大学、海天瑞声、语音之家共同主办的第二届中文连续视觉语音识别挑战赛 CNVSRC 2024 即日启动&#xff0c;诚邀参与报名。 视觉语音识别&#xff0c;也称唇语识别&#xff0c;是一项通过口唇动作来推断发音内容的技术。…

如何在华企盾DSC防泄密系统中设置文件自动加密?

在华企盾DSC系统中设置文件自动加密的过程&#xff0c;简单且用户友好&#xff0c;确保了企业数据的安全&#xff0c;同时不干扰日常工作流程。以下是设置文件自动加密的步骤&#xff1a; 系统安装与配置&#xff1a;确保华企盾DSC数据防泄密系统已经在企业的网络中正确安装和配…

【刷题(3)】双指针

一、双指针问题基础 二、283. 移动零 1 题目 2 解题思路 &#xff08;1&#xff09;遍历数组&#xff0c;把不是零的放到一个新容器中 &#xff08;2&#xff09;遍历结束后&#xff0c;在新容器末尾插入0 &#xff08;3&#xff09;注意&#xff0c;必须在不复制数组的情况…

mobarxtem应用与华为设备端口绑定技术

交换机端口绑定 华为交换机的基础配置与MOBAXTERM终端连接 实验步骤&#xff1a; 一、给每个交换机划分vlan并添加端口 1.单个vlan的划分 2.批量划分vlan 在高端交换机CE6800上批量划分连续编号的VLAN&#xff0c;本例中连续的vlan20到vlan25 [~CE6800]vlan b 20 to 25 3…

使用websocket和服务建立链接慢的原因分析

1、java 项目使用websocketHandler创建websocket服务&#xff0c;在拦截器HttpSessionHandshakeInterceptor中&#xff0c;beforeHandshake日志到的很快&#xff0c;afterHandshake很慢 建立链接一直在连接中 2、原因分析&#xff1a; 找到服务器上的进程名 jps -l 3、使用…