【数据结构】图和基本算法

文章目录

  • 1. 图的基本概念
    • 1.1 图本身的定义
    • 1.2 相关概念
  • 2. 图的存储结构
    • 2.1 邻接矩阵
    • 2.2 邻接表
  • 3. 图的遍历
    • 3.1 广度优先遍历(BFS)
    • 3.2 深度优先遍历(DFS)
  • 4. 最小生成树
    • 4.1 Kruskal算法
    • 4.2 Prim算法
  • 5. 最短路径
    • 5.1 单源最短路径–Dijkstra算法
    • 5.2 单源最短路径–Bellman-Ford算法
    • 5.3 多源最短路径–floyd-Warshall算法

1. 图的基本概念

1.1 图本身的定义

图(Graph)是由顶点和顶点之间的关系组成的一种结构,其中顶点(Vertex)和边(Edge)是图的两个要素,所以我们把一个图表示为G=(V, E)

在一个图里面会有若干个顶点,我们描述这些顶点用的是一个集合,在数学上的表示方式就是:**顶点集合V = {x | x术语某个数据对象集} **, V是一个有穷非空集合。

要描述一个边的时候,本质上是在描述两个顶点之间的关系,所以一条边的要素就是对应的两个顶点。对于这个边来说,他有可能是单向的也有可能是双向的,如果这个边是双向的,就用**(x,y)来表示x和y之间的一条双向边(无向边),如果是单向的,就用path(x,y)**来描述一条从x到y的边

在一个图里面会有若干个边,所以边也要组成一个集合,描述方式为**边的集合 E = {(x,y) | x,y属于V} **或者 E = {<x, y> | x,y属于V && path(x, y)}E 是顶点间关系的有穷集合

(x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即 Path(x, y)是有方向的。

1.2 相关概念

  • 顶点和边: 图中节点称为顶点, 第i个顶点记作vi, 两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,途中的第k条边记作ek, ek = (vi, vj) 或 <vi, vj>

  • 有向图和无向图:有向和无向是边的属性,如果一个图的边是有向的path(x,y)和path(y,x)不是同一条边),那么这个图就被称为是有向图,反之则是无向图. 比如下图G3和G4为有向图。在无向图中,顶点对(x, y)是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x)是同一条边,比如下图G1和G2为无向图。注意:无向边(x, y)等于有向边<x, y>和<y, x>。比

    注意:无向边(x, y)等于有向边<x,y>和<y,x>。

  • 完全图:在有n个顶点的无向图中,若有n * (n-1)/2条边,即任意两个顶点之间有且仅有一条边, 则称此图为无向完全图,比如下图G1;在n个顶点的有向图中,若有n * (n-1)条边,即任意两个 顶点之间有且仅有方向相反的边,则称此图为有向完全图,比如下图G4

  • 邻接顶点:在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依附于顶点u和v;在有向图G中,若<u, v>是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶点u,并称边<u, v>与顶点u和顶点v相关联

  • 顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点的度等于该顶点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。注意:对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)

image-20240503113859178

  • 路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径
  • 路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和
  • 权值: 边附带的数据信息

image-20240503114413579

  • 简单路径与回路:若路径上各顶点v1,v2,v3,…,vm均不重复,则称这样的路径为简单路径。若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环

image-20240503114523926

子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。

image-20240503114539451

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

强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj到vi的路径,则称此图是强连通图

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

2. 图的存储结构

在上面我们了解到图的基础概念,那么要使用图的话,就需要把他在计算机内描述出来,由于图的要素有节点和边两个,对于图的描述,也就是存储结构,只需要保存节点和边的关系即可. 对于节点的表示,非常简单,使用一段连续的空间即可.主要是对于边的保存怎么处理? 我们有两种方式邻接矩阵邻接表

2.1 邻接矩阵

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

image-20240503122629890

如上图中G1,A-B之间有一条边,所以在矩阵内[A,B]和[B,A]对应的位置就会被置为1

值得注意的是:

  1. 无向图的邻接矩阵是对称的,第i行(列)元素之和,就是顶点i的度。有向图的邻接矩阵则不一定是对称的,第i行(列)元素之后就是顶点i 的出(入)度。
  2. 如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替
  3. 用邻接矩阵存储图的有点是能够快速知道两个顶点是否连通,缺陷是如果顶点比较多,边比较少时,矩阵中存储了大量的0成为系数矩阵,比较浪费空间,并且要求两个节点之间的路径不是很好求

image-20240503122733489

使用邻接矩阵存储的图结构设计

//  V:顶点类型    W:权值类型   MAX_W:权值的默认值     Direction:是否为有向图 
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
private:map<V, int> _vIndexMap; // 顶点和对应下标的映射vector<V> _vertexs; // 顶点的集合vector<vector<W>> _matrix; // 邻接矩阵
public:typedef Graph<V, W, MAX_W, Direction> Self;Graph(const V* vertexs, int n){// 初始化顶点集合for (int i = 0; i < n; ++i){_vertexs.push_back(vertexs[i]);_vIndexMap.insert({ vertexs[i] , i });}// 初始化所有的边,默认没有边,所有的权值都为INT_MAXint weight = MAX_W;if (Direction == false)weight = 0; // 无向图用01表示即可_matrix.resize(n);for (auto& e : _matrix){e.resize(n, weight);}}int GetVertexIndex(const V& v){auto it = _vIndexMap.find(v);if (it == _vIndexMap.end())return -1;elsereturn it->second;}void AddEdge(const V& src, const V& dst, W w = 1){int srcidx = GetVertexIndex(src);int dstidx = GetVertexIndex(dst);if (srcidx == -1 || dstidx == -1){cout << "输入的边有误" << endl;return;}if (Direction == false){_matrix[srcidx][dstidx] = _matrix[dstidx][srcidx] = w;}else{_matrix[srcidx][dstidx] = 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;}}}}
};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();
}

2.2 邻接表

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

  1. 无向图邻接表存储

image-20240503124919599

注意:无向图中同一条边在邻接表中出现了两次。如果想知道顶点vi的度,只需要知道顶点vi边链表集合中结点的数目即可

  1. 有向图邻接表存储

image-20240503125047851

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

使用邻接矩阵存储的图结构设计

template<class W>
struct Edge // 边
{int srci; // 边的起点编号int dsti; // 边的终点编号W w; // 边的权值Edge(int srci_, int dsti_, W w_) : srci(srci_), dsti(dsti_), w(w_) {}
};template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{typedef Edge<W> Edge;private:map<V, int> _vIndexMap; // 存储顶点到顶点编号的下标vector<list<Edge>> _linkTable; // 存储以对应编号为起点的边的链表public:Graph(const V* vertexs, int n){_linkTable.resize(n);for (int i = 0; i < n; ++i){_vIndexMap[vertexs[i]] = i;}}int GetIndex(const V& v){auto ret = _vIndexMap.find(v);if (ret != _vIndexMap.end())return ret->second;elsereturn -1;}void AddEdge(const V& src, const V& dst, W w){int srci = GetIndex(src);int dsti = GetIndex(dst);if (srci == -1 || dsti == -1){cout << "找不到指定顶点,插入失败" << endl;}_linkTable[srci].push_back(Edge(srci, dsti, w));if (Direction == false) // 无向图需要添加dsti->srci的边{_linkTable[dsti].push_back(Edge(dsti, srci, w));}}void Print(){for (auto& e : _vIndexMap){cout << e.first << "-" << e.second << " ";}cout << endl;for (int i = 0; i < _linkTable.size(); ++i){cout << i << " : [";for (auto& edge : _linkTable[i]){cout << edge.srci << "->" << edge.dsti << ":" << edge.w << "  ";}cout << "]" << endl;}}
};

3. 图的遍历

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

请思考树以前是怎么遍历的,此处可以直接用来遍历图吗?为什么?

3.1 广度优先遍历(BFS)

广度优先遍历就是从一个位置出发,根据他的边的连接关系,一层一层的遍历所有节点

image-20240503125216632

void _BFS(int idx, vector<bool>& check)
{queue<int> q;q.push(idx);check[idx] = true;while (!q.empty()){int tmp = q.front();q.pop();cout << GetVertex(tmp) << " ";for (int i = 0; i < _vertexs.size(); ++i){if (_matrix[tmp][i] != MAX_W && check[i] == false){q.push(i);check[i] = true;}}}
}
void BFS(const V& v)
{vector<bool> check(_vertexs.size(), false);int idx = GetIndex(v);if (idx == -1)return;_BFS(idx, check); // 从指定节点v处开始遍历、// 当一次BFS走完之后,如果在图内有节点与v不联通,那么这些节点将不会被访问,所以接下来找到没有访问的节点进行BFSfor (int i = 0; i < check.size(); ++i){if (check[i] == false)_BFS(i, check);}cout << endl;
}
void Test()
{string people[] = { "张三", "李四", "王五", "赵六" };Graph<string, int, INT_MAX, false> g(people, 4);g.AddEdge("张三", "赵六", 6);g.AddEdge("张三", "李四", 9);//g.AddEdge("王五", "赵六", 0);g.BFS("张三");
}

image-20240513003545923

3.2 深度优先遍历(DFS)

image-20240503132536205

void _DFS(int srci, vector<bool>& check)
{cout << GetVertex(srci) << " ";check[srci] = true;for (int i = 0; i < _vertexs.size(); ++i){if (_matrix[srci][i] != MAX_W && check[i] == false){_DFS(i, check);}}
}
void DFS(const V& v)
{int idx = GetIndex(v);if (idx == -1)return;vector<bool> check(_vertexs.size(), false); // 标记数组_DFS(idx, check);// 此时,可能还有一些顶点是没有遍历的(孤岛)for (int i = 0; i < check.size(); ++i){if (check[i] == false) _DFS(i, check);}cout << endl;
}
void Test()
{string people[] = { "张三", "李四", "王五", "赵六" };Graph<string, int, INT_MAX, false> g(people, 4);g.AddEdge("张三", "赵六", 6);g.AddEdge("张三", "李四", 9);//g.AddEdge("王五", "赵六", 0);g.Print();g.DFS("张三");
}

image-20240513003639018

4. 最小生成树

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

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

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

构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。

4.1 Kruskal算法

给一个有n个顶点的连通网络N={V,E}

**首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},**其中每个顶点自成一个连通分量;

其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止。

核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。

image-20240503134623233

W Kruskal(Self& minTree)
{// 初始化,minTree中包含所有的顶点,清空所有的边minTree = *this;for (int i = 0; i < _vertexs.size(); ++i){for (int j = 0; j < _vertexs.size(); ++j){minTree._matrix[i][j] = MAX_W;}}// 此时,这n个顶点组成、不含任何边的图G={V,NULL}  《==》 minTree// 接下来要添加边了priority_queue<Edge, vector<Edge>, greater<Edge>> pq;int 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){pq.push(Edge(i, j, _matrix[i][j]));}}}// 此时,所有的边都在pq中,并且排序完成了// 从小到大拿出来n-1条边,添加到minTree里面,就完成了最小生成树的构造int EdgeCount = n - 1;UnionFindSet ufs(n); // 使用并查集判断两个顶点是否已经联通W total = W(); // 计算最小生成树的权值while (!pq.empty() && EdgeCount){Edge front = pq.top(); // 选出当前的最小边pq.pop();if (ufs.Same(front.srci, front.dsti) == false) // 如果这条边的两个顶点不在同一个集合,就添加这条边{minTree._AddEdge(front.srci, front.dsti, front.w);ufs.Union(front.srci, front.dsti);EdgeCount--;total += front.w;cout << "添加边:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;}else // 如果这条边的两个顶点在同一个集合,添加将会构成环,所以不能添加{cout << "构成环:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;}}if (EdgeCount != 0) // 如果最后边的个数不是n-1,那么就证明没有构造完成最小生成树return W();return total;
}

image-20240513121508386

4.2 Prim算法

image-20240503140202050

W Prim(const V& v, Self& minTree) // Prim算法需要一个起始点
{// 初始化,minTree中包含所有的顶点,清空所有的边minTree = *this;for (int i = 0; i < _vertexs.size(); ++i){for (int j = 0; j < _vertexs.size(); ++j){minTree._matrix[i][j] = MAX_W;}}W total = W();set<int> inSet; // 保存连通在最小生成树中的节点int srci = GetIndex(v);inSet.insert(srci);int n = _vertexs.size();priority_queue<Edge, vector<Edge>, greater<Edge>> pq; // 保存边的堆for (int i = 0; i < n; ++i){if (_matrix[srci][i] != MAX_W) // 让所有以srci为起点的边进堆{pq.push(Edge(srci, i, _matrix[srci][i]));}}while (!pq.empty()){// 循环,每次从堆顶拿权值最小的边Edge front = pq.top();pq.pop();if (inSet.find(front.dsti) == inSet.end()) // 如果当前节点的终点不在连通的顶点集合里面{// 就添加这条边进来minTree._AddEdge(front.srci, front.dsti, front.w);inSet.insert(front.dsti);total += front.w;// 添加这条边为起点的所有边for (int i = 0; i < n; ++i){if (_matrix[front.dsti][i] != MAX_W){pq.push(Edge(front.dsti, i, _matrix[front.dsti][i]));}}cout << "添加边:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;}else{// cout << "构成环:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;}}if (inSet.size() == n)return total;elsereturn W();
}
void Test_MinTree()
{char str[] = "abcdefghi";Graph<char, int> g(str, strlen(str));g.AddEdge('a', 'b', 4);g.AddEdge('a', 'h', 8);g.AddEdge('h', 'b', 11);g.AddEdge('h', 'i', 7);g.AddEdge('h', 'g', 1);g.AddEdge('g', 'f', 2);g.AddEdge('i', 'c', 2);g.AddEdge('c', 'd', 7);g.AddEdge('b', 'c', 8);g.AddEdge('c', 'f', 4);g.AddEdge('d', 'e', 9);g.AddEdge('d', 'f', 14);g.AddEdge('f', 'e', 10);g.AddEdge('i', 'g', 6);Graph<char, int> g1, g2;cout << "Kruskal:" << endl;auto ret = g.Kruskal(g1);cout << "权值=" << ret << endl;cout << "Prim:" << endl;ret = g.Prim('a', g2);cout << "权值=" << ret << endl;
}

image-20240513134803535


我们可以发现,不管是Kruskal还是Prim都能够找到最小生成树,但是选的边却并不相同,这是因为对于一个连通图来说,能够产生的最小生成树并不唯一

5. 最短路径

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

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 中找出一个从起点s到该结点代价最小的结点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-20240511180310526

// Dijkstra算法是单源的最短路径算法,需要传起点,dist[i]表示编号为i的顶点到起点的最短路径
// 为了方便能够找到某一个节点的路径,这里保存最短路径的情况下的每个节点的上一个节点路径
void Dijkstra(const V& src, vector<W>& dist, vector<int>& parentPath)
{int n = _vertexs.size();vector<bool> S(n, false); // 标记已经确定最短路径的节点int srci = GetIndex(src); // 起点dist.resize(n, MAX_W);parentPath.resize(n, -1);dist[srci] = W(); // 初始化起点到起点的路径parentPath[srci] = srci;// 更新所有与src顶点相连的顶点的最短路径for (int j = 0; j < n; ++j){// 选择当前未确定的最短路径去更新新路径W minW = MAX_W;int u = srci;for (int i = 0; i < n; ++i){if (S[i] == false && dist[i] < minW){u = i;minW = dist[i];}}// 此时u就是未确定的最短路径节点,我们现在认为现在的dist[u]就是到u的最短路径S[u] = true; // 把所以以u为起点的边对应的终点的最短路径更新   s->u   u->v   ===>   s->vfor (int v = 0; v < n; ++v){if (_matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v]){dist[v] = dist[u] + _matrix[u][v];parentPath[v] = u;}}}
}
// 将选出的最短路径打印出来
void PrintShortPath(const V& src, const vector<int>& dist, const vector<int>& pPath)
{int n = _vertexs.size();int srci = GetIndex(src);for (int i = 0; i < n; ++i){if (i != srci){vector<int> path;int parent = i;while (parent != srci){path.push_back(parent);parent = pPath[parent];}path.push_back(srci);reverse(path.begin(), path.end());for (auto& idx : path){cout << GetVertex(idx) << "->";}cout << dist[i] << endl;;}}
}
void Test_Dijkstra()
{char str[] = "stxyz";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', 'z', 2);g.AddEdge('y', 'x', 9);g.AddEdge('t', 'y', 2);g.AddEdge('t', 'x', 1);g.AddEdge('z', 'x', 6);g.AddEdge('z', 's', 7);g.AddEdge('x', 'z', 4);g.Print();vector<int> dist, pPath;g.Dijkstra('s', dist, pPath);g.PrintShortPath('s', dist, pPath);
}

image-20240514000813796

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

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

image-20240511180434490

void BellmanFord(const V& src, vector<W>& dist, vector<int>& parentPath)
{dist.resize(_vertexs.size(), MAX_W);parentPath.resize(_vertexs.size(), -1);int srci = GetIndex(src);int n = _vertexs.size();dist[srci] = 0;parentPath[srci] = srci;for (int k = 0; k < n; ++k) // 一遍暴力更新可能会出现问题// 问题:由于更新的边顺序不确定,如果节点x的最短路径包含边y->x,在更新完x的最短路径后y的最短路径又更新,此时x的最短路径的权值将不会再次更新// 解决方案:将上述暴力更新再进行1次,将会让x的最短路径权值被更新到正确状态,但是依赖x的下一条边将会出问题,所以还需要更新,最终由于一条路径最多有n-1条边,所以需要多更新n-1次{bool flag = false;// 直接进行暴力更新for (int i = 0; i < n; ++i){for (int j = 0; j < n; ++j){// 更新 i->j的边if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){dist[j] = dist[i] + _matrix[i][j];parentPath[j] = i;cout << "更新边:" << GetVertex(i) << "->" << GetVertex(j) << ":" << dist[j] << endl;flag = true;}}}if (flag == false) // 优化:如果本次循环没有更新边,那么后续也就不再需要更新边了break;}
}

与不带负权的图求最短路径不同的是,带有负权图的最短路径问题可能是没有解的,如果在这个图中形成了一条负权回路(这条回路的路径为负数),此时所有点的最短路径都会无限次更新,因为经过这个负权路径多一次,就会让最短路径边小,所以BellmanFord算法的代码需要更改一点

bool BellmanFord(const V& src, vector<W>& dist, vector<int>& parentPath)
{dist.resize(_vertexs.size(), MAX_W);parentPath.resize(_vertexs.size(), -1);int srci = GetIndex(src);int n = _vertexs.size();dist[srci] = 0;parentPath[srci] = srci;for (int k = 0; k < n; ++k) // 一遍暴力更新可能会出现问题// 问题:由于更新的边顺序不确定,如果节点x的最短路径包含边y->x,在更新完x的最短路径后y的最短路径又更新,此时x的最短路径的权值将不会再次更新// 解决方案:将上述暴力更新再进行1次,将会让x的最短路径权值被更新到正确状态,但是依赖x的下一条边将会出问题,所以还需要更新,最终由于一条路径最多有n-1条边,所以需要多更新n-1次{bool flag = false;// 直接进行暴力更新for (int i = 0; i < n; ++i){for (int j = 0; j < n; ++j){// 更新 i->j的边if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){dist[j] = dist[i] + _matrix[i][j];parentPath[j] = i;cout << "更新边:" << GetVertex(i) << "->" << GetVertex(j) << ":" << dist[j] << endl;flag = true;}}}if (flag == false) // 优化:如果本次循环没有更新边,那么后续也就不再需要更新边了break;}for (int i = 0; i < n; ++i){for (int j = 0; j < n; ++j){// 检查有没有负权回路:如果更新了n轮之后,还能找到更短的路径,那么就证明图中存在负权回路if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){return false;}}}return true;
}

测试代码:

void Test_BellmanFord()
{char str1[] = "stxyz";Graph<char, int, INT_MAX, true> g1(str1, strlen(str1));g1.AddEdge('s', 't', 6);g1.AddEdge('s', 'y', 7);g1.AddEdge('y', 'z', 9);g1.AddEdge('y', 'x', -3);g1.AddEdge('z', 's', 2);g1.AddEdge('z', 'x', 7);g1.AddEdge('t', 'x', 5);g1.AddEdge('t', 'y', 8);g1.AddEdge('t', 'z', -4);g1.AddEdge('x', 't', -2);vector<int> dist;vector<int> parentPath;if (g1.BellmanFord('s', dist, parentPath))g1.PrintShortPath('s', dist, parentPath);elsecout << "存在负权回路" << endl;cout << endl;// 微调图结构,带有负权回路的测试char str2[] = "syztx";Graph<char, int, INT_MAX, true> g2(str2, strlen(str2));g2.AddEdge('s', 't', 6);g2.AddEdge('s', 'y', 7);g2.AddEdge('y', 'x', -3);g2.AddEdge('y', 'z', 9);g2.AddEdge('y', 'x', -3);g2.AddEdge('y', 's', 1); // 新增g2.AddEdge('z', 's', 2);g2.AddEdge('z', 'x', 7);g2.AddEdge('t', 'x', 5);g2.AddEdge('t', 'y', -8); // 更改g2.AddEdge('t', 'z', -4);g2.AddEdge('x', 't', -2);vector<int> dist2;vector<int> parentPath2;if (g2.BellmanFord('s', dist2, parentPath2))g2.PrintShortPath('s', dist2, parentPath2);elsecout << "存在负权回路" << endl;
}

image-20240514161558082

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

image-20240511180453715

image-20240511180502855

即Floyd算法本质是三维动态规划,D[i][j][k]表示从点i到点j只经过0到k个点最短路径,然后建立起转移方程,然后通过空间优化,优化掉最后一维度,变成一个最短路径的迭代算法,最后即得到所以点的最短路

image-20240511180520506

// 使用二维数组存储任意两点之间的最短路径,vvDist[i][j]表示从i到j的最短路径,vvpPath[i][j]表示i到j的最段路径中j的前驱顶点编号
void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
{int n = _vertexs.size();vvDist.resize(n);vvpPath.resize(n);for (int i = 0; i < n; ++i){vvDist[i].resize(n, MAX_W);vvpPath[i].resize(n, -1); // 用-1表示没有连接路径}// 将所有直接连接的边初始化,for (int i = 0; i < n; ++i){for (int j = 0; j < n; ++j){if (_matrix[i][j] != MAX_W){vvDist[i][j] = _matrix[i][j];vvpPath[i][j] = i; // 这里认为i->j直接相连}// 顶点到自己本身的路径为0if (i == j){vvDist[i][j] = 0;vvpPath[i][j] = -1; // 这里认为路径为-1}}}// 认为i->j中间最多经过n个节点,依次使用这n个节点进行更新,如果遇到更短的路径就更新for (int k = 0; k < n; ++k){for (int i = 0; i < n; ++i){for (int j = 0; j < n; ++j){// i->j 和  i->k  +  k->j if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W && vvDist[i][j] > vvDist[i][k] + vvDist[k][j]){vvDist[i][j] = vvDist[i][k] + vvDist[k][j]; // 更新最短路径// 更新前驱节点,这里我们认为更新之后i到j的路径为i到k再到j,所以i到j中j的前驱节点和k到j中j的前驱节点相同vvpPath[i][j] = vvpPath[k][j];}}}// 打印权值和路径矩阵观察数据//for (size_t i = 0; i < n; ++i)//{//	for (size_t j = 0; j < n; ++j)//	{//		if (vvDist[i][j] == MAX_W)//		{//			printf("%3c", '*');//		}//		else//		{//			printf("%3d", vvDist[i][j]);//		}//	}//	cout << endl;//}//cout << endl;//for (size_t i = 0; i < n; ++i)//{//	for (size_t j = 0; j < n; ++j)//	{//		printf("%3d", vvpPath[i][j]);//	}//	cout << endl;//}//cout << "=================================" << endl;}
}

按照上面的图构建用例进行测试:

void Test_FloydWarshall()
{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;vector<vector<int>> dist, pPath;g.FloydWarshall(dist, pPath);// 打印从任意一个位置开始的所有节点的最短路径for (int i = 0; i < strlen(str); ++i){g.PrintShortPath(str[i], dist[i], pPath[i]);cout << endl;}
}

image-20240515151736981


最后附上本节的所有代码

#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <set>
#include <queue>
using namespace std;class UnionFindSet
{
private:vector<int> _ufs;
public:UnionFindSet(int n){_ufs.resize(n, -1);}void Union(int x, int y){int xroot = Find(x);int yroot = Find(y);if (xroot == yroot) return;_ufs[xroot] += _ufs[yroot];_ufs[yroot] = xroot;}int Find(int x){int root = x;while (_ufs[root] >= 0){root = _ufs[root];}return root;}bool Same(int x, int y){return Find(x) == Find(y);}
};template<class W>
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 e2) const{return w > e2.w;}
};
//        顶点类型  权值类型  边不存在时的权值        是否是有向图,true表示是
template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{typedef Graph<V, W, MAX_W, Direction> Self;typedef Edge<W> Edge;
private:map<V, int> _vIndexMap;			// 从顶点映射到下标map<int, V> _iVertexMap;        // 从编号映射到顶点vector<int> _vertexs;           // 顶点的集合vector<vector<W>> _matrix;      // 邻接矩阵
public:Graph() = default;Graph(V* v, int n) // 初始化图{_vertexs.resize(n);for (int i = 0; i < n; ++i){//_IndexMap[v[i]] = i;_vIndexMap.insert({ v[i], i });_iVertexMap[i] = v[i];_vertexs[i] = i;}_matrix.resize(n);for (auto& v : _matrix){v.resize(n, MAX_W);}}int GetIndex(const V& v) // 查找v对应的下标,如果没找到就返回-1{auto it = _vIndexMap.find(v);if (it != _vIndexMap.end()){return it->second;}else{return -1;}}void _AddEdge(int srci, int dsti, const W& w){_matrix[srci][dsti] = w;if (Direction == false) // 无向图多的处理{_matrix[dsti][srci] = w; // }}void AddEdge(const V& v1, const V& v2, const W& w) // 添加边{int idx1 = GetIndex(v1);int idx2 = GetIndex(v2);if (idx1 == -1 || idx2 == -1){cout << "顶点不存在,添加失败" << endl;}_AddEdge(idx1, idx2, w);}V GetVertex(const int pos) // 通过下标找到对应顶点{auto it = _iVertexMap.find(pos);if (it != _iVertexMap.end()){return it->second;}else{return V();}}void _BFS(int idx, vector<bool>& check){queue<int> q;q.push(idx);check[idx] = true;while (!q.empty()){int tmp = q.front();q.pop();cout << GetVertex(tmp) << " ";for (int i = 0; i < _vertexs.size(); ++i){if (_matrix[tmp][i] != MAX_W && check[i] == false){q.push(i);check[i] = true;}}}}void BFS(const V& v){vector<bool> check(_vertexs.size(), false);int idx = GetIndex(v);if (idx == -1)return;_BFS(idx, check); // 从指定节点v处开始遍历、// 当一次BFS走完之后,如果在图内有节点与v不联通,那么这些节点将不会被访问,所以接下来找到没有访问的节点进行BFSfor (int i = 0; i < check.size(); ++i){if (check[i] == false)_BFS(i, check);}cout << endl;}void _DFS(int srci, vector<bool>& check){cout << GetVertex(srci) << " ";check[srci] = true;for (int i = 0; i < _vertexs.size(); ++i){if (_matrix[srci][i] != MAX_W && check[i] == false){_DFS(i, check);}}}void DFS(const V& v){int idx = GetIndex(v);if (idx == -1)return;vector<bool> check(_vertexs.size(), false); // 标记数组_DFS(idx, check);// 此时,可能还有一些顶点是没有遍历的(孤岛)for (int i = 0; i < check.size(); ++i){if (check[i] == false) _DFS(i, check);}cout << endl;}W Kruskal(Self& minTree){// 初始化,minTree中包含所有的顶点,清空所有的边minTree = *this;for (int i = 0; i < _vertexs.size(); ++i){for (int j = 0; j < _vertexs.size(); ++j){minTree._matrix[i][j] = MAX_W;}}// 此时,这n个顶点组成、不含任何边的图G={V,NULL}  《==》 minTree// 接下来要添加边了priority_queue<Edge, vector<Edge>, greater<Edge>> pq;int 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){pq.push(Edge(i, j, _matrix[i][j]));}}}// 此时,所有的边都在pq中,并且排序完成了// 从小到大拿出来n-1条边,添加到minTree里面,就完成了最小生成树的构造int EdgeCount = n - 1;UnionFindSet ufs(n); // 使用并查集判断两个顶点是否已经联通W total = W(); // 计算最小生成树的权值while (!pq.empty() && EdgeCount){Edge front = pq.top(); // 选出当前的最小边pq.pop();if (ufs.Same(front.srci, front.dsti) == false) // 如果这条边的两个顶点不在同一个集合,就添加这条边{minTree._AddEdge(front.srci, front.dsti, front.w);ufs.Union(front.srci, front.dsti);EdgeCount--;total += front.w;cout << "添加边:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;}else // 如果这条边的两个顶点在同一个集合,添加将会构成环,所以不能添加{// cout << "构成环:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;}}if (EdgeCount != 0) // 如果最后边的个数不是n-1,那么就证明没有构造完成最小生成树return W();return total;}W Prim(const V& v, Self& minTree) // Prim算法需要一个起始点{// 初始化,minTree中包含所有的顶点,清空所有的边minTree = *this;for (int i = 0; i < _vertexs.size(); ++i){for (int j = 0; j < _vertexs.size(); ++j){minTree._matrix[i][j] = MAX_W;}}W total = W();set<int> inSet; // 保存连通在最小生成树中的节点int srci = GetIndex(v);inSet.insert(srci);int n = _vertexs.size();priority_queue<Edge, vector<Edge>, greater<Edge>> pq; // 保存边的堆for (int i = 0; i < n; ++i){if (_matrix[srci][i] != MAX_W) // 让所有以srci为起点的边进堆{pq.push(Edge(srci, i, _matrix[srci][i]));}}while (!pq.empty()){// 循环,每次从堆顶拿权值最小的边Edge front = pq.top();pq.pop();if (inSet.find(front.dsti) == inSet.end()) // 如果当前节点的终点不在连通的顶点集合里面{// 就添加这条边进来minTree._AddEdge(front.srci, front.dsti, front.w);inSet.insert(front.dsti);total += front.w;// 添加这条边为起点的所有边for (int i = 0; i < n; ++i){if (_matrix[front.dsti][i] != MAX_W){pq.push(Edge(front.dsti, i, _matrix[front.dsti][i]));}}cout << "添加边:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;}else{// cout << "构成环:" << GetVertex(front.srci) << "->" << GetVertex(front.dsti) << " " << front.w << endl;}}if (inSet.size() == n)return total;elsereturn W();}void Print(){// 打印顶点和下标映射关系for (const auto& e : _vIndexMap){cout << e.first << "-" << e.second << " ";}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 (_matrix[i][j] != MAX_W){cout << GetVertex(_vertexs[i]) << "-" << GetVertex(_vertexs[j]) << ":" <<_matrix[i][j] << endl;}}}cout << endl;}// Dijkstra算法是单源的最短路径算法,需要传起点,dist[i]表示编号为i的顶点到起点的最短路径// 为了方便能够找到某一个节点的路径,这里保存最短路径的情况下的每个节点的上一个节点路径void Dijkstra(const V& src, vector<W>& dist, vector<int>& parentPath){int n = _vertexs.size();vector<bool> S(n, false); // 标记已经确定最短路径的节点int srci = GetIndex(src); // 起点dist.resize(n, MAX_W);parentPath.resize(n, -1);dist[srci] = W(); // 初始化起点到起点的路径parentPath[srci] = srci;// 更新所有与src顶点相连的顶点的最短路径for (int j = 0; j < n; ++j){// 选择当前未确定的最短路径去更新新路径W minW = MAX_W;int u = srci;for (int i = 0; i < n; ++i){if (S[i] == false && dist[i] < minW){u = i;minW = dist[i];}}// 此时u就是未确定的最短路径节点,我们现在认为现在的dist[u]就是到u的最短路径S[u] = true; // 把所以以u为起点的边对应的终点的最短路径更新   s->u   u->v   ===>   s->vfor (int v = 0; v < n; ++v){if (_matrix[u][v] != MAX_W && dist[u] + _matrix[u][v] < dist[v]){dist[v] = dist[u] + _matrix[u][v];parentPath[v] = u;}}}}bool BellmanFord(const V& src, vector<W>& dist, vector<int>& parentPath){dist.resize(_vertexs.size(), MAX_W);parentPath.resize(_vertexs.size(), -1);int srci = GetIndex(src);int n = _vertexs.size();dist[srci] = 0;parentPath[srci] = srci;for (int k = 0; k < n; ++k) // 一遍暴力更新可能会出现问题// 问题:由于更新的边顺序不确定,如果节点x的最短路径包含边y->x,在更新完x的最短路径后y的最短路径又更新,此时x的最短路径的权值将不会再次更新// 解决方案:将上述暴力更新再进行1次,将会让x的最短路径权值被更新到正确状态,但是依赖x的下一条边将会出问题,所以还需要更新,最终由于一条路径最多有n-1条边,所以需要多更新n-1次{bool flag = false;// 直接进行暴力更新for (int i = 0; i < n; ++i){for (int j = 0; j < n; ++j){// 更新 i->j的边if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){dist[j] = dist[i] + _matrix[i][j];parentPath[j] = i;cout << "更新边:" << GetVertex(i) << "->" << GetVertex(j) << ":" << dist[j] << endl;flag = true;}}}if (flag == false) // 优化:如果本次循环没有更新边,那么后续也就不再需要更新边了break;}for (int i = 0; i < n; ++i){for (int j = 0; j < n; ++j){// 检查有没有负权回路:如果更新了n轮之后,还能找到更短的路径,那么就证明图中存在负权回路if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]){return false;}}}return true;}// 使用二维数组存储任意两点之间的最短路径,vvDist[i][j]表示从i到j的最短路径,vvpPath[i][j]表示i到j的最段路径中j的前驱顶点编号void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath){int n = _vertexs.size();vvDist.resize(n);vvpPath.resize(n);for (int i = 0; i < n; ++i){vvDist[i].resize(n, MAX_W);vvpPath[i].resize(n, -1); // 用-1表示没有连接路径}// 将所有直接连接的边初始化,for (int i = 0; i < n; ++i){for (int j = 0; j < n; ++j){if (_matrix[i][j] != MAX_W){vvDist[i][j] = _matrix[i][j];vvpPath[i][j] = i; // 这里认为i->j直接相连}// 顶点到自己本身的路径为0if (i == j){vvDist[i][j] = 0;vvpPath[i][j] = -1; // 这里认为路径为-1}}}// 认为i->j中间最多经过n个节点,依次使用这n个节点进行更新,如果遇到更短的路径就更新for (int k = 0; k < n; ++k){for (int i = 0; i < n; ++i){for (int j = 0; j < n; ++j){// i->j 和  i->k  +  k->j if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W && vvDist[i][j] > vvDist[i][k] + vvDist[k][j]){vvDist[i][j] = vvDist[i][k] + vvDist[k][j]; // 更新最短路径// 更新前驱节点,这里我们认为更新之后i到j的路径为i到k再到j,所以i到j中j的前驱节点和k到j中j的前驱节点相同vvpPath[i][j] = vvpPath[k][j];}}}// 打印权值和路径矩阵观察数据//for (size_t i = 0; i < n; ++i)//{//	for (size_t j = 0; j < n; ++j)//	{//		if (vvDist[i][j] == MAX_W)//		{//			printf("%3c", '*');//		}//		else//		{//			printf("%3d", vvDist[i][j]);//		}//	}//	cout << endl;//}//cout << endl;//for (size_t i = 0; i < n; ++i)//{//	for (size_t j = 0; j < n; ++j)//	{//		printf("%3d", vvpPath[i][j]);//	}//	cout << endl;//}//cout << "=================================" << endl;}}// 将选出的最短路径打印出来void PrintShortPath(const V& src, const vector<int>& dist, const vector<int>& pPath){int n = _vertexs.size();int srci = GetIndex(src);for (int i = 0; i < n; ++i){if (i != srci){vector<int> path;int parent = i;while (parent != srci){path.push_back(parent);parent = pPath[parent];}path.push_back(srci);reverse(path.begin(), path.end());for (auto& idx : path){cout << GetVertex(idx) << "->";}cout << dist[i] << endl;}}}
};void Test()
{string people[] = { "张三", "李四", "王五", "赵六" };Graph<string, int, INT_MAX, false> g(people, 4);g.AddEdge("张三", "赵六", 6);g.AddEdge("张三", "李四", 9);//g.AddEdge("王五", "赵六", 0);g.Print();// g.BFS("张三");g.DFS("张三");
}void Test_MinTree()
{char str[] = "abcdefghi";Graph<char, int> g(str, strlen(str));g.AddEdge('a', 'b', 4);g.AddEdge('a', 'h', 8);g.AddEdge('h', 'b', 11);g.AddEdge('h', 'i', 7);g.AddEdge('h', 'g', 1);g.AddEdge('g', 'f', 2);g.AddEdge('i', 'c', 2);g.AddEdge('c', 'd', 7);g.AddEdge('b', 'c', 8);g.AddEdge('c', 'f', 4);g.AddEdge('d', 'e', 9);g.AddEdge('d', 'f', 14);g.AddEdge('f', 'e', 10);g.AddEdge('i', 'g', 6);Graph<char, int> g1, g2;cout << "Kruskal:" << endl;auto ret = g.Kruskal(g1);cout << "权值=" << ret << endl;cout << "Prim:" << endl;ret = g.Prim('a', g2);cout << "权值=" << ret << endl;
}
void Test_Dijkstra()
{char str[] = "stxyz";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', 'z', 2);g.AddEdge('y', 'x', 9);g.AddEdge('t', 'y', 2);g.AddEdge('t', 'x', 1);g.AddEdge('z', 'x', 6);g.AddEdge('z', 's', 7);g.AddEdge('x', 'z', 4);g.Print();vector<int> dist, pPath;g.Dijkstra('s', dist, pPath);g.PrintShortPath('s', dist, pPath);
}
void Test_BellmanFord()
{char str1[] = "stxyz";Graph<char, int, INT_MAX, true> g1(str1, strlen(str1));g1.AddEdge('s', 't', 6);g1.AddEdge('s', 'y', 7);g1.AddEdge('y', 'z', 9);g1.AddEdge('y', 'x', -3);g1.AddEdge('z', 's', 2);g1.AddEdge('z', 'x', 7);g1.AddEdge('t', 'x', 5);g1.AddEdge('t', 'y', 8);g1.AddEdge('t', 'z', -4);g1.AddEdge('x', 't', -2);vector<int> dist;vector<int> parentPath;if (g1.BellmanFord('s', dist, parentPath))g1.PrintShortPath('s', dist, parentPath);elsecout << "存在负权回路" << endl;cout << endl;// 微调图结构,带有负权回路的测试char str2[] = "syztx";Graph<char, int, INT_MAX, true> g2(str2, strlen(str2));g2.AddEdge('s', 't', 6);g2.AddEdge('s', 'y', 7);g2.AddEdge('y', 'x', -3);g2.AddEdge('y', 'z', 9);g2.AddEdge('y', 'x', -3);g2.AddEdge('y', 's', 1); // 新增g2.AddEdge('z', 's', 2);g2.AddEdge('z', 'x', 7);g2.AddEdge('t', 'x', 5);g2.AddEdge('t', 'y', -8); // 更改g2.AddEdge('t', 'z', -4);g2.AddEdge('x', 't', -2);vector<int> dist2;vector<int> parentPath2;if (g2.BellmanFord('s', dist2, parentPath2))g2.PrintShortPath('s', dist2, parentPath2);elsecout << "存在负权回路" << endl;
}
void Test_FloydWarshall()
{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;vector<vector<int>> dist, pPath;g.FloydWarshall(dist, pPath);// 打印从任意一个位置开始的所有节点的最短路径for (int i = 0; i < strlen(str); ++i){g.PrintShortPath(str[i], dist[i], pPath[i]);cout << endl;}
}int main()
{// Test();// Test_BellmanFord();Test_FloydWarshall();return 0;
}

最后注:本节算法图参考自《算法导论》
本节完…

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

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

相关文章

家用充电桩远程监控安全管理系统解决方案

家用充电桩远程监控安全管理系统解决方案 在当今电动汽车日益普及的背景下&#xff0c;家用充电桩的安全管理成为了广大车主关注的重点问题。为了实现对充电桩的高效、精准、远程监控&#xff0c;一套完善的家用充电桩远程监控安全管理系统解决方案应运而生。本方案旨在通过先…

【nfs服务部署服务端和客户端搭建】

原理 NFS&#xff08;Network File System&#xff09;是文件服务器之一。它的功能是可以通过网络&#xff0c;让不同的机器、不同的操作系统可以彼此共享数据文件。 NFS服务器可以让服务端的共享目录挂载到本地端的文件系统中&#xff0c;其他服务器如果想访问共享目录&#…

webpack优化构建体积示例-并行压缩:

uglifyjs-webpack-plugin和terser-webpack-plugin都可以开启多进程并进行压缩来减小构件体积大小。 当在 Webpack 配置中启用 minimize: true 时&#xff0c;构建时间通常会增加&#xff0c;这是因为 Webpack 会在构建过程中添加一个额外的步骤&#xff1a;代码压缩。代码压缩是…

2024年第十届中西部外语翻译大赛

2024年第十届中西部外语翻译大赛 竞赛信息 “由中西部翻译协会共同体指导发起&#xff0c;各省市译协共建学术指导委员会&#xff0c;2024年第十届中西部外语翻译大赛由中西部翻译协会共同体秘书处&#xff08;武汉公仪网络科技有限公司&#xff09;承办。” - 获奖证书样图 -…

Retrying,一个神奇优雅的 Python 库

大家好&#xff01;我是爱摸鱼的小鸿&#xff0c;关注我&#xff0c;收看每期的编程干货。 一个简单的库&#xff0c;也许能够开启我们的智慧之门&#xff0c; 一个普通的方法&#xff0c;也许能在危急时刻挽救我们于水深火热&#xff0c; 一个新颖的思维方式&#xff0c;也许能…

win10共享文件夹到ubuntu22

win10共享文件夹 新建用户 新建用户、设置密码。避免共享给EveryOne&#xff0c;导致隐私问题。 点击左下角的开始菜单&#xff0c;选择“设置”&#xff08;WinI&#xff09;打开设置窗口。在设置窗口中&#xff0c;搜索或直接点击“账户”进入账户设置。在账户设置中&…

Pathlib,一个不怕迷路的 Python 向导

大家好&#xff01;我是爱摸鱼的小鸿&#xff0c;关注我&#xff0c;收看每期的编程干货。 一个简单的库&#xff0c;也许能够开启我们的智慧之门&#xff0c; 一个普通的方法&#xff0c;也许能在危急时刻挽救我们于水深火热&#xff0c; 一个新颖的思维方式&#xff0c;也许能…

震撼发布!GPT-4o 上线!

5 月 14日凌晨一点&#xff0c;OpenAI 发布了 GPT-4o&#xff01; 新模型的功能简单概括就是&#xff1a;更快、更智能、更像人类。 秉承着持续更新的态度&#xff0c;Hulu AI 快速接入 GPT-4o 啦&#xff01; 继 5 月份上线 Suno 之后&#xff0c;这次是 Hulu AI 的又一重大…

【微服务最全详解】

文章目录 微服务微服务的介绍微服务服务架构演变 微服务网关微服务的负载均衡微服务的容灾机制服务崩溃服务容灾机制微服务熔断机制微服务限流Sentinel怎么实现限流微服务限流算法1.令牌桶算法2.漏斗桶算法 服务监控日志收集 微服务 微服务的介绍 微服务是一种软件架构风格&a…

练习队列的相关操作:循环队列

1. 思路解析 循环队列就是在只有有限的空间时使用队列实现循环存储数据&#xff0c;有双向链表和数组两种选择&#xff0c;这里我们使用数组实现循环队列&#xff08;因为链表我不会 >-<&#xff09; 2. 相关函数及其实现 2.1 判空与判满 判空&#xff1a;直接返回头尾…

四川景源畅信:如何更好的为抖音小店做引流?

在数字化营销的浪潮中&#xff0c;抖音小店作为新兴的电商形态&#xff0c;正以其独特的社交属性和流量优势吸引着众多商家的目光。如何为抖音小店引流&#xff0c;成为许多店主心中的疑问。本文将深入探讨有效提升店铺流量的策略&#xff0c;助你在抖音平台上快速崛起。 一、内…

postgreSQL安装配置

安装 在ubuntu界面执行 sudo apt install postgresql安装完成后&#xff0c;切换到postgres &#xff08;安装过程中自动创建&#xff09; sudo su - postgres#然后执行psql&#xff0c;进入数据库 psql创建数据库用户 在数据库中执行create命令创建用户&#xff0c;并带有…