C++篇(21)图

1、图的基本概念

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

注意:树是一种特殊(无环、连通)的图,图不一定是树。树关注的是结点(顶点)中存的值,图关注的是顶点及边的权值。

在有向图中,顶点对<x, y>是有序的,称为顶点x到顶点y的一条边,<x, y>和<y, x>是两条不同的边。在无向图中,顶点对(x, y)是无序的,(x, y)和(y, x)是同一条边。

完全图:在有n个顶点的无向图中,若有n*(n-1)/2 条边(即任意两个顶点之间有且仅有一条边),称该图为无向完全图 ;在有n个顶点的有向图中,若有n*(n-1) 条边(即任意两个顶点之间有且仅有方向相反的边),称该图为有向完全图 。总之,任意两个顶点之间都直接相连的图称为完全图

顶点的度:顶点V的度是指与它相关联的边的条数,记作deg(V)。在有向图中,顶点的度等于入度(以V为终点的有向边的条数,记作indeg(V))与出度(以V为起始点的有向边的条数,记作outdeg(V))之和。对于无向图而言,顶点的度等于该顶点的入度,也等于该顶点的出度。

路径:从顶点Vi出发,有一组边可使其到达顶点Vj,则称顶点Vi到Vj的顶点序列称为路径。对于不带权的图,一条路径的路径长度是指该路径上边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和。

若路径上各顶点V1,V2,V3,...,Vm均不重复,称这样的路径为简单路径 。若路径上第一个顶点V1与最后一个顶点Vm重合,称这样的路径为回路/环

连通图:在无向图 中,若从顶点V1到顶点V2有路径,则称顶点V1与顶点V2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图

强连通图:在有向图中,若在每一对顶点Vi与顶点Vj之间都存在一条从Vi到Vj的路径,也存在一条从Vj到Vi的路径,则称此图为强连通图

生成树:一个连通图的最小连通子图 称为该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。

2、图的存储结构

在图的存储中,只需要保存结点和边的关系即可。结点的保存很简单,只需要一段连续的空间即可,关键是边的关系该如何保存呢?

或许有人会这么想,用vector<pair<V, V>> edges来存储边的关系。但是这样做又会面临一个问题,如何判断两个顶点是否相连?相连的话权值又是多少?所以我们引入了下面两种方法:

2.1 邻接矩阵

因为结点与结点之间的关系就是连通与否,即为0或1,所以邻接矩阵(二维数组)就是先用一个数组将节点保存,然后采用矩阵来表示节点与节点之间的关系。

无向图的邻接矩阵是对称的,有向图的邻接矩阵不一定是对称的。

如果边带有权值并且两个节点之间是连通的,上面的边关系就可以用权值代替。如果两个顶点不同,则使用无穷大来代替。

邻接矩阵的存储方式非常适合稠密图,能O(1)地判断两个顶点的连接关系并取到权值。但是相对而言不适合查找一个顶点连接的所有边(因为时间复杂度为O(N))

cpp 复制代码
namespace matrix
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
	class Graph
	{
	public:
		Graph(const V* a, size_t n)
		{
			_vertexs.reserve(n);
			for (size_t i = 0;i < n;i++)
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;
			}

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

		size_t GetVertexIndex(const V& v)
		{
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				//assert(false);
				throw invalid_argument("顶点不存在");
				return -1;
			}
		}

		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;
			// 无向图
			if (Direction == false)
			{
				_matrix[dsti][srci] = w;
			}
		}

		void Print()
		{
			//顶点
			for (size_t i = 0;i < _vertexs.size();i++)
			{
				cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;

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

	private:
		vector<V> _vertexs;         // 顶点集合
		map<V, int> _indexMap;      // 顶点映射下标
		vector<vector<W>> _matrix;  // 邻接矩阵
	};


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

		g.Print();
	}
}

2.2 邻接表

使用数组表示顶点的集合,使用链表表示边的关系

一般情况下,有向图存储一个出边表即可。

邻接表适合存储稀疏图,适合查找一个顶点连接出去的边;不适合确定两个顶点是否相连以及边的权值。

cpp 复制代码
namespace link_table
{
	template<class W>
	struct Edge
	{
		//int _srci;
		int _dsti;   // 目标点的下标
		W _w;        // 权值
		Edge<W>* _next;

		Edge(int dsti, const W& w)
			:_dsti(dsti)
			,_w(w)
			,_next(nullptr)
		{ }
	};

	template<class V, class W, bool Direction = false>
	class Graph
	{
		typedef Edge<W> Edge;
	public:
		Graph(const V* a, size_t n)
		{
			_vertexs.reserve(n);
			for (size_t i = 0;i < n;i++)
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;
			}

			_tables.resize(n, nullptr);
		}

		size_t GetVertexIndex(const V& v)
		{
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				//assert(false);
				throw invalid_argument("顶点不存在");
				return -1;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			size_t srci = GetVertexIndex(src);
			size_t dsti = GetVertexIndex(dst);
			 
			// 1->2
			Edge* eg = new Edge(dsti, w);
			eg->_next = _tables[srci];
			_tables[srci] = eg;

			// 2->1
			if (Direction == false)
			{
				Edge* eg = new Edge(srci, w);
				eg->_next = _tables[dsti];
				_tables[dsti] = eg;
			}
		}

		void Print()
		{
			//顶点
			for (size_t i = 0;i < _vertexs.size();i++)
			{
				cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;

			for (size_t i = 0;i < _tables.size();++i)
			{
				cout << _vertexs[i] << "[" << i << "]->";
				Edge* cur = _tables[i];
				while (cur)
				{
					cout << _vertexs[cur->_dsti] << "[" << cur->_dsti << "]" << cur->_w << "->";
					cur = cur->_next;
				}
				cout << "nullptr" << endl;
			}
		}

	private:
		vector<V> _vertexs;         // 顶点集合
		map<V, int> _indexMap;      // 顶点映射下标
		vector<Edge*> _tables;      // 邻接表
	};


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

		g1.Print();
	}
}

3、图的遍历

遍历即对结点进行某种操作。给定一个图G和其中任意一个顶点V0,从V0出发,沿着图中各边访问图中的所有顶点,且每个顶点仅被遍历一次。

3.1 广度优先遍历

广度优先遍历在初阶数据结构部分讲过了,重点就是借助队列这个数据结构。那么,如何处理节点会被重复遍历的问题?我们可以用一个容器负责标记,只要入队列就标记一下,比如A出队列,入BCD。B出队列入ACE,当我们入队列就标记的话,A和C就不会重复入队列了。

cpp 复制代码
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;

	size_t n = _vertexs.size();

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

3.2 深度优先遍历

cpp 复制代码
void _DFS(size_t srci, vector<bool>& visited)
{
	cout << srci << ":" << _vertexs[srci] << endl;
	visited[srci] = true;

	// 找一个srci相邻的没有访问过的点,往深度遍历
	for (size_t i = 0;i < _vertexs.size();++i)
	{
		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);
}

4、最小生成树

在前面我们介绍了连通图和生成树的概念,生成树必含n个顶点和n-1条边。因此,最小生成树就是让构成生成树的这些边加起来的权值是最小的,也就是用最小的成本让这n个顶点连通。

构成最小生成树有两个很著名的方法:Kruskal算法Prim算法 ,这两个算法都采用了逐步求解的贪心策略

贪心算法:在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的选择,而是某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解。

4.1 Kruskal算法

不断从边的集合中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,就将这条边加入到图中。

判断是否构成回路的核心------并查集。

cpp 复制代码
typedef Graph<V, W, MAX_W, Direction> Self;

void _AddEdge(size_t srci, size_t dsti, const W& w)
{
	_matrix[srci][dsti] = w;
	// 无向图
	if (Direction == false)
	{
		_matrix[dsti][srci] = w;
	}
}

struct Edge
{
	size_t _srci;
	size_t _dsti;
	W _w;

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

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

W Kruskal(Self& mintree)
{
	priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
	size_t n = _vertexs.size();
	for (size_t i = 0;i < n;i++)
	{
		for (size_t j = 0;j < n;j++)
		{
			if (i < j && _matrix[i][j] != MAX_W)
			{
				minque.push(Edge(i, j, _matrix[i][j]));
			}
		}
	}

	// 选出n-1条边
	int size = 0;
	W totalW = W();
	UnionFindSet ufs(n);
	while (!minque.empty())
	{
		Edge min = minque.top();
		minque.pop();

		if (!ufs.InSet(min._srci, min._dsti))
		{
			mintree._AddEdge(min._srci, min._dsti, min._w);
			ufs.Union(min._srci, min._dsti);
			++size;
			totalW += min._w;
		}
	}

	if (size == n - 1)
	{
		return totalW;
	}
	else
	{
		return W();
	}
}

4.2 Prim算法

Prim算法的原理与Dijkstra最短路径算法类似(该算法在下一节中讨论)。从任意一个节点开始,每一步选择一条轻量级的边加入到集合A中。

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

	/*set<int> X;
	set<int> Y;
	X.insert(srci);
	for (size_t i = 0;i < n;i++)
	{
		if (i != srci)
		{
			Y.insert(i);
		}
	}*/

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

	size_t size = 0;
	W totalW = W();
	while (!minque.empty())
	{
		Edge min = minque.top();
		minque.pop();

		// 最小边的目标点也在X集合,则构成环
		if (X[min._dsti])
		{
			cout << "构成环" << endl;
		}
		else
		{
			minTree._AddEdge(min._srci, min._dsti, min._w);
			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])
				{
					minque.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
				}
			}
		}
	}

	if (size == n - 1)
	{
		return totalW;
	}
	else
	{
		return W();
	}
}

5、最短路径

最短路径问题:从带权有向图G中的某一顶点出发,找出一条通往另一顶点的最短路径,也就是沿路径各边的权值总和达到最小。

5.1 单源最短路径------Dijkstra算法

Dijkstra算法适用于解决带权的有向图上的单源最短路径问题,同时该算法要求图中所有边的权值非负(如果有负权路径,则可能找不到一些路径的最短路径)

cpp 复制代码
void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
{
	size_t srci = GetVertexIndex(src);
	size_t n = _vertexs.size();
	dist.resize(n, MAX_W);
	pPath.resize(n, -1);

	dist[srci] = W();
	pPath[srci] = srci;

	// 已经确定最短路径的顶点集合
	vector<bool> S(n, false);

	for (size_t j = 0;j < n;j++)
	{
		// 选最短路径顶点且不在S更新其他路径
		int u = srci;
		W min = MAX_W;
		for (size_t i = 0;i < n;i++)
		{
			if (S[i] == false && dist[i] < min)
			{
				u = i;
				min = dist[i];
			}
		}

		S[u] = true;
		// 松弛更新u连接的顶点  srci->u + u->v < srci->v  更新
		for (size_t v = 0;v < n;v++)
		{
			if (S[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v])
			{
				dist[v] = dist[u] + _matrix[u][v];
				pPath[v] = u;
			}
		}
	}
}

5.2 单源最短路径------Bellman-Ford算法

之前提到过,Dijkstra算法只能用来解决正权图的单源最短路径问题,而Bellman-Ford算法可以解决负权图的单源最短路径问题。它的优点是可以解决有负权边的单源最短路径问题,而且可以判断是否有负权回路。但是它也有明显的缺点,时间复杂度为O(N*E),其中N为顶点,E为边数。时间复杂度是普遍高于Dijkstra算法(O(N^2))的。

cpp 复制代码
bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
{
	size_t srci = GetVertexIndex(src);
	size_t n = _vertexs.size();

	dist.resize(n, MAX_W);
	pPath.resize(n, -1);

	dist[srci] = W();

	// 总体最多更新n轮
	for (size_t k = 0;k < n;k++)
	{
		bool update = false;
		// i->j更新松弛
		for (size_t i = 0;i < n;i++)
		{
			for (size_t j = 0;j < n;j++)
			{
				// srci->i + i->j  <  srci->j
				if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
				{
					update = true;
					dist[j] = dist[i] + _matrix[i][j];
					pPath[j] = i;
				}
			}
		}

		if (update == false)
		{
			break;
		}
	}
	
	// 若果还能更新,就是带负权回路
	for (size_t i = 0;i < n;i++)
	{
		for (size_t j = 0;j < n;j++)
		{
			if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
			{
				return false;
			}
		}
	}

	return true;
}

5.3 多源最短路径------Floyd-Warshall算法

Floyd-Warshall算法是解决任意两点之间的最短路径 的一种算法,算法原理是动态规划

cpp 复制代码
void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
{
	size_t n = _vertexs.size();
	vvDist.resize(n);
	vvpPath.resize(n);

	// 初始化权值和路径矩阵
	for (size_t i = 0;i < n;i++)
	{
		vvDist[i].resize(n, MAX_W);
		vvpPath[i].resize(n, -1);
	}

	// 直接相连的边更新一下
	for (size_t i = 0;i < n;i++)
	{
		for (size_t j = 0;j < n;j++)
		{
			if (_matrix[i][j] != MAX_W)
			{
				vvDist[i][j] = _matrix[i][j];
				vvpPath[i][j] = i;
			}

			if (i == j)
			{
				vvDist[i][j] = W();
			}
		}
	}

	// 最短路径的更新  i->{其他顶点}->j
	for (size_t k = 0;k < n;k++)
	{
		for (size_t i = 0;i < n;i++)
		{
			for (size_t j = 0;j < n;j++)
			{
				// k作为中间点尝试去更新i->j的路径
				if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W && vvDist[i][k] + vvDist[k][j] < vvDist[i][j])
				{
					vvDist[i][j] = vvDist[i][k] + vvDist[k][j];
					vvpPath[i][j] = vvpPath[k][j];
				}
			}
		}
	}
}
相关推荐
星轨初途40 分钟前
C++入门(算法竞赛类)
c++·经验分享·笔记·算法
Bona Sun1 小时前
单片机手搓掌上游戏机(十三)—pico运行fc模拟器之硬件准备
c语言·c++·单片机·游戏机
Bona Sun1 小时前
单片机手搓掌上游戏机(十八)—pico运行fc模拟器之更大屏幕
c语言·c++·单片机·游戏机
chenyuhao20241 小时前
MySQL索引特性
开发语言·数据库·c++·后端·mysql
没书读了2 小时前
数据结构-考前记忆清单
数据结构
灰灰勇闯IT2 小时前
KMP算法在鸿蒙系统中的应用:从字符串匹配到高效系统级开发(附实战代码)
算法·华为·harmonyos
小龙报2 小时前
【算法通关指南:数据结构和算法篇 】队列相关算法题:3.海港
数据结构·c++·算法·贪心算法·创业创新·学习方法·visual studio
csuzhucong2 小时前
一阶魔方、一阶金字塔魔方、一阶五魔方
算法
五花就是菜2 小时前
P12906 [NERC 2020] Guide 题解
算法·深度优先·图论