高阶数据结构之图

        在日常生活中,会出现很多场景,比如QQ、微信的人际关系,地图的交通、或者说微博、抖音这种,都需要图这种数据结构来记录每个账号之间的关系,每个城市之间的交通等,而本篇博客就是讲解什么是图,并且了解图的结构。

图的概念

        图是由顶点集合和顶点之间的关系组成的一种数据结构。

        可以用 G = {V,E} 表示,V表示顶点集合,E表示边的集合。

        其中顶点集合我们可以看做是二叉树中的一个个节点,因为二叉树本身就是一种特殊的图

        而边的集合可以看做是一种通路,一般记作:(x,y),其中的 x 和 y 是顶点集合中的顶点。而图又分为无向图和有向图(x,y) 是无向图的概念,即通路,表示可以从 x 到 y ,也可以从 y 到 x

Path(x,y) 则是有向图的概念,表示可以从 x 到 y,无法从 y 到 x,这是有序的一种体现,也可以看做是有方向的。

        单纯看概念是有点迷糊的,我们可以配着图来看哪些是有向图哪些是无向图。

        从图中可以看到,无向图是没有方向的,A能够到 B,那么B一定能够到A;而有向图有方向,A能够去B,但是B不一定能够去A。

        了解了图的基础概念,以及有向图无向图的概念后,我们再来了解一些其他的概念。

  • 完全图:假设无向图有 N 个顶点,而这个图中有 (N*(N-1))/2 条边时,就称这个图是完全图,而有向图中有N个顶点,而图中有  N*(N-1) 条边时,这个有向图就是完全图
  • 邻接顶点:在无向图中,(u,v) 是 u 和 v 的一条边,即 u 和 v 互为邻接顶点,并且边(u,v)依附于顶点 u 和 v ;有向图中,<u,v> 是 图中的一条边, 称 u 邻接于 v ,v 邻接自 u,边 <u,v> 与 u、v相关联
  • 顶点的度:顶点的度是指顶点 v 与和它相关联的边的条数,记作 deg(v); 有向图中,顶点的度是出度和入度之和无向图中则是等于出度和入度;出度指 v 为起点的边的条数,记作indev(v),入度则是 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 是连通的,若所有顶点都是连通的,则该图是连通图
  • 强连通图: 在有向图中,若每一对顶点 v1 到 v2 有路径,且 v2 到 v1 之间也有路径,那这个有向图是强连通图
  • 生成树: 一个连通图的最小连通子图称为该图的生成树,即 n 个顶点的连通图有 n-1 条边(生成树也许并不唯一)

        图的概念了解之后,那么图是如何存储顶点之间的关系呢?让我们继续学习。

图的存储结构

        在图中除了顶点需要存储之外,我们还需要存储顶点之间的关系,即边,而存储边一般有两种方式:邻接矩阵和邻接表。

邻接矩阵

        顶点之间要么就是连通,要么就是不连通,那么我们可以采用一个二维数组下标对应顶点,采用矩阵来表示顶点之间的关系。 

        在下面的情况下,没有权值,则1表示有连接,0表示无连接,而如果有权值,我们可以设置INT_MAX 表示无连接,其他为有连接

邻接矩阵的优缺点

  • 优点
    • 适合稠密图
    • 判断两个顶点的关系的时间是 O(1) 级别的
  • 缺点
    • 不适合稀疏图
    • 查询一个顶点的所有边的时间是 O(N) 级别的

        为什么邻接矩阵判断两个顶点的关系的时间是O(1) 级别的呢?首先我们要知道,邻接矩阵的下标就对应着顶点的下标,因此我们能够很快的从顶点得到下标,这样我们如果想要查询两个顶点的关系,只用获取下标就能够直接从矩阵中获得了。

        而为何查询一个顶点的所有边的时间是 O(N) 级别的呢?这是因为邻接矩阵下,一个顶点所对应的那一组数据中,包含所有顶点的数据,无论他们是否相连都包含,而想查询一个顶点的所有边只能一个个去遍历,就降低了效率。 

邻接矩阵的图的实现

        作为一个图,内部必然包含一个储存顶点的数组,而为了快速获取顶点的下标,我们可以用一个map进行哈希,而邻接矩阵,我们可以采用一个二维数组进行保存。

         而一个图至少有一个功能:新增边。

        新增边的功能很简单,获取到定顶点对应的下标,然后进到矩阵中间存储权值即可,如果是无向图,就需要再反过来添加一遍。

        下面给出完整代码: 

namespace matrix {

	template<class V,class W, W MAX_W = INT_MAX,bool is_direct = false>
	class Graph {
		typedef Graph<V, W, INT_MAX, is_direct> Self;
	public:
		Graph() = default;
		//初始化
		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 GetIndex(const V& v)
		{
			auto t = _indexMap.find(v);
			if (t != _indexMap.end())
			{
				return t->second;
			}
			else {
				cout << "该顶点不存在" << endl;
				return -1;
			}
		}

		void _AddEdge(const int& srci, const int& dsti, W w)
		{
			//并且存储到矩阵中
			_matrix[srci][dsti] = w;

			//如果是无向图,就需要再添加一次
			if (is_direct == false)
			{
				_matrix[dsti][srci] = w;
			}
		}

		//添加边的起点和终点,以及权值
		void AddEdge(const V& src, const V& dst, W w)
		{
			//获取两个顶点对应的下标
			size_t srci = GetIndex(src);
			size_t dsti = GetIndex(dst);
			
			_AddEdge(srci, dsti, w);
		
		}
	private:
		//一个存储顶点的数组
		vector<V> _vertexs;
		//存储顶点对应的下标
		map<V, int> _indexMap;
		//邻接矩阵,下标对应顶点
		vector<vector<W>> _matrix;
	};
};

 邻接表

        和邻接矩阵相对的是邻接表,它内部采用 数组 + 链表的方式存储数据。

 

邻接表的优缺点

  • 优点
    • 适用于稀缺图
    • 查询一个顶点的所有边的时间是 O(1) 级别的
  • 缺点
    • 不适用于稠密图

        可以看到,邻接表和邻接矩阵是相对的,互补的。

        其查询一个顶点的边的时间是 O(1) 的原因也是因为一个顶点对应只存储有关的节点。

邻接表的图的实现 

        而由于我们需要用链表存储边,因此我们需要创建一个边的结构体。

        一条边应该有它终点的下标,对应的权值,以及它的下一条边的地址。

        除了这点,其他功能和邻接矩阵并没有不同。


//邻接表的图
namespace link_table {
	template<class W>
	struct Edge {
		int _dsti;
		W _w;
		Edge<W>* next;
		Edge(const int dsti, const W w)
			:_dsti(dsti),
			_w(w),
			next(nullptr)
		{

		}
	};
	
	template<class V, class W, bool is_direct = false>
	class Graph {
	public:
		typedef Edge<W> Edge;
		//初始化
		Graph(const V* a, size_t n)
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; i++)
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;
			}
			_link_table.resize(n,nullptr);
		}

		//获取对应顶点的下标
		size_t GetIndex(const V& v)
		{
			auto t = _indexMap.find(v);
			if (t != _indexMap.end())
			{
				return t->second;
			}
			else {
				cout << "该顶点不存在" << endl;
				return -1;
			}
		}

		//添加边的起点和终点,以及权值
		void AddEdge(const V& src, const V& dst, W w)
		{
			//获取两个顶点对应的下标
			size_t srci = GetIndex(src);
			size_t dsti = GetIndex(dst);

			//并且头插到表中
			Edge* eg = new Edge(dsti, w);
			eg->next = _link_table[srci];
			_link_table[srci] = eg;

			//如果是无向图,就需要再添加一次
			if (is_direct == false)
			{
				Edge* eg = new Edge(srci, w);
				eg->next = _link_table[dsti];
				_link_table[dsti] = eg;
			}
		}

		void Print()
		{
			for (int i = 0; i < _vertexs.size(); i++)
			{
				cout << "vertexs[" << i << "]->" << _vertexs[i] << endl;
			}

			for (int i = 0; i < _link_table.size(); i++)
			{
				Edge* cur = _link_table[i];
				cout << i << "->";
				while (cur)
				{
					cout <<"[" << cur->_dsti << ": " << cur->_w <<"]" << "->";
					cur = cur->next;
				}
				cout <<"nullptr" << endl;
			}
		}

	private:
		//一个存储顶点的数组
		vector<V> _vertexs;
		//存储顶点对应的下标
		map<V, int> _indexMap;
		//邻接矩阵,下标对应顶点
		vector<Edge*> _link_table;
	};
};

        虽然邻接矩阵和邻接表是互补的,但是实际上邻接矩阵用的更多,因此下面的实现都是采用的邻接矩阵的实现。

 图的搜索

        图的搜索和树的搜索其实都是类似的,图的广度优先遍历和树的广度优先遍历其实都类似,采用队列进行遍历,但是不同的是,图有成环的问题,树没有成环问题,因此图的搜索只有队列是不行的,还需要额外的访问数组来保证一个顶点只被访问一次,而且图还有孤岛问题,也需要访问数组来保证每个顶点都被访问到。

广度优先遍历

        类似树的层序遍历,图的广度优先遍历也是先找到一个顶点,然后将和它关联的顶点全部放入队列中,并且用访问数组保存,下一次遍历就只遍历队列中的顶点。

步骤:

  1. 将BFS的起始遍历顶点放入队列中,并且标记该顶点已经被遍历过了
  2. 将起始顶点的相连顶点一一放入队列中,并且标记已经被遍历过了
  3. 一直循环上述步骤,知道所有顶点被遍历了
  4. 如果有孤岛顶点,可以再重新遍历一遍标记数组,看哪些顶点未被遍历

        因此其代码实现很简单。

		//从v开始进行广度遍历
		void BFS(const V& v)
		{
			int srci = GetIndex(v);
			
			queue<int> q;
			vector<bool> is_visited(_vertexs.size(), false);

			q.push(srci);
			is_visited[srci] = true;
			
			int levelsize = q.size();
			int n = _vertexs.size();
			//然后遍历
			while (!q.empty())
			{
				//每次只在这一层遍历
				for (int i = 0; i < levelsize; i++)
				{
					//这是这一层内的顶点
					int front = q.front();
					q.pop();
					cout << front << ": " <<_vertexs[front]<< "->";
					//此时就需要找到和这一个顶点相连的所有顶点
					for (int j = 0; j < n; j++)
					{
						//为 MAX_W 表示两个顶点没边
						if (_matrix[front][j] != MAX_W)
						{
							if (is_visited[j] == false)
							{
								cout << _vertexs[j] << "->";
								q.push(j);
								is_visited[j] = true;
							}
						}
					}
					cout <<"nullptr"<< endl;
				}
			}

			//防止孤岛问题
			for (int i = 0; i < is_visited.size(); i++)
			{
				//此时就是孤岛问题
				if (is_visited[i] == false)
				{
					BFS(_vertexs[i]);
				}
			}

		}

深度优先遍历

         深度优先遍历也很简单,类比树的深度优先遍历,主要方案采用递归,主要步骤和广度优先遍历类似。

步骤:

  1. 将起始顶点标记一下,然后去遍历寻找和起始顶点关联的顶点,找到一个就去递归并标记。
  2. 重复上面步骤,直到所有顶点遍历完
  3. 可能会有孤岛问题,因此需要在最后遍历一遍标记数组
		void _DFS(int srci, vector<bool>& is_visited)
		{
			is_visited[srci] = true;
			cout << srci << ": " << _vertexs[srci] << "->" << endl;
			int n = _vertexs.size();
			for (int i = 0; i < n; i++)
			{
				if (_matrix[srci][i] != MAX_W && !is_visited[i])
				{
					_DFS(i, is_visited);
				}
			}

		}

		//从v开始进行深度遍历
		void DFS(const V& v)
		{
			int srci = GetIndex(v);
			vector<bool> is_visited(_vertexs.size(), false);
			_DFS(srci, is_visited);

			//处理孤岛问题
			for (int i = 0; i < is_visited.size(); i++)
			{
				if (!is_visited[i])
				{
					_DFS(i, is_visited);
				}
			}
		}

         以上就是图的搜索,其实步骤和二叉树的大差不差,不过由于孤岛问题和成环问题,需要用队列和标记数组辅助进行。

最小生成树

        每一颗生成树,都是原连通图中的一个最大的无环子图,即删去一个边就不连通,引入一个新边就构成回路。

        其有三个准则:

  1. 只能用图中的边构成最小生成树
  2. 只能用 n-1 条边连接 n 个顶点
  3. 选用的 n-1 条边不能构成回路

        构成最小生产树的算法有两种:Kruskal 算法 和  Prim 算法,二者都用了逐步的贪心算法。

        注意,最小生成树一般都是在无向图中使用的。

Kruskal算法

步骤:

  1. 首先构造一个由n个顶点组成的图,该图不含任何边
  2. 从原图中选取权值最小的边(有多条边则任意选其一),如果边的顶点来自不同的连通分量,则将此边加入到图中
  3. 每次添加边的时候需要通过并查集判断是否会构成环,添加后也需要将两边的顶点加入到并查集中
  4. 重复上述操作最后生成最小生成树

        而从最小生成树的概念中有一个很重要的难题----如何不构成回路?

         其实很简单,采用并查集即可,如果构成回路了,那么两个顶点一定有相同的连通分量,换句话说,即在并查集中有相同的主节点。

        假设在 B->A->C->D 这条路径中,其在并查集中一定是都存在的,如果又添加 B->D 这条边,我们可以发现,B和D一定都有相同的主节点,这就构成了回路,因此我们可以在每次添加边的时候,都可以将其在并查集中进行 union 操作,这样就能够保证最小生成树中没有环存在。

 下面给出代码:

	//返回生成最小生成树的权值
	//该算法的思路是根据边的权值,每次取最小的权值
	//如果该权值成环就抛弃,否则就添加
	//是否成环可以采用并查集
	W Kruskal(Self& minTree)
	{
		//初始化最小生成树
		int n = _vertexs.size();

		minTree._vertexs = _vertexs;
		minTree._indexMap = _indexMap;
		minTree._matrix.resize(n);

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

		//创建优先级队列,存储所有边
		priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
		for (int i = 0; i < n; i++)
		{
			for (int j = 0; j < n; j++)
			{
				if (i < j && _matrix[i][j] != MAX_W)
				{
					Edge eg(i, j, _matrix[i][j]);
					pq.push(eg);
				}
			}
		}

		//创建并查集,要记住,最小生成树最多 n - 1 条边,size就是记录这条边是否只有 n-1条边
		DSU dsu(n);
		W total = W();
		int size = 0;
		while (!pq.empty())
		{
			Edge eg = pq.top();
			pq.pop();

			if (!dsu.InSet(eg._srci, eg._dsti))
			{
				minTree._AddEdge(eg._srci, eg._dsti, eg._w);
				dsu.Union(eg._srci, eg._dsti);	
				size++;
				total += eg._w;
			}
			else {
				cout << "成环" << endl;
			}
		}

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

Prim算法 
    

        Prim 算法的步骤和后续的最短路径算法的 Dijkstra 算法类似,可以先了解一下。

Prim算法步骤

  1. 首先构造一个由n个顶点组成的图,该图不含任何边
  2. 创建两个集合,一个表示在最小生成树的顶点集合X,一个表示不在最小生成树的顶点集合Y
  3. 将起始顶点放入X,并且获取它的所有边
  4. 从边的集合中选权值最小的边,然后获取的终点位置,将终点顶点放入X,并且获取它所有边
  5. 每次选取边的时候需要判断终点顶点是否在X中,在就会成环,不在才能添加
  6. 重复上面的操作,最终得出最小生成树

        实际上Prim算法可以看成是一颗树成长的过程,起始顶点是一颗种子,每次都去选择和这个种子相关的边,然后获取到另一个种子,然后从另一个种子中选择相关的边,最后称为最小生成树。

W Prim(Self& minTree,const V& src)
{
	int n = _vertexs.size();

	minTree._vertexs = _vertexs;
	minTree._indexMap = _indexMap;
	minTree._matrix.resize(n);

	for (int i = 0; i < n; i++)
	{
		minTree._matrix[i].resize(n, MAX_W);
	}
	
	int srci = GetIndex(src);
	//两个集合,一个是已经在最小生成树中了,一个不在最小生成树
	vector<bool> X(n, false);
	vector<bool> Y(n, true);
	
	//起始的顶点已经在最小生成树中了
	X[srci] = true;
	Y[srci] = false;
	
	//然后将 src 相连的边放入优先级队列中
	priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
	for (int i = 0; i < n; i++)
	{
		if (_matrix[srci][i] != MAX_W)
		{
			minq.push(Edge(srci, i, _matrix[srci][i]));
		}
	}

	//记录最小生成树的大小是否是 n-1,以及总权值
	size_t size = 0;
	W totalW = W();

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

		//如果队列中的边在最小生成树的集合中,那这条边就会成环
		if (X[eg._dsti])
		{
			cout << "成环 : " << eg._dsti << endl;
		}
		else {
			//此时就是正常的最小生成树了
	/*		cout << eg._srci << " " << eg._dsti << endl;*/
			minTree._AddEdge(eg._srci, eg._dsti, eg._w);
			X[eg._dsti] = true;
			Y[eg._dsti] = false;

			size++;
			totalW += eg._w;
			if (size == n - 1)
			{
				break;
			}

			//每有一个顶点加入到生成树,就需要将这个顶点相连的边放入队列中
			for (int j = 0; j < n; j++)
			{
				//和这个顶点相连的边并且不在最小生成树之中,即可加入到队列中
				if (_matrix[eg._dsti][j] != MAX_W && Y[j])
				{
					minq.push(Edge(eg._dsti, j, _matrix[eg._dsti][j]));
				}
			}
		}
		
	}
	if (size == n - 1)
	{
		return totalW;
	}
	else {
		return W();
	}
}

 最短路径算法

最短路径概念

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

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

Dijkstra 算法思路:

        首先设置两个集合:S表示在最短路径的顶点集合,Q表示不在最短路径的顶点集合,初始时,将起始顶点 s 放入S中,然后从Q 中获取 s 的起始边,这个起始边 一定是 s 的所有边中,相连代价最小的,然后将这个边的终止顶点 u 放入 S 中,并且去找 u 的所有邻接顶点 v,并且进行松弛操作

松弛操作: 即判断 s-> v 的路径长度是否比 s->u->v 的路径长度大,如果大,就说明 s->u->v 才是最短路径,需要更新。

        Dijkstra 算法是很高效的一种算法,但是它有一个缺点:不支持图中带负权的图。 

        原因在于它每次去找边,只会取权值最小的边,大的边虽然会记录,但是不会从权值大的边来找最短路径,而有可能大的边的最短路径中带有负数,总体的权值比其他的小,但是 Dijkstra 算法无法考虑这种状况,因此不支持带负权的图

        该算法虽然不支持带负权的图,不过时间复杂度是 O(N^2) ,是比较高效的算法。

        接下来我们直接看代码。

	//dist 记录从 v 顶点到其他顶点的距离
	//pPath 则记录每个顶点最短路径中的上一个顶点
	void Dijkstra(const V& v, vector<W>& dist, vector<int>& pPath)
	{
		size_t srci = GetIndex(v);
		size_t n = _vertexs.size();
		
		//初始化 dist 和 pPath
		dist.resize(n, MAX_W);
		pPath.resize(n, -1);

		//设置起始顶点的距离为0,以及起始顶点的父节点为自己,这样方便后续起始操作
		dist[srci] = 0;
		pPath[srci] = srci;

		//记录已经有最短路径的顶点
		vector<bool> S(n, false);

		for (int i = 0; i < n; i++)
		{

			//记录顶点,以及该顶点的最小权值
			int u = 0;
			W min = MAX_W;
			for (int j = 0; j < n; j++)
			{
				//如果找到的顶点没有最短路径,并且有相关联的边,就存储下来,即找到最短路径的起始顶点
				//这里也是 Dijkstra 算法的贪心策略,即找到最短路径中
				if (S[j] == false && dist[j] < min)
				{
					u = j;
					min = dist[j];
				}
			}

			S[u] = true;

			//此时已经找到了最短路径的下一个顶点了,就去找这个顶点相连的顶点,进行松弛操作
			//松弛操作: srci -> u -> v,即判断 srci->v 的路径长度和 srci->u + u->v 的路径长度的大小,找到最短的那个路径
			for (size_t v = 0; v < n; v++)
			{
				// dist[u] + _matrix[u][v] 表示 srci->u 到 u->v 的路径长度, dist[v] 表示 srci->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;
				}
			}
		}
	}

        这里的 pPath 其实类似并查集的查询方式,对应下标存储的是最短路径中,这个顶点的上一级顶点的下标,这样我们想要知道一条最短路径经历了哪些顶点,可以通过这个数组快速的找到 

        这一块是 Dijkstra 算法的贪心策略,即每次只在集合 Q 中找顶点 u,S中的顶点是已经有最短路径的顶点了,因此不用管。

        而第一次寻找可以视作是 起始顶点 v 到自身的距离,我们一般设为0,也是为了能够启动这个算法而初始化的。 

        而找到一个最小权值的边的顶点后,就将这个顶点放入S,并且找这个顶点的所有相连顶点,进行松弛操作。 

        如果是初始顶点,也可以认为是从初始顶点开始去延伸找它的邻接顶点。

        最后就能找到一条最短路径了。 

单源最短路径算法---Bellman-ford算法

        Dijkstra算法是比较高效,但是它无法解决负权的图的问题。

        而Bellman-ford 能够解决这个问题。

Bellman-ford 算法思路:

该算法通过暴力求解,即初始化完后,将所有顶点都进行松弛操作,并且重复进行N次。

        如果我们是邻接矩阵的存储方式这个算法的时间复杂度就到了 O(N^3) ,而如果是采用的邻接表,则是 O(N*E)次,E表示边的个数。

        接下来我们直接看代码,然后解释为什么需要重复进行N次松弛操作才可以。

		//该算法通过暴力算法进行求解最短路径算法
		bool BellmanFord(const V& v, vector<int>& dist, vector<int>& pPath)
		{
			size_t srci = GetIndex(v);
			size_t n = _vertexs.size();

			//初始化 dist 和 pPath
			dist.resize(n, MAX_W);
			pPath.resize(n, -1);

			//设置起始顶点的距离为0,以及起始顶点的父节点为自己,这样方便后续起始操作
			dist[srci] = W();
			for (int k = 0; k < n; k++)
			{
				//去遍历每个顶点,如果两个顶点之间有边,就去进行松弛操作
				//即 srci->j 和 srci->i->j 的距离哪个小
				for (int i = 0; i < n; i++)
				{
					for (int j = 0; j < n; j++)
					{
						//但是每次更新最短路径都有可能会影响其他路径,因此这个操作需要进行n次
						if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
						{
							dist[j] = dist[i] + _matrix[i][j];
							pPath[j] = i;
						}
					}
				}
			}
			
			//判断负权回路
			for (int i = 0; i < n; i++)
			{
				for (int j = 0; j < n; j++)
				{
					if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
					{
						return false;
					}
				}
			}
			return true;
		}

         然后我们创建一张图,来看看这张图的正常的最短路径是多少。

	void TestGraphBellmanFord()
	{
		const char* str = "syztx";
		matrix::Graph<char, int, INT_MAX, true> g(str, strlen(str));
		g.AddEdge('s', 't', 6);
		g.AddEdge('s', 'y', 7);
		g.AddEdge('y', 'z', 9);
		g.AddEdge('y', 'x', -3);
		//g.AddEdge('y', 's', 1); // 新增
		g.AddEdge('z', 's', 2);
		g.AddEdge('z', 'x', 7);
		g.AddEdge('t', 'x', 5);
		//g.AddEdge('t', 'y', -8); //更改
		g.AddEdge('t', 'y', 8);

		g.AddEdge('t', 'z', -4);
		g.AddEdge('x', 't', -2);
		vector<int> dist;
		vector<int> parentPath;
		if (g.BellmanFord('s', dist, parentPath))
			g.PrintShortPath('s', dist, parentPath);
		else
			cout << "带负权回路" << endl;
	}

        可以看到正常的最短路径,确实是求出来了。 

        

        而如果我们屏蔽了第一层for循环,然后来看最短路径,就会发现有错误。

        其他路径虽然没问题,但是 s->y->x->t->z 这条并不是最短路径,最短路径应该是 -2.

         那为什么会导致这样呢?我们可以在 Bellman 算法中加个打印。

        根据打印我们发现最短路径的计算过程是这样的。 

        从下面的动图我们可以发现,先是找到 s->t-z 这条权值为 2 的路径,然后发现 s->y->x->t 为2 的这条最短路径,但是没有去更新s->y->x->t->z 这条最短路径去。

         

        虽然打印的时候找到了 s->y->x->t->z 这条最短路径,但是权值却没有计算出来,我们可以根据动图发现,z 的路径的上一个顶点本来就是 t,而在真正的最短路径中,z 的上一个顶点也是 t,所有打印出来的时候就正好是最短路径,但实际上,s->y->x->t 这条最短路径虽然找到了,但是 z 的路径依旧是 s->t->z,并不是 s->y->x->t->z 这条最短路径。

        说了这么多,其实想说的就一句话,Bellman-ford 算法如果不进行 n 次最短路径查找,就会出现找到了最终的最短路径,但是影响了其他路径的情况。 

负权回路

        Bellman-ford 算法还有一个缺点,就是无法求出带有负权回路的图的最短路径。

        因为本身该算法就是一个暴力求解算法,而如果图带有负权回路,那么每次去查找一个最短路径时,都会找到一个更短的路径,因为每遍历一次,权值都比上一次小,这是无法解决的。

        就拿上面的例子来说,我们修改一下数据。

        我们新增一条边,更改一条边。

         图就变成了这样,其中标红的就是负权回路。

        我们发现,这条负权回路每遍历一次,权值就会变小。

        第一次:s->y->x->t = -5

        第二次:s->y->x->t->y->x->t = -18

        第三次: s->y->x->t->y->x->t->y->x->t = -21

        ..... 

        这样就没完没了了,无法找到最短路径,因为每次都是有更短路径出现,因此如果出现负权回路时,需要判断一下是否构成负权回路。

多源最短路径算法 ---Floyd-Shall算法

         该算法通过动态规划的方式来计算任意两个顶点之间的最短路径。

        Dijk 表示从 顶点 i 到顶点 j 这条路径中,经过包括 (l....k) 这个集合的0个或多个顶点的最短路径的长度。

        如果最短路径经过 k 顶点,Dijk = Dik(k-1) + Dkj(k-1);

        如果不经过则 Dijk = Dij(k-1) 。

        这就是这个算法的状态转移方程。

        接下来直接看代码。

		void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
		{
			int n = _vertexs.size();

			vvDist.resize(n);
			vvpPaht.resize(n);
			
			for (int i = 0; i < n; i++)
			{
				vvDist[i].resize(n, MAX_W);
				vvpPath[i].resize(n, -1);
			}

			for (int i = 0; i < n; i++)
			{
				for (int j = 0; j < n; j++)
				{
					if (_matrix[i][j] != MAX_W)
					{
						vvDist[i][j] = _matrix[i][j];
						vvpPath[i][j] = i;
					}
					else {
						vvpPath[i][j] = -1;
					}
					if (i == j)
					{
						vvDist[i][j] = W();
						vvpPath[i][j] = -1;
					}
				}
			}

			for (int k = 0; k < n; k++)
			{
				for (int i = 0; i < n; i++)
				{
					for (int j = 0; j < n; j++)
					{
						if (_matrix[i][k] != MAX_W && _matrix[k][j] != MAX_W
							&& _matrix[k][j] + vvDist[i][k] < vvDist[i][j])
						{
							vvDist[i][j] = _matrix[k][j] + vvDist[i][k];
							vvpPath[i][j] = vvpPath[k][j];
						}
					}
				}
			}
		}

        由于计算的是任意两个顶点之间的最短路径的长度,因此 vvDist 和 vvpPath 都是二维结构。

        vvDist 的第 i 行表示其他顶点距离下标为 i 的顶点的最短路径长度

        vvpPath 的第 i 行表示以 i 顶点为起始位置,第 j 列表示以 j 顶点为终止位置,存储的值表示以 i 顶点为起始,j 顶点为终止的最短路径,j 顶点的上一个顶点下标。 

         这里是对两个矩阵进行初始化。

        这里 k 是 i 到 j 这条路径的中间节点,如果 i 到 k 到 j 有一条路径时,就进行一个松弛操作,而如果松弛操作成功了,就修改最短路径长度,同时设置上一个顶点是哪个。 

         有一个问题是 为什么 设置 vvpPath[i][j] = vvpPath[k][j] 。

        实际上这是因为可能经过 k 这个顶点了,但是可能 k 并不是最后一个顶点。

        可能在 i->k->j 这条路径中,实际上是 i->k->z->j ,此时 j 的上一个顶点应该是 z ,因此需要这样设置。

总结

        图作为一个数据结构,是比较复杂的一类。包含顶点集合和边的集合,又分为有向图和无向图两类,图的存储方式又分为邻接矩阵和邻接表。

        而图有成环问题,因此图的遍历需要使用标记数组标记。

        而最小生成树的 Kruskal 算法和 Prim 算法,为了防止成环,Kruskal 算法每次找边的时候都会将边放入并查集进行查询,而 Prim 算法用顶点集合来判断边的终点是否已经在最小生成树中。

        最短路径算法有三种, Dijkstra算法虽然效率高,但是无法解决带负权的图,而Bellman算法虽然解决了负权的图,但是采用暴力求解方案,效率降低,而且遇到了负权回路无法解决。

        多源最短路径算法 Floyd-Shall 算法采用动规来解决任意两个顶点的最短路径算法,不过效率也低。

        最后将代码整体粘贴在这供各位学习。

#include<iostream>
#include<map>
#include<vector>
#include<queue>
#include"并查集.hpp"
using namespace std;

//邻接矩阵的图
namespace matrix {

	template<class V,class W, W MAX_W = INT_MAX,bool is_direct = false>
	class Graph {
		typedef Graph<V, W, INT_MAX, is_direct> Self;
	public:
		Graph() = default;
		//初始化
		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 GetIndex(const V& v)
		{
			auto t = _indexMap.find(v);
			if (t != _indexMap.end())
			{
				return t->second;
			}
			else {
				cout << "该顶点不存在" << endl;
				return -1;
			}
		}

		void _AddEdge(const int& srci, const int& dsti, W w)
		{
			//并且存储到矩阵中
			_matrix[srci][dsti] = w;

			//如果是无向图,就需要再添加一次
			if (is_direct == false)
			{
				_matrix[dsti][srci] = w;
			}
		}

		//添加边的起点和终点,以及权值
		void AddEdge(const V& src, const V& dst, W w)
		{
			//获取两个顶点对应的下标
			size_t srci = GetIndex(src);
			size_t dsti = GetIndex(dst);
			
			_AddEdge(srci, dsti, w);
		
		}
		//从v开始进行广度遍历
		void BFS(const V& v)
		{
			int srci = GetIndex(v);
			
			queue<int> q;
			vector<bool> is_visited(_vertexs.size(), false);

			q.push(srci);
			is_visited[srci] = true;
			
			int levelsize = q.size();
			int n = _vertexs.size();
			//然后遍历
			while (!q.empty())
			{
				//每次只在这一层遍历
				for (int i = 0; i < levelsize; i++)
				{
					//这是这一层内的顶点
					int front = q.front();
					q.pop();
					cout << front << ": " <<_vertexs[front]<< "->";
					//此时就需要找到和这一个顶点相连的所有顶点
					for (int j = 0; j < n; j++)
					{
						//为 MAX_W 表示两个顶点没边
						if (_matrix[front][j] != MAX_W)
						{
							if (is_visited[j] == false)
							{
								cout << _vertexs[j] << "->";
								q.push(j);
								is_visited[j] = true;
							}
						}
					}
					cout <<"nullptr"<< endl;
				}
			}

			//防止孤岛问题
			for (int i = 0; i < is_visited.size(); i++)
			{
				//此时就是孤岛问题
				if (is_visited[i] == false)
				{
					BFS(_vertexs[i]);
				}
			}

		}

		void _DFS(int srci, vector<bool>& is_visited)
		{
			is_visited[srci] = true;
			cout << srci << ": " << _vertexs[srci] << "->" << endl;
			int n = _vertexs.size();
			for (int i = 0; i < n; i++)
			{
				if (_matrix[srci][i] != MAX_W && !is_visited[i])
				{
					_DFS(i, is_visited);
				}
			}

		}

		//从v开始进行深度遍历
		void DFS(const V& v)
		{
			int srci = GetIndex(v);
			vector<bool> is_visited(_vertexs.size(), false);
			_DFS(srci, is_visited);

			//处理孤岛问题
			for (int i = 0; i < is_visited.size(); i++)
			{
				if (!is_visited[i])
				{
					_DFS(i, is_visited);
				}
			}
		}

		struct Edge {
			int _srci;
			int _dsti;
			W _w;
			Edge(const int srci, const int dsti, W w)
				:_srci(srci),
				_dsti(dsti),
				_w(w)
			{
			}
			//比较权值
			bool operator > (const Edge& e) const 
			{
				return _w > e._w;
			}
		};

		//返回生成最小生成树的权值
		//该算法的思路是根据边的权值,每次取最小的权值
		//如果该权值成环就抛弃,否则就添加
		//是否成环可以采用并查集
		W Kruskal(Self& minTree)
		{
			//初始化最小生成树
			int n = _vertexs.size();

			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			minTree._matrix.resize(n);

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

			//创建优先级队列,存储所有边
			priority_queue<Edge, vector<Edge>, greater<Edge>> pq;
			for (int i = 0; i < n; i++)
			{
				for (int j = 0; j < n; j++)
				{
					if (i < j && _matrix[i][j] != MAX_W)
					{
						Edge eg(i, j, _matrix[i][j]);
						pq.push(eg);
					}
				}
			}

			//创建并查集,要记住,最小生成树最多 n - 1 条边,size就是记录这条边是否只有 n-1条边
			DSU dsu(n);
			W total = W();
			int size = 0;
			while (!pq.empty())
			{
				Edge eg = pq.top();
				pq.pop();

				if (!dsu.InSet(eg._srci, eg._dsti))
				{
					minTree._AddEdge(eg._srci, eg._dsti, eg._w);
					dsu.Union(eg._srci, eg._dsti);	
					size++;
					total += eg._w;
				}
				else {
					cout << "成环" << endl;
				}
			}

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

		W Prim(Self& minTree,const V& src)
		{
			int n = _vertexs.size();

			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			minTree._matrix.resize(n);

			for (int i = 0; i < n; i++)
			{
				minTree._matrix[i].resize(n, MAX_W);
			}
			
			int srci = GetIndex(src);
			//两个集合,一个是已经在最小生成树中了,一个不在最小生成树
			vector<bool> X(n, false);
			vector<bool> Y(n, true);
			
			//起始的顶点已经在最小生成树中了
			X[srci] = true;
			Y[srci] = false;
			
			//然后将 src 相连的边放入优先级队列中
			priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
			for (int i = 0; i < n; i++)
			{
				if (_matrix[srci][i] != MAX_W)
				{
					minq.push(Edge(srci, i, _matrix[srci][i]));
				}
			}

			//记录最小生成树的大小是否是 n-1,以及总权值
			size_t size = 0;
			W totalW = W();

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

				//如果队列中的边在最小生成树的集合中,那这条边就会成环
				if (X[eg._dsti])
				{
					cout << "成环 : " << eg._dsti << endl;
				}
				else {
					//此时就是正常的最小生成树了
			/*		cout << eg._srci << " " << eg._dsti << endl;*/
					minTree._AddEdge(eg._srci, eg._dsti, eg._w);
					X[eg._dsti] = true;
					Y[eg._dsti] = false;

					size++;
					totalW += eg._w;
					if (size == n - 1)
					{
						break;
					}

					//每有一个顶点加入到生成树,就需要将这个顶点相连的边放入队列中
					for (int j = 0; j < n; j++)
					{
						//和这个顶点相连的边并且不在最小生成树之中,即可加入到队列中
						if (_matrix[eg._dsti][j] != MAX_W && Y[j])
						{
							minq.push(Edge(eg._dsti, j, _matrix[eg._dsti][j]));
						}
					}
				}
				
			}
			if (size == n - 1)
			{
				return totalW;
			}
			else {
				return W();
			}
		}

		void PrintShortPath(const V& src, const vector<W>& dist, const vector<int>& pPath)
		{
			size_t srci = GetIndex(src);
			size_t n = _vertexs.size();
			for (size_t i = 0; i < n; ++i)
			{
				if (i != srci)
				{
					// 找出i顶点的路径
					vector<int> path;
					size_t parenti = i;
					while (parenti != srci)
					{
						path.push_back(parenti);
						parenti = pPath[parenti];
					}
					path.push_back(srci);
					reverse(path.begin(), path.end());

					for (auto index : path)
					{
						cout << _vertexs[index] << "->";
					}
					cout << dist[i] << endl;
				}
			}
		}


		//void PrintShortPath(const V& src, const vector<W>& dist, const vector<int>& pPath)
		//{
		//	size_t srci = GetVertexIndex(src);
		//	size_t n = _vertexs.size();
		//	for (size_t i = 0; i < n; ++i)
		//	{
		//		if (i != srci)
		//		{
		//			// 找出i顶点的路径
		//			vector<int> path;
		//			size_t parenti = i;
		//			while (parenti != srci)
		//			{
		//				path.push_back(parenti);
		//				parenti = pPath[parenti];
		//			}
		//			path.push_back(srci);
		//			reverse(path.begin(), path.end());

		//			for (auto index : path)
		//			{
		//				cout << _vertexs[index] << "->";
		//			}
		//			cout << "权值和:" << dist[i] << endl;
		//		}
		//	}
		//}

		//dist 记录从 v 顶点到其他顶点的距离
		//pPath 则记录每个顶点最短路径中的上一个顶点
		void Dijkstra(const V& v, vector<W>& dist, vector<int>& pPath)
		{
			size_t srci = GetIndex(v);
			size_t n = _vertexs.size();
			
			//初始化 dist 和 pPath
			dist.resize(n, MAX_W);
			pPath.resize(n, -1);

			//设置起始顶点的距离为0,以及起始顶点的父节点为自己,这样方便后续起始操作
			dist[srci] = 0;
			pPath[srci] = srci;

			//记录已经有最短路径的顶点
			vector<bool> S(n, false);

			for (int i = 0; i < n; i++)
			{

				//记录顶点,以及该顶点的最小权值
				int u = 0;
				W min = MAX_W;
				for (int j = 0; j < n; j++)
				{
					//如果找到的顶点没有最短路径,并且有相关联的边,就存储下来,即找到最短路径的起始顶点
					//这里也是 Dijkstra 算法的贪心策略,即找到最短路径中
					if (S[j] == false && dist[j] < min)
					{
						u = j;
						min = dist[j];
					}
				}

				S[u] = true;

				//此时已经找到了最短路径的下一个顶点了,就去找这个顶点相连的顶点,进行松弛操作
				//松弛操作: srci -> u -> v,即判断 srci->v 的路径长度和 srci->u + u->v 的路径长度的大小,找到最短的那个路径
				for (size_t v = 0; v < n; v++)
				{
					// dist[u] + _matrix[u][v] 表示 srci->u 到 u->v 的路径长度, dist[v] 表示 srci->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;
					}
				}
			}
		}

		//该算法通过暴力算法进行求解最短路径算法
		bool BellmanFord(const V& v, vector<int>& dist, vector<int>& pPath)
		{
			size_t srci = GetIndex(v);
			size_t n = _vertexs.size();

			//初始化 dist 和 pPath
			dist.resize(n, MAX_W);
			pPath.resize(n, -1);

			//设置起始顶点的距离为0,以及起始顶点的父节点为自己,这样方便后续起始操作
			dist[srci] = W();
			for (int k = 0; k < n; k++)
			{
				//去遍历每个顶点,如果两个顶点之间有边,就去进行松弛操作
				//即 srci->j 和 srci->i->j 的距离哪个小
				for (int i = 0; i < n; i++)
				{
					for (int j = 0; j < n; j++)
					{
						//但是每次更新最短路径都有可能会影响其他路径,因此这个操作需要进行n次
						if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
						{
							cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << endl;
							dist[j] = dist[i] + _matrix[i][j];
							pPath[j] = i;
						}
					}
				}
				cout << endl;
			}
			
			//判断负权回路
			for (int i = 0; i < n; i++)
			{
				for (int j = 0; j < n; j++)
				{
					if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
					{
						return false;
					}
				}
			}
			return true;
		}
		void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
		{
			int n = _vertexs.size();

			vvDist.resize(n);
			vvpPaht.resize(n);
			
			for (int i = 0; i < n; i++)
			{
				vvDist[i].resize(n, MAX_W);
				vvpPath[i].resize(n, -1);
			}

			for (int i = 0; i < n; i++)
			{
				for (int j = 0; j < n; j++)
				{
					if (_matrix[i][j] != MAX_W)
					{
						vvDist[i][j] = _matrix[i][j];
						vvpPath[i][j] = i;
					}
					else {
						vvpPath[i][j] = -1;
					}
					if (i == j)
					{
						vvDist[i][j] = W();
						vvpPath[i][j] = -1;
					}
				}
			}

			for (int k = 0; k < n; k++)
			{
				for (int i = 0; i < n; i++)
				{
					for (int j = 0; j < n; j++)
					{
						if (_matrix[i][k] != MAX_W && _matrix[k][j] != MAX_W
							&& _matrix[k][j] + vvDist[i][k] < vvDist[i][j])
						{
							vvDist[i][j] = _matrix[k][j] + vvDist[i][k];
							vvpPath[i][j] = vvpPath[k][j];
						}
					}
				}
			}
		}
		void Print()
		{
			for (int i = 0; i < _vertexs.size(); i++)
			{
				cout << "vertexs[" << i << "]->" << _vertexs[i] << endl;
			}

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

	private:
		//一个存储顶点的数组
		vector<V> _vertexs;
		//存储顶点对应的下标
		map<V, int> _indexMap;
		//邻接矩阵,下标对应顶点
		vector<vector<W>> _matrix;
	};
};

//邻接表的图
namespace link_table {
	template<class W>
	struct Edge {
		int _dsti;
		W _w;
		Edge<W>* next;
		Edge(const int dsti, const W w)
			:_dsti(dsti),
			_w(w),
			next(nullptr)
		{

		}
	};
	
	template<class V, class W, bool is_direct = false>
	class Graph {
	public:
		typedef Edge<W> Edge;
		//初始化
		Graph(const V* a, size_t n)
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; i++)
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;
			}
			_link_table.resize(n,nullptr);
		}

		//获取对应顶点的下标
		size_t GetIndex(const V& v)
		{
			auto t = _indexMap.find(v);
			if (t != _indexMap.end())
			{
				return t->second;
			}
			else {
				cout << "该顶点不存在" << endl;
				return -1;
			}
		}

		//添加边的起点和终点,以及权值
		void AddEdge(const V& src, const V& dst, W w)
		{
			//获取两个顶点对应的下标
			size_t srci = GetIndex(src);
			size_t dsti = GetIndex(dst);

			//并且头插到表中
			Edge* eg = new Edge(dsti, w);
			eg->next = _link_table[srci];
			_link_table[srci] = eg;

			//如果是无向图,就需要再添加一次
			if (is_direct == false)
			{
				Edge* eg = new Edge(srci, w);
				eg->next = _link_table[dsti];
				_link_table[dsti] = eg;
			}
		}

		void Print()
		{
			for (int i = 0; i < _vertexs.size(); i++)
			{
				cout << "vertexs[" << i << "]->" << _vertexs[i] << endl;
			}

			for (int i = 0; i < _link_table.size(); i++)
			{
				Edge* cur = _link_table[i];
				cout << i << "->";
				while (cur)
				{
					cout <<"[" << cur->_dsti << ": " << cur->_w <<"]" << "->";
					cur = cur->next;
				}
				cout <<"nullptr" << endl;
			}
		}

	private:
		//一个存储顶点的数组
		vector<V> _vertexs;
		//存储顶点对应的下标
		map<V, int> _indexMap;
		//邻接矩阵,下标对应顶点
		vector<Edge*> _link_table;
	};
};

	void TestGraph1()
	{
		matrix::Graph<char, int, INT_MAX, false> 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.Print();
		//g.BFS('0');
		//g.DFS('0');
		matrix::Graph<char, int, INT_MAX, false> t;
		g.Kruskal(t);
		t.Print();
		matrix::Graph<char, int, INT_MAX, false> p;
		g.Prim(p, '0');
		p.Print();
	}

	void TestGraph2()
	{
		link_table::Graph<char, int, 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();
	}

	void TestGraphBellmanFord()
	{
		const char* str = "syztx";
		matrix::Graph<char, int, INT_MAX, true> g(str, strlen(str));
		g.AddEdge('s', 't', 6);
		g.AddEdge('s', 'y', 7);
		g.AddEdge('y', 'z', 9);
		g.AddEdge('y', 'x', -3);
		g.AddEdge('y', 's', 1); // 新增
		g.AddEdge('z', 's', 2);
		g.AddEdge('z', 'x', 7);
		g.AddEdge('t', 'x', 5);
		g.AddEdge('t', 'y', -8); //更改
		//g.AddEdge('t', 'y', 8);

		g.AddEdge('t', 'z', -4);
		g.AddEdge('x', 't', -2);
		vector<int> dist;
		vector<int> parentPath;
		if (g.BellmanFord('s', dist, parentPath))
			g.PrintShortPath('s', dist, parentPath);
		else
			cout << "带负权回路" << endl;
	}

int main()
{
	TestGraphBellmanFord();
	return 0;
}

        以上就是本篇博客的总结,相信能够为各位的学习提供帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值