图|dfs bfs|最小生成树|最短路|一篇搞定图的所有知识点

文章目录

    • 前言
    • 项目代码仓库
    • 图的基本概念
    • 图的表示方法
      • 邻接矩阵
      • 邻接表
      • 图的一些相关概念
    • 图的遍历
      • bfs
      • dfs
      • 如果给的图不是连通图?
    • 最小生成树
      • Kruskal算法
      • Prim算法
    • 最短路径
      • 单源最短路径--Dijkstra算法
      • 单源最短路径--Bellman-Ford算法
      • 多源最短路径--Floyd-Warshall算法

前言

这里分享我的一些博客专栏,都是干货满满的。

  • 手撕数据结构专栏
  • 高质量博客专栏

项目代码仓库

  • CPlusPlus-review-main/

图的基本概念

一般表示成 G = (V, E)。

图的表示方法

  1. 邻接矩阵

  2. 邻接表

邻接矩阵

邻接矩阵存储方式非常适合稠密图。邻接矩阵可以O(1)判断两个顶点的连接关系,并拿到权值。

邻接矩阵不适合用来查找,一个顶点连接的所有边,这个是O(n)的。

邻接矩阵的表示:

namespace yufc_graph_matrix
{template <class vertex_type, class weight_type, weight_type __max_weight = INT_MAX, bool direction = false>class graph{private:std::vector<vertex_type> __vertexs;             // 顶点的集合std::map<vertex_type, int> __index_map;         // 顶点映射的下标std::vector<std::vector<weight_type>> __matrix; // 邻接矩阵public:// 图的创建/*三种创建图的方法:1. IO输入 -- 不方便测试,oj中更合适2. 图结构关系写到文件,读取文件3. 手动添加边*/graph(const vertex_type *a, size_t n){__vertexs.reserve(n);for (size_t i = 0; i < n; i++){__vertexs.push_back(a[i]); // 每个顶点存进去__index_map[a[i]] = i;     // 顶点映射的下标,就是这个顶点是__vertexs中哪一个下标上的}__matrix.resize(n);for (size_t i = 0; i < __matrix.size(); ++i){__matrix[i].resize(n, __max_weight);}}size_t get_vertex_index(const vertex_type &v){/* 确定顶点的下标 */auto it = __index_map.find(v);if (it != __index_map.end())return it->second;else{throw std::invalid_argument("vertex not exists");return -1;}}void add_edge(const vertex_type &src, const vertex_type &dest, const weight_type &weight){size_t srci = get_vertex_index(src);size_t desti = get_vertex_index(dest);__matrix[srci][desti] = weight;// 如果是无向图if (direction == false)__matrix[desti][srci] = weight;}public:void print(){// 把顶点打印出来for (size_t i = 0; i < __vertexs.size(); i++)std::cout << "[" << i << "]"<< "->" << __vertexs[i] << std::endl;std::cout << std::endl;// 打印一下矩阵// 先打印一下下标std::cout << "  ";for (size_t i = 0; i < __vertexs.size(); ++i)std::cout << i << " ";std::cout << std::endl;for (size_t i = 0; i < __matrix.size(); ++i){// 先打印一下下标std::cout << i << " ";for (size_t j = 0; j < __matrix[i].size(); ++j)if (__matrix[i][j] == __max_weight)std::cout << "* ";elsestd::cout << __matrix[i][j] << " ";std::cout << std::endl;}}};
}

邻接表

邻接表是一个指针数组,把自己连接的顶点都挂在下面。

适合存储稀疏图。适合查找一个顶点连接出去的边。

不适合确定两个顶点是否相连,及其权值

邻接表的表示:

namespace yufc_graph_link_table
{template <class weight_type>struct edge{int __dest_idx;            // 目标点的下标weight_type __weight;      // 权值edge<weight_type> *__next; // 链接指针edge(int desti, const weight_type &weight): __dest_idx(desti), __weight(weight), __next(nullptr) {}};template <class vertex_type, class weight_type, bool direction = false>class graph{private:typedef edge<weight_type> Edge;std::vector<vertex_type> __vertexs;     // 顶点的集合std::map<vertex_type, int> __index_map; // 顶点映射的下标std::vector<Edge *> __table;            // 下面挂边public:graph(const vertex_type *a, size_t n){__vertexs.reserve(n);for (size_t i = 0; i < n; i++){__vertexs.push_back(a[i]); // 每个顶点存进去__index_map[a[i]] = i;     // 顶点映射的下标,就是这个顶点是__vertexs中哪一个下标上的}// 初始化邻接表__table.resize(n, nullptr);}size_t get_vertex_index(const vertex_type &v){/* 确定顶点的下标 */auto it = __index_map.find(v);if (it != __index_map.end())return it->second;else{throw std::invalid_argument("vertex not exists");return -1;}}void add_edge(const vertex_type &src, const vertex_type &dest, const weight_type &weight){size_t srci = get_vertex_index(src);size_t desti = get_vertex_index(dest);Edge *eg = new Edge(desti, weight);// 头插eg->__next = __table[srci];__table[srci] = eg;if (direction == false){// 无向图反过来还要弄一下才行Edge *eg = new Edge(srci, weight);eg->__next = __table[desti];__table[desti] = eg;}}public:void print(){// 把顶点打印出来for (size_t i = 0; i < __vertexs.size(); i++)std::cout << "[" << i << "]"<< "->" << __vertexs[i] << std::endl;std::cout << std::endl;//for (size_t i = 0; i < __table.size(); ++i){std::cout << __vertexs[i] << "[" << i << "]->";Edge *cur = __table[i]; // 遍历一下单链表而已while (cur){std::cout << cur->__weight << "->" << __vertexs[cur->__dest_idx] << "[" << cur->__dest_idx << "]->";cur = cur->__next;}std::cout << "nullptr" << std::endl;}}};
}

图的一些相关概念

完全图:在有n个顶点的无向图中,若有 n ∗ ( n − 1 ) / 2 n*(n-1)/2 n(n1)/2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图。在n个顶点的有向图中,若有 n ∗ ( n − 1 ) n*(n-1) n(n1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图。

邻接顶点:在无向图中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)

路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶 点序列为从顶点vi到顶点vj的路径。

路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和。

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

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

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

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

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

#BUG: 0206发现潜在问题:如果一直给无向图add_edge,重复的边权值矛盾了怎么处理,要记得处理一下(还未处理)

图的遍历

两种遍历针对的都是图的顶点。

然后下面的代码先用了邻接矩阵的方式来实现了,不过用邻接表也是一样的,自行改一下就行,主要是思路而已。

bfs

广度优先遍历,然后我们都用这个图。

第一次先把A入队列。

此时队列为:A

然后A出来,把和A相连的带进来(BCD)。

此时队列为:B C D

此时B出来,把和B相连的带进来,和B相连的有E,C,A。不用说,E肯定是要进来的,AC进不进队列呢?

A不要进了,C可以进,因为此时C还没有被访问过。这时候虽然队列里面有两个C,但是也没问题,到时候的访问过的节点,不要再去访问就行了。

然后怎么标记呢,可以用一个set去记录一下下标(元素可能是其他类型,不好搞,所以就记录下标就行了)

当然可以不这样标记也可以,我们可以把进队列的标记一下,这样效率高一点

代码如下所示:

void bfs(const vertex_type &src)
{// 需要给一个起点size_t srci = get_vertex_index(src); // 找到起点的下标std::queue<int> q;std::vector<bool> visited(__vertexs.size(), false); // 所有顶点一开始先标记成falseq.push(srci);                                       // 起点入队列visited[srci] = true;                               // 标记起点,因为起点已经入队列了while (!q.empty()){// 队列不为空就继续遍历int front = q.front();                                             // 队头的数据std::cout << __vertexs[front] << "[" << front << "]" << " "; // 访问这个值q.pop();// 把和front相连的顶点入队for (size_t i = 0; i < __vertexs.size(); ++i){if (__matrix[front][i] != __max_weight && visited[i] == false){q.push(i);         // 和当前顶点相连的节点visited[i] = true; // 入队列的,标记一下}}}std::cout << std::endl << "bfs done!" << std::endl;
}

输出:

A[0] B[1] C[2] D[3] E[4] F[5] G[6] H[7] I[8] 
bfs done!

这个也符合我们的预期。

同时,我们也可以给这个bfs进行改进,因为做力扣的时候是有一个二叉树的层序遍历的题目的,如果我想知道每一层分别是什么,我们在代码中也可以控制,下面是改进后的版本。

// 遍历
void bfs(const vertex_type &src)
{// 需要给一个起点size_t srci = get_vertex_index(src); // 找到起点的下标std::queue<int> q;std::vector<bool> visited(__vertexs.size(), false); // 所有顶点一开始先标记成falseq.push(srci);                                       // 起点入队列visited[srci] = true;                               // 标记起点,因为起点已经入队列了int levelSize = 1;while (!q.empty()){// 控制一次出一层for (size_t i = 0; i < levelSize; i++){// 队列不为空就继续遍历int front = q.front(); // 队头的数据std::cout << __vertexs[front] << "[" << front << "]"<< " "; // 访问这个值q.pop();// 把和front相连的顶点入队for (size_t i = 0; i < __vertexs.size(); ++i){if (__matrix[front][i] != __max_weight && visited[i] == false){q.push(i);         // 和当前顶点相连的节点visited[i] = true; // 入队列的,标记一下}}}std::cout << std::endl; // 出完每一层才去换行levelSize = q.size();   // 此时levelSize就是下一层个数,就是现在队列的元素个数,因为我们已经把当前层出完了,剩下的都是下一层的}std::cout << "bfs done!" << std::endl;
}

输出:

A[0] 
B[1] C[2] D[3] 
E[4] F[5] 
G[6] H[7] 
I[8] 
bfs done!

这样的结果也是符合预期的。

dfs

深度优先遍历。

深度优先遍历可以用递归。

void __dfs(size_t srci, std::vector<bool>& visited)
{}
void dfs(const vertex_type &src)
{size_t srci = get_vertex_index(src);
}

肯定就要写一个子函数了,因为从A开始走要转化成从B开始走,从C开始走…

然后带上一个visited数组就行,记录哪些点是走过的。

完成后的代码如下所示:

void __dfs(size_t srci, std::vector<bool> &visited)
{std::cout << __vertexs[srci] << "[" << srci << "]" << " ";visited[srci] = true; // 标记访问过了// 找一个srci相邻的,没有访问过的点去往深度遍历for (size_t i = 0; i < __vertexs.size(); ++i)if (__matrix[srci][i] != __max_weight && visited[i] == false) // 遍历矩阵里面有连接的点就行__dfs(i, visited);
}
void dfs(const vertex_type &src)
{size_t srci = get_vertex_index(src);std::vector<bool> visited(__vertexs.size(), false);__dfs(srci, visited);std::cout << std::endl << "dfs done" << std::endl;
}

输出结果:

A[0] B[1] C[2] F[5] D[3] H[7] I[8] E[4] G[6] 
dfs done

这个结果,看图,也是符合我们dfs预期的

如果给的图不是连通图?

如果给的图不是连通图?以某个点为起点就不能遍历完成,那么怎么保证遍历完剩下的点?

  • 在visited数组找没有遍历过的点然后再去遍历就行了

最小生成树

一定是在连通图中找生成树。

连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不再连通

反之,在其中引入任何一条新边,都会形成一条回路。

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

  1. 只能使用图中的边来构造最小生成树

  2. 只能使用恰好n-1条边来连接图中的n个顶点

  3. 选用的n-1条边不能构成回路

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

  • 贪心算法:是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的的选择,而是某种意义上的局部最优解。

  • 贪心算法不是对所有的问题都能得到整体最优解。

Kruskal算法

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

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

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

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

那怎么判断新加入的边是否会构成环呢?并查集就行了。

如果新加入的边的两个顶点已经属于同一个并查集了,那么这两个点已经连通,因此这条新的边不能加入到生成树当中。

我们直接看例子,这个例子是我在《算法导论》上截取下来的例子。

下面就是代码实现。

这里的测试用例,使用的是算法导论上面这张图的例子进行测试的。

// 最小生成树
weight_type Kruskal(self &minTree)
{// 要先初始化最小生成树size_t n = __vertexs.size();minTree.__vertexs = this->__vertexs;minTree.__index_map = this->__index_map;minTree.__matrix.resize(n);for (size_t i = 0; i < n; i++)minTree.__matrix[i].resize(n, __max_weight);assert(direction == false); // 只能无向图// 如果有最小生成树,就返回到minTree里面,如果没有,就返回一个默认的weight_type// 然后比较边,优先队列要重载一个比较才行std::priority_queue<__edge, std::vector<__edge>, std::greater<__edge>> minq;for (size_t i = 0; i < n; i++)for (size_t j = 0; j < n; j++)if (i < j && __matrix[i][j] != __max_weight) // 表示有这条边,那就把这条边加入到优先队列中, i<j 可以保证无向图不会重复添加边minq.push(__edge(i, j, __matrix[i][j]));// 找到最小的边,依次添加int size = 0;                             // 选出n-1条边就行了weight_type total_weight = weight_type(); // 总的权值union_find_disjoint_set<int> ufs(n); // 这里不能用size_twhile (!minq.empty()){__edge min = minq.top(); // 选出一条边minq.pop();if (ufs.InSet(min.__srci, min.__dsti))continue;                                             // 如果这个边的两个顶点在一个集合里面,直接跳过本轮minTree.__add_edge(min.__srci, min.__dsti, min.__weight); // 添加边就行了ufs.Union(min.__srci, min.__dsti);size++;total_weight += min.__weight;}if (size == n - 1)return total_weight;elsereturn weight_type(); // 如果找不到,就返回一个缺省值
}

这个输出也是没有问题的,符合预期。

Kruskal:37
[0]->a
[1]->b
[2]->c
[3]->d
[4]->e
[5]->f
[6]->g
[7]->h
[8]->i0 1 2 3 4 5 6 7 8 
0 * 4 * * * * * 8 * 
1 4 * * * * * * * * 
2 * * * 7 * 4 * * 2 
3 * * 7 * 9 * * * * 
4 * * * 9 * * * * * 
5 * * 4 * * * 2 * * 
6 * * * * * 2 * 1 * 
7 8 * * * * * 1 * * 
8 * * 2 * * * * * *

当然,如果想看选边的过程,也是在代码中打印出来,就能看到了。

Prim算法

与Kruskal算法类似。Prim算法的工作原理与Dijkstra的最短路径算法相似。Prim算法所具有的一个性质是集合A中的边总是构成一棵树。如图所示,还是这个图,这棵树从一个任意的根结点r开始,一直长大到覆盖V中的所有结点时为止。算法每一步在连接集合A和A之外的结点的所有边中,选择一条轻量级边加入到A中。这条规则所加入的边都是对A安全的边。因此,当算法终止时,A中的边形成一棵最小生成树。本策略也属于贪心策略,因为每一步所加人的边都必须是使树的总权重增加量最小的边。

Prim算法代码:

weight_type Prim(self &minTree, const weight_type &src)
{size_t srci = get_vertex_index(src);size_t n = __vertexs.size();minTree.__vertexs = __vertexs;minTree.__index_map = __index_map;minTree.__matrix.resize(n);for (size_t i = 0; i < n; ++i){minTree.__matrix[i].resize(n, __max_weight);}std::vector<bool> X(n, false);std::vector<bool> Y(n, true);X[srci] = true;Y[srci] = false;// 从X->Y集合中连接的边里面选出最小的边std::priority_queue<__edge, std::vector<__edge>, std::greater<__edge>> minq;// 先把srci连接的边添加到队列中for (size_t i = 0; i < n; ++i)if (__matrix[srci][i] != __max_weight)minq.push(__edge(srci, i, __matrix[srci][i]));// std::cout << "Prim开始选边" << std::endl;size_t size = 0;weight_type totalW = weight_type();while (!minq.empty()){__edge min = minq.top();minq.pop();// 最小边的目标点也在X集合,则构成环if (X[min.__dsti]){// 构成环// print logs or do nothing}else{minTree.__add_edge(min.__srci, min.__dsti, min.__weight);X[min.__dsti] = true;Y[min.__dsti] = false;++size;totalW += min.__weight;if (size == n - 1) // 找够了边break;// 把Y集合连出去的边都加入加入到优先队列里面,准备下一次循环选最小的边for (size_t i = 0; i < n; ++i)if (__matrix[min.__dsti][i] != __max_weight && Y[i])minq.push(__edge(min.__dsti, i, __matrix[min.__dsti][i]));}}if (size == n - 1)return totalW;elsereturn weight_type();
}

最短路径

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

单源最短路径–Dijkstra算法

单源最短路径问题:给定一个图G=(V, E)。求源结点s∈V到图中每个结点v∈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算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径。

代码如下所示:

void Dijkstra(const vertex_type &src, std::vector<weight_type> &dist, std::vector<int> &parent_path)
{// 用一个数组存 s->{yztx} 的距离size_t srci = get_vertex_index(src);size_t n = __vertexs.size();dist.resize(n, __max_weight);parent_path.resize(n, -1);dist[srci] = 0; // 自己到自己的长度就是0parent_path[srci] = srci;std::vector<bool> S(n, false); // 已经确定最短路径的顶点集合for (size_t cnt = 0; cnt < n; ++cnt){// 选最短路径顶点且不在S更新其他路径int u = 0;weight_type min = __max_weight;for (size_t i = 0; i < n; ++i){if (S[i] == false && dist[i] < min){u = i;min = dist[i];}}// 选到最小的点叫uS[u] = true;// 松弛更新 srci->u 已经确定了 u->... 需要更新for (size_t v = 0; v < n; v++){// 确认u连接的所有边v/*srci->u 就是 dist[u]u->v 就是 __matrix[u][v]srci->v 就是 dist[v]如果 srci->u + u->v < srci->v 更新 srci->v*/if (S[v] == false && __matrix[u][v] != __max_weight && dist[u] + __matrix[u][v] < dist[v]){dist[v] = dist[u] + __matrix[u][v];parent_path[v] = u; // v的父亲路径节点就不是srci了,就是u了}}}
}

所以我们也需要一个打印最短路径的接口,后面我们也要用到的。

// 打印最短路径
void print_short_path(const vertex_type &src, std::vector<weight_type> &dist, const std::vector<int> &parent_path)
{size_t srci = get_vertex_index(src);size_t n = __vertexs.size();for (size_t i = 0; i < n; ++i){if (i == srci)continue;// 找出i顶点的路径std::vector<int> current_path;size_t parent_i = i;while (parent_i != srci){current_path.push_back(parent_i); // 先把自己存进去,然后找上一层父亲parent_i = parent_path[parent_i];}current_path.push_back(srci); // 最后把原点加进去就行了std::reverse(current_path.begin(), current_path.end());// 此时已经找到路径了for (const auto &e : current_path)std::cout << __vertexs[e] << "[" << e << "]"<< "->";std::cout << "<" << dist[i] << ">" << std::endl;}
}

迪杰斯特拉算法中,如果顶点数量为N,时间复杂度: O(N^2),空间复杂度: O(N)

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

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Bellman-Ford算法的SPFA优化:SPFA算法

  1. 第一轮更新:所有边入队列
  2. 后面的轮次:更新最短路径的边入队列

代码:

bool BellManFord(const vertex_type &src, std::vector<weight_type> &dist, std::vector<int> &parent_path)
{/*return value:false: There is a negative weight circuit presenttrue: Find the shortest path*/size_t N = __vertexs.size();size_t srci = get_vertex_index(src);dist.resize(N, __max_weight);parent_path.resize(N, -1);dist[srci] = weight_type();for (size_t k = 0; k < N; k++){std::cout << "update round: " << k << std::endl;// i->j 更新k次bool is_update = false;for (size_t i = 0; i < N; i++)for (size_t j = 0; j < N; j++)// srci->i + i->j < src->jif (__matrix[i][j] != __max_weight && dist[i] + __matrix[i][j] < dist[j]){dist[j] = dist[i] + __matrix[i][j];parent_path[j] = i;is_update = true;}// 如果此轮没有更新出最短路径,后面的轮次就不用再走了if (is_update == false)break;}// 前面其实已经跟新完成了,如果还能发生更新,说明会出现负环情况for (size_t i = 0; i < N; i++)for (size_t j = 0; j < N; j++)if (__matrix[i][j] != __max_weight && dist[i] + __matrix[i][j] < dist[j])return false; // 如果还能发生更新,返回falsereturn true;
}

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

Floyd-Warshall算法的原理是动态规划

D i , j , k D_{i, j, k} Di,j,k 为从 i i i j j j 的只以 ( 1.. k ) (1 . . k) (1..k) 集合中的节点为中间节点的最短路径的长度。

  1. 若最短路径经过点 k \mathrm{k} k , 则 D i , j , k = D i , k , k − 1 + D k , j , k − 1 D_{i, j, k}=D_{i, k, k-1}+D_{k, j, k-1} Di,j,k=Di,k,k1+Dk,j,k1 ;
  2. 若最短路径不经过点 k \mathrm{k} k , 则 D i , j , k = D i , j , k − 1 D_{i, j, k}=D_{i, j, k-1} Di,j,k=Di,j,k1

因此, D i , j , k = min ⁡ ( D i , j , k − 1 , D i , k , k − 1 + D k , j , k − 1 ) D_{i, j, k}=\min \left(D_{i, j, k-1}, D_{i, k, k-1}+D_{k, j, k-1}\right) Di,j,k=min(Di,j,k1,Di,k,k1+Dk,j,k1)

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

代码:

void FloydWarshall(std::vector<std::vector<weight_type>> &vv_dist, std::vector<std::vector<int>> &vv_parent_path)
{// 初始化size_t n = __vertexs.size();vv_dist.resize(n);vv_parent_path.resize(n);for (size_t i = 0; i < n; ++i){vv_dist[i].resize(n, __max_weight);vv_parent_path[i].resize(n, -1);}// 先把直接相连的边更新一下for (size_t i = 0; i < n; ++i)for (size_t j = 0; j < n; ++j){if (__matrix[i][j] != __max_weight){vv_dist[i][j] = __matrix[i][j];vv_parent_path[i][j] = i;}if (i == j)vv_dist[i][j] = 0;}// 最短路径的更新 i->{其他顶点}->j// k作为i->j的中间点,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 (vv_dist[i][k] != __max_weight && vv_dist[k][j] != __max_weight && vv_dist[i][k] + vv_dist[k][j] < vv_dist[i][j]){vv_dist[i][j] = vv_dist[i][k] + vv_dist[k][j];vv_parent_path[i][j] = vv_parent_path[k][j]; // 注意,这里不是=k// 应该要找跟j相连的上一个邻接顶点,但是k不一定是邻接顶点哦!// 所以要找 vv_parent_path[k][j]/*如果k->j直接相连,上一个点就是k,vv_parent_path[k][j]就是k如果k->j不直接相连,k->...->x->j,vv_parent_path[k][j]是x*/}
}

本部分原理讲解及图文大量参考了《算法导论》和《殷人昆 数据结构:用面向对象方法与C++语言描述 (第二版)》的内容。

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

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

相关文章

WiFi模块助力少儿编程:创新学习与实践体验

随着科技的飞速发展&#xff0c;少儿编程已经成为培养孩子们创造力和问题解决能力的重要途径之一。在这个过程中&#xff0c;WiFi模块的应用为少儿编程领域注入了新的活力&#xff0c;使得学习编程不再是单一的代码教学&#xff0c;而是一个充满创新与实践的综合性体验。 物联网…

Python快速入门系列-2(Python基础语法)

第三章&#xff1a;Python基础语法 3.1 变量与数据类型3.1.1 变量的定义与赋值3.1.2 数据类型3.1.3 类型转换 3.2 注释与缩进3.2.1 注释3.2.2 缩进 3.3 条件语句与循环结构3.3.1 条件语句3.3.2 循环结构 3.4 函数与模块3.4.1 函数3.4.2 参数和返回值3.4.3 模块3.4.4 标准库中的…

链表|142.环形链表

ListNode *detectCycle(ListNode *head) {ListNode *fast head, *slow head;while (fast && fast->next) {// 这里判断两个指针是否相等&#xff0c;所以移位操作放在前面slow slow->next;fast fast->next->next;if (slow fast) { // 相交&#xff0c…

考研复试要想顺利通关,务必掌握的一些问题

亲爱的学弟学妹们&#xff0c;大家好&#xff01; 我是研一的学姐&#xff0c;深知考研路上的艰辛与不易。如今&#xff0c;为了回馈广大考研学子&#xff0c;我决定将自己精心整理的考研复试资料拿出来与大家分享&#xff0c;希望能为你们的复试之路添砖加瓦&#xff0c;助你…

【C语言】linux内核dev_queue_xmit

一、中文注释 这两个函数是Linux内核网络子系统中负责发起网络包&#xff08;sk_buff结构&#xff09;传输的代码。下面我将用中文对这两个函数做一个简单的注释&#xff1a; /*** __dev_queue_xmit - 发送一个buffer* skb: 要发送的buffer* sb_dev: 子设备&#xff0c;用于层…

12. 建立用户表并使用雪花算法生成用户ID

文章目录 一、建立用户表二、雪花算法生成唯一ID三、将雪花算法整合到我们的项目中 一、建立用户表 上一节我们搭建完了脚手架&#xff0c;从这一节开始&#xff0c;就正式进入到业务逻辑的开发了。首先要开发的就是博客系统的用户注册与登录功能。 既然涉及到用户&#xff0…

XSS靶场-DOM型初级关卡

一、环境 XSS靶场 二、闯关 1、第一关 先看源码 使用DOM型&#xff0c;获取h2标签&#xff0c;使用innerHTML将内容插入到h2中 我们直接插入<script>标签试一下 明显插入到h2标签中了&#xff0c;为什么不显示呢&#xff1f;看一下官方文档 尽管插入进去了&#xff0…

存算一体成为突破算力瓶颈的关键技术?

大模型的训练和推理需要高性能的算力支持。以ChatGPT为例&#xff0c;据估算&#xff0c;在训练方面&#xff0c;1746亿参数的GPT-3模型大约需要375-625台8卡DGX A100服务器训练10天左右&#xff0c;对应A100 GPU数量约3000-5000张。 在推理方面&#xff0c;如果以A100 GPU单卡…

【Leetcode每日一刷】数组|双指针篇:977. 有序数组的平方、76. 最小覆盖子串(附滑动窗口法详解)

力扣每日刷题 一、977. 有序数组的平方1.1题目1.2、解题思路1.3、代码实现——C 二、76. 最小覆盖子串2.1&#xff1a;题目2.2、解题思路2.3&#xff1a;代码实现——c2.4&#xff1a;易错点 一、977. 有序数组的平方 1.1题目 [题目链接]( 1.2、解题思路 题型&#xff1a;双…

思维调试:为什么FormatMessage提示找不到资源?

在不调试的情况下解决下面的问题&#xff0c;说明你的思维调试能力又进阶了。 问题 我在调用 FormatMessage 函数加载一个插入的资源字符串&#xff0c;由于某种未知的原因&#xff0c;它没能按预期那样工作。 我要加载的字符串类似于这样的 “Blah blah blah %1. Blah blah …

JavaEE——简单认识JS(Web API)

文章目录 一、认识什么是 WebAPI二、认识事件三、操作元素1. innerHTML 属性2. 获取 / 修改元素内容3. 获取 / 修改 元素属性4. 获取 / 修改 表单元素属性5. 获取 / 修改 样式属性6. 创建 / 删除元素 一、认识什么是 WebAPI 1.什么是API 在我们了解 WebAPI 之前&#xff0c;我们…

MYSQL使用mysqldump备份、复原数据库

参考 添加链接描述 1. 备份数据库 C:\Windows\system32>mysqldump -uroot -p test student>C:\student.sql Enter password: ****2. 备份多个数据库 mysqldump -u root -p --databases test mysql>C:\testandmysql.sql3. 备份所有数据库 mysqldump -u root -p -…

【Python】外网远程登录访问jupyter notebook+pycharm使用ipython

第一步&#xff1a;创建python虚拟环境 conda create -n py3610 python3.6.10第二步&#xff1a;安装ipython pip install ipython pip install ipython notebook第三步&#xff1a;创建 IPython Notebook 服务器配置文件 # 进入python交互shell&#xff0c;设置密码 >&…

数论<1>——数论基础

这期博客是一个数论入门介绍&#xff0c;dalao们可以自动忽略。 Part 1:素数(质数) 说到数论&#xff0c;小学奥数里也有。我最先想到的就是质数了。素数就是一个只能被1和它自己整除的数。判断的方法也很简单&#xff0c;可以扫一遍就结束了&#xff0c;但是没必要。由于一个…

Hudi入门

一、Hudi编译安装 1.下载 https://archive.apache.org/dist/hudi/0.9.0/hudi-0.9.0.src.tgz2.maven编译 mvn clean install -DskipTests -Dscala2.12 -Dspark33.配置spark与hudi依赖包 [rootmaster hudi-spark-jars]# ll total 37876 -rw-r--r-- 1 root root 38615211 Oct …

【NR 定位】3GPP NR Positioning 5G定位标准解读(十一)-增强的小区ID定位

前言 3GPP NR Positioning 5G定位标准&#xff1a;3GPP TS 38.305 V18 3GPP 标准网址&#xff1a;Directory Listing /ftp/ 【NR 定位】3GPP NR Positioning 5G定位标准解读&#xff08;一&#xff09;-CSDN博客 【NR 定位】3GPP NR Positioning 5G定位标准解读&#xff08;…

QT中使用QProcess执行命令,实时获取数据,例如进度条

前言 因为之前写了一个接收和发送文件的脚本&#xff0c;然后又需要获取进度&#xff0c;同步到进度条中。 效果&#xff1a; 使用正则匹配&#xff0c;获取命令行命令中的以下数据&#xff0c;然后同步到进度条 源码demo&#xff1a; 非完整代码&#xff1a; #include <Q…

nodejs web服务器 -- 搭建开发环境

一、配置目录结构 1、使用npm生成package.json&#xff0c;我创建了一个nodejs_network 文件夹&#xff0c;cd到这个文件夹下&#xff0c;执行&#xff1a; npm init -y 其中-y的含义是yes的意思&#xff0c;在init的时候省去了敲回车的步骤&#xff0c;如此就生成了默认的pac…

山泉还可以申请商标不,现阶段通过率如何!

在32类类别啤酒饮料是许多生产水企业主要申请注册的类别&#xff0c;那现在山泉在这个类别还可以申请注册商标不&#xff0c;山泉在这个类别基本上是通用词&#xff0c;首先是需要前面词具有显著性&#xff0c;没的相同或近似才可以。 经普推知产老杨检索发现&#xff0c;在32…

手机APP测试——如何进行安装、卸载、运行?

手机APP测试——主要针对的是安卓( Android )和苹果IOS两大主流操作系统,主要考虑的就是功能性、兼容性、稳定性、易用性、性能等测试&#xff0c;今天先来讲讲如何进行安装、卸载、运行的内容。 一、App安装 1、点击运行APP安装包,检测安装包是否正常; . 2、进入[安装向导]…