迪杰斯特拉(Dijkstra)算法是一种用于在加权图中找到单源最短路径的算法。
工作原理
-
初始化:算法以一个源点开始,将所有顶点的最短路径值初始化为无限大,源点到自身的最短路径值设为0。
-
集合的概念:维护两个顶点集合,一个包含已经找到最短路径的顶点(已确定最短路径),另一个包含其余顶点(未确定最短路径)。
-
迭代过程:在未确定最短路径的顶点中,找到距离源点最近的顶点,将其从未确定集合中移动到已确定集合。
-
更新距离:更新所有与这个新确定顶点相邻的未确定顶点的最短路径值。更新方法是比较当前记录的最短路径值和通过这个新确定顶点到达它们的路径长度,取二者中较小的一个。
-
重复步骤:重复步骤3和步骤4,直到所有顶点都被移动到已确定集合。
特点
- 加权图:适用于带有非负权重的图。
- 贪心算法:每一步都选择当前看起来最优的选择。
- 单源最短路径:仅从一个源点出发到所有其他顶点的最短路径。
时间复杂度
取决于使用的数据结构。使用优先队列时,时间复杂度为 O((V+E)*logV)),其中V表示顶点数,E表示边的数量。
计算迪杰斯特拉算法使用优先队列时的时间复杂度涉及到算法中每个主要步骤的成本。这个成本主要取决于两件事:顶点的处理次数以及每次处理时涉及的操作。
迪杰斯特拉算法的关键操作:
-
从优先队列中提取最小元素:这通常是对数时间操作,具体是
O(log V)
,其中V
是顶点的数量。这是因为优先队列(通常实现为二叉堆)允许在对数时间内添加和删除元素。 -
减少优先队列中元素的优先级:在迪杰斯特拉算法中,当找到到某个顶点更短的路径时,会更新这个顶点的距离。在优先队列实现中,这通常通过将元素删除然后再用新的优先级重新插入来完成。每次操作也是
O(log V)
。 -
遍历所有边:算法需要查看图中的每条边来更新顶点的距离。如果图用邻接表表示,总共需要 O(E) 时间来查看所有边,其中
E
是边的数量。
时间复杂度计算:
-
每个顶点至多被加入队列一次(因为一旦顶点的最短路径被确定,就不会再改变),因此总共有
V
次O(log V)
的插入操作。 -
每条边最多导致一次优先级减少操作,因此有
E
次O(log V)
的减少优先级操作。
综上所述,总的时间复杂度是 O((V + E) log V)
。这里的 V log V
来自于顶点操作,而 E log V
来自于边操作。
C++代码实现
- 头文件的包含
#include<iostream> #include<queue> #include<vector>
- 有权图边的定义
struct Edge { int to;//表示目标点 int weight;//表示边长权重,非负 Edge(int t, int w) :to(t), weight(w) {}//构造函数初始化边 };
- 用邻接表表示图,用边的关系来初始化图
int n = 5;//顶点数 vector<vector<Edge>>graph(n);//邻接表表示这个图 graph[0].push_back(Edge(1, 10));//从0到1顶点的边权重为10 graph[0].push_back(Edge(2, 5));//同理 graph[1].push_back(Edge(2, 2)); graph[1].push_back(Edge(3, 1)); graph[2].push_back(Edge(1, 3)); graph[2].push_back(Edge(3, 9)); graph[2].push_back(Edge(4, 2)); graph[3].push_back(Edge(4, 4)); graph[4].push_back(Edge(3, 6)); graph[4].push_back(Edge(0, 7));
全部代码和测试用例
#include <iostream>
#include <vector>
#include<queue>
using namespace std;
struct Edge {
int to;
int weight;
Edge(int t, int w) :to(t), weight(w) {}
};
//graph是用邻接表表示的图
vector<int>dijkstra(const vector<vector<Edge>>& graph, int src) {
int n = graph.size();
//容器定义
//储存各个顶点到src顶点的距离
vector<int>dis(n, INT_MAX);
//记录访问过的顶点
vector<bool>vis(n, false);
//用优先级队列来处理距离最短的顶点,pair<int,int>的第一个int存储距离,第二个int存储顶点;底层用vector来存储这个队列;greater表示从小到大排
priority_queue<pair<int, int>,vector<pair<int,int>>,greater<pair<int,int>>>pq;
//src顶点到自己的距离为0
dis[src] = 0;
pq.push({0,src});
while (!pq.empty()) {
//v表示当前距离最短的顶点
int v = pq.top().second; pq.pop();
//若是访问过当前顶点则跳过
if (vis[v])continue;
vis[v] = true;
//访问邻接顶点
for (const auto&edge: graph[v]) {
int t = edge.to;
int w = edge.weight;
if (!vis[t]&&w + dis[v] < dis[t]) {
dis[t] = w + dis[v];
pq.push({ dis[t],t });
}
}
}
return dis;
}
int main() {
int n = 5;//顶点数
vector<vector<Edge>>graph(n);
graph[0].push_back(Edge(1, 10));
graph[0].push_back(Edge(2, 5));
graph[1].push_back(Edge(2, 2));
graph[1].push_back(Edge(3, 1));
graph[2].push_back(Edge(1, 3));
graph[2].push_back(Edge(3, 9));
graph[2].push_back(Edge(4, 2));
graph[3].push_back(Edge(4, 4));
graph[4].push_back(Edge(3, 6));
graph[4].push_back(Edge(0, 7));
vector<int>shortest_path = dijkstra(graph, 0);
cout << shortest_path[3];//输出9
return 0;
}
s->0,t->1,y->2,x->3,z->4
实现步骤
dijkstra算法的本质是贪心算法,每一步都是在寻找局部最小值(最短路径)
-
初始化:
dis[]
:一个数组,用于存储从源节点到每个目标节点的最短距离。初始时,源节点到自己的距离设为0,到所有其他节点的距离设为无限大。vis[]
:一个布尔数组,用于标记每个节点是否已被访问。初始时,所有节点都未被访问。pq[]
:一个优先队列,用于存储待处理的节点及其到源点的距离。优先队列按距离升序排列,以确保总是先处理当前距离最短的节点。这里使用优先级队列的目的是降低寻找距离最短节点的时间复杂度而且可以直接调库,能够保证时间复杂度是O(logN)(进行堆排序的过程)。也可以用max_element迭代寻找,只是时间复杂度会高。
-
访问起始节点:
- 将起始节点(比如 'A')的距离设为0,标记为已访问,并将其添加到优先队列中。
-
处理优先队列中的节点:
- 从优先队列中取出一个节点(起初是起始节点)。
- 遍历所有从该节点出发的边。对于每一条边:
- 计算从起始节点通过当前节点到达邻接节点的总距离。
- 如果这个总距离小于当前记录在
dis[]
中的那个邻接节点的距离,则更新dis[]
中对应的距离,并将该邻接节点(如果尚未访问)添加到优先队列中。
-
重复直至所有节点被访问:
继续从优先队列中取出节点并更新距离,直到队列为空,此时所有可达节点的最短距离都已找到。
应用场景
迪杰斯特拉算法在许多领域都有应用,如网络路由协议、地图导航、社交网络中的路径查找等。
限制
- 不适用于有负权重边的图,因为算法可能无法正确处理负权重导致的更短路径。
- 算法假定图中没有从源点无法到达的顶点,否则这些顶点的最短路径值将保持为无限大。
迪杰斯特拉算法由于其相对简单的实现和高效的性能,成为了图论中非常基础且广泛应用的算法之一。