算法 - 图的实例 - 最短路径 (Shortest Path)
本文将介绍最短路径的基础知识,并用C++实现迪杰斯特拉算法(Dijkstra)和弗洛伊德算法(Floyed)。
在查看本文之前,需要一些数据结构和程序语言的基础。
尤其是“矩阵”、“矩阵的压缩(matrix)”、“图(graph)”等的知识。
1 最短路径简述 (Introduction)
最短路径问题是找到一个顶点到另一个顶点之间权值之和最小的路径。
-
一个顶点称为源点
-
另一个顶点称为终点
其图是有向带权图。
2 迪杰斯特拉算法 (Dijkstra)
迪杰斯特拉算法 (Dijkstra) 适用于:
-
(1)源点只有一个;
-
(2)权值必须都是大于等于零。
2.1 算法基本思想 (Basic Idea)
Dijkstra算法意指按照路径长度的递增次序,逐步的产生最短路径。
-
(1)首先、求出长度最短的一条最短路径;
-
(2)其次,参照它求出长度次短的一条最短路径;
-
(3)最后,重复步骤(2)直到求出所有最短路径。
假设原图,起始点A
:
其从A开始到达各个点的权值,取最小值就是路径:
到达点\经过边数 | 1 | 2 | 3 |
---|---|---|---|
B | AB(10) | ||
C | ABC(60), ADC(50) | ||
D | AD(30) | ||
E | AE(90) | ADE(100) | ABCE(70), ADCE(60) |
F |
2.2 算法设计思路 (Design Idea)
其算法描述:
- 假设 n 个顶点, e 条边的图,求得最短路径的集合为 P, 顶点为 { v0, v1, … , vn }, 权值和的数组为 cost[n]。
- (1)初始化: P = { v0 } , cost[vadj] = Edge[v0][vadj] ;(adj为邻接顶点)
- (2)获取最短路径,并添加顶点: cost[vmin] = min(cost) , P = { v0, vmin } ;
- (3)更新路径长度与路径:cost[vminadj] = min(cost[vminadj], cost[vmin] + Edge[vmin][vminadj]) ;
- (4)重复步骤(2)和(3):直到所有顶点检测完成。
其 n 个顶点的图,时间复杂度 O(n2) 。
2.3 C++代码 (C++ Code)
// Author: https://blog.youkuaiyun.com/DarkRabbit
// Shortest Path
// 迪杰斯特拉算法
// params:
// graph: 需要计算的图
// begin: 起始顶点
// paths: 输出的顺序
// costs: 输出的权值和
// return:
// bool: 是否出错
bool Dujkstra(AMGraphInt* graph,
int begin,
std::vector<std::vector<int>>& paths,
std::vector<double>& costs)
{
if (graph == nullptr || !graph->IsOriented()) // 无向图返回
{
return false;
}
int size = graph->GetVertexCount(); // 顶点数量
if (begin < 0 || begin >= size) // 起始点不在图中,直接返回
{
return false;
}
double infinity = graph->GetDefaultWeight(); // 没有边的权值,即正无穷
paths.assign(size, std::vector<int>()); // 初始化空路径
costs.assign(size, infinity); // 初始化权值和
std::vector<bool> marks(size, false); // 顶点标记(集合)
// 起始顶点
costs[begin] = 0;
marks[begin] = true;
paths[begin].push_back(begin);
// 初始化costs,从起始点开始到达的首个顶点
double weight = 0;
for (int adj = graph->FirstNeighbor(begin);
adj != -1;
adj = graph->NextNeighbor(begin, adj))
{
graph->TryGetWeight(begin, adj, weight);
paths[adj].push_back(begin); // begin 加入到 adj 的路径
costs[adj] = weight;
}
// 循环,路径最多需要加入 size - 1 个顶点
for (int i = 1; i < size; i++)
{
int minVertex = -1; // 选出未访问过的,cost最小的顶点
double minCost = infinity; // 选出的最小cost
// 选出未访问过的,cost最小的顶点
for (int j = 0; j < size; j++)
{
// 如果顶点没有访问过,且cost更小
if (!marks[j] && costs[j] < minCost)
{
minVertex = j;
minCost = costs[j];
}
}
if (minVertex == -1) // 未选出顶点,说明其它顶点无法到达
{
break;
}
marks[minVertex] = true; // 访问此顶点
paths[minVertex].push_back(minVertex); // 加入路径
// 查找minVertex的邻接顶点,并更新路径和cost
for (int adj = graph->FirstNeighbor(minVertex);
adj != -1;
adj = graph->NextNeighbor(minVertex, adj))
{
graph->TryGetWeight(minVertex, adj, weight);
// 如果新cost更小,则更新,新cost = 到达上一个顶点的cost + 到达此顶点的 cost
if (!marks[adj] && costs[minVertex] + weight < costs[adj])
{
costs[adj] = costs[minVertex] + weight; // 更新cost
paths[adj].clear();
paths[adj].insert(paths[adj].begin(),
paths[minVertex].begin(),
paths[minVertex].end()); // 更新路径
}
}
}
return true;
}
3 弗洛伊德算法 (Floyed)
弗洛伊德算法 (Floyed) 相比较于 迪杰斯特拉算法 (Dijkstra) 它:
-
(1)可以有多个源点;
-
(2)权值可以是负数,但负数权值不能组成回路。
-
(3)复杂度更高。
3.1 算法基本思想 (Basic Idea)
对有 n n n 个顶点, e e e 条边,权值为 ( e i j ) n × n (e_{ij})_{n \times n} (eij)n×n 的图:
-
(1)它是通过一个 n n n 阶方阵求出每两点间的最短路径,即
A = { A ( 0 ) , A ( 1 ) , ⋯   , A ( n ) } A = \{ A^{(0)}, A^{(1)}, \cdots, A^{(n)} \} A={ A(0),A(1),⋯,A(n)}
其中:
A ( 0 ) = ( a i j ) n × n ( 0 ) , ( a i j ) ( 0 ) = e i j A ( k ) = ( a i j ) n × n ( k ) , ( a i j ) ( k ) = min { ( a i j ) ( k − 1 ) , ( a i k ) ( k − 1 ) + ( a k j ) ( k − 1 ) } , k ∈ [ 1 , n ] \begin{aligned} A^{(0)} &=(a_{ij})^{(0)}_{n \times n}, &(a_{ij})^{(0)} &=e_{ij} \\ A^{(k)} &=(a_{ij})^{(k)}_{n \times n}, &(a_{ij})^{(k)} &= \min \{(a_{ij})^{(k-1)}, (a_{ik})^{(k-1)} + (a_{kj})^{(k-1)}\}, & k \in [1, n] \end{aligned} A(0)A(k)=(aij)n×n(0),=(aij)n×n(k),(aij)(0)(aij)(k)=eij=min{ (aij)(k−1),(aik)(k−1)+(akj)(k−1)},k∈[1,n] -
(2) ( a i j ) ( 0 ) (a_{ij})^{(0)} (aij)(0) 是从顶点 v i v_i vi 到 v j v_j vj ,以 v 0 v_0 v0 为中介的最短路径长度;
-
(3) ( a i j ) ( k ) (a_{ij})^{(k)} (aij)(k) 是从顶点 v i v_i vi 到 v j v_j vj ,以 v 0 , v 2 , ⋯   , v k v_0, v_2, \cdots, v_k v0,v2,⋯,vk 为中介的最短路径长度;
-
(4)