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

文章目录

  • 一、最短路径
  • 二、单源最短路径 -- 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;}//图不连通,直接返回0else{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);//注意源节点到源节点的最短路径一定是0dist[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);//注意源节点到源节点的最短路径一定是0dist[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;}//图不连通,直接返回0else{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);//注意源节点到源节点的最短路径一定是0dist[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->jif (_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->jif (_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;}//图不连通,直接返回0else{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);//注意源节点到源节点的最短路径一定是0dist[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->jif (_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->jif (_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的上一个父路径一定是ivvpPath[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]的值其实也是xvvpPath[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.mzph.cn/news/714063.shtml

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

相关文章

Python调用ChatGPT API使用国内中转key 修改接口教程

大家好&#xff0c;我是淘小白~ 有的客户使用4.0的apikey ,直接使用官方直连的apikey消费很高&#xff0c;有一位客户一个月要消费2万&#xff0c;想使用4.0中转的apikey&#xff0c;使用中转的apikey 需要修改官方的openai库&#xff0c;下面具体说下。 1、首先确保安装的op…

Java ElasticSearch-Linux面试题

Java ElasticSearch-Linux面试题 前言1、守护线程的作用&#xff1f;2、链路追踪Skywalking用过吗&#xff1f;3、你对G1收集器了解吗&#xff1f;4、你们项目用的什么垃圾收集器&#xff1f;5、内存溢出和内存泄露的区别&#xff1f;6、什么是Spring Cloud Bus&#xff1f;7、…

安装ProxySQL,教程及安装链接(网盘自提)

一、网盘下载&#xff0c;本地直传 我网盘分享的是proxysql-2.5.5-1-centos8.x86_64.rpm&#xff0c;yum或者dnf直接安装就行 提取码&#xff1a;rhelhttps://pan.baidu.com/s/1nmx8-h8JEhrxQE3jsB7YQw 官方安装地址 官网下载地址https://repo.proxysql.com/ProxySQL/ 二、…

【AIGC】“光影交织的恋曲:绝美情侣在蓝天下的深情互动“

外貌特征 (Physical Appearance)&#xff1a;给远景镜头&#xff0c;这对情侣拥有出众的容貌和气质。男子身材挺拔&#xff0c;五官立体鲜明&#xff0c;阳光洒在他俊朗的脸庞上&#xff0c;更显英气逼人&#xff1b;女子则拥有一头柔顺亮丽的秀发&#xff0c;明亮的眼睛如同星…

GPT-4论文精读【论文精读·53】

Toolformer 今天我们来聊一下 GPT 4&#xff0c;但其实在最开始准备这期视频的时候&#xff0c;我是准备讲 Toolformer 这篇论文的&#xff0c;它是 Meta AI 在2月初的时候放出来的一篇论文。说这个大的语言模型可以利用工具了&#xff0c;比如说它就可以去调用各种各样的API&a…

腾讯云优惠券领取的三个渠道,一个比一个优惠!

腾讯云代金券领取渠道有哪些&#xff1f;腾讯云官网可以领取、官方媒体账号可以领取代金券、完成任务可以领取代金券&#xff0c;大家也可以在腾讯云百科蹲守代金券&#xff0c;因为腾讯云代金券领取渠道比较分散&#xff0c;腾讯云百科txybk.com专注汇总优惠代金券领取页面&am…

Unity(第二十四部)UI

在游戏开发中&#xff0c;用户界面&#xff08;UI&#xff09;是至关重要的一部分。它负责与玩家进行交互&#xff0c;提供信息&#xff0c;并增强游戏的整体体验。Unity 提供了强大的工具和功能来创建和管理 UI。 ui的底层就是画布&#xff0c;创建画布的时候会同时创建一个事…

C++ STL标准程序库开发指南学习笔记

一、类模板简介&#xff1a; 在现今的C标准模板库中&#xff0c;几乎所有的东西都被设计为template形式&#xff0c;不支持模板&#xff0c;就无法使用标准程序库。模板库可以认为是针对一个或多个尚未明确的类型而编写一套函数或类型。模板是C的一个新特性。通过使用模板&…

【前端素材】推荐优质电商类后台管理系统网页Vuesax平台模板(附源码)

一、需求分析 在线后台管理系统是指供管理员或运营人员使用的Web应用程序&#xff0c;用于管理和监控网站、应用程序或系统的运行和数据。它通常包括一系列工具和功能&#xff0c;用于管理用户、内容、权限、数据等。下面是关于在线后台管理系统的详细分析&#xff1a; 1、功…

前端 css 实现标签的效果

效果如下图 直接上代码&#xff1a; <div class"label-child">NEW</div> // css样式 // 父元素 class .border-radius { position: relative; overflow: hidden; } .label-child { position: absolute; width: 150rpx; height: 27rpx; text-align: cente…

web服务器nginx下载及在win11的安装

一.背景 还是为了公司安排的师带徒任务。 操作系统版本&#xff1a;win11 家庭版 mginx版本&#xff1a;1.24.0 二.nginx版本选择与下载 我之前也写过下载nginx下载_ngnix stable 下载-CSDN博客 不想看寻找过程的&#xff0c;直接点这里去下载https://nginx.org/download…

【王道操作系统】ch1计算机系统概述-04操作系统结构

文章目录 【王道操作系统】ch1计算机系统概述-04操作系统结构操作系统的内核操作系统的体系结构考纲新增内容&#xff08;红色为全新内容&#xff0c;黄色为原有内容&#xff09;&#xff1a;01 分层结构02 模块化03 宏内核&#xff08;大内核&#xff09;和微内核04 外核 【王…

redis03 八种数据类型

思维草图 String类型 字符串类型&#xff0c;是redis中最简单的存储类型&#xff0c;可以包含任何数据&#xff0c;例如jpg图片或者序列化的对象等&#xff0c;底层都是以字节数组形式存储&#xff0c;最大能存储512MB的数据。 常用命令 KEY命名规范 加前缀&#xff0c;分…

802.11局域网的 MAC 帧

目录 802.11 局域网的 MAC 帧 802.11 数据帧的三大部分 1.关于 802.11 数据帧的地址 最常用的两种情况 2.序号控制字段、持续期字段和帧控制字段 802.11 局域网的 MAC 帧 802.11 帧共有三种类型&#xff1a;控制帧、数据帧和管理帧。 802.11 数据帧的三大部分 MAC 首部&…

基于 STM32U5 片内温度传感器正确测算温度

目录预览 1、引言 2、问题 3、小结 01 引言 STM32 在内部都集成了一个温度传感器&#xff0c;STM32U5 也不例外。这个位于晶圆上的温度传感器虽然不太适合用来测量外部环境的温度&#xff0c;但是用于监控晶圆上的温度还是挺好的&#xff0c;以防止芯片过温运行。 02 问题…

sqllabs第五关floor报错注入

实验环境sqllabs第五关 floor()报错注入的原因是group by在向临时表插入数据时&#xff0c;由于rand()多次计算导致插入临时表时主键重复&#xff0c;从而报错&#xff0c;又因为报错前concat()中的SQL语句或函数被执行&#xff0c;所以该语句报错且被抛出的主键是SQL语句或函…

深入理解Lambda表达式:基础概念与实战演练【第114篇—python:Lambda表达式】

深入理解Lambda表达式&#xff1a;基础概念与实战演练 在现代编程语言中&#xff0c;Lambda表达式作为一种轻量级的匿名函数形式&#xff0c;越来越受到程序员的青睐。特别是在函数式编程兴起的今天&#xff0c;Lambda表达式在简化代码、提高可读性方面发挥着重要作用。本文将…

C++笔记(六)--- 静态成员变量/函数(static)

目录 C语言中静态变量 C静态成员变量 C静态成员函数 C语言中静态变量 1.函数内部用static修饰的变量&#xff0c;只能在此函数使用&#xff0c;但所修饰的变量不随函数释放而释放&#xff0c;下次调用时的值仍为上次结束时的值2.static修饰的全局变量只能在其定义的文件使用…

数据可视化原理-腾讯-热力图

在做数据分析类的产品功能设计时&#xff0c;经常用到可视化方式&#xff0c;挖掘数据价值&#xff0c;表达数据的内在规律与特征展示给客户。 可是作为一个产品经理&#xff0c;&#xff08;1&#xff09;如果不能够掌握各类可视化图形的含义&#xff0c;就不知道哪类数据该用…

CSP-201712-2-游戏

CSP-201712-2-游戏 解题思路 初始化变量&#xff1a;定义整数变量n和k&#xff0c;分别用来存储小朋友的总数和淘汰的特定数字。然后定义了num&#xff08;用来记录当前报的数&#xff09;和peopleIndex&#xff08;用来记录当前报数的小朋友的索引&#xff09;。 初始化小朋…