最短路(堆优化dijkstra,spfa,bellman-ford)
Dijkstra:(不适合负权图)
Dijkstra算法是一种用于寻找图中单个源点到所有其他节点的最短路径的经典算法。传统Dijkstra算法使用优先队列(通常是基于数组的最小堆)进行优化,可以显著提高算法效率,特别是在处理边权非负的大型稀疏图时。
基本概念
-
图(Graph):由顶点和连接顶点的边组成的集合。
-
有向图(Directed Graph):边有方向的图。
-
无向图(Undirected Graph):边无方向的图。
-
加权图(Weighted Graph):图中的每条边都有一个权重(通常为距离或成本)。
-
最短路径问题:在图中找到从一个顶点到其他所有顶点的最短路径。
算法基础(不适合负权):
-
Dijkstra算法是一种贪心算法,其基础假设是从当前的所有可能中就能找到全局的最优解。这一假设在存在负权边的图中通常不成立。
-
负权边的影响:
- 在负权图中,通过添加一条负权边,可能会使得之前已经确定为最短路径的某条路径不再是最短的。而Dijkstra算法在每次选择最短路径时,仅考虑当前已知的最短路径,不考虑未来可能通过负权边找到的更短路径。
-
更新机制:
-
Dijkstra算法在更新顶点距离时,仅根据已访问顶点的邻接边进行更新。如果存在负权边,且该负权边连接了未访问的顶点和已访问的顶点,那么未访问顶点的最短距离可能会被低估,因为算法没有考虑通过这条负权边可能达到的更短路径。
-
传统Dijkstra算法的局限
传统的Dijkstra算法使用邻接矩阵或邻接表来表示图,并在每次选择最短未访问顶点时,都通过遍历所有顶点来找到距离最小的顶点。这种方法在顶点数较多时效率较低。
堆优化的Dijkstra算法
堆优化的Dijkstra算法使用最小堆(或优先队列)来维护当前已知的最短路径长度最小的顶点集合。这样做可以极大地减少每次寻找最小距离顶点的操作时间复杂度。
算法步骤
- 初始化:
- 创建一个数组
dist[]
,用于存储从源点到各个顶点的最短路径估计值,初始时,源点到自身的距离为0,到其他所有顶点的距离为无穷大(或图中的最大可能值)。 - 创建一个最小堆(优先队列),将源点及其距离为0的信息加入堆中。
- 创建一个集合
visited
,用于记录已找到最短路径的顶点。
- 创建一个数组
- 循环:
- 当堆不为空时,执行以下步骤:
- 从堆中取出距离最小的顶点
u
(及其对应的距离),标记为已访问。 - 遍历顶点u的所有邻接点v:
- 如果找到了一条从源点到
v
的更短路径(即dist[u] + weight(u, v) < dist[v]
),则更新dist[v]
的值,并将v
(及其新的距离)加入到堆中(如果v
已经在堆中,则更新其距离并重新调整堆)。
- 如果找到了一条从源点到
- 从堆中取出距离最小的顶点
- 当堆不为空时,执行以下步骤:
- 结束:
- 当所有顶点都被访问过时,算法结束。此时,
dist[]
数组中存储的就是从源点到各个顶点的最短路径长度。
- 当所有顶点都被访问过时,算法结束。此时,
堆操作的复杂性
- 插入:通常具有
O(log n)
的时间复杂度,其中n
是堆中的元素数量。 - 删除最小元素:同样具有
O(log n)
的时间复杂度。 - 更新元素:如果直接更新,然后重新插入堆,则为
O(log n)
;
代码演示:
#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
using namespace std;
typedef pair<int, int> pii;
const int N = 300;
int inf = 1e9;
int dist[N], vis[N]; // dist[i]表示从起点开始到i点的最短距离,vis表示该点是否已经确定了他的dist值
vector<pii> e[N];
void dijkstra() {求一条单源最短路从起点1开始
memset(dist, 0x3f, sizeof dist); // 初始化给dist数组赋值一个较大的数,ans取不到的值
dist[1] = 0; //从起点一开始,所以自己到自己的距离一定是0, dist数组含义就是从起点开始,到该点的最短距离
priority_queue<pii, vector<pii>, greater<pii>> q;
q.push({0, 1});
while(q.size()) {
auto[dis, ver] = q.top(); q.pop();
if(vis[ver]) continue;
vis[ver] = 1; //优先队列维护最小值,所以堆顶的元素如果vis为0,则说明当前节点可以确定最终的dist值
for(pii t : e[ver]) {
int d = t.first, v = t.second;
if(!vis[v] && dist[ver] + d < dist[v]) { // 用当前确定的dist_ver去更新和ver相连的其他点的dist值
dist[v] = dist[ver] + d;
q.push({dist[v], v});
}
}
}
}
int main() {
int n; cin >> n;
for(int i = 1; i < n; i++) {
for(int j = i+1; j <= n; j++) {
int x; cin >> x;
e[i].push_back({x, j}); //视情况建边,看题目需要建有向图还是无向图,这里只见了单向边,是有向图
}
}
dijkstra();
cout << dist[n];
return 0;
}
Bellman-ford(单源最短路,负权, 求最短路时可以有负环,O(n*m)):
Bellman-Ford算法能够处理图中存在负权边的情况.
Bellman-Ford算法的基本思想是通过不断松弛边来更新源点到所有其他顶点的最短路径估计值。
松弛操作:
- 对图中的每条边(u, v)执行以下操作,共进行|V|-1轮迭代(|V|表示图中结点个数):
- 如果
dist[u] + weight(u, v) < dist[v]
,则更新dist[v] = dist[u] + weight(u, v)
,并可能更新prev[v] = u
(如果需要重构路径)。这里的迭代次数k的含义是该路径在不超过k条边的最短路径,所以为什么只要进行|V|-1轮迭代,因为|V|个点,若该路径经过所有点,也就|V| - 1 条边,所以最多只需要这么多次迭代。若经过了|V|-1次迭代还能进行迭代,则一定会有一个点多次出现在这条路径上,则说明有负权环存在。所以Bellman-ford算法也可以判断是否存在负环。
对于每一次迭代,需要遍历所有边,进行判断dist[u] + weight(u, v) < dist[v], 进行相应的更新
这里主要是想表达:第k次更新是通过第k-1次更新的dist值来更新,因为k-1次更新的dist数组含义是不超过k-1条边的最短路径。所以实现的时候需要存储一下上一次更新的dist值。
Bellman-ford例题:
ac代码:
#include<iostream>
#include<vector>
#include<cstring>
using namespace std;
const int M = 1e4 + 10, N = 500 + 10;
struct bri{
int u, v, w;
}e[M];
int n, m, k;
int dist[N], last[N];
bool bellman() {
memset(dist, 0x3f, sizeof dist);
dist[1] = last[0] = 0;
for(int j = 1; j <= k; j++) { //迭代k次的含义
memcpy(last, dist, sizeof dist); //用一个数组类记录k-1次的dist值
for(int i = 1; i <= m; i++) {
int u = e[i].u, v = e[i].v, w = e[i].w;
if(last[u] + w < dist[v]) dist[v] = last[u] + w;
}
}
if(dist[n] >= 0x3f3f3f3f / 2) return false;//例如1-5到不了,所以dist5=0x3f3f3f3f, 若6结点只有5-6这一条边,并且weight(5,6) = -4,则dist[6] = 0x3f3f3f3f - 4, dist[6] != 0x3f3f3f3f,所以判断是用0x3f3f3f3f / 2来判断
return true;
}
int main() {
cin >> n >> m >> k;
for(int i = 1; i <= m; i++) {
int x , y, z; cin >> x >> y >> z;
e[i] = {x, y, z};
}
if(bellman()) cout << dist[n];
else cout << "impossible";
return 0;
}
SPFA(负权边,求最短路时不能有负环):
SPFA(Shortest Path Faster Algorithm)算法, 是Bellman-Ford算法的改进版本,它利用了一个队列来优化更新过程,从而在某些情况下可以更快地找到最短路径。该算法的基本思想是:从源点开始,不断尝试更新所有相邻节点的最短路径估计值,并将被更新过且可能进一步更新其他节点最短路径的节点加入队列中,直到队列为空。这里不能存在负环,不然就会死循环,队列中始终会有节点一直在更新。
我们直接用宽搜来实现这个算法
P3371 【模板】单源最短路径(弱化版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
#include<iostream>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
typedef pair<int, int> pii;
using ll = long long;
ll inf = 1e12 + 10;
const int N = 1e4 + 10, M = 5e5 + 10;
int n, m, s;
vector<pii> e[N];
ll dist[N];
bool st[N]; //这个数组的含义是判断当前节点是否在队列中
void spfa(int s) {
for(int i = 0; i < N; i++) dist[i] = inf;
dist[s] = 0;
queue<int> q;
q.push(s);
st[s] = true; //为true则表示在队列中
while(q.size()) {
int u = q.front(); q.pop();
st[u] = false; // 出队后记得将状态转移成不在队列中
for(pii t : e[u]) {
int v = t.first, w = t.second;
if(dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
if(!st[v]) {
q.push(v);
st[v] = true; //记得将状态标记为在队列中
}
}
}
}
}
int main() {
cin >> n >> m >> s;
for(int i = 1; i <= m; i++) {
int x, y, z; cin >> x >> y >> z;
e[x].push_back({y, z});
}
spfa(s);
for(int i = 1; i <= n; i++) {
if(dist[i] == inf) cout << (1<<31) - 1 << ' ';
else cout << dist[i] << ' ';
}
return 0;
}