Dijkstra算法是图论中最经典的单源最短路径算法之一,由荷兰计算机科学家Edsger Dijkstra在1956年提出。这个算法在路由选择、地图导航、网络流量优化等领域有着广泛应用。理解其核心思想对于掌握图算法至关重要。
Dijkstra算法的核心是基于贪心策略,逐步确定从源点到图中所有其他顶点的最短路径。它维护两个关键集合:
算法每次从Q中选择距离源点最近的顶点u,将其加入S,然后对u的所有邻接顶点进行松弛操作(relaxation)。这个过程不断重复,直到所有顶点都加入S或者目标顶点被加入S。
关键点:Dijkstra算法的正确性依赖于一个基本假设——图中所有边的权重必须为非负数。这个假设保证了每次从Q中选出的顶点u,其当前距离已经是源点到u的最短距离。
让我们通过一个具体例子来理解算法的执行流程:
初始化阶段:
主循环阶段:
终止条件:
Dijkstra算法最适合以下场景:
但它也有明确的限制:
在实际应用中,我们通常使用优先队列优化版本,这也是接下来要重点讨论的实现方式。
高效的Dijkstra实现需要合理选择数据结构。以下是关键数据结构的选择:
图的表示:使用邻接表(vector<vector<pair<int, int>>>)
距离数组:vector
前驱数组:vector
优先队列:priority_queue<pair<int, int>>
以下是经过充分优化的Dijkstra算法C++实现:
cpp复制#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;
// 邻接表表示图
vector<vector<pair<int, int>>> adj; // adj[u] = {v, weight}
vector<int> dist; // 存储从源点到每个顶点的最短距离
vector<int> parent; // 存储最短路径中每个顶点的前驱顶点
void dijkstra(int src, int n) {
dist.assign(n, INT_MAX);
parent.assign(n, -1);
dist[src] = 0;
// 最小堆优先队列
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
pq.push({0, src});
while (!pq.empty()) {
int u = pq.top().second;
int d = pq.top().first;
pq.pop();
// 懒惰删除处理
if (d > dist[u]) continue;
for (auto& edge : adj[u]) {
int v = edge.first;
int weight = edge.second;
// 松弛操作
if (dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
parent[v] = u;
pq.push({dist[v], v});
}
}
}
}
void printPath(int dest) {
if (parent[dest] == -1) {
cout << dest;
return;
}
printPath(parent[dest]);
cout << " -> " << dest;
}
int main() {
int n = 5;
adj.resize(n);
// 构建图
adj[0].push_back({1, 4});
adj[0].push_back({2, 1});
adj[1].push_back({3, 1});
adj[1].push_back({4, 7});
adj[2].push_back({1, 2});
adj[2].push_back({3, 5});
adj[3].push_back({4, 3});
int source = 0;
dijkstra(source, n);
cout << "从顶点 " << source << " 到各顶点的最短距离:" << endl;
for (int i = 0; i < n; i++) {
cout << "顶点 " << i << ": ";
if (dist[i] == INT_MAX) {
cout << "不可达";
} else {
cout << "距离 = " << dist[i] << ", 路径: ";
printPath(i);
}
cout << endl;
}
return 0;
}
优先队列的使用:
懒惰删除技术:
松弛操作优化:
路径重建:
标准库的priority_queue在某些情况下可能不是最优选择。我们可以通过自定义数据结构获得更好性能:
cpp复制struct Edge {
int to, weight;
Edge(int t, int w) : to(t), weight(w) {}
};
struct Node {
int id, dist;
Node(int i, int d) : id(i), dist(d) {}
bool operator>(const Node& other) const {
return dist > other.dist;
}
};
void dijkstra_custom(int src, int n) {
vector<int> dist(n, INT_MAX);
vector<int> parent(n, -1);
dist[src] = 0;
priority_queue<Node, vector<Node>, greater<Node>> pq;
pq.push(Node(src, 0));
while (!pq.empty()) {
Node node = pq.top();
pq.pop();
int u = node.id;
if (node.dist > dist[u]) continue;
for (Edge& edge : adj[u]) {
int v = edge.to;
int new_dist = dist[u] + edge.weight;
if (new_dist < dist[v]) {
dist[v] = new_dist;
parent[v] = u;
pq.push(Node(v, dist[v]));
}
}
}
}
这种实现方式:
对于内存敏感的场景,可以使用更紧凑的实现:
cpp复制vector<int> dijkstra_compact(int src, int n, const vector<vector<pair<int, int>>>& graph) {
vector<int> dist(n, INT_MAX);
dist[src] = 0;
vector<bool> visited(n, false);
for (int i = 0; i < n; i++) {
int u = -1;
for (int j = 0; j < n; j++) {
if (!visited[j] && (u == -1 || dist[j] < dist[u])) {
u = j;
}
}
if (dist[u] == INT_MAX) break;
visited[u] = true;
for (const auto& edge : graph[u]) {
int v = edge.first, w = edge.second;
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
}
}
}
return dist;
}
这个版本:
对于大规模图计算,可以考虑并行化优化:
多源点并行:
邻接边并行处理:
图分区:
不同实现方式的时间复杂度差异显著:
| 实现方式 | 时间复杂度 | 适用场景 |
|---|---|---|
| 朴素实现 | O(V²) | 稠密图,顶点数少 |
| 优先队列 | O((V+E)logV) | 稀疏图,边较少 |
| 斐波那契堆 | O(VlogV + E) | 理论最优,实现复杂 |
| 双向搜索 | O((V+E)^(1/2)) | 单目标搜索 |
空间消耗主要来自:
优化方向:
根据实际场景选择合适实现:
负权重边:
整数溢出:
图连通性:
优先队列优化:
内存局部性:
预处理:
错误的最短路径:
性能不达预期:
内存消耗过大:
在实际项目中实现Dijkstra算法时,我通常会先使用优先队列的标准实现作为基准,然后根据具体场景进行针对性优化。对于性能关键的应用,建议实现多个版本并进行基准测试,选择最适合当前数据和硬件环境的实现方式。