作为一名算法竞赛选手,最短路问题是我在图论领域遇到的第一个重要课题。记得初学Dijkstra算法时,那种从迷茫到顿悟的过程至今难忘。本文将系统梳理最短路问题的核心算法及其典型应用场景,帮助读者建立完整的知识框架。
最短路问题的本质是在加权图中寻找两个顶点之间路径权值和最小的路径。根据图的性质(有无负权边、稀疏程度等),我们需要选择不同的算法:
实际应用中,90%的情况使用Dijkstra的堆优化版本就能解决问题。这也是为什么我在初学阶段就重点掌握了这个算法。
Dijkstra算法采用贪心策略,每次从尚未确定最短路径的顶点中选择距离起点最近的一个,然后更新其邻接顶点的距离估计。这个过程需要维护一个优先队列(小根堆)来高效获取当前最小距离顶点。
cpp复制// 典型Dijkstra实现框架
void dijkstra(int s) {
priority_queue<PII, vector<PII>, greater<PII>> pq;
dist[s] = 0;
pq.push({0, s});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (vis[u]) continue;
vis[u] = true;
for (auto &[v, w] : adj[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
}
初学者常犯的错误包括:
调试时建议:
虽然Dijkstra不能直接求最长路,但可以通过以下方式解决:
cpp复制// 最长路转换示例
for (auto &e : edges) {
e.w = -e.w; // 边权取反
}
dijkstra(start);
longest_path = -dist[end]; // 结果取反
当需要计算所有顶点对之间的最短路时,Floyd-Warshall算法是最佳选择。其核心思想是动态规划:
cpp复制// Floyd-Warshall算法实现
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
次短路可以通过修改Dijkstra算法,同时维护到每个顶点的最短路和次短路。k短路则通常使用A*算法或Yen's算法。
当顶点不是数字而是字符时,需要建立映射关系。常见解决方案:
cpp复制// 字符串顶点映射示例
unordered_map<string, int> node_map;
int get_id(const string &s) {
if (!node_map.count(s)) {
int id = node_map.size() + 1;
node_map[s] = id;
}
return node_map[s];
}
对于"去程+回程"类问题,可以通过正反向建图将多源单终点问题转化为单源最短路问题:
cpp复制// 正反向建图示例
vector<Edge> forward_edges, backward_edges;
// 构建正向图(去程)
build_graph(forward_edges);
dijkstra(forward_dist, start);
// 构建反向图(回程)
build_reverse_graph(backward_edges);
dijkstra(backward_dist, end);
// 结果计算
for (int i = 1; i <= n; ++i) {
total[i] = forward_dist[i] + backward_dist[i];
}
对于二维网格中的最短路问题,可以将每个网格点视为图中的一个顶点,相邻网格点之间建立边:
cpp复制// 网格图Dijkstra实现
struct Node {
int x, y, dist;
bool operator<(const Node &other) const {
return dist > other.dist; // 小根堆
}
};
void grid_dijkstra(int start_x, int start_y) {
priority_queue<Node> pq;
pq.push({start_x, start_y, grid[start_x][start_y]});
dist[start_x][start_y] = grid[start_x][start_y];
while (!pq.empty()) {
auto node = pq.top(); pq.pop();
if (vis[node.x][node.y]) continue;
vis[node.x][node.y] = true;
for (int i = 0; i < 4; ++i) {
int nx = node.x + dx[i], ny = node.y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (dist[nx][ny] > dist[node.x][node.y] + grid[nx][ny]) {
dist[nx][ny] = dist[node.x][node.y] + grid[nx][ny];
pq.push({nx, ny, dist[nx][ny]});
}
}
}
}
计算最短路数量的关键在于发现:当找到一条更短的路径时,重置计数;当找到等长路径时,累加计数:
cpp复制if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
cnt[v] = cnt[u]; // 重置计数
pq.push({dist[v], v});
} else if (dist[v] == dist[u] + w) {
cnt[v] += cnt[u]; // 累加计数
cnt[v] %= MOD; // 可能需要取模
}
当图中存在特殊边(如可免费使用的边、可改变权重的边)时,可以通过建立分层图来建模:
对于边权会随时间变化的图,可以考虑:
当顶点数很大时(1e5以上),可以考虑:
对于时间敏感的题目:
最短路问题看似简单,但深入掌握需要大量练习和思考。我在初学阶段曾反复实现Dijkstra算法十余次,每次都有新的收获。建议读者不要满足于AC,而要深入理解每个细节的实现原理和优化空间。