【C++从0到王者】第四十八站:最短路径

news2024/11/13 15:02:41

文章目录

  • 一、最短路径
  • 二、单源最短路径 -- Dijkstra算法
    • 1.单源最短路径问题
    • 2.算法思想
    • 3.代码实现
    • 4.负权值带来的问题
  • 三、单源最短路径 -- Bellman-Ford算法
    • 1.算法思想
    • 2.算法实现
    • 3.SPFA优化
    • 4.负权回路
  • 四、多源最短路径 -- Floyd-Warshall算法
    • 1.算法思想
    • 2.算法实现

一、最短路径

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

一般来说,最短路径都是针对于有向图的,但是对于无向图也是可以的!

二、单源最短路径 – Dijkstra算法

1.单源最短路径问题

单源最短路径问题:给定一个图G = ( V , E ) ,求源结点s ∈ V到图中每个结点v ∈ V的最短路径。Dijkstra算法就适用于解决带权重的有向图上的单源最短路径问题,同时算法要求图中所有边的权重非负。一般在求解最短路径的时候都是已知一个起点和一个终点,所以使用Dijkstra算法求解过后也就得到了所需起点到终点的最短路径。

2.算法思想

针对一个带权有向图G,将所有结点分为两组S和Q,S是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点s放入,毕竟源节点到自己的代价是0),Q 为其余未确定最短路径的结点集合,每次从Q 中找出一个起点到该结点代价最小的结点u ,将u 从Q 中移出,并放入S中,对u 的每一个相邻结点v进行松弛操作。松弛即对每一个相邻结点v ,判断源节点s到结点u的代价与u 到v 的代价之和是否比原来s 到v 的代价更小,若代价比原来小则要将s 到v 的代价更新为s 到u 与u 到v 的代价之和,否则维持原样。如此一直循环直至集合Q 为空,即所有节点都已经查找过一遍并确定了最短路径,至于一些起点到达不了的结点在算法循环后其代价仍为初始设定的值,不发生变化。Dijkstra算法每次都是选择V-S中最小的路径节点来进行更新,并加入S中,所以该算法使用的是贪心策略

注意:Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径。

image-20240222011135364

3.代码实现

namespace matrix
{
	//V代表顶点, W是weight代表权值,MAX_W代表权值的最大值,Direction代表是有向图还是无向图,flase表示无向
	template<class V, class W, W Max_W = INT_MAX, bool Direction = false>
	class Graph
	{
		typedef Graph<V, W, Max_W, Direction> Self;
	public:
		Graph() = default;
		//图的创建
		//1. IO输入 不方便测试
		//2. 图结构关系写到文件,读取文件
		//3. 手动添加边
		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 < _matrix.size(); i++)
			{
				_matrix[i].resize(n, Max_W);
			}
		}
		size_t GetVertexIndex(const V& v)
		{
			//return _indexMap[v];
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				//assert(false)
				throw invalid_argument("顶点不存在");
				return -1;
			}
		}
		void _AddEdge(size_t srci, size_t dsti, const W& w)
		{
			_matrix[srci][dsti] = w;
			if (Direction == false)
			{
				_matrix[dsti][srci] = w;
			}
		}
		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 << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;

			cout << "   ";
			for (int i = 0; i < _vertexs.size(); i++)
			{
				//cout << _vertexs[i] << " ";
				printf("%-3d ", i);
			}
			cout << endl;
			for (size_t i = 0; i < _matrix.size(); i++)
			{
				//cout << _vertexs[i] << " ";
				printf("%d ", i);
				for (size_t j = 0; j < _matrix[i].size(); j++)
				{
					if (_matrix[i][j] == INT_MAX)
					{
						cout << " *  ";
					}
					else
					{
						printf(" %d  ", _matrix[i][j]);
						//cout << _matrix[i][j] << " ";
					}
				}
				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 BFS(const V& src)
		{
			int srci = GetVertexIndex(src);
			queue<int> q; //广度遍历的队列
			vector<bool> visited(_vertexs.size(), false); //标记数组
			q.push(srci); //起点入队
			visited[srci] = true; //已经被遍历过了
			while (!q.empty())
			{
				int front = q.front();
				q.pop();
				cout << front << ":" << _vertexs[front] << endl;
				//把front顶点的邻接顶点入队列
				for (size_t i = 0; i < _matrix[front].size(); i++)
				{
					if (_matrix[front][i] != Max_W)
					{
						if (visited[i] == false)
						{
							q.push(i);
							visited[i] = true;
						}
					}
				}
			}
		} 

		void BFSLevel(const V& src)
		{
			int srci = GetVertexIndex(src);
			queue<int> q; //广度遍历的队列
			vector<bool> visited(_vertexs.size(), false); //标记数组
			q.push(srci); //起点入队
			visited[srci] = true; //已经被遍历过了
			int levelSize = 1;
			while (!q.empty())
			{
				for (int i = 0; i < levelSize; i++)
				{
					int front = q.front();
					q.pop();
					cout << front << ":" << _vertexs[front] << " ";
					//把front顶点的邻接顶点入队列
					for (size_t i = 0; i < _matrix[front].size(); i++)
					{
						if (_matrix[front][i] != Max_W)
						{
							if (visited[i] == false)
							{
								q.push(i);
								visited[i] = true;
							}
						}
					}
				}
				cout << endl;
				levelSize = q.size();
			}
		}
		void _DFS(size_t srci, vector<bool>& visited)
		{
			cout << srci << ":" << _vertexs[srci] << endl;
			visited[srci] = true;
			for (int i = 0; i < _matrix[srci].size(); i++)
			{
				if (_matrix[srci][i] != Max_W && visited[i] == false)
				{
					_DFS(i, visited);
				}
			}
		}
		void DFS(const V& src)
		{
			int srci = GetVertexIndex(src);
			vector<bool> visited(_vertexs.size(), false);
			_DFS(srci, visited);
		}

		struct Edge
		{
			int _srci;
			int _dsti;
			W _w;
			Edge(int srci, int dsti, W w)
				:_srci(srci)
				,_dsti(dsti)
				,_w(w)
			{}
			bool operator>(const Edge& e) const
			{
				return this->_w > e._w;
			}
		};

		//传入的是一个只有结点的,没有边的图
		W Kruskal(Self& minTree)
		{
			//先将所有的边,按照小堆的方式进行组织起来
			priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
		   	size_t n = _vertexs.size();
			for (int i = 0; i < n; i++)
			{
				for (int j = 0; j < n; j++)
				{
					//由于这里是无向图,他是一个对称矩阵,但是我们的边只考虑一半就已经可以了。剩下的就重复了。
					if (i < j && _matrix[i][j] != Max_W)
					{
						//已经按照自身的,带有边的图,将所有的边的信息全部组织好了
						minque.push(Edge(i, j, _matrix[i][j]));
					}
				}
			}
			//因为最小生成树一定是n-1条边,所以我们现在要选出n-1条边,size是计数器
			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))
				{
					//我们可以看看我们选出来的边
					cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;
					//该边是符合的,我们直接为这个图加上边
					minTree._AddEdge(min._srci, min._dsti, min._w);
					//加上之后,就连通了,我们让他们形成集合
					ufs.Union(min._srci, min._dsti);
					//我们一定只有n-1条边,我们需要计数
					++size;
					//将总的权值要加起来
					totalW += min._w;
				}
				//成环的情况,我们只是看看这是哪条边
				else
				{
					cout << "构成环啦!:";
					cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;
				}
			}
			//上面的循环中,如果图是连通的,那么最终一定选出来的是n-1条边。除非图是不连通的。
			if (size == n - 1)
			{
				return totalW;
			}
			//图不连通,直接返回0
			else
			{
				return W();
			}
		}


		W Prim(Self& minTree, const V& src)
		{
			size_t srci = GetVertexIndex(src);
			int n = _vertexs.size();
			//使用集合的方式
			//set<int> X;
			//set<int> Y;
			//X.insert(srci);
			//for (int i = 0; i < n; i++)
			//{
			//	if (i != srci)
			//	{
			//		Y.insert(i);
			//	}
			//}

			//利用vector的方式,去记录两个集合。
			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;
			//把目前为止X集合(仅仅只有起点)的相关的边,全部放入优先级队列中
			for (int i = 0; i < n; i++)
			{
				if (_matrix[srci][i] != Max_W)
				{
					minq.push(Edge(srci, i, _matrix[srci][i]));
				}
			}
			//size用来判断是否达到最小生成树的个数n-1,totalW用来计算权值之和
			size_t size = 0;
			W totalW = W();
			//我们开始在优先级队列中去寻找
			while (!minq.empty())
			{
				//在优先级队列中找到一个最小的元素,由于优先级队列中的一定是我们X集合可以延申的边。所以是满足Prim的选边条件的
				Edge min = minq.top();
				//如果边使用了,那么就不用了,如果不使用,那肯定是因为环才导致的,那也不要了
				minq.pop();
				//这里比较巧妙,因为根据我们的算法思想,我们选边的时候一定是从X集合的某一个顶点开始的,然后去找一个不在X集合里面的顶点
				//所以这里我们可以直接判断目的点是否在X集合里面,如果在,那么一定是环。如果不是,才可以把这条边给加上去
				if (X[min._dsti])
				{
					//cout << "构成环:";
					//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
					continue;
				}

				//把边给加上去
				minTree._AddEdge(min._srci, min._dsti, min._w);
				//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
				
				//X.insert(min._dsti);
				//Y.erase(min._dsti);
				
				//处理一下目的点的边
				X[min._dsti] = true;
				Y[min._dsti] = false;

				++size;
				totalW += min._w;
				//这里相当于一次优化,因为该循环一定可以保证选出来的n-1条边是最小生成树,
				//后面的优先级队列中的任何一条边一定会导致出现环,会在前面的检测目的点是否在X集合中被处理掉。
				//这里则是直接不用继续入其他的边进入队列了。可以提高一些效率,减少无用的操作
				if (size == n - 1)
				{
					break;
				}
				//当一条边添加完成后,它就属于X集合了,我们可以将该点所延申出的边给加入到优先级队列中
				//只有该边存在,且目的地没有被加入过的时候,才会入队列。值得耐人寻味的是,这里虽然已经处理过一次可能出现环的情况了
				//但是可能由于在添加边的时候,导致某些优先级队列中的边会导致构成环了,所以就有了前面的再次根据目的地时候在X集合中去判环
				for (int i = 0; i < n; i++)
				{
					if (_matrix[min._dsti][i] != Max_W && X[i] == false)
					{
						minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
					}
				}
			}
			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 = GetVertexIndex(src);
			int n = _vertexs.size();
			for (int i = 0; i < n; i++)
			{
				if (i != srci)
				{
					cout << "[" << "pathlenth:" << dist[i] << "]";
					stack<int> path;
					size_t parent = i;
					while (parent != srci)
					{
						path.push(parent);
						parent = pPath[parent];
					}
					path.push(parent);
					cout << "path:";
					while (!path.empty())
					{ 
						int top = path.top();
						path.pop();
						cout << _vertexs[top] << "->";
					}
					cout << "nullptr" << endl;

				}
			}

		}
		//src是起始结点,dist数组的内容是存放最短路径值的权值,即每个元素的内容代表着从起始结点到该结点的最短路径
		//pPath数组是路径的数组,因为有时候我们需要求出路径的具体走法,所以我们可以用数组的方式,类似于并查集去寻找路径的方式,建立一颗树
		void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
		{
			//确定好起始结点的下标
			size_t srci = GetVertexIndex(src);
			int n = _vertexs.size();
			//我们先让所有的最短路径无穷。即还未求出来
			dist.resize(n, Max_W);
			//注意源节点到源节点的最短路径一定是0
			dist[srci] = 0;
			//我们让所有的路径都设置为-1,意味着所有的结点都没有找出来最短路径
			pPath.resize(n, -1);
			//这是表示起点它的路径就是它自己,这步其实只是为了让起点和为求出最短路径给分开表示。不然都用-1可能会混乱
			pPath[srci] = srci;
			//这个S集合代表我们已经求出最短路径的集合。如果是true代表着这个结点早已求出了最短路径。false代表未求出
			vector<bool> S(n, false);
			//注意这里我们做一下特殊处理,虽然我们起点它本来就应该放在这个集合里面,但是我们还是先让它为false。这里我们其实是想与下面的循环进行合并
			//所以迫不得已做的操作,因为一旦将某个结点设置为true,那么它的相邻的结点路径也应该被更新一下,那么这里就要把下面的对于代码拷贝一份。
			//所以为了让代码简洁,我们直接不写这一步了,和下面的进行合并
			//S[srci] = true;

			//这里只是控制一下循环次数,因为我们要求所有的结点都要被遍历一下,而每次只会遍历一个结点,即将一个结点给放入S集合。
			//我们每次点亮的一定是之前从未遍历过的结点
			for(int j = 0; j < n; j++)
			{
				//Dijkstra算法要求每次都要找出一个,还没有被访问过的(不在S集合的),并且是有路径的,且路径是当前最小的一个结点
				//对于第一次找到的就是srci以及所对应的0值
				int u = 0;
				W min = Max_W;
				for (int i = 0; i < n; i++)
				{
					if (S[i] == false && dist[i] < min)
					{
						u = i;
						min = dist[i];
					}
				}
				//找到以后,我们可以让他加入S集合,代表它已经被访问过了。借助这里找到了srci,并且使他加入到S集合
				S[u] = true;
				//松弛更新u连接顶点v, srci->u + u->v < srci->v 就更新
				//注意,这里我们需要的是找到我们新加入S集合的邻接的顶点,然后判断是不是需要更新最短路径,
				//但是我们一开始并不知道,所以我们遍历所有的结点,依次判断条件是否满足
				for (int 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;
					}
				}
			}
		}


	private:
		vector<V> _vertexs; //顶点集合
		map<V, int> _indexMap; //顶点对应的下标关系
		vector<vector<W>> _matrix; //临界矩阵
	};
	void TestGraphDijkstra()
	{
		const char* str = "syztx";
		Graph<char, int, INT_MAX, true> g(str, strlen(str));
		g.AddEdge('s', 't', 10);
		g.AddEdge('s', 'y', 5);
		g.AddEdge('y', 't', 3);
		g.AddEdge('y', 'x', 9);
		g.AddEdge('y', 'z', 2);
		g.AddEdge('z', 's', 7);
		g.AddEdge('z', 'x', 6);
		g.AddEdge('t', 'y', 2);
		g.AddEdge('t', 'x', 1);
		g.AddEdge('x', 'z', 4);
		vector<int> dist;
		vector<int> parentPath;
		g.Dijkstra('s', dist, parentPath);
		g.PrintShortPath('s', dist, parentPath);
	}



	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();
	}
	void TestGraphBDFS()
	{
		string a[] = { "张三", "李四", "王五", "赵六", "周七" };
		Graph<string, int> g1(a, sizeof(a) / sizeof(string));
		g1.AddEdge("张三", "李四", 100);
		g1.AddEdge("张三", "王五", 200);
		g1.AddEdge("王五", "赵六", 30);
		g1.AddEdge("王五", "周七", 30);
		g1.Print();
		g1.BFS("张三");
		cout << endl;
		g1.BFSLevel("张三");
		cout << endl;
		g1.DFS("张三");
	}
	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;
		//Graph<char, int> kminTree(str, strlen(str));
		//cout << "Kruskal:" << g.Kruskal(kminTree) << endl;
		//kminTree.Print();
		//Graph<char, int> pminTree(str, strlen(str));
		//cout << "Prim:" << g.Prim(pminTree, 'a') << endl;
		//pminTree.Print();

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


	}
}

运行结果如下

image-20240222013452523

上面的算法具体解释已经放在了注释当中,我们将代码单独拉出来方便我们阅读

注意的是,已经访问过的结点,一定不能再次访问了!

		//src是起始结点,dist数组的内容是存放最短路径值的权值,即每个元素的内容代表着从起始结点到该结点的最短路径
		//pPath数组是路径的数组,因为有时候我们需要求出路径的具体走法,所以我们可以用数组的方式,类似于并查集去寻找路径的方式,建立一颗树
		void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
		{
			//确定好起始结点的下标
			size_t srci = GetVertexIndex(src);
			int n = _vertexs.size();
			//我们先让所有的最短路径无穷。即还未求出来
			dist.resize(n, Max_W);
			//注意源节点到源节点的最短路径一定是0
			dist[srci] = 0;
			//我们让所有的路径都设置为-1,意味着所有的结点都没有找出来最短路径
			pPath.resize(n, -1);
			//这是表示起点它的路径就是它自己,这步其实只是为了让起点和为求出最短路径给分开表示。不然都用-1可能会混乱
			pPath[srci] = srci;
			//这个S集合代表我们已经求出最短路径的集合。如果是true代表着这个结点早已求出了最短路径。false代表未求出
			vector<bool> S(n, false);
			//注意这里我们做一下特殊处理,虽然我们起点它本来就应该放在这个集合里面,但是我们还是先让它为false。这里我们其实是想与下面的循环进行合并
			//所以迫不得已做的操作,因为一旦将某个结点设置为true,那么它的相邻的结点路径也应该被更新一下,那么这里就要把下面的对于代码拷贝一份。
			//所以为了让代码简洁,我们直接不写这一步了,和下面的进行合并
			//S[srci] = true;

			//这里只是控制一下循环次数,因为我们要求所有的结点都要被遍历一下,而每次只会遍历一个结点,即将一个结点给放入S集合。
			//我们每次点亮的一定是之前从未遍历过的结点
			for(int j = 0; j < n; j++)
			{
				//Dijkstra算法要求每次都要找出一个,还没有被访问过的(不在S集合的),并且是有路径的,且路径是当前最小的一个结点
				//对于第一次找到的就是srci以及所对应的0值
				int u = 0;
				W min = Max_W;
				for (int i = 0; i < n; i++)
				{
					if (S[i] == false && dist[i] < min)
					{
						u = i;
						min = dist[i];
					}
				}
				//找到以后,我们可以让他加入S集合,代表它已经被访问过了。借助这里找到了srci,并且使他加入到S集合
				S[u] = true;
				//松弛更新u连接顶点v, srci->u + u->v < srci->v 就更新
				//注意,这里我们需要的是找到我们新加入S集合的邻接的顶点,然后判断是不是需要更新最短路径,
				//但是我们一开始并不知道,所以我们遍历所有的结点,依次判断条件是否满足
				for (int 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;
					}
				}
			}
		}

4.负权值带来的问题

我们看一下下面的测试用例

对于下面的图

image-20240222013946282

	void TestGraphDijkstra()
	{
		const char* str = "sytx";
		Graph<char, int, INT_MAX, true> g(str, strlen(str));
		g.AddEdge('s', 't', 10);
		g.AddEdge('s', 'y', 5);
		g.AddEdge('t', 'y', -7);
		g.AddEdge('y', 'x', 3);
		vector<int> dist;
		vector<int> parentPath;
		g.Dijkstra('s', dist, parentPath);
		g.PrintShortPath('s', dist, parentPath);
	}

我们的运行结果为

image-20240222014018356

其实我们可以很容易的发现,第一条路径和第三条路径的最短距离,直接从图中去看的话应该是3和6才对,但是结果居然是5和8.

它的具体步骤如下

image-20240222014504325

所以它的最终是出现了问题

Dijkstra算法无法解决负权值问题主要有两个原因:

  1. Dijkstra算法的基本思想是贪心算法,它总是选择当前距离起始点最近(或说权重最小)的顶点进行处理。如果图中存在负权重的边,那么已经确定最短路径的顶点的最短路径可能会因为这个负权重的边而改变,这与Dijkstra算法“确定的最短路径就是永久不变的”初衷相矛盾。
  2. 如果图中存在负权重的环路,Dijkstra算法可能会陷入无限循环中,因为每次经过这个负权重的环,都能使得路径的权值变小。

因此,如果图中边的权值存在负值,我们通常使用Bellman-Ford算法或者Floyd-Warshall算法,这些算法能正确处理负权值的情况。

这个算法的时间复杂度是O(N²),空间复杂度是O(N)

三、单源最短路径 – Bellman-Ford算法

1.算法思想

Dijkstra算法只能用来解决正权图的单源最短路径问题,但有些题目会出现负权图。这时这个算法就不能帮助我们解决问题了,而bellman—ford算法可以解决负权图的单源最短路径问题。它的优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。它也有明显的缺点,它的时间复杂度 O(N*E) (N是点数,E是边数)普遍是要高于Dijkstra算法O(N²)的。像这里如果我们使用邻接矩阵实现,那么遍历所有边的数量的时间复杂度就是O(N^3),这里也可以看出来Bellman-Ford就是一种暴力求解更新

image-20240224125506279

2.算法实现

如下代码所示

namespace matrix
{
	//V代表顶点, W是weight代表权值,MAX_W代表权值的最大值,Direction代表是有向图还是无向图,flase表示无向
	template<class V, class W, W Max_W = INT_MAX, bool Direction = false>
	class Graph
	{
		typedef Graph<V, W, Max_W, Direction> Self;
	public:
		Graph() = default;
		//图的创建
		//1. IO输入 不方便测试
		//2. 图结构关系写到文件,读取文件
		//3. 手动添加边
		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 < _matrix.size(); i++)
			{
				_matrix[i].resize(n, Max_W);
			}
		}
		size_t GetVertexIndex(const V& v)
		{
			//return _indexMap[v];
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				//assert(false)
				throw invalid_argument("顶点不存在");
				return -1;
			}
		}
		void _AddEdge(size_t srci, size_t dsti, const W& w)
		{
			_matrix[srci][dsti] = w;
			if (Direction == false)
			{
				_matrix[dsti][srci] = w;
			}
		}
		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 << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;

			cout << "   ";
			for (int i = 0; i < _vertexs.size(); i++)
			{
				//cout << _vertexs[i] << " ";
				printf("%-3d ", i);
			}
			cout << endl;
			for (size_t i = 0; i < _matrix.size(); i++)
			{
				//cout << _vertexs[i] << " ";
				printf("%d ", i);
				for (size_t j = 0; j < _matrix[i].size(); j++)
				{
					if (_matrix[i][j] == INT_MAX)
					{
						cout << " *  ";
					}
					else
					{
						printf(" %d  ", _matrix[i][j]);
						//cout << _matrix[i][j] << " ";
					}
				}
				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 BFS(const V& src)
		{
			int srci = GetVertexIndex(src);
			queue<int> q; //广度遍历的队列
			vector<bool> visited(_vertexs.size(), false); //标记数组
			q.push(srci); //起点入队
			visited[srci] = true; //已经被遍历过了
			while (!q.empty())
			{
				int front = q.front();
				q.pop();
				cout << front << ":" << _vertexs[front] << endl;
				//把front顶点的邻接顶点入队列
				for (size_t i = 0; i < _matrix[front].size(); i++)
				{
					if (_matrix[front][i] != Max_W)
					{
						if (visited[i] == false)
						{
							q.push(i);
							visited[i] = true;
						}
					}
				}
			}
		} 

		void BFSLevel(const V& src)
		{
			int srci = GetVertexIndex(src);
			queue<int> q; //广度遍历的队列
			vector<bool> visited(_vertexs.size(), false); //标记数组
			q.push(srci); //起点入队
			visited[srci] = true; //已经被遍历过了
			int levelSize = 1;
			while (!q.empty())
			{
				for (int i = 0; i < levelSize; i++)
				{
					int front = q.front();
					q.pop();
					cout << front << ":" << _vertexs[front] << " ";
					//把front顶点的邻接顶点入队列
					for (size_t i = 0; i < _matrix[front].size(); i++)
					{
						if (_matrix[front][i] != Max_W)
						{
							if (visited[i] == false)
							{
								q.push(i);
								visited[i] = true;
							}
						}
					}
				}
				cout << endl;
				levelSize = q.size();
			}
		}
		void _DFS(size_t srci, vector<bool>& visited)
		{
			cout << srci << ":" << _vertexs[srci] << endl;
			visited[srci] = true;
			for (int i = 0; i < _matrix[srci].size(); i++)
			{
				if (_matrix[srci][i] != Max_W && visited[i] == false)
				{
					_DFS(i, visited);
				}
			}
		}
		void DFS(const V& src)
		{
			int srci = GetVertexIndex(src);
			vector<bool> visited(_vertexs.size(), false);
			_DFS(srci, visited);
		}

		struct Edge
		{
			int _srci;
			int _dsti;
			W _w;
			Edge(int srci, int dsti, W w)
				:_srci(srci)
				,_dsti(dsti)
				,_w(w)
			{}
			bool operator>(const Edge& e) const
			{
				return this->_w > e._w;
			}
		};

		//传入的是一个只有结点的,没有边的图
		W Kruskal(Self& minTree)
		{
			//先将所有的边,按照小堆的方式进行组织起来
			priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
		   	size_t n = _vertexs.size();
			for (int i = 0; i < n; i++)
			{
				for (int j = 0; j < n; j++)
				{
					//由于这里是无向图,他是一个对称矩阵,但是我们的边只考虑一半就已经可以了。剩下的就重复了。
					if (i < j && _matrix[i][j] != Max_W)
					{
						//已经按照自身的,带有边的图,将所有的边的信息全部组织好了
						minque.push(Edge(i, j, _matrix[i][j]));
					}
				}
			}
			//因为最小生成树一定是n-1条边,所以我们现在要选出n-1条边,size是计数器
			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))
				{
					//我们可以看看我们选出来的边
					cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;
					//该边是符合的,我们直接为这个图加上边
					minTree._AddEdge(min._srci, min._dsti, min._w);
					//加上之后,就连通了,我们让他们形成集合
					ufs.Union(min._srci, min._dsti);
					//我们一定只有n-1条边,我们需要计数
					++size;
					//将总的权值要加起来
					totalW += min._w;
				}
				//成环的情况,我们只是看看这是哪条边
				else
				{
					cout << "构成环啦!:";
					cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;
				}
			}
			//上面的循环中,如果图是连通的,那么最终一定选出来的是n-1条边。除非图是不连通的。
			if (size == n - 1)
			{
				return totalW;
			}
			//图不连通,直接返回0
			else
			{
				return W();
			}
		}


		W Prim(Self& minTree, const V& src)
		{
			size_t srci = GetVertexIndex(src);
			int n = _vertexs.size();
			//使用集合的方式
			//set<int> X;
			//set<int> Y;
			//X.insert(srci);
			//for (int i = 0; i < n; i++)
			//{
			//	if (i != srci)
			//	{
			//		Y.insert(i);
			//	}
			//}

			//利用vector的方式,去记录两个集合。
			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;
			//把目前为止X集合(仅仅只有起点)的相关的边,全部放入优先级队列中
			for (int i = 0; i < n; i++)
			{
				if (_matrix[srci][i] != Max_W)
				{
					minq.push(Edge(srci, i, _matrix[srci][i]));
				}
			}
			//size用来判断是否达到最小生成树的个数n-1,totalW用来计算权值之和
			size_t size = 0;
			W totalW = W();
			//我们开始在优先级队列中去寻找
			while (!minq.empty())
			{
				//在优先级队列中找到一个最小的元素,由于优先级队列中的一定是我们X集合可以延申的边。所以是满足Prim的选边条件的
				Edge min = minq.top();
				//如果边使用了,那么就不用了,如果不使用,那肯定是因为环才导致的,那也不要了
				minq.pop();
				//这里比较巧妙,因为根据我们的算法思想,我们选边的时候一定是从X集合的某一个顶点开始的,然后去找一个不在X集合里面的顶点
				//所以这里我们可以直接判断目的点是否在X集合里面,如果在,那么一定是环。如果不是,才可以把这条边给加上去
				if (X[min._dsti])
				{
					//cout << "构成环:";
					//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
					continue;
				}

				//把边给加上去
				minTree._AddEdge(min._srci, min._dsti, min._w);
				//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
				
				//X.insert(min._dsti);
				//Y.erase(min._dsti);
				
				//处理一下目的点的边
				X[min._dsti] = true;
				Y[min._dsti] = false;

				++size;
				totalW += min._w;
				//这里相当于一次优化,因为该循环一定可以保证选出来的n-1条边是最小生成树,
				//后面的优先级队列中的任何一条边一定会导致出现环,会在前面的检测目的点是否在X集合中被处理掉。
				//这里则是直接不用继续入其他的边进入队列了。可以提高一些效率,减少无用的操作
				if (size == n - 1)
				{
					break;
				}
				//当一条边添加完成后,它就属于X集合了,我们可以将该点所延申出的边给加入到优先级队列中
				//只有该边存在,且目的地没有被加入过的时候,才会入队列。值得耐人寻味的是,这里虽然已经处理过一次可能出现环的情况了
				//但是可能由于在添加边的时候,导致某些优先级队列中的边会导致构成环了,所以就有了前面的再次根据目的地时候在X集合中去判环
				for (int i = 0; i < n; i++)
				{
					if (_matrix[min._dsti][i] != Max_W && X[i] == false)
					{
						minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
					}
				}
			}
			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 = GetVertexIndex(src);
			int n = _vertexs.size();
			for (int i = 0; i < n; i++)
			{
				if (i != srci)
				{
					cout << "[" << "pathlenth:" << dist[i] << "]";
					stack<int> path;
					size_t parent = i;
					while (parent != srci)
					{
						path.push(parent);
						parent = pPath[parent];
					}
					path.push(parent);
					cout << "path:";
					while (!path.empty())
					{ 
						int top = path.top();
						path.pop();
						cout << _vertexs[top] << "->";
					}
					cout << "nullptr" << endl;

				}
			}

		}
		//src是起始结点,dist数组的内容是存放最短路径值的权值,即每个元素的内容代表着从起始结点到该结点的最短路径
		//pPath数组是路径的数组,因为有时候我们需要求出路径的具体走法,所以我们可以用数组的方式,类似于并查集去寻找路径的方式,建立一颗树
		void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
		{
			//确定好起始结点的下标
			size_t srci = GetVertexIndex(src);
			int n = _vertexs.size();
			//我们先让所有的最短路径无穷。即还未求出来
			dist.resize(n, Max_W);
			//注意源节点到源节点的最短路径一定是0
			dist[srci] = 0;
			//我们让所有的路径都设置为-1,意味着所有的结点都没有找出来最短路径
			pPath.resize(n, -1);
			//这是表示起点它的路径就是它自己,这步其实只是为了让起点和为求出最短路径给分开表示。不然都用-1可能会混乱
			pPath[srci] = srci;
			//这个S集合代表我们已经求出最短路径的集合。如果是true代表着这个结点早已求出了最短路径。false代表未求出
			vector<bool> S(n, false);
			//注意这里我们做一下特殊处理,虽然我们起点它本来就应该放在这个集合里面,但是我们还是先让它为false。这里我们其实是想与下面的循环进行合并
			//所以迫不得已做的操作,因为一旦将某个结点设置为true,那么它的相邻的结点路径也应该被更新一下,那么这里就要把下面的对于代码拷贝一份。
			//所以为了让代码简洁,我们直接不写这一步了,和下面的进行合并
			//S[srci] = true;

			//这里只是控制一下循环次数,因为我们要求所有的结点都要被遍历一下,而每次只会遍历一个结点,即将一个结点给放入S集合。
			//我们每次点亮的一定是之前从未遍历过的结点
			for(int j = 0; j < n; j++)
			{
				//Dijkstra算法要求每次都要找出一个,还没有被访问过的(不在S集合的),并且是有路径的,且路径是当前最小的一个结点
				//对于第一次找到的就是srci以及所对应的0值
				int u = 0;
				W min = Max_W;
				for (int i = 0; i < n; i++)
				{
					if (S[i] == false && dist[i] < min)
					{
						u = i;
						min = dist[i];
					}
				}
				//找到以后,我们可以让他加入S集合,代表它已经被访问过了。借助这里找到了srci,并且使他加入到S集合
				S[u] = true;
				//松弛更新u连接顶点v, srci->u + u->v < srci->v 就更新
				//注意,这里我们需要的是找到我们新加入S集合的邻接的顶点,然后判断是不是需要更新最短路径,
				//但是我们一开始并不知道,所以我们遍历所有的结点,依次判断条件是否满足
				for (int 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;
					}
				}
			}
		}

		bool BellmanFord(const V& src, vector<W>& dist, vector<int>& parentPath)
		{
			size_t n = _vertexs.size();
			size_t srci = GetVertexIndex(src);

			dist.resize(n, Max_W);
			dist[srci] = W();
			parentPath.resize(n, -1);
			parentPath[srci] = srci;
			
			for (int k = 0; k < n; k++)
			{
				bool update = false;
				for (int i = 0; i < n; i++)
				{
					for (int j = 0; j < n; j++)
					{
						update = true;
						//srci->i + i->j => srci->j
						if (_matrix[i][j] != Max_W && dist[i] + _matrix[i][j] < dist[j])
						{
							dist[j] = dist[i] + _matrix[i][j];
							parentPath[j] = i;
						}
					}
				}
				if (update == false)
				{
					break;
				}
			}
			//还能更新就是带有负权回路
			for (int i = 0; i < n; i++)
			{
				for (int j = 0; j < n; j++)
				{
					//srci->i + i->j => srci->j
					if (_matrix[i][j] != Max_W && dist[i] + _matrix[i][j] < dist[j])
					{
						return false;
					}
				}
			}

		}

	private:
		vector<V> _vertexs; //顶点集合
		map<V, int> _indexMap; //顶点对应的下标关系
		vector<vector<W>> _matrix; //临界矩阵
	};

	void TestGraphBellmanFord()
	{
		const char* str = "syztx";
		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('z', 's', 2);
		g.AddEdge('z', 'x', 7);
		g.AddEdge('t', 'x', 5);
		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;
		}
	}


	void TestGraphDijkstra()
	{
		//const char* str = "syztx";
		//Graph<char, int, INT_MAX, true> g(str, strlen(str));
		//g.AddEdge('s', 't', 10);
		//g.AddEdge('s', 'y', 5);
		//g.AddEdge('y', 't', 3);
		//g.AddEdge('y', 'x', 9);
		//g.AddEdge('y', 'z', 2);
		//g.AddEdge('z', 's', 7);
		//g.AddEdge('z', 'x', 6);
		//g.AddEdge('t', 'y', 2);
		//g.AddEdge('t', 'x', 1);
		//g.AddEdge('x', 'z', 4);
		//vector<int> dist;
		//vector<int> parentPath;
		//g.Dijkstra('s', dist, parentPath);
		//g.PrintShortPath('s', dist, parentPath);

		const char* str = "sytx";
		Graph<char, int, INT_MAX, true> g(str, strlen(str));
		g.AddEdge('s', 't', 10);
		g.AddEdge('s', 'y', 5);
		g.AddEdge('t', 'y', -7);
		g.AddEdge('y', 'x', 3);
		vector<int> dist;
		vector<int> parentPath;
		g.Dijkstra('s', dist, parentPath);
		g.PrintShortPath('s', dist, parentPath);

	}



	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();
	}
	void TestGraphBDFS()
	{
		string a[] = { "张三", "李四", "王五", "赵六", "周七" };
		Graph<string, int> g1(a, sizeof(a) / sizeof(string));
		g1.AddEdge("张三", "李四", 100);
		g1.AddEdge("张三", "王五", 200);
		g1.AddEdge("王五", "赵六", 30);
		g1.AddEdge("王五", "周七", 30);
		g1.Print();
		g1.BFS("张三");
		cout << endl;
		g1.BFSLevel("张三");
		cout << endl;
		g1.DFS("张三");
	}
	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;
		//Graph<char, int> kminTree(str, strlen(str));
		//cout << "Kruskal:" << g.Kruskal(kminTree) << endl;
		//kminTree.Print();
		//Graph<char, int> pminTree(str, strlen(str));
		//cout << "Prim:" << g.Prim(pminTree, 'a') << endl;
		//pminTree.Print();

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


	}
}

Bellman-Ford算法的基本思路是:

  • 首先是遍历所有的边。只要srci->i + i->j的距离小于之前的srci->j的距离。那么就进行更新,同时更新父路径
  • 不过这里可能存在的一个问题是权值和路径对不上的问题,因为只要更新出了更短的一条路径,可能就会影响其他路径。不过我们会发现如果在更新一次的话,那么就会修正这个问题,但是更新路径又会影响其他路径。所以还需要继续更新,最多会更新n轮。最多也就是每个结点都更新一次。
  • 一旦我们发现某一轮没有更新,那么后序的也绝不可能被影响到,所以后面可以不用更新了。

运行结果为

image-20240224143254165

3.SPFA优化

上面代码的时间复杂度是O(N³),我们可以使用SPFA优化

SPFA优化的基本思想是

  • 第一轮更新,所有边入队列
  • 后面的轮次,更新最短路径的边入队列
  • 每次只对队列里面的边进行操作

4.负权回路

如下测试用例所示

	void TestGraphBellmanFord()
	{
		//微调图结构,带有负权回路的测试
		const char* str = "syztx";
		Graph<char, int, INT_MAX, true> g(str, strlen(str));
		g.AddEdge('s', 't', 6);
		g.AddEdge('s', 'y', 7);
		g.AddEdge('y', 'x', -3);
		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', '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;
		}
	}

在这个图中存在负权回路

我们用上面的Bellman-Ford算法可以去检测负权回路,因为最多更新n轮,如果还继续更新,那么只能是负权回路惹的祸。所以可以借此来检测负权回路

运行结果为

image-20240224143600279

注意:

负权回路,神仙难救,求不出最短路径

四、多源最短路径 – Floyd-Warshall算法

1.算法思想

Floyd-Warshall算法是解决任意两点间的最短路径的一种算法。
Floyd算法考虑的是一条最短路径的中间节点,即简单路径p={v1,v2,…,vn}上除v1和vn的任意节点。
设k是p的一个中间节点,那么从i到j的最短路径p就被分成i到k和k到j的两段最短路径p1,p2。p1是从i到k且中间节点属于{1,2,…,k-1}取得的一条最短路径。p2是从k到j且中间节点属于{1,2,…,k-1}取得的一条最短路径。

image-20240224161137658

image-20240224161210688

2.算法实现

namespace matrix
{
	//V代表顶点, W是weight代表权值,MAX_W代表权值的最大值,Direction代表是有向图还是无向图,flase表示无向
	template<class V, class W, W Max_W = INT_MAX, bool Direction = false>
	class Graph
	{
		typedef Graph<V, W, Max_W, Direction> Self;
	public:
		Graph() = default;
		//图的创建
		//1. IO输入 不方便测试
		//2. 图结构关系写到文件,读取文件
		//3. 手动添加边
		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 < _matrix.size(); i++)
			{
				_matrix[i].resize(n, Max_W);
			}
		}
		size_t GetVertexIndex(const V& v)
		{
			//return _indexMap[v];
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->second;
			}
			else
			{
				//assert(false)
				throw invalid_argument("顶点不存在");
				return -1;
			}
		}
		void _AddEdge(size_t srci, size_t dsti, const W& w)
		{
			_matrix[srci][dsti] = w;
			if (Direction == false)
			{
				_matrix[dsti][srci] = w;
			}
		}
		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 << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;

			cout << "   ";
			for (int i = 0; i < _vertexs.size(); i++)
			{
				//cout << _vertexs[i] << " ";
				printf("%-3d ", i);
			}
			cout << endl;
			for (size_t i = 0; i < _matrix.size(); i++)
			{
				//cout << _vertexs[i] << " ";
				printf("%d ", i);
				for (size_t j = 0; j < _matrix[i].size(); j++)
				{
					if (_matrix[i][j] == INT_MAX)
					{
						cout << " *  ";
					}
					else
					{
						printf(" %d  ", _matrix[i][j]);
						//cout << _matrix[i][j] << " ";
					}
				}
				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 BFS(const V& src)
		{
			int srci = GetVertexIndex(src);
			queue<int> q; //广度遍历的队列
			vector<bool> visited(_vertexs.size(), false); //标记数组
			q.push(srci); //起点入队
			visited[srci] = true; //已经被遍历过了
			while (!q.empty())
			{
				int front = q.front();
				q.pop();
				cout << front << ":" << _vertexs[front] << endl;
				//把front顶点的邻接顶点入队列
				for (size_t i = 0; i < _matrix[front].size(); i++)
				{
					if (_matrix[front][i] != Max_W)
					{
						if (visited[i] == false)
						{
							q.push(i);
							visited[i] = true;
						}
					}
				}
			}
		} 

		void BFSLevel(const V& src)
		{
			int srci = GetVertexIndex(src);
			queue<int> q; //广度遍历的队列
			vector<bool> visited(_vertexs.size(), false); //标记数组
			q.push(srci); //起点入队
			visited[srci] = true; //已经被遍历过了
			int levelSize = 1;
			while (!q.empty())
			{
				for (int i = 0; i < levelSize; i++)
				{
					int front = q.front();
					q.pop();
					cout << front << ":" << _vertexs[front] << " ";
					//把front顶点的邻接顶点入队列
					for (size_t i = 0; i < _matrix[front].size(); i++)
					{
						if (_matrix[front][i] != Max_W)
						{
							if (visited[i] == false)
							{
								q.push(i);
								visited[i] = true;
							}
						}
					}
				}
				cout << endl;
				levelSize = q.size();
			}
		}
		void _DFS(size_t srci, vector<bool>& visited)
		{
			cout << srci << ":" << _vertexs[srci] << endl;
			visited[srci] = true;
			for (int i = 0; i < _matrix[srci].size(); i++)
			{
				if (_matrix[srci][i] != Max_W && visited[i] == false)
				{
					_DFS(i, visited);
				}
			}
		}
		void DFS(const V& src)
		{
			int srci = GetVertexIndex(src);
			vector<bool> visited(_vertexs.size(), false);
			_DFS(srci, visited);
		}

		struct Edge
		{
			int _srci;
			int _dsti;
			W _w;
			Edge(int srci, int dsti, W w)
				:_srci(srci)
				,_dsti(dsti)
				,_w(w)
			{}
			bool operator>(const Edge& e) const
			{
				return this->_w > e._w;
			}
		};

		//传入的是一个只有结点的,没有边的图
		W Kruskal(Self& minTree)
		{
			//先将所有的边,按照小堆的方式进行组织起来
			priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
		   	size_t n = _vertexs.size();
			for (int i = 0; i < n; i++)
			{
				for (int j = 0; j < n; j++)
				{
					//由于这里是无向图,他是一个对称矩阵,但是我们的边只考虑一半就已经可以了。剩下的就重复了。
					if (i < j && _matrix[i][j] != Max_W)
					{
						//已经按照自身的,带有边的图,将所有的边的信息全部组织好了
						minque.push(Edge(i, j, _matrix[i][j]));
					}
				}
			}
			//因为最小生成树一定是n-1条边,所以我们现在要选出n-1条边,size是计数器
			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))
				{
					//我们可以看看我们选出来的边
					cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;
					//该边是符合的,我们直接为这个图加上边
					minTree._AddEdge(min._srci, min._dsti, min._w);
					//加上之后,就连通了,我们让他们形成集合
					ufs.Union(min._srci, min._dsti);
					//我们一定只有n-1条边,我们需要计数
					++size;
					//将总的权值要加起来
					totalW += min._w;
				}
				//成环的情况,我们只是看看这是哪条边
				else
				{
					cout << "构成环啦!:";
					cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << min._w << endl;
				}
			}
			//上面的循环中,如果图是连通的,那么最终一定选出来的是n-1条边。除非图是不连通的。
			if (size == n - 1)
			{
				return totalW;
			}
			//图不连通,直接返回0
			else
			{
				return W();
			}
		}


		W Prim(Self& minTree, const V& src)
		{
			size_t srci = GetVertexIndex(src);
			int n = _vertexs.size();
			//使用集合的方式
			//set<int> X;
			//set<int> Y;
			//X.insert(srci);
			//for (int i = 0; i < n; i++)
			//{
			//	if (i != srci)
			//	{
			//		Y.insert(i);
			//	}
			//}

			//利用vector的方式,去记录两个集合。
			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;
			//把目前为止X集合(仅仅只有起点)的相关的边,全部放入优先级队列中
			for (int i = 0; i < n; i++)
			{
				if (_matrix[srci][i] != Max_W)
				{
					minq.push(Edge(srci, i, _matrix[srci][i]));
				}
			}
			//size用来判断是否达到最小生成树的个数n-1,totalW用来计算权值之和
			size_t size = 0;
			W totalW = W();
			//我们开始在优先级队列中去寻找
			while (!minq.empty())
			{
				//在优先级队列中找到一个最小的元素,由于优先级队列中的一定是我们X集合可以延申的边。所以是满足Prim的选边条件的
				Edge min = minq.top();
				//如果边使用了,那么就不用了,如果不使用,那肯定是因为环才导致的,那也不要了
				minq.pop();
				//这里比较巧妙,因为根据我们的算法思想,我们选边的时候一定是从X集合的某一个顶点开始的,然后去找一个不在X集合里面的顶点
				//所以这里我们可以直接判断目的点是否在X集合里面,如果在,那么一定是环。如果不是,才可以把这条边给加上去
				if (X[min._dsti])
				{
					//cout << "构成环:";
					//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
					continue;
				}

				//把边给加上去
				minTree._AddEdge(min._srci, min._dsti, min._w);
				//cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
				
				//X.insert(min._dsti);
				//Y.erase(min._dsti);
				
				//处理一下目的点的边
				X[min._dsti] = true;
				Y[min._dsti] = false;

				++size;
				totalW += min._w;
				//这里相当于一次优化,因为该循环一定可以保证选出来的n-1条边是最小生成树,
				//后面的优先级队列中的任何一条边一定会导致出现环,会在前面的检测目的点是否在X集合中被处理掉。
				//这里则是直接不用继续入其他的边进入队列了。可以提高一些效率,减少无用的操作
				if (size == n - 1)
				{
					break;
				}
				//当一条边添加完成后,它就属于X集合了,我们可以将该点所延申出的边给加入到优先级队列中
				//只有该边存在,且目的地没有被加入过的时候,才会入队列。值得耐人寻味的是,这里虽然已经处理过一次可能出现环的情况了
				//但是可能由于在添加边的时候,导致某些优先级队列中的边会导致构成环了,所以就有了前面的再次根据目的地时候在X集合中去判环
				for (int i = 0; i < n; i++)
				{
					if (_matrix[min._dsti][i] != Max_W && X[i] == false)
					{
						minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
					}
				}
			}
			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 = GetVertexIndex(src);
			int n = _vertexs.size();
			for (int i = 0; i < n; i++)
			{
				if (i != srci)
				{
					cout << "[" << "pathlenth:" << dist[i] << "]";
					stack<int> path;
					size_t parent = i;
					while (parent != srci)
					{
						path.push(parent);
						parent = pPath[parent];
					}
					path.push(parent);
					cout << "path:";
					while (!path.empty())
					{ 
						int top = path.top();
						path.pop();
						cout << _vertexs[top] << "->";
					}
					cout << "nullptr" << endl;

				}
			}

		}
		//src是起始结点,dist数组的内容是存放最短路径值的权值,即每个元素的内容代表着从起始结点到该结点的最短路径
		//pPath数组是路径的数组,因为有时候我们需要求出路径的具体走法,所以我们可以用数组的方式,类似于并查集去寻找路径的方式,建立一颗树
		void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
		{
			//确定好起始结点的下标
			size_t srci = GetVertexIndex(src);
			int n = _vertexs.size();
			//我们先让所有的最短路径无穷。即还未求出来
			dist.resize(n, Max_W);
			//注意源节点到源节点的最短路径一定是0
			dist[srci] = 0;
			//我们让所有的路径都设置为-1,意味着所有的结点都没有找出来最短路径
			pPath.resize(n, -1);
			//这是表示起点它的路径就是它自己,这步其实只是为了让起点和为求出最短路径给分开表示。不然都用-1可能会混乱
			pPath[srci] = srci;
			//这个S集合代表我们已经求出最短路径的集合。如果是true代表着这个结点早已求出了最短路径。false代表未求出
			vector<bool> S(n, false);
			//注意这里我们做一下特殊处理,虽然我们起点它本来就应该放在这个集合里面,但是我们还是先让它为false。这里我们其实是想与下面的循环进行合并
			//所以迫不得已做的操作,因为一旦将某个结点设置为true,那么它的相邻的结点路径也应该被更新一下,那么这里就要把下面的对于代码拷贝一份。
			//所以为了让代码简洁,我们直接不写这一步了,和下面的进行合并
			//S[srci] = true;

			//这里只是控制一下循环次数,因为我们要求所有的结点都要被遍历一下,而每次只会遍历一个结点,即将一个结点给放入S集合。
			//我们每次点亮的一定是之前从未遍历过的结点
			for(int j = 0; j < n; j++)
			{
				//Dijkstra算法要求每次都要找出一个,还没有被访问过的(不在S集合的),并且是有路径的,且路径是当前最小的一个结点
				//对于第一次找到的就是srci以及所对应的0值
				int u = 0;
				W min = Max_W;
				for (int i = 0; i < n; i++)
				{
					if (S[i] == false && dist[i] < min)
					{
						u = i;
						min = dist[i];
					}
				}
				//找到以后,我们可以让他加入S集合,代表它已经被访问过了。借助这里找到了srci,并且使他加入到S集合
				S[u] = true;
				//松弛更新u连接顶点v, srci->u + u->v < srci->v 就更新
				//注意,这里我们需要的是找到我们新加入S集合的邻接的顶点,然后判断是不是需要更新最短路径,
				//但是我们一开始并不知道,所以我们遍历所有的结点,依次判断条件是否满足
				for (int 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;
					}
				}
			}
		}

		bool BellmanFord(const V& src, vector<W>& dist, vector<int>& parentPath)
		{
			size_t n = _vertexs.size();
			size_t srci = GetVertexIndex(src);

			dist.resize(n, Max_W);
			dist[srci] = W();
			parentPath.resize(n, -1);
			parentPath[srci] = srci;
			
			for (int k = 0; k < n; k++)
			{
				bool update = false;
				for (int i = 0; i < n; i++)
				{
					for (int j = 0; j < n; j++)
					{
						update = true;
						//srci->i + i->j => srci->j
						if (_matrix[i][j] != Max_W && dist[i] + _matrix[i][j] < dist[j])
						{
							dist[j] = dist[i] + _matrix[i][j];
							parentPath[j] = i;
						}
					}
				}
				if (update == false)
				{
					break;
				}
			}
			//还能更新就是带有负权回路
			for (int i = 0; i < n; i++)
			{
				for (int j = 0; j < n; j++)
				{
					//srci->i + i->j => srci->j
					if (_matrix[i][j] != Max_W && dist[i] + _matrix[i][j] < dist[j])
					{
						return false;
					}
				}
			}

		}

		//vvDist中,vvDist[i][j]代表着以i为起点,到j的最短路径
		//vvpPath中,vvpPath[i][j]代表着以i为起点,j为终点的最短路径的上一个父路径。
		void FloydWarShall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
		{
			size_t n = _vertexs.size();
			vvDist.resize(n, vector<W>(n, Max_W));
			vvpPath.resize(n, vector<int>(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];
						//i和j直接相连,即i->j,那么j的上一个父路径一定是i
						vvpPath[i][j] = i;
					}
					if (i == j)
					{
						vvDist[i][j] = W();
					}
				}
			}
			//最短路径的更新,k要考虑每一个结点作为中间结点
			for (int k = 0; k < n; k++)
			{
				for (int i = 0; i < n; i++)
				{
					for (int 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];
							// 从i到j的父路径
							// 也就是说如果K和j直接相连,那么它的父路径就是k本身,而此时vvPath[k][j]正好就是k本身
							// 如果k和j不直接相连,即 i->...->k->...->x->j,此时父路径就是x,而此时vvpPath[k][j]的值其实也是x
							vvpPath[i][j] = vvpPath[k][j];
						}
					}
				}

				//打印一下权值和路径矩阵
				for (int i = 0; i < n; i++)
				{
					for (int j = 0; j < n; j++)
					{
						if (vvDist[i][j] == Max_W)
						{
							printf("%3c", '*');
						}
						else
						{
							printf("%3d", vvDist[i][j]);
						}
					}
					cout << endl;
				}
				cout << endl;
				for (int i = 0; i < n; i++)
				{
					for (int j = 0; j < n; j++)
					{
						printf("%3d", vvpPath[i][j]);
					}
					cout << endl;
				}
				cout << "=================" << endl;



			}


		} 


	private:
		vector<V> _vertexs; //顶点集合
		map<V, int> _indexMap; //顶点对应的下标关系
		vector<vector<W>> _matrix; //临界矩阵
	};


	void TestFloydWarShall()
	{
		const char* str = "12345";
		Graph<char, int, INT_MAX, true> g(str, strlen(str));
		g.AddEdge('1', '2', 3);
		g.AddEdge('1', '3', 8);
		g.AddEdge('1', '5', -4);
		g.AddEdge('2', '4', 1);
		g.AddEdge('2', '5', 7);
		g.AddEdge('3', '2', 4);
		g.AddEdge('4', '1', 2);
		g.AddEdge('4', '3', -5);
		g.AddEdge('5', '4', 6);
		vector<vector<int>> vvDist;
		vector<vector<int>> vvParentPath;
		g.FloydWarShall(vvDist, vvParentPath);
		// 打印任意两点之间的最短路径
		for (size_t i = 0; i < strlen(str); ++i)
		{
			g.PrintShortPath(str[i], vvDist[i], vvParentPath[i]);
			cout << endl;
		}
	}

	void TestGraphBellmanFord()
	{
		//const char* str = "syztx";
		//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('z', 's', 2);
		//g.AddEdge('z', 'x', 7);
		//g.AddEdge('t', 'x', 5);
		//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;
		//}
		//微调图结构,带有负权回路的测试
		const char* str = "syztx";
		Graph<char, int, INT_MAX, true> g(str, strlen(str));
		g.AddEdge('s', 't', 6);
		g.AddEdge('s', 'y', 7);
		g.AddEdge('y', 'x', -3);
		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', '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;
		}
	}


	void TestGraphDijkstra()
	{
		//const char* str = "syztx";
		//Graph<char, int, INT_MAX, true> g(str, strlen(str));
		//g.AddEdge('s', 't', 10);
		//g.AddEdge('s', 'y', 5);
		//g.AddEdge('y', 't', 3);
		//g.AddEdge('y', 'x', 9);
		//g.AddEdge('y', 'z', 2);
		//g.AddEdge('z', 's', 7);
		//g.AddEdge('z', 'x', 6);
		//g.AddEdge('t', 'y', 2);
		//g.AddEdge('t', 'x', 1);
		//g.AddEdge('x', 'z', 4);
		//vector<int> dist;
		//vector<int> parentPath;
		//g.Dijkstra('s', dist, parentPath);
		//g.PrintShortPath('s', dist, parentPath);

		const char* str = "sytx";
		Graph<char, int, INT_MAX, true> g(str, strlen(str));
		g.AddEdge('s', 't', 10);
		g.AddEdge('s', 'y', 5);
		g.AddEdge('t', 'y', -7);
		g.AddEdge('y', 'x', 3);
		vector<int> dist;
		vector<int> parentPath;
		g.Dijkstra('s', dist, parentPath);
		g.PrintShortPath('s', dist, parentPath);

	}



	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();
	}
	void TestGraphBDFS()
	{
		string a[] = { "张三", "李四", "王五", "赵六", "周七" };
		Graph<string, int> g1(a, sizeof(a) / sizeof(string));
		g1.AddEdge("张三", "李四", 100);
		g1.AddEdge("张三", "王五", 200);
		g1.AddEdge("王五", "赵六", 30);
		g1.AddEdge("王五", "周七", 30);
		g1.Print();
		g1.BFS("张三");
		cout << endl;
		g1.BFSLevel("张三");
		cout << endl;
		g1.DFS("张三");
	}
	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;
		//Graph<char, int> kminTree(str, strlen(str));
		//cout << "Kruskal:" << g.Kruskal(kminTree) << endl;
		//kminTree.Print();
		//Graph<char, int> pminTree(str, strlen(str));
		//cout << "Prim:" << g.Prim(pminTree, 'a') << endl;
		//pminTree.Print();

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

运行结果为

image-20240224161523162

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

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

相关文章

python笔记_数据类型

定义&#xff1a;python的变量在使用前必须赋值&#xff0c;数据类型指的是变量指定的内存数据的类型 string字符串类型使用引号int整型整数float浮点型小数bool布尔值(逻辑)输出true/false A&#xff0c;整数类型 整型字节 1,python的整数有十六进制&#xff0c;十进制&#…

HDSRNet | 入局图像超分,异构动态卷积玩出新花样~

首发AIWalker&#xff0c;欢迎关注~ https://arxiv.org/abs/2402.15704 https://github.com/hellloxiaotian/HDSRNet 卷积神经网络可以通过深度网络架构和给定的输入样本自动学习特征。然而&#xff0c;所获得的模型的鲁棒性在不同的场景中可能具有挑战性。网络架构的差异越大…

基于C#开发OPC DA客户端——基于OPCDAAuto

OPC DA OPC DA(OPC Data Access)&#xff0c;即OPC数据访问接口&#xff0c;定义了数据交换的规范&#xff0c;包括&#xff1a;过程值、更新时间、数据品质等信息。 自动化接口中共定义了6类对象&#xff1a;OPCServer对象、OPCBrowser对象、OPCGroups对象、OPCGroup对象、O…

《最新出炉》系列初窥篇-Python+Playwright自动化测试-34-处理https 安全问题或者非信任站点-下篇

1.简介 这一篇宏哥主要介绍playwright如何在IE、Chrome和Firefox三个浏览器上处理不信任证书的情况&#xff0c;我们知道&#xff0c;有些网站打开是弹窗&#xff0c;SSL证书不可信任&#xff0c;但是你可以点击高级选项&#xff0c;继续打开不安全的链接。举例来说&#xff0c…

运用JProfiler分析Java程序中的OOM问题

前言 在Java开发过程中&#xff0c;内存管理是一项至关重要的任务。作为开发者&#xff0c;我们时常会遇到一个让人头疼的问题——Java堆空间溢出&#xff08;OutOfMemoryError&#xff0c;简称OOM&#xff09;。当程序试图分配超出Java虚拟机&#xff08;JVM&#xff09;堆大…

解决error: the following arguments are required问题

今天在运行代码的时候&#xff0c;文件报错如下&#xff1a; mcts.py: error: the following arguments are required: --num_sims, --levels 根据报错信息可以看出这应该是说--num_sims和--levels两个属性并没有定义&#xff0c;但在代码中找了许久&#xff0c;不知道要在哪里…

LeetCode238题:除自身以外数组的乘积(python3)

代码思路&#xff1a; 当前位置的结果就是它左部分的乘积再乘以它右部分的乘积&#xff0c;因此需要进行两次遍历&#xff0c;第一次遍历求左部分乘积&#xff0c;第二次遍历求右部分的乘积&#xff0c;再将最后的计算结果一起求出来。 class Solution:def productExceptSelf(…

外包干了7个月,技术退步明显.......

先说一下自己的情况&#xff0c;大专生&#xff0c;18年通过校招进入北京某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落! 而我已经在一个企业干了四年的功能测…

上位机图像处理和嵌入式模块部署(当前机器视觉新形态)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 过去的机器视觉处理&#xff0c;大部分都是集中在上位机、或者是服务器领域&#xff0c;这种形式维持了很长的时间。这种业务形态下&#xff0c;无…

如何将一台电脑主机分裂成两台、三台?

有用户问&#xff1a;如何将一台电脑主机拆分成两台、三台甚至更多台使用&#xff1f; 这是什么意思&#xff1f; 简单解释一下&#xff1a;在一台计算机主机上&#xff0c;连接两台、三台或者更多台显示器&#xff0c;然后将这台主机的硬件资源分配给这些显示器&#xff0c;然…

Tomcat -2

1. 动静分离 ① 单机反向代理 7-2 代理服务器 7-5 tomcat 设置 7-3 测试&#xff1a; 代理服务器那里写什么就显示什么

外泌体相关基因肝癌临床模型预测——2-3分纯生信文章复现——02.数据格式整理(2)

内容如下&#xff1a; 1.外泌体和肝癌TCGA数据下载 2.数据格式整理 3.差异表达基因筛选 4.预后相关外泌体基因确定 5.拷贝数变异及突变图谱 6.外泌体基因功能注释 7.LASSO回归筛选外泌体预后模型 8.预后模型验证 9.预后模型鲁棒性分析 10.独立预后因素分析及与临床的…

【论文笔记】Improving Language Understanding by Generative Pre-Training

Improving Language Understanding by Generative Pre-Training 文章目录 Improving Language Understanding by Generative Pre-TrainingAbstract1 Introduction2 Related WorkSemi-supervised learning for NLPUnsupervised pre-trainingAuxiliary training objectives 3 Fra…

AttributeError: ‘list‘ object has no attribute ‘view‘

问题描述 训练yolov9的时候遇到了下面的问题。 In loss_tal.py: pred_distri, pred_scores torch.cat([xi.view(feats[0].shape[0], self.no, -1) for xi in feats], 2).split( (self.reg_max * 4, self.nc), 1) The error is as follows&#xff1a; AttributeError: list …

rtt的io设备框架面向对象学习-touch设备

目录 1.触摸设备基类2.触摸设备基类的子类3.初始化/构造流程3.1设备驱动层3.2 设备驱动框架层3.3 设备io管理层 4.总结5.使用5.1实例 1.触摸设备基类 此层处于设备驱动框架层。此层的类是抽象类。 在/ components / drivers / include / drivers /touch.h定义了如下touch设备…

unity 场景烘焙中植物叶片(单面网络)出现的白面

Unity版本 2021.3.3 平台 Windows 在场景烘焙中烘焙植物的模型的时候发现植物的叶面一面是合理的&#xff0c;背面是全白的&#xff0c;在材质球上勾选了双面烘焙&#xff0c;情况如下 这个问题可能是由于植物叶片的单面网格导致的。在场景烘焙中&#xff0c;单面网格只会在一…

nginx:rewrite重写指令及防盗链

目录 一、ngx_http_rewrite_module模块指令 1、if指令 1.1 if指令基本语法 1.2 if指令操作 1.2.1 案例一 1.2.2 案例二 2、return命令 3、set命令 4、break指令 5、rewrite指令 5.1 rewrite指令基本语法 5.1.1 regex正则表达式 5.1.2 flag可选标记 5.2 rewrite指…

【MySQL面试复习】发现了某个SQL语句执行很慢,如何进行分析?

系列文章目录 在MySQL中&#xff0c;如何定位慢查询&#xff1f; 系列文章目录发现了某个SQL语句执行很慢&#xff0c;如何进行分析&#xff1f; 发现了某个SQL语句执行很慢&#xff0c;如何进行分析&#xff1f; 一般SQL语句执行过慢的话需要考虑是否是聚合查询和多表查询&a…

第六节:Vben Admin权限-后端控制方式

系列文章目录 第一节:Vben Admin介绍和初次运行 第二节:Vben Admin 登录逻辑梳理和对接后端准备 第三节:Vben Admin登录对接后端login接口 第四节:Vben Admin登录对接后端getUserInfo接口 第五节:Vben Admin权限-前端控制方式 文章目录 系列文章目录前言一、角色权限(后端…

店匠科技颁布 Shoplazza Awards:品牌出海迎历史性机遇,赋能品牌腾飞

在全球化的今天&#xff0c;中国品牌在全球市场的地位日益显著&#xff0c;品牌意识的提升推动了企业出海战略的全新转型。以全球电商市场发展为例&#xff0c;根据 ecommerceBD 数据&#xff0c;2023 年全球零售电子商务销售额预计 6.3 万亿美元&#xff0c;到 2026 年&#x…