【C++】深入浅出“图”——图的遍历与最小生成树算法


各位读者大佬好,我是落羽!一个坚持不断学习进步的学生。
如果您觉得我的文章还不错,欢迎多多三连分享交流,一起学习进步!

欢迎关注我的blog主页: 落羽的落羽

文章目录

一、图的遍历

遍历一个图,针对的是遍历所有顶点 。主要是两种思路:广度优先(BFS)和深度优先(DFS)

上一篇文章讲过了图可以用领接矩阵和领接表存储边,我们以领接矩阵的模版进行讲解

1. BFS

图的广度优先遍历思路是:从起始顶点出发,先访问当前顶点的所有直接邻接顶点(一层),再依次访问这些邻接顶点的邻接节点(下一层),以此类推,直到遍历完所有可达顶点。

曾经我们讲二叉树的广度优先遍历时,是利用了队列结构,这里也是一样的。每次队头元素出队列时,队头元素顶点的所有领接顶点全部入队列。为了防止一个顶点多次遍历,还需要一个数组用于标记。

cpp 复制代码
// 参数是遍历的起始顶点
void BFS(const V& src)
{
	// 得到起始顶点的下标
	size_t srcindex = GetVertexIndex(src);
	// 防止一个顶点被多次遍历,用一个数组标记被遍历过的下标
	vector<bool> visited;
	visited.resize(_vertexs.size(), false);

	// 起点入队列
	queue<int> q;
	q.push(srcindex);
	visited[srcindex] = true;

	cout << "BFS遍历: ";
	while (!q.empty())
	{
		size_t front = q.front();
		// 打印出当前遍历顶点
		cout << _vertexs[front] << ' ';

		// 队头元素出队列
		q.pop();

		// 队头元素顶点所有没遍历过的相邻顶点入队列,在领接矩阵中查询相邻顶点
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			if (visited[i] == false && _matrix[front][i] != MAX_W)
			{
				// 遍历过的顶点标记为true
				visited[i] = true;
				q.push(i);
			}
		}
		
	}
	
	// 如果该图不是连通图,这种方法会使某些顶点没遍历到
	for (bool check : visited)
	{
		if (check == false)
		{
			cout << "该图不是连通图,还有未遍历到的顶点";
		}
	}
	cout << endl;
}

2. DFS

图的深度优先遍历核心思想是 "一条路走到黑":从起始顶点出发,沿着一条路径尽可能深地探索,直到无法继续(遇到已访问节点或无邻接顶点),再回溯到上一个顶点,继续探索其他未走的分支。为了防止一个顶点多次遍历,也需要一个数组用于标记。

cpp 复制代码
void _DFS(size_t srcIndex, vector<bool>& visited)
{
	// 当前遍历顶点
	cout << _vertexs[srcIndex] << ' ';
	visited[srcIndex] = true;

	// 找srcIndex的相邻顶点,遍历下去
	for (size_t i = 0; i < _vertexs.size(); ++i)
	{
		if (visited[i] == false && _matrix[srcIndex][i] != MAX_W)
		{
			_DFS(i, visited);
		}
	}
}

void DFS(const V& src)
{
	// 得到起始顶点的下标
	size_t srcindex = GetVertexIndex(src);

	// 防止一个顶点被多次遍历,用一个数组标记被遍历过的下标
	vector<bool> visited;
	visited.resize(_vertexs.size(), false);

	cout << "DFS遍历: ";
	_DFS(srcindex, visited);
	cout << endl;
}

3. 测试

我们用这张图进行测试:

完整代码:

cpp 复制代码
#pragma once
#include<iostream>
#include<vector>
#include<map>
#include<queue>
using namespace std;

// 邻接矩阵 图
namespace Matrix
{
	// V顶点类型 W边权值类型 MAX_W表示边不存在的值 Direction表示图是否有向
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph
	{
	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;
			}

			// MAX_W 作为不存在边的标识值
			// 初始化时默认没有边,边需要一条一条手动添加,用AddEdge函数
			_matrix.resize(n);
			for (auto& e : _matrix)
			{
				e.resize(n, MAX_W);
			}
		}

		// 找到一个顶点的映射下标
		size_t GetVertexIndex(const V& v)
		{
			auto ret = _vIndexMap.find(v);
			if (ret != _vIndexMap.end())
			{
				return ret->second;
			}
			else
			{
				throw invalid_argument("不存在的顶点");
				return -1;
			}
		}

		// 添加一条边,src和dst代表两端顶点,w是权值
		void AddEdge(const V& src, const V& dst, const W& w)
		{
			size_t srci = GetVertexIndex(src);
			size_t dsti = GetVertexIndex(dst);

			_matrix[srci][dsti] = w;
			//如果是无向图,则[dsti][srci]也需添加边
			if (Direction == false)
			{
				_matrix[dsti][srci] = w;
			}
		}

		// 参数是遍历的起始顶点
		void BFS(const V& src)
		{
			// 得到起始顶点的下标
			size_t srcindex = GetVertexIndex(src);
			// 防止一个顶点被多次遍历,用一个数组标记被遍历过的下标
			vector<bool> visited;
			visited.resize(_vertexs.size(), false);

			// 起点入队列
			queue<int> q;
			q.push(srcindex);
			visited[srcindex] = true;

			cout << "BFS遍历: ";
			while (!q.empty())
			{
				size_t front = q.front();
				// 打印出当前遍历顶点
				cout << _vertexs[front] << ' ';

				// 队头元素出队列
				q.pop();

				// 队头元素顶点所有没遍历过的相邻顶点入队列,在领接矩阵中查询相邻顶点
				for (size_t i = 0; i < _vertexs.size(); ++i)
				{
					if (visited[i] == false && _matrix[front][i] != MAX_W)
					{
						// 遍历过的顶点标记为true
						visited[i] = true;
						q.push(i);
					}
				}
				
			}

			// 如果该图不是连通图,这种方法会使某些顶点没遍历到
			for (bool check : visited)
			{
				if (check == false)
				{
					cout << "该图不是连通图,还有未遍历到的顶点";
				}
			}
			cout << endl;
		}


		void _DFS(size_t srcIndex, vector<bool>& visited)
		{
			// 当前遍历顶点
			cout << _vertexs[srcIndex] << ' ';
			visited[srcIndex] = true;

			// 找srcIndex的相邻顶点,遍历下去
			for (size_t i = 0; i < _vertexs.size(); ++i)
			{
				if (visited[i] == false && _matrix[srcIndex][i] != MAX_W)
				{
					_DFS(i, visited);
				}
			}
		}

		void DFS(const V& src)
		{
			// 得到起始顶点的下标
			size_t srcindex = GetVertexIndex(src);

			// 防止一个顶点被多次遍历,用一个数组标记被遍历过的下标
			vector<bool> visited;
			visited.resize(_vertexs.size(), false);

			cout << "DFS遍历: ";
			_DFS(srcindex, visited);
			cout << endl;
		}

	private:
		map<V, size_t> _vIndexMap;   // 每个顶点映射一个下标
		vector<V> _vertexs;			 // 顶点集合
		vector<vector<W>> _matrix;   // 领接矩阵 存储边
	};

}

int main()
{
	char arr[] = {'C','A','D','B','E'};
	Matrix::Graph<char, int> graph(arr, sizeof(arr)/sizeof(char));
	// 添加边,权值不用管随便写的
	graph.AddEdge('A', 'D', 1);
	graph.AddEdge('D', 'B', 2);
	graph.AddEdge('D', 'E', 3);
	graph.AddEdge('B', 'E', 4);
	graph.AddEdge('B', 'C', 5);

	graph.BFS('A');
	graph.BFS('B');

	graph.DFS('A');
	graph.DFS('B');

	return 0;
}

结果分析,符合BFS与DFS的规则:

二、图的最小生成树算法

连通图的每一棵生成树,都是原图的一个极大无环子图。最小生成树,就是指所有边的权值加起来总权最小的生成树 ,可以理解为用最小的成本构成的生成树。

最小生成树也是生成树,要符合:

  • 要包括原图的所有顶点,只能使用原图中的边来构造
  • 只能使用恰好n-1条边来连接图中n个顶点
  • 选择的n-1条边不能构成回路
  • 边的总权值要最小

构造最小生成树一般有两种算法:克鲁斯卡尔(Kruskal)算法、普里姆(Prim)算法,都是用了逐步求解的贪心策略。

1. Kruskal算法

这种算法的思路是"从小到大选边":将所有边按权值从小到大排序,依次选择最小的边,若这条边连接的两个顶点不在同一个已连通集合中,就将这条边加入生成树;否则跳过,避免形成环。重复此过程,直到选够n−1条边。

判断两个顶点是否在一个已连通集合,可以利用并查集!详见:并查集的原理与使用

cpp 复制代码
typedef Graph<V, W, MAX_W, Direction> Self;
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;
	}
};

Graph() = default;

// 传递一个图,作为构造最小生成树的结果。返回总权值
W Kruskal(Self& minTree)
{
	// 所有顶点拷贝,初始不带任何边
	minTree._vertexs = _vertexs;
	minTree._vIndexMap = _vIndexMap;
	minTree._matrix.resize(_vertexs.size());
	for (auto& e : minTree._matrix)
	{
		e.resize(_vertexs.size(), MAX_W);
	}

	// priority_queue用于按照权值排序边
	priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
	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)
			{
				pq.push(Edge(i, j, _matrix[i][j]));
			}
		}
	}

	// 记录总权值
	W total = W();

	// 贪心算法,从最小的边开始选,将选出的边两端顶点放入一个集合
	// size记录已选出边数
	int size = 0;

	UnionFindSet ufs(_vertexs.size());

	while (!pq.empty())
	{
		Edge min = pq.top();
		pq.pop();
		// 边两端顶点不在一个集合,说明不会构成环,则添加这条边到最小生成树,两个顶点放到一个集合
		if (ufs.FindRoot(min._srci) != ufs.FindRoot(min._dsti))
		{
			minTree.AddEdge(min._srci, min._dsti, min._w);
			total += min._w;
			size++;

			ufs.Union(min._srci, min._dsti);
		}
	}

	// 若size不等于n-1,说明构建最小生成树失败,返回一个默认值W()
	if (size == _vertexs.size() - 1)
	{
		return total;
	}
	else
	{
		return W();
	}
}

2. Prim算法

Prim算法,是按点贪心:X集合存放已连入生成树的点,Y集合存放未连入生成树的点。一开始所有顶点都在Y中,首先将参数起点放入X并从Y中删除。从X中所有点连出的边中选出"权最小的且有一端顶点在Y中的边",插入到最小生成树中,再把这条边的端点放入X中并从Y中删除。如此循环往复,直到所有顶点都在X中。

这种算法天然避免了环的发生!

cpp 复制代码
// 给一个起点
W Prim(Self& minTree, const V& src)
{
	size_t srci = GetVertexIndex(src);
	size_t n = _vertexs.size();

	minTree._vertexs = _vertexs;
	minTree._vIndexMap = _vIndexMap;
	minTree._matrix.resize(n);
	for (size_t i = 0; i < n; ++i)
	{
		minTree._matrix[i].resize(n, MAX_W);
	}

	// X和Y集合
	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;
	// 先把srci连接的边添加到队列中
	for (size_t i = 0; i < n; ++i)
	{
		if (_matrix[srci][i] != MAX_W)
		{
			minq.push(Edge(srci, i, _matrix[srci][i]));
		}
	}

	size_t size = 0;
	W total = W();

	while (!minq.empty())
	{
		Edge min = minq.top();
		minq.pop();

		if (!X[min._dsti])
		{
			minTree.AddEdge(min._srci, min._dsti, min._w);
			
			X[min._dsti] = true;
			Y[min._dsti] = false;
			++size;
			total += 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)
	{
		return total;
	}
	else
	{
		return W();
	}
}

本篇完,感谢阅读

相关推荐
极地星光2 小时前
VMware+Ubuntu+LVM 虚拟机存储扩容全流程(解决分区/空间不识别问题)
linux·运维·ubuntu
独自归家的兔2 小时前
Qwen3-Omni-Captioner:通义千问 3-Omni 基座的智能音频描述开源模型
人工智能·语音识别
yesyesyoucan2 小时前
AI证件照生成技术全解析:人脸识别、背景分割与格式合规性实现方案
人工智能·考研·高考
FL16238631292 小时前
[C#][winform]基于yolov11的齿轮缺陷检测系统C#源码+onnx模型+评估指标曲线+精美GUI界面
人工智能·yolo
txp玩Linux2 小时前
rk3568上webrtc处理稳态噪声实践
算法·webrtc
却道天凉_好个秋2 小时前
OpenCV(四十三):分水岭法
人工智能·opencv·计算机视觉·图像分割·分水岭法
CoovallyAIHub2 小时前
从空地对抗到空战:首个无人机间追踪百万级基准与时空语义基线MambaSTS深度解析
深度学习·算法·计算机视觉
"YOUDIG"2 小时前
从算法到3D美学——一站式生成个性化手办风格照片
算法·3d
HalvmånEver2 小时前
Linux:库制作与原理(二)
linux·运维·服务器