08 图 | 数据结构与算法
1. 图的基本概念
1. 图的定义
-
图:图 是由顶点的有穷非空集合和顶点之间边的集合组成的一种数据结构,通常表示为: \(G=( V, E )\) ,\(V\)是顶点的集合,\(E\)是顶点之间边的集合。通常用顶点表示数据元素,边表示数据元素之间的逻辑关系
-
图的分类
- 无向图
- 无向边:如果顶点\(v_i,v_j\)之间的边之间没有方向,则称这条边为 无向边,表示为\((v_i,v_j)\)
- 无向图:如果图的任意两个顶点之间的边都是无向边,那么该图被称为 无向图
- 有向图
- 有向边:如果顶点\(v_i,v_j\)之间的边之间有方向,则称这条边为 有向边(弧),表示为\(<v_i,v_j>\),其中\(v_i\)被称为 弧尾,\(v_j\)被称为 弧头
- 有向图:如果图的任意两个顶点之间的边都是有向边,那么该图被称为 有向图
- 无向图
-
完全图
- 无向图:在具有\(n\)个顶点的无向图中,最大弧数为\(n(n-1)/2\)
- 有向图:在具有\(n\)个顶点的有向图中,最大弧数为\(n(n-1)\)
-
顶点的度
- 无向图的度:一个顶点\(v\)的度是与它相关联的 边的条数
- 有向图的度
- 出度:以顶点\(v\)为 弧尾(起点) 的弧的数目
- 入度:以顶点\(v\)为 弧头(终点) 的弧的数目
-
路径
- 无向图的路径:在无向图\(G=(V,E)\)中,如果存在顶点序列\(v_p,v_{i1},v_{i2},\dots,v_{im},v_q\),使得\((v_p,v_{i1}),(v_{i1},v_{i2}),\dots,(v_{im},v_q)\in E\),则称该序列为从\(v_p\)到\(v_q\)的一条 路径
- 有向图的路径:在有向图\(G=(V,E)\)中,如果存在顶点序列\(v_p,v_{i1},v_{i2},\dots,v_{im},v_q\),使得\(<v_p,v_{i1}>,<v_{i1},v_{i2}>,\dots,<v_{im},v_q>\in E\),则称该序列为从\(v_p\)到\(v_q\)的一条 有向路径
- 路径的 长度:路径上边的数量
- 带权图的路径长度:路径上各边的权值之和
- 简单路径:如果路径上的各顶点\(v_1,v_2,\dots,v_m\)互不相同,则称这样的路径为 简单路径
-
子图:如果图\(G=(V,E),G^{\prime} = (V^{\prime}, E^{\prime})\),其中\(V^{i}\in V,E^{\prime}\in E\),那么\(G^{\prime}\)是\(G\)的 子图
-
连通图和连通分量(无向图)
- 连通性:顶点的连通性:在无向图中,若从顶点\(v_i\)到顶点\(v_j (i\ne j)\)有路径,则称顶点\(v_i\)与\(v_j\)是连通的
- 连通图:如果一个无向图中任意一对顶点都是连通的, 则称此图是 连通图
- 连通分量:非连通图的极大连通子图叫做 连通分量
-
强连通图与强连通分量
- 顶点的强连通性:在有向图中, 若对于每一对顶点\(v_i\)和\(v_j (i\ne j)\),都存在一条从\(v_i\)到\(v_j\)和从\(v_i\)到\(v_j\)的有向路径,则称顶点\(v_i\)与\(v_j\)是 强连通的
- 强连通图:如果一个有向图中任意一对顶点都是强连通的, 则称此有向图是 强连通图
- 强连通分量:非强连通图的极大强连通子图叫做 强连通分量
-
生成树:假设一个连通图有 \(n\)个顶点和 \(e\) 条边,其中 \(n-1\) 条边和 \(n\) 个顶点构成一个极小连通子图,称该极小连通子图为此连通图的 生成树
2. 图与其他数据结构的比较
- 比较一
1. 在线性结构中,数据元素之间仅具有线性关系(1:1)
2. 在树结构中,结点之间具有层次关系(1:n)
3. 在图结构中,任意两个点之间都可能有关系(m:n) - 比较二
1. 在线性结构中,数据元素之间的关系为 前驱和 后继
2. 在树结构中,结点之间的关系为 双亲和孩子
3. 在图结构中,顶点之间的关系为 邻接
3. 图的存储
-
邻接矩阵表示法
- 顶点表:一个记录各个顶点信息的一维数组
- 邻接矩阵:一个表示各个顶点之间的关系(边或弧)的二维数组
-
定义:设图 \(G = (V, E)\)是一个有\(n\)个顶点的图,则图的邻接矩阵\(A\)定义为
\[A= \left\{\begin{array}{l} 1,<v_i,v_j>\in E\space or\space (v_i,v_j)\in E \\ 0, 其他 \end{array}\right. \] -
示例
-
特点
- 无向图的邻接矩阵是对称的
- 有向图的邻接矩阵可能是不对称的
- 在无向图中, 第 \(i\) 行 (列) 1 的个数就是顶点\(i\)的度
- 在有向图中:第 \(i\) 行 1 的个数就是顶点 \(i\) 的出度,第 \(j\) 列 1 的个数就是顶点 \(j\) 的入度
-
网(带权图)的邻接矩阵
- 定义\[A= \left\{\begin{array}{l} W_{i,j},<v_i,v_j>\in E\space or\space (v_i,v_j)\in E \\ \infty, 其他 \end{array}\right. \]
- 示例
- 定义
-
-
邻接表
-
邻接表:是图的一种链式存储结构
-
边(弧)的结点结构
adjvex
*nextArc
*info
该边(弧)所指向的顶点 指向下一条边(弧)指针 该边(弧)相关信息指针 -
顶点的结点结构
data
*firstArc
顶点信息 指向第一条依附该顶点的边(弧) -
有向图的邻接表
- 邻接表 (出边表)
- 逆邻接表 (入边表)
-
邻接表存储表示
#define MAXVEX 10000 // 边表节点 struct EdgeNode { int adjvex; //边指向哪个顶点的索引 int weight; EdgeNode* next; }; // 顶点表结构 struct VertexNode { int value; //顶点的值,为了简化与序号相同,第一个是0 EdgeNode *firstedge; }; // 图结构 struct GraphList { VertexNode adjList[MAXVEX]; int numVertex; int numEdges; };
-
邻接表建立时间复杂度:设图中有 n 个顶点,e 条边,若顶点信息即为顶点的下标,则时间复杂度为\(O(n+e)\)
-
-
有向图的十字链表
-
无向图的邻接多重表
2. 图的遍历
1. 图的遍历
- 定义:从图中某一顶点出发访遍图中其余顶点,且使每个顶点仅被访问一次,就叫做图的遍历
- 分类:深度优先搜索\(DFS\)和广度优先搜索\(BFS\)
2. 深度优先搜索\(DFS\)
-
定义:在访问图中某一起始顶点 \(v\) 后,由 \(v\) 出发,访问它的任一邻接顶点 \(w1\);再从 \(w1\) 出发,访问与 \(w1\)邻接但还没有访问过的顶点 \(w2\);然后再从 \(w2\) 出发,进行类似的访问;如此进行下去,直至到达所有的邻接顶点都被访问过的顶点 \(u\) 为止。接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止
-
示例:
-
算法:运用栈(递归)结构存储待\(DFS\)的结点
/*存储结构*/ /*g[i][j]表示第i个顶点与g[i][j]顶点有边相连接*/ /*从v0开始*/ void dfs(vector<vector<int>>& g, int i, vector<int>& visited) { cout << i << ' ' ; visited[i] = 1; for (int& neighbor: g[i]) { if (visited[neighbor] == 0) dfs(g, neighbor, visited); } } void GraphTraverse(vector<vector<int>>& g) { int n = g.size(); vector<int> visited(n, 0); // initialize visited array for (int i = 0; i < n; i++) { if (visited[i] == 0) dfs(g, i, visited); } }
-
时间复杂度
- 邻接表:扫描边的时间为\(O(e)\);而且对所有顶点递归访问1次,所以遍历图的时间复杂性为\(O(n+e)\)
- 邻接矩阵:查找每一个顶点的所有的边,所需时间为\(O(n)\),则遍历图中所有的顶点所需的时间为\(O(n^2)\)
3. 广度优先搜索\(BFS\)
-
定义:在访问了起始顶点 \(v\) 之后, 由 \(v\) 出发, 依次访问 \(v\) 的各个未被访问过的邻接顶点 \(w1, w2, \dots, wt\), 然后再顺序访问\(w1, w2, \dots, wt\)的所有还未被访问过的邻接顶点。再从这些访问过的顶点出发,再访问它们的所有还未被访问过的邻接顶点,如此做下去,直到图中所有顶点都被访问到为止
-
示例:
-
算法:运用队列结构存储待\(BFS\)的结点
/* 存储结构 */ /* g[i][j]表示第i个顶点与g[i][j]顶点有边相连接 */ /* 从v0开始 */ void bfs(vector<vector<int>>& g) { int n = g.size(); vector<int> visited(n, 0); // initialize visited array queue<int> q; q.push(0); while (!q.empty()) { int i = q.front(); cout << i << ' ' ; q.pop(); for (int& neighbor: g[i]) { if (visited[neighbor] == 0) { q.push(neighbor); } } } }
-
时间复杂度
- 邻接表:遍历图的时间复杂性为\(O(e)\)
- 邻接矩阵:遍历图中所有的顶点所需的时间为\(O(n^2)\)
3. 并查集
1. 并查集的定义
- 并查集:对于一个集合\(S=\{a_1, a_2,\dots, a_{n-1}, a_n\}\),我们还可以对集合\(S\)进一步划分: \(S_1,S_2,\dots,S_m\),并查集 希望能够快速确定\(S\)中的两两元素是否属于\(S\)的同一子集
- 并查集的基本结构:用树表示集合,不同的树是不同的集合,并查集中包含了多棵树,表示并查集中不同的子集,树的集合是森林,所以并查集属于森林
2. 并查集的基本操作
- 初始化
int father[n]; // n:顶点数量 for (int i = 0; i < n; i++) { father[i] = i; }
find
:返回node
所属集合树的根节点/*递归版*/ int find(int node) { return father[node] == node ? node : find(father[node]); } /*迭代版*/ int find(int node) { while(father[node] != node) { node = father[node]; } return node; }
join
:合并两个点所属的集合void join(int node1, int node2) { int p1 = find(node1); int p2 = find(ndoe2); father[node1] = node2; }
3. 并查集的优化
- 优化原因:上面的实现,每一次
find
时间复杂度为\(O(H)\),\(H\)为树的高度,因为没有对树做特殊处理,所以树的不断合并可能会使树严重不平衡,最坏情况每个节点都只有一个子节点,时间复杂度为\(O(n)\) - 优化方法
- 方法一:按秩合并:记录这棵树的高度(记为\(rank\));接下来当合并两棵树时,先对两棵树的高度进行判断,如不同,则让高度小的树的根指向高度大的根
/*initialize*/ int father[n]; // n:顶点数量 int rank[n]; for(int i=0; i<n; i++) { father[i] = i; rank[i] = 0; } /*find*/ int find(int node) { return father[node] == node ? node : find(father[node]); } /*join:优化*/ void join(int node1, int node2) { int p1 = find(node1); int p2 = find(node2); if(p1 == p2) return ; else if(rank[p1] > rank[p2]) { father[p2] = p1; } else { father[p1] = p2; if(rank[p1] == rank[p2]) ++rank[p2]; } }
- 方法二:路径压缩:查询时我们需沿着元素所在的树从下往上查询,最终找到这棵树的根,表明这个元素与其根对应元素属于同一组。因为在此查询过程中我们会经过许多节点,而如果我们能将这个元素直接指向根节点,那么就能节省许多查询的时间。同时,在查询过程中,每次经过的节点,我们都可以同时将他们一起直接指向根节点。这样做的话,我们再查询这些节点时,就能很快找到根
/*initialize*/ int father[n]; // n:顶点数量 for(int i=0; i<n; i++) { father[i] = i; } /*find:优化*/ int find(int node) { return node == father[node] ? node : (father[node] = find(father[node])); } /*join*/ void join(int node1, int node2) { int p1 = find(node1); int p2 = find(node2); father[p1] = p2; }
- 方法一:按秩合并:记录这棵树的高度(记为\(rank\));接下来当合并两棵树时,先对两棵树的高度进行判断,如不同,则让高度小的树的根指向高度大的根
4. 最小生成树
1. 最小生成树
- 生成树的代价:设\(G=(V,E)\)是一个无向连通网,\(E\)中的每一条边上有一个权值,生成树各边的权值之和被称为 生成树的代价
- 最小生成树\(MST\):在图\(G\)中的所有生成树中,代价最小的生成树被称为 最小生成树
- 最小生成树构造准则
- 尽可能用网络中权值最小的边
- 必须使用且仅使用 \(n-1\) 条边来联结网络中的 n个顶点
- 不能使用产生回路的边
2. \(Prim\)算法
-
基本思想:从连通网络 \(N = { V, E }\)中的某一顶点 \(u_0\)出发,选择与它关联的具有最小权值的边\((u_0, v)\),将其顶点加入到生成树的顶点集合\(U\)中。以后每一步从一个顶点在\(U\)中,而另一个顶点不在\(U\)中的各条边中选择权值最小的边\((u, v)\),把该边加入到生成树的边集中,把它的顶点加入到集合\(U\)中;如此重复执行,直到网络中的所有顶点都加入到生成树顶点集合\(U\)中为止
-
示例:从
v2
开始执行\(Prim\)算法 -
注意
- 若候选轻权边集中的轻权边不止一条,可任选其中的一条扩充到生成树中
- 连通图的最小生成树不一定是唯一的,但它们的权相等
- 设连通网络有 \(n\) 个顶点, 则该算法的时间复杂度为 \(O(n^2)\),它适用于边稠密的网络
-
算法
/* * 存储结构:g[i][j]表示第i个顶点与第j个顶点的权值 * 从beginNode开始Prim算法 * 返回选择的边的数组 */ struct edge { int begin; // begin of the edge int end; // end of the edge int weight; // weight of the edge edge(int begin, int end, int weight):begin(begin), end(end), weight(weight){} }; vector<edge> Prim(vector<vector<int>>& g, const int beginNode) { int n = g.size(); vector<int> lowcost(n, 0);//lowcost存储生成树顶点集内顶点到树外各顶点各边上当前最小权值 vector<int> adjvex(n, 0); // adjvex 记录生成树顶点集合外各顶点距离集合内哪个顶点最近 vector<edge> MST; // MST 存储最小生成树的边,最后返回 for(int i = 0; i < n; ++i) { lowcost[i] = g[beginNode][i]; // 起始顶点到各点的最小代价 adjvex[i] = beginNode; } adjvex[beginNode] = -1; // -1 表示已经访问 for(int i = 0; i < n; ++i) { if(i == beginNode) continue; int minCost = INT_MAX, pos = -1; for(int j = 0; j < n; ++j) { if(adjvex[j] != -1 && minCost > lowcost[j]) { //选择侯选边最小边 minCost = lowcost[j]; pos = j; } } if(pos != -1) { // pos != -1 表示找到要求顶点 adjvex[pos] = -1; MST.push_back( edge(adjvex[pos], pos, g[adjvex[pos][pos]]) ); for(int j = 0; j < n; ++j) { //更新最小代价数组 if(adjvex[j] != -1 && lowcost[j] > g[pos][j]) { lowcost[j] = g[pos][j]; adjvex[j] = pos; } } } } return MST; }
3. \(Kruskal\)算法
-
基本思想:设有一个有 \(n\) 个顶点的连通网络 \(N = \{ V, E \}\),最初先构造一个只有 \(n\)个顶点,没有边的非连通图 \(T = \{ V,\varnothing\}\)图中每个顶点自成一个连通分量。当在\(E\)中选到一条具有最小权值的边时,若该边的两个顶点落在不同的连通分量上,则将此边加入到\(T\)中;否则将此边舍去,重新选择一条权值最小的边。如此重复下去,直到所有顶点在同一个连通分量上为止
-
示例
-
算法
/* * 存储结构:边的数组, n为结点数量 * Kruskal 算法 * 返回选择的边的数组 */ struct edge { int begin; // begin of the edge int end; // end of the edge int weight; // weight of the edge edge(int begin, int end, int weight):begin(begin), end(end), weight(weight){} }; /*并查集*/ vector<int> father; int find(int node) { return (father[node] == node) ? node : find(father[node]); } void join(int a, int b) { father[find(a)] = find(b); } /*Kruskal*/ vector<edge> Kruskal(vector<edge>& edges, const int n) { father.resize(n); vector<edge> MST; sort(edges.begin(), edges.end(), [](const edge& a, const edge& b) { return a.weight < b.weight; }); // 将边按权值从小到大排序 for (int i = 0; i < n; ++i) { father[i] = i; //初始化各节点的根为自己 } for (int i = 0; i < edges.size() && MST.size() < n - 1; ++i) { int u = edges[i].begin, v = edges[i].end; if(find(u) != find(v)) { MST.push_back(edges[i]); join(u, v); } } return MST; }
5. 拓扑排序
1. \(AOV\)网
- \(AOV\)网:用顶点表示活动,用有向边\(<vi, vj>\)表示活动间的优先关系。\(vi\) 必须先于活动\(vj\) 进行,这种有向图叫做顶点表示活动的AOV网络\((Activity On Vertices)\)
- 如果AOV网络中存在有向环,此\(AOV\)网络所代表的工程是不可行的
- 拓扑序列: 即将各个顶点 (代表各个活动)排列成一个线性有序的序列,使得所有弧尾结点都排在弧头结点的前面
- 拓扑排序:构造\(AOV\)网络全部顶点的拓扑有序序列的运算就叫做拓扑排序
2. 拓扑排序
-
基本思想:在\(AOV\)网络中选一个没有直接前驱的顶点,从图中删去该顶点, 同时删去所有它发出的有向边,重复以上步骤,全部顶点均已输出,拓扑有序序列形成,拓扑排序完成;图中还有未输出的顶点,但已跳出处理循环。说明图中还剩下一些顶点, 它们都有直接前驱。这时网络中必存在有向环
-
示例:
-
算法
/* * 存储结构:g[i][j]表示第i个顶点指向g[i][j] * 拓扑排序 算法 * 返回拓扑序列 */ vector<int> top_sort(vector<vector<int>>& g) { int n = g.size(); vector<int> indeg(n, 0); //存储顶点的入度 vector<int> ans; for (int i = 0; i < n; ++i) { for(int& node: g[i]) { ++indeg[node]; } } queue<int> q; for(int i = 0; i < n; ++i) { if(indeg[i] == 0) { q.push(i); // 选择没有直接前驱的顶点 } } while(!q.empty()) { int node = q.front(); ans.push_back(node); q.pop(); for (int& i: g[node]) { --indeg[i]; // 去除边 if(indeg[i] == 0) { q.push(i); } } } if(ans.size() != n) cout << "exist ring, can not sort" << endl; return ans.size() == n ? ans : vector<int>(); }
6. \(AOE\)网
1. \(AOE\)网
- \(AOE\)网:如果在无有向环的带权有向图中,用有向边表示一个工程中的各项活动,用边上的权值表示活动的持续时间,用顶点表示事件,则这样的有向图叫做用边表示活动的网络,被称为\(AOE\)网\((Activity On Edges)\)网络
- 意义
- 完成整个工程至少需要多少时间(假设网络中没有环)
- 为缩短完成工程所需的时间, 应当加快哪些活动
- 源点与汇点:在\(AOE\)网络中, 有些活动顺序进行,有些活动并行进行,入度为零的点叫源点,出度为零的点叫汇点
- 关键路径:完成整个工程所需的时间取决于从源点到汇点的最长路径长度,即在这条路径上所有活动的持续时间之和,这条路径长度最长的路径就叫做关键路径
- 关键活动:关键路径上的活动为 关键活动
2. 关键路径求解算法
-
事件最早可能开始的时间
ve[i]
- 概念:从源点\(v_0\)到顶点\(v_i\)的最长路径长度
- 算法:从
ve[0] = 0
开始往前递推,ve[i] = max{ve[j] + time<vj, vi>}
-
事件最迟允许发生的时间
vl[i]
- 概念:在保证汇点\(v_{n-1}\)在
ve[n-1]
时刻完成的前提下,事件\(v_i\)的允许的最迟开始时间 - 算法:从
vl[n-1]=ve[n-1]
开始开始反推,vl[i] = min{vl[j] - time<vi, vj>}
- 概念:在保证汇点\(v_{n-1}\)在
-
活动开始的最早时间
e[k]
- 概念:设活动
ak
在带权有向边<vi, vj>
上,其持续时间为time<vi, vj>
,其最早发生时间e[k]=ve[i]
- 概念:设活动
-
活动最迟允许开始时间
l[k]
- 概念:
l[k]
是在不会引起时间延误的前提下, 该活动允许的最迟开始时间,l[k] = vl[j] - time<i, j>
- 概念:
-
时间余量
l[k]-e[k]
:表示活动ak
的最早可能开始时间和最迟允许开始时间的时间余量。l[k] == e[k]
表示活动ak
是没有时间余量的关键活动 -
示例
vertex
1 2 3 4 5 6 7 8 ve
0 8 12 22 28 40 46 58 vl
0 8 12 22 28 40 46 58 由公式计算出:
edge(activity)
1 2 3 4 5 6 7 8 9 e
0 8 12 12 22 22 28 40 46 l
0 8 12 12 22 32 28 40 46 因此除了
a6
不是关键活动,其他都是,因此得出关键路径 -
算法
// time[i][j]表示从i到j的边的权值,如果没有边相连,值为 0 // 以v0作为源点,vn作为汇点,且无有向环 struct { int begin; // begin of the edge int end; // end of the edge int weight; // weight of the edge edge(int begin, int end, int weight):begin(begin), end(end), weight(weight){} }; vector<edge> Critical_Path(vector<vector<int>>& time) { int n = time.size(); //n -- vertex number; vector<int> ve(n), vl(n); vector<edge> edges; vector<edge> critical_path; ve[0] = 0; for (int i = 1; i < n; ++i) { int minVal = 0; for (int j = 0; j < n; ++j) { if (time[i][j] != 0) { edges.push_back(edge(i, j, time[i][j])); minVal = min(minVal, time[i][j]); } } ve[i] = minVal; } vl[n - 1] = ve[n - 1]; for (int i = n - 2; i >= 0; --i) { int maxVal = INT_MAX; for (int j = 0; j < n; ++j) { if(time[i][j] != 0) { maxVal = max(maxVal, time[i][j]); } } vl[i] = maxVal; } vector<int> e(m), l(m); for (int i = 0; i < edges.size(); ++i) { e[i] = ve[edges[i].begin]; l[i] = vl[edges[i].end] - edges[i].weight; if(e[i] == l[i]) { critical_path.push_back(edges[i]); } } return critical_path; }
7. 最短路径
1. 最短路径问题
- 最短路径:如果图是一个带权图,则路径长度为路径上各边的权值之和,两个顶点之间的路径长度最短的路径为两个点之间的最短路径,其长度是 最短路径长度
- 算法
- \(Dijkstra\)算法:边上权值非负情形的单源最短路径问题
- \(Floyd\)算法:所有顶点之间的最短路径
2. \(Dijkstra\)算法
-
基本思想:按路径长度的递增次序, 逐步产生最短路径的算法。首先求出长度最短的一条最短路径,再参照它求出长度次短的一条最短路径,依次类推,直到从顶点v到其它各顶点的最短路径全部求出为止
-
示例:计算从
v4
顶点到各顶点的最短路径(pre
表示连接的前驱点,dist
表示当前的最短路径长度)v1
v2
v3
v4
v5
v6
dist
\(\infty\) 20 \(\infty\) 0 \(\infty\) 15 pre
v4
v4
v4
v4
v4
v4
dist
30 20 \(\infty\) ✔️ \(\infty\) 15 pre
v2
v4
v4
✔️ v4
v4
dist
30 20 45 ✔️ 50 ✔️ pre
v2
v4
v1
✔️ v2
✔️ dist
30 ✔️ 45 ✔️ 50 ✔️ pre
v2
✔️ v1
✔️ v2
✔️ dist
✔️ ✔️ 45 ✔️ 50 ✔️ pre
✔️ ✔️ v1
✔️ v2
✔️ dist
✔️ ✔️ ✔️ ✔️ 50 ✔️ pre
✔️ ✔️ ✔️ ✔️ v2
✔️ dist
✔️ ✔️ ✔️ ✔️ ✔️ ✔️ pre
✔️ ✔️ ✔️ ✔️ ✔️ ✔️ 因此从顶点
v4
出发到其余顶点的最短路径及长度vertex
length
path
v1
30 v4->v2->v1
v2
20 v4->v2
v3
45 v4->v2->v1->v3
v4
0 v4
v5
50 v4->v2->v5
v6
15 v4->v6
-
算法
- 基于
dist[]
数组的\(Dijkstra\)算法(时间复杂度\(O(n^2)\),如果求每个点到各点的最短路径则\(O(n^3)\))/* * 从beginNode开始的到各顶点的最短路径 * g[i][0:2]表示g[0]点到g[1]点的权g[2] * n 为顶点数量 */ vector<int> Dijkstra(vector<vector<int>>& g, int n, int beginNode) { int inf = 1e9; vector<vector<int>> adj(n, vector<int>(n, inf)); for(vector<int>& edge: g) { adj[edge[0]][edge[1]] = edge[2]; } vector<bool> visited(n, false); vector<int> dist(n, inf); dist[beginNode] = 0; for(int i = 0; i < n; ++i) { int x = -1; for(int j = 0; j < n; ++j) { if(!visited[j] && (x == -1 || dist[j] < dist[x])) { x = j; } } visited[x] = true; for(int j = 0; j < n; ++j) { dist[j] = min(dist[j], dist[x] + adj[x][j]); } } return dist; }
- 使用一个小根堆来寻找未确定节点中与起点距离最近的点的\(Dijkstra\)算法
/* * 从beginNode开始的到各顶点的最短路径 * g[i][0:2]表示g[0]点到g[1]点的权g[2] * n 为顶点数量 */ vector<int> Dijkstra(vector<vector<int>>& g, int n, int beginNode) { int inf = 1e9; vector<vector<pair<int, int>>> adj(n); for (vector<int>& edge: g) { int x = edge[0], y = edge[1]; adj[x].push_back({y, edge[2]}); } vector<int> dist(n, inf); dist[beginNode] = 0; priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q; q.push({0, beginNode}); while (!q.empty()) { auto p = q.top(); q.pop(); int val = p.first, x = p.second; if (dist[x] < val) { continue; } for (auto &e : adj[x]) { int y = e.first, d = dist[x] + e.second; if (d < dist[y]) { dist[y] = d; q.push({d, y}); } } } return dist; }
- 基于
3. \(Floyd\) 算法
- 基本思想:从初始的邻接矩阵\(A_0\)开始,递推地生成矩阵序列\(A_1,A_2,\dots,A_n\)
- 递推公式:\(A_0[i][j] = C[i][j]; A_{k+1}[i][j] = min(A_k[i][j], A_k[i][k]+A_k[k][j])\)
- 路径记录:显然,\(A\)中记录了所有顶点对之间的最短路径长度。若要求得到最短路径本身,还必须设置一个路径矩阵\(P[n][n]\),在第\(k\)次迭代中求得的
path[i][j]
,是从i
到j
的中间点序号不大于\(k\)的最短路径上顶点i的后继顶点。算法结束时,由path[i][j]
的值就可以得到从i
到j
的最短路径上的各个顶点 - 算法
/* * g[i][0:2]表示g[0]点到g[1]点的权g[2] * n 为顶点数量 */ vector<vector<int>> Floyd(vector<vector<int>>& g, int n) { int inf = 1e9; vector<vector<int>> A(n, vector<int>(n, inf)); for(vector<int>& edge: g) { A[edge[0]][edge[1]] = edge[2]; } for(int i = 0; i < n; ++i) { A[i][i] = 0; } for(int k = 0; k < n; ++k) { for(int i = 0; i < n; ++i) { for(int j = 0; j < n; ++j) { A[i][j] = min(A[i][j], A[i][k] + A[k][j]); } } } return A; }