【高阶数据结构】图论

> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:了解什么是图,并能掌握深度优先遍历和广度优先遍历。

> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

> 专栏选自:数据结构

> 望小伙伴们点赞👍收藏✨加关注哟💕💕

一、图的基本概念


1.1 图的定义

概念:

图是一种非线性的数据结构:G=(V,E),它由节点(也称为顶点)和连接这些节点的边组成。图可以用来表示现实世界中的各种关系,如社交网络、交通网络、电路网络等。

1.2 术语解释

  • 顶点(Vertex):图中的基本元素,可以表示任何实体,如人、地点、事物等。顶点集合V={x|x属于某个数据对象集}是有穷非空集合
  • 边(Edge):连接两个顶点的线,表示顶点之间的关系。边的集合:E={(x,y)|x,y属于V}(无向图)或者E={<x,y>|x,y属于V&&Path(x,y)}是顶点间关系的有穷集合
  • 邻接(Adjacent):如果两个顶点通过一条边直接相连,则称这两个顶点是邻接的。
  • 度(Degree):对于无向图而言,一个顶点的度是指与该顶点相连的边的数量。但是对于有向图来说,一个顶点的度分为入度和出度,顶点的入度是以该顶点为终点的有向边的条数,出度则是以该顶点为起点的有向边的条数,顶点的度等于入度和出度之和
  • 路径(Path):顶点序列,其中每对连续的顶点都是邻接的。
  • 环(Cycle):路径中的第一个和最后一个顶点是同一个顶点的情况。
  • 连通图:对于任意两个顶点,都存在一条路径连接它们,即图中每一个顶点都不是单独存在的,它们都可以通过各种路径互相到达(如果有顶点单独存在,没有与其它任意顶点相连,则称这个顶点为一个孤岛)
  • 连通分量:一个图中的不连通部分。

1.3 图的分类

无向图(Undirected Graph)

连接两个顶点的边没有方向,没有方向意味着两个顶点是互相连通的,这种常见的如朋友关系图:我是你的好友,同样你也是我的好友

有向图(Directed Graph)

边有方向,有方向意味着一个顶点可以到另一个顶点,但是反过来不行,这种关系常见的如网页链接图:我可以点击这个链接跳到一个图片,但是无法通过这个图片再跳回原来的界面。

简单图(Simple Graph)

没有重复的边和自环(顶点连接到自身的边)。

多重图(Multigraph)

允许有重复的边和自环。

带权图(Weighted Graph)

每条边上都有权重,一般是数字,可以表示距离、成本等。比如,我们在修从杭州到西安的高铁时,我们可以选择经过郑州,也可以选择经过武汉,这就产生了不同的路径,我们可以比较两个路径的开支来选择经过哪个城市,这就是权值

二、图的存储结构

因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和
边关系即可。节点保存比较简单,只需要一段连续空间即可,那边关系该怎么保存呢?

2.1 邻接矩阵

邻接矩阵概念

使用二维数组来表示图,其中矩阵的元素表示顶点之间的连接情况,顶点之间的关系只有连通与不连通,所以我们可以用0和1来表示。

注意:

  • 无向图的邻接矩阵是对称的,但是有向图的邻接矩阵不一定对称。
  • 如果边上带权值,可以用权值来代替上面的0和1,相连通的顶点可以用权值来表示,不连通的可以用无穷来表示。

  • 邻接矩阵的有点是可以直观的看出两个顶点之间是否相连,但是当顶点过多、边过少的时候,就会存储大量的0,就会很不方便。

简单模拟实现:

#include<iostream>
#include<vector>
#include<string>
#include<map>
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
public:typedef Graph<V, W, MAX_W, Direction> Self;Graph() = default;Graph(const V* vertexs, size_t n){_vertexs.reserve(n);for (size_t i = 0; i < n; ++i){_vertexs.push_back(vertexs[i]);_vIndexMap[vertexs[i]] = i;}// MAX_W 作为不存在边的标识值_matrix.resize(n);for (auto& e : _matrix){e.resize(n, MAX_W);}}size_t GetVertexIndex(const V& v){auto ret = _vIndexMap.find(v);if (ret != _vIndexMap.end()){return ret->second;}else{throw invalid_argument("不存在的顶点");return -1;}}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 << _vertexs[i] << "-" << i << " ";}cout << endl << endl;cout << " ";for (size_t i = 0; i < _vertexs.size(); ++i){cout << i << " ";}cout << endl;// 打印矩阵for (size_t i = 0; i < _matrix.size(); ++i){cout << i << " ";for (size_t j = 0; j < _matrix[i].size(); ++j){if (_matrix[i][j] != MAX_W)cout << _matrix[i][j] << " ";elsecout << "#" << " ";}cout << endl;}cout << endl << 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;}}}}private:map<V, size_t> _vIndexMap;vector<V> _vertexs; // 顶点集合vector<vector<W>> _matrix;  //存储边集合的矩阵
};
void TestGraph()
{Graph<char, int, INT_MAX, true> g("0123", 4);g.AddEdge('0', '1', 1);g.AddEdge('0', '3', 4);g.AddEdge('1', '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();
}
int main()
{TestGraph();return 0;
}

2.2 邻接表

邻接表概念

使用列表来表示图,每个顶点对应一个列表,列表中包含所有与该顶点相连的顶点。

无向邻接表存储:

有向邻接表存储:

注意:

有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其他所有顶点对应的边链表,看有多少边顶点的dst取值是i。

简单模拟实现:

template<class W>
struct LinkEdge
{int _srcIndex;int _dstIndex;W _w;LinkEdge<W>* _next;LinkEdge(const W& w): _srcIndex(-1), _dstIndex(-1), _w(w), _next(nullptr){}
};
template<class V, class W, bool Direction = false>
class Graph
{typedef LinkEdge<W> Edge;
public:Graph(const V* vertexs, size_t n){_vertexs.reserve(n);for (size_t i = 0; i < n; ++i){_vertexs.push_back(vertexs[i]);_vIndexMap[vertexs[i]] = i;}_linkTable.resize(n, nullptr);}size_t GetVertexIndex(const V& v){auto ret = _vIndexMap.find(v);if (ret != _vIndexMap.end()){return ret->second;}else{throw invalid_argument("不存在的顶点");return -1;}}void AddEdge(const V& src, const V& dst, const W& w){size_t srcindex = GetVertexIndex(src);size_t dstindex = GetVertexIndex(dst);// 0 1Edge* sd_edge = new Edge(w);sd_edge->_srcIndex = srcindex;sd_edge->_dstIndex = dstindex;sd_edge->_next = _linkTable[srcindex];_linkTable[srcindex] = sd_edge;// 1 0// 无向图if (Direction == false){Edge* ds_edge = new Edge(w);ds_edge->_srcIndex = dstindex;ds_edge->_dstIndex = srcindex;ds_edge->_next = _linkTable[dstindex];_linkTable[dstindex] = ds_edge;}}
private:map<string, int> _vIndexMap;vector<V> _vertexs; // 顶点集合vector<Edge*> _linkTable;   // 边的集合的临接表
};
void TestGraph()
{string a[] = { "张三", "李四", "王五", "赵六" };Graph<string, int> g1(a, 4);g1.AddEdge("张三", "李四", 100);g1.AddEdge("张三", "王五", 200);g1.AddEdge("王五", "赵六", 30);
}

2.3 边列表(了解)

边列表概念:

使用列表来存储图中的所有边,每条边由两个顶点表示。(这个不常用,在这里不做过多解释,想要了解的可以自行搜索一下)

2.4 邻接矩阵和邻接表的优劣性

邻接矩阵优劣性:

  • 邻接矩阵的存储方式非常适合稠密图。
  • 邻接矩阵O(1)判断两个顶点的连接关系,并取到权值。
  • 不适合查找一个顶点连接的所有边——O(N)。

邻接表优劣性:

  • 邻接表的存储方式非常适合稀疏图。
  • 适合找一个顶点连出去的所有边。
  • 不适合确定两点是否相连以及权值——O(N)。

三、图的遍历


3.1 广度优先遍历

图解:

简单模拟实现:

层序遍历 但是没有一层一层出
void BFS(const V& src) //src表示我们的起点
{size_t srci = GetVertexIndex(src);//找到起点的下标queue<int> q;//存储下标的队列size_t n = _vertexs.size();//表示有多少个节点vector<bool> check(n);q.push(srci);check[srci] = true;//入队列就标记while (!q.empty()){int front = q.front();//取队头q.pop();cout << _vertexs[front] << ":"<<front<<endl;//然后让他的朋友进for (size_t i = 0; i < n; ++i)if (_matrix[front][i] != MAX_W && check[i] == false){q.push(i);check[i] = true;}}
}

如果你对广度优先遍历算法感兴趣的话,大家可以康康下面的算法题:

刷题训练之解决 FloodFill 算法-CSDN博客

3.2 深度优先遍历

图解:

简单模拟实现:

void _DFS(size_t srci, vector<bool>& check) //下一个位置的起点以及需要一个标记数组
{cout << srci << ":" << _vertexs[srci] << " " << endl; //先访问 然后标记为访问过check[srci] = true;for (size_t i = 0; i < _vertexs.size(); ++i)if (_matrix[srci][i] != MAX_W && check[i] == false)_DFS(i, check);
}void DFS(const V& src) //需要有一个起点
{size_t srci = GetVertexIndex(src);//找到起点的下标vector<bool> check(_vertexs.size()); //标记数组_DFS(srci, check);//如果是一个非连通图,可以在后面再进行一层检查check数组,然后对false的数组再进行一次访问
}

如果你对深度优先遍历算法感兴趣的话,大家可以康康下面的算法题:

刷题训练之解决最短路径问题-CSDN博客

四、最小生成树

在了解最小生成树算法之前,我们首先要先了解以下的准则:

连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。

若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。因此构造最小生成树的准则有三条:

  • 只能使用图中的边来构造最小生成树。
  • 只能使用恰好n-1条边来连接图中的n个顶点。
  • 选用的n-1条边不能构成回路(少一条就不连通,多一条就会形成回路)。

4.1 Kruskal算法

算法说明:

任给一个有n个顶点的连通网络N={V,E},首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},其中每个顶点自成一个连通分量,其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止。

核心:

每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边(意思就是不能导致成环),加入生成树。

简单模拟实现:

namespace Matrix  //以邻接矩阵的形式封装
{template<class V,class W, W MAX_W = INT_MAX,bool Direction = false>   //V表示顶点  W表示权重  MAX_W表示默认的权重值  Direction表示是有向图还是无向图    后面两个是非类型模版参数(缺省) class Graph{public://图创建的方式//1、io输入  不方便测试 再oj中较为合适//2、图结构关系写到文件,读取文件//3、手动去添加边,这样会更方便测试!!Graph(const V* vertexs,size_t n) //传一个顶点相关的集合进行初始化{_vertexs.reserve(n);for (size_t i = 0; i < n; ++i) //初始化顶点集合{_vertexs.push_back(vertexs[i]);//存到顶点集合里_IndexMap[vertexs[i]] = i;//建立顶点和下标的映射关系,方便我们进行查找}//初始化邻接矩阵_matrix.resize(n);for (auto& e : _matrix)   e.resize(n, MAX_W);}//获取顶点的下标(为什么要单独给这样一个函数呢?因为可能给的是一个错误的顶点,要检查一下)size_t GetVertexIndex(const V& v){//有可能顶点会给错,这样在map中就找不到 所以要先检查一下auto it = _IndexMap.find(v);if (it != _IndexMap.end())  return it->second;else{throw invalid_argument("不存在的顶点");//抛异常(异常被捕获后可以不做处理)//断言太暴力了,会直接终止程序,并且断言在release版本下会被屏蔽//异常被捕获后可以不作处理,程序从捕获位置继续执行。 而断言是完全无法忽略的,程序在断言失败处立即终止。//  因此断言通常用于调试版本,用来发现程序中的逻辑错误。 虽然异常也能起到这样的作用,但是不应该用异常代替断言: // 1) 如果发现了逻辑错误,必须修改程序,而不可能在程序中进行处理和恢复,所以不需要向外传送,没有必要使用异常。//  2) 使用断言的开销比异常小得多,而且断言可以从发布版中完全去除。return -1; //还是要返回,因为编译器不会在乎运行,而是会检查你在这边有没有返回}}//利用序号为两个节点添加边void _AddEdge(size_t srci, size_t dsti, const W& w){_matrix[srci][dsti] = w;//如果是无向图if (Direction == false)   _matrix[dsti][srci] = w;}//对两个节点添加边,以及权重关系  src代表起点 dst代表中点 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 BFS(const V& src) //src表示我们的起点//{//	size_t srci = GetVertexIndex(src);//找到起点的下标//	queue<int> q;//存储下标的队列//	size_t n = _vertexs.size();//表示有多少个节点//	vector<bool> check(n);//	q.push(srci);//	check[srci] = true;//入队列就标记//	while (!q.empty())//	{//		int front = q.front();//取队头//		q.pop();//		cout << _vertexs[front] << ":"<<front<<endl;//		//然后让他的朋友进//		for (size_t i = 0; i < n; ++i)//			if (_matrix[front][i] != MAX_W && check[i] == false)//			{//				q.push(i);//				check[i] = true;//			}//	}//}//控制一层一层出void BFS(const V& src) //src表示我们的起点{size_t srci = GetVertexIndex(src);//找到起点的下标queue<size_t> q;//存储下标的队列size_t n = _vertexs.size();//表示有多少个节点vector<bool> check(n);q.push(srci);check[srci] = true;//入队列就标记while (!q.empty()){//控制一层一层出size_t sz = q.size();for (size_t i = 0; i < sz; ++i) //一层出完了再去走下一层{size_t front = q.front();//取队头q.pop();cout << front<< ":" << _vertexs[front] << " ";//然后让他的朋友进for (size_t i = 0; i < n; ++i)if (_matrix[front][i] != MAX_W && check[i] == false){q.push(i);check[i] = true;}}cout << endl;}}void _DFS(size_t srci, vector<bool>& check) //下一个位置的起点以及需要一个标记数组{cout << srci << ":" << _vertexs[srci] << " " << endl; //先访问 然后标记为访问过check[srci] = true;for (size_t i = 0; i < _vertexs.size(); ++i)if (_matrix[srci][i] != MAX_W && check[i] == false)_DFS(i, check);}void DFS(const V& src) //需要有一个起点{size_t srci = GetVertexIndex(src);//找到起点的下标vector<bool> check(_vertexs.size()); //标记数组_DFS(srci, check);//如果是一个非连通图,可以在后面再进行一层检查check数组,然后对false的数组再进行一次访问}void Print(){//顶点for (size_t i = 0; i <_vertexs.size() ; ++i)cout << "[" << i << "]" << "->" << _vertexs[i] << endl; //下标映射顶点集合cout << endl;//打印横一行的下标cout << "  ";for (size_t i = 0; i < _vertexs.size(); ++i) cout << i << " ";  cout << endl;//打印矩阵for (size_t i = 0; i < _matrix.size(); ++i){cout << i << " ";for (size_t j = 0; j < _matrix[i].size(); ++j)if (_matrix[i][j] == MAX_W) cout << '*' << " ";else cout << _matrix[i][j] << " ";cout << endl;}cout << endl;}private:map<V, size_t> _IndexMap;    //建立顶点和下标之间的关系,方便我们根据顶点去找他的下标     比如两个顶点是否存在关系,就可以快速找到两个顶点的下标,然后去邻接矩阵看一下vector<V> _vertexs;			 // 顶点集合  vector<vector<W>> _matrix;   // 存储边集合的矩阵};

4.2 Prim算法

算法说明:

简单模拟现实:

W Prim(Self& minTree, const V& src)  //Prim算法需要有一个起点     Prim适合稠密图 因为他的选取模式跟顶点有关系
{size_t n = _vertexs.size();//先初始化一下最小生成树minTree._vertexs = _vertexs;minTree._IndexMap = _IndexMap;minTree._matrix.resize(n);for (auto& e : minTree._matrix)  e.resize(n, MAX_W); //初始化成初始状态//  用一个set 或者是两个bool数组来帮助我们判环size_t srci = GetVertexIndex(src);set<size_t> inSet; //帮助我们存已经选过的顶点集合inSet.insert(srci);//先将起点丢进set中priority_queue<Edge, vector<Edge>, greater<Edge>> heap;//建小堆 for (size_t i = 0; i < n; ++i)//先将跟起点相连的所有边丢到优先级队列中(无差别入队列,但是多一层判断)if (_matrix[srci][i] != MAX_W)heap.push(Edge(srci, i, _matrix[srci][i]));//开始走选边的主逻辑W total = W();//统计总权重值while (inSet.size() < n && !heap.empty()){Edge min = heap.top();heap.pop();//看看两个点是否同时在set中if (inSet.find(min._dsti) == inSet.end())//必然是从srci出发的,所以我们只要保证dsti不在set中即可{cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << _matrix[min._srci][min._dsti] << endl;//都不在set中,此时将边放到最小生成树中.minTree._AddEdge(min._srci, min._dsti, min._w);total += min._w;//然后将新丢进去的顶点的相连的边丢进优先级队列中for (size_t i = 0; i < n; ++i)//先将跟起点相连的所有边丢到优先级队列中if (_matrix[min._dsti][i] != MAX_W && inSet.find(i) == inSet.end()) //Set中有的就不要丢进去了heap.push(Edge(min._dsti, i, _matrix[min._dsti][i]));inSet.insert(min._dsti);}else{cout << "成环了所以不选:";cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << _matrix[min._srci][min._dsti] << endl;}}//检测  因为该图可能是一个不连通的图,如果不连通的话就返回W的默认构造!if (inSet.size() == n) return total;else return W();
}

4.3 总结

Kruskal算法的时间复杂度为O(ElogE),其中E为边数。  E表示往优先级队列中丢边的过程,而logE表示优先级队列的调整。所以说边越少的复杂度就越低,所以说Kruskal算法适合稀疏图。

Prim算法的时间复杂度是O(N^2),遍历多久是取决于有多少个顶点,如果是稀疏图的话,他的边少,但是他的顶点也不一定少,所以说Prim算法更适合稠密图!!!

Prim算法得到的最小生成树是以一个顶点为起点的树形结构,而Kruskal算法得到的最小生成树是以边为基础的森林,需要进行额外的处理(依赖并查集判环)才能形成树。要根据不同的场景去选择。

五、最短路径


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

单源最短路径问题:

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

针对一个带权有向图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算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径。

图解:

简单模拟现实:

	//时间复杂度 O(N^2)  空间复杂度O(N)void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath) //src表示起点, dist表示最短路径  path表示父路径(可以帮助我们找到当前记录权值所经过的全部路径){//先初始化一下我们的数组size_t srci = GetVertexIndex(src);size_t n = _vertexs.size();dist.resize(n, MAX_W);pPath.resize(n, -1);dist[srci] = W();//给一个初始值,方便第一次找到这个节点//需要设置一个bool数组帮助我们确定已经确定的最短路径的集合vector<bool> s(n);//先去我的各个路径去更新一下  一方面是去更新我们的dsti 一方面去找到最短的顶点ufor (size_t i = 0; i < n; ++i) //一次走一个顶点 走n次{W min = MAX_W;//记录最小权重size_t u = srci;//记录最小顶点//先找到起点for (size_t j = 0; j < n; ++j)if (s[j] == false && dist[j] < min){min = dist[j];u = j;}s[u] = true;//表示确定了点//以这个往外做松弛操作  更新一遍u连接的所有边  看看有没有可以更新的for (size_t v = 0; v < n; ++v)if (s[v] == false && _matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v]){dist[v] = dist[u] + _matrix[u][v];pPath[v] = u;}}}// 为了方便测试,实现一个打印路径的函数
void PrinrtShotPath(const V & src, const vector<W>&dist, const vector<int>&pPath)
{size_t srci = GetVertexIndex(src);size_t n = _vertexs.size();for (size_t i = 0; i < n; ++i){if (i == srci) continue;vector<int> path;size_t parenti = i;while (parenti != srci){path.push_back(parenti);parenti = pPath[parenti];}path.push_back(srci);reverse(path.begin(), path.end());for (auto& index : path)cout << _vertexs[index] << "->";cout << "权值和:"<<dist[i];cout << endl;}
}

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

Bellman—ford算法:

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

图解:

简单模拟现实:

bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
{//先初始化一下我们的数组size_t srci = GetVertexIndex(src);size_t n = _vertexs.size();dist.resize(n, MAX_W);pPath.resize(n, -1);dist[srci] = W();//给一个初始值,方便第一次找到这个节点bool flag = false;for (size_t k = 0; k < n; ++k) //按道理来说只需要进行n-1次必然就不需要松弛了{flag = false;//前置为falsefor (size_t i = 0; i < n; ++i)for (size_t j = 0; j < n; ++j)if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){dist[j] = dist[i] + _matrix[i][j];pPath[j] = i;flag = true;}if (flag == false)  break;//如果没有发生松弛 }//但是还得检查负权值回路//循环的最后一趟就是用来检查负权值的,如果最后一趟后flag仍为true 说明就有负权回路 应该return false   负权回路神仙难救return !flag;
}

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

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}取得的一条最短路径。

说明:

其实多源最短路径按照Dijkstra算法或者是Bellman-Ford算法以所有点为起点也可以解决这个问题,但是前者不能解决负权值,后者的效率太低,因此都不够完美,所以才需要Floyd-Warshall算法来帮助我们解决多源最短路径问题,其实本质上是一种动态规划思想。

图解:

简单代码实现:

	void FloydWarshall(vector<vector<W>>& dist, vector<vector<int>>& pPath)  //二维权值矩阵和父路径矩阵{//初始化 权值和父路径矩阵size_t n = _vertexs.size();dist.resize(n);pPath.resize(n);for (size_t i = 0; i < n; ++i){dist[i].resize(n,MAX_W);pPath[i].resize(n,-1);}//先将已有相连的边初始化到dist数组中for (size_t i = 0; i < n; ++i)for (size_t j = 0; j < n; ++j){if (_matrix[i][j] != MAX_W){dist[i][j] = _matrix[i][j];pPath[i][j] = i;}if (i == j) dist[i][j] = W();//方便和图对比,并且0不会影响其他路径}//利用k作为中间结点,去动态规划更新i->j的路径for (size_t k = 0; k < n; ++k){for (size_t i = 0; i < n; ++i)for (size_t j = 0; j < n; ++j)if (dist[i][k] != MAX_W && dist[k][j] != MAX_W && dist[i][k] + dist[k][j] < dist[i][j]){dist[i][j] = dist[i][k] + dist[k][j];pPath[i][j] = pPath[k][j];//这里要填的是j的上一个邻接顶点}//打印权值和路径矩阵观察数据 ctrl+k ctrl+f 自动格式化// 打印权值和路径矩阵观察数据for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j)if (dist[i][j] == MAX_W) printf("%3c", '*');else printf("%3d", dist[i][j]);cout << endl;}cout << endl;for (size_t i = 0; i < n; ++i){for (size_t j = 0; j < n; ++j)printf("%3d", pPath[i][j]+1);//方便和图对比cout << endl;}cout << "=================================" << endl;}}

六、结束语 

今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

​​ 

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

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

相关文章

日期(练习)

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title></title> </head> <body></body> <script>// 定义一个函数&#xff0c;实现格式化日期对象&#xff0c;返回yyyy-MM-dd…

【IDEA】解决总是自动导入全部类(.*)问题

文章目录 问题描述解决方法 我是一名立志把细节说清楚的博主&#xff0c;欢迎【关注】&#x1f389; ~ 原创不易&#xff0c; 如果有帮助 &#xff0c;记得【点赞】【收藏】 哦~ ❥(^_-)~ 如有错误、疑惑&#xff0c;欢迎【评论】指正探讨&#xff0c;我会尽可能第一时间回复…

企业使用知识管理工具与技术的好处(举例说明)

我们都知道“知识就是力量”这句老话&#xff0c;无论是在工作还是个人生活中&#xff0c;我们每一天都越来越认识到这句话的真谛。近年来&#xff0c;不可否认的是&#xff0c;全球范围内我们都在某种程度上缺乏对于许多企业和大型公司至关重要的高端技术技能。 当然&#xf…

机器学习系列-决策树

文章目录 1. 决策树原理决策树的构建流程 2. 案例步骤 1&#xff1a;计算当前节点的熵步骤 2&#xff1a;对每个特征计算分裂后的熵(1) 按“天气”分裂数据集(2) 计算分裂后的加权熵 步骤 3&#xff1a;计算分裂依据信息增益信息增益率GINI系数&#xff08;二叉树&#xff09; …

resnet50,clip,Faiss+Flask简易图文搜索服务

一、实现 文件夹目录结构&#xff1a; templates -----upload.html faiss_app.py 前端代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widt…

爬虫重定向问题解决

一&#xff0c;问题 做爬虫时会遇到强制重定向的链接&#xff0c;此时可以手动获取重定向后的链接 如下图情况 第二个链接是目标要抓取的&#xff0c;但它是第一个链接重定向过去的&#xff0c;第一个链接接口状态也是302 二&#xff0c;解决方法 请求第一个链接&#xff0c…

一个小的可编辑表格问题引起的思考

11.21工作中遇到的问题 预期&#xff1a;当每行获取红包金额的时候若出现错误&#xff0c;右侧当行会出现提示 结果&#xff1a;获取红包金额出现错误&#xff0c;右侧对应行并没有出现错误提示 我发现&#xff0c;当我们设置readonly的时候&#xff0c;其实render函数依旧是…

高效集成:金蝶盘亏单数据对接管易云

金蝶盘亏单数据集成到管易云的技术实现 在企业日常运营中&#xff0c;数据的高效流转和准确对接是确保业务顺利进行的关键。本文将聚焦于一个具体的系统对接集成案例&#xff1a;如何将金蝶云星空中的盘亏单数据无缝集成到管易云的其他出库模块。 为了实现这一目标&#xff0…

神经网络问题之一:梯度消失(Vanishing Gradient)

梯度消失&#xff08;Vanishing Gradient&#xff09;问题是深度神经网络训练中的一个关键问题&#xff0c;它主要发生在反向传播过程中&#xff0c;导致靠近输入层的权重更新变得非常缓慢甚至几乎停滞&#xff0c;严重影响网络的训练效果和性能。 图1 在深度神经网络中容易出现…

单神经元 PID 解耦控制

单神经元 PID 解耦控制是一种将单神经元自适应控制与解耦控制相结合的方法&#xff0c;适用于多输入多输出&#xff08;MIMO&#xff09;系统。其核心是利用单神经元的自适应能力实现 PID 参数在线调整&#xff0c;同时通过解耦策略减少变量之间的相互影响&#xff0c;提高控制…

数据库类型介绍

1. 关系型数据库&#xff08;Relational Database, RDBMS&#xff09;&#xff1a; • 定义&#xff1a;基于关系模型&#xff08;即表格&#xff09;存储数据&#xff0c;数据之间通过外键等关系相互关联。 • 特点&#xff1a;支持复杂的SQL查询&#xff0c;数据一致性和完整…

线性回归 - 最小二乘法

线性回归 一 简单的线性回归应用 webrtc中的音视频同步。Sender Report数据包 NTP Timestamp&#xff08;网络时间协议时间戳&#xff09;&#xff1a;这是一个64位的时间戳&#xff0c;记录着发送SR的NTP时间戳&#xff0c;用于同步不同源之间的时间。RTP Timestamp&#xff1…

AWD脚本编写_1

AWD脚本编写_1 shell.php&#xff08;放在网站根目录下&#xff09; <?php error_reporting(0); eval($_GET["yanxiao"]); ?>脚本编写成功 后门文件利用与解析 import requests import base64def get_flag(url, flag_url, method, passwd, flag_path):cmd…

Linux环境基础开发工具的使用(yum、vim、gcc、g++、gdb、make/Makefile)

目录 Linux软件包管理器 - yum Linux下安装软件包的方式 认识yum 查找软件包 安装软件 如何实现本地机器和云服务器之间的文件互传 卸载软件 Linux编辑器 - vim vim的基本概念 vim下各模式的切换 批量化注释 vim的简单配置 Linux编译器 - gcc/g gcc/g的作用 gcc/g语…

IDEA如何设置编码格式,字符编码,全局编码和项目编码格式

前言 大家好&#xff0c;我是小徐啊。我们在开发Java项目&#xff08;Springboot&#xff09;的时候&#xff0c;一般都是会设置好对应的编码格式的。如果设置的不恰当&#xff0c;容易造成乱码的问题&#xff0c;这是要避免的。今天&#xff0c;小徐就来介绍下我们如何在IDEA…

【Redis】实现点赞功能

一、实现笔记点赞 使用redis实现点赞功能&#xff0c;对于一个笔记来说&#xff0c;不同用户只能是点赞和没点赞&#xff0c;点赞过的笔记再点击就应该取消点赞&#xff0c;所以实际上根据需求&#xff0c;我们只需要将点赞的数据存到对应的笔记里&#xff0c;查看对应的笔记相…

InstantStyle容器构建指南

一、介绍 InstantStyle 是一个由小红书的 InstantX 团队开发并推出的图像风格迁移框架&#xff0c;它专注于解决图像生成中的风格化问题&#xff0c;旨在生成与参考图像风格一致的图像。以下是关于 InstantStyle 的详细介绍&#xff1a; 1.技术特点 风格与内容的有效分离 &a…

Redisson学习教程(B站诸葛)

弱智级别 package org.example.controller;public class IndexController {Autowiredprivate Redisson redisson;Autowiredprivate StringRedisTemplate stringRedisTemplate;RequestMapping("/deduct_storck")public String deductStock() {String lockKey "…

蓝桥杯每日真题 - 第19天

题目&#xff1a;&#xff08;费用报销&#xff09; 题目描述&#xff08;13届 C&C B组F题&#xff09; 解题思路&#xff1a; 1. 问题抽象 本问题可以看作一个限制条件较多的优化问题&#xff0c;核心是如何在金额和时间约束下选择最优方案&#xff1a; 动态规划是理想…

科研实验室的数字化转型:Spring Boot系统

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理实验室管理系统的相关信息成为必然。开发合…