关于为什么dijkstra不能处理带负权的图,但为什么spfa可以,网上一些解释在举例子的时候,用的都是正权图,让我一直不太明白,所以我用一个负权图的例子,我是小白,所以请大家多多包含。
首先借助大佬的图展示一下图论中一些方法所处理的场景,以及时间复杂度
下边是一个例子
4
A ——————> B
| |
2| | -5
| |
v v
C <———— D
1
// 朴素板
int dijkstra()
{
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;
for(int i = 1; i <= n; i ++)
{
int t = -1;
for(int j = 1; j <= n; j ++)
{
if(!st[j] && (t == -1 || dist[t] > dist[j]))
{
t = j;
}
}
st[t] = true;
for(int j = h[t]; j != -1; j = ne[j]) // ne 是 j 这条边的下一个边的索引
{
int k = e[j]; // j 这条边所指向的顶点
dist[k] = min(dist[k], dist[t] + w[j]);
}
// h[t] 表示从顶点 t 出发的第一条边的索引。
// j 作为当前处理的边的索引。
// ne[j] 表示边 j 在邻接表中的下一条边的索引。如果 ne[j] == -1 则表示这是该顶点的最后一边。
// 这个循环遍历所有从顶点 t 出发的边。
}
}
假设我们从A点开始使用Dijkstra算法:
- 初始化:A的距离设为0,其他点的距离设为无穷大。
- 选择A:将A标记为已访问,更新邻接点B和C的距离:
- B的距离更新为 0+4=4 dist[B] = 4
- C的距离更新为 0+2=2 dist[A] = 2
- 选择C(因为2比4小):将C标记为已访问,C没有出边,不更新任何距离,记住此时由于已经将C标记为以访问,此后C已经确定,不会改动
- 选择B:将B标记为已访问。此时B的距离为4:
- 通过B到达D的距离更新为 4+(−5)=−1
- D的距离更新为-1。
- 选择D:将D标记为已访问。此时D的距离为-1:
- 通过D到达C的距离更新为 −1+1=0-1 + 1 = 0−1+1=0
- C的距离为2,因此不会更新C的距离。
所以dijkstra不能处理负权边,是因为她不能回头,他是基于贪心的策略,从已知的最短路的集合S 中选择未被确定的点,来更新未知最短路径的点,而负权边的存在可能使得从其他边到达该点的距离再次变短。 ( dijkstra要求每个点被确定后st[j] = true,dist[j]就是最短距离了,之后就不能再被更新了(一锤子买卖),而如果有负权边的话,那已经确定的点的dist[j]不一定是最短了)
接下来我们看一下spfa算法,实际上spfa是在bellman_ford算法上进行优化,所以我们先来看一下bellman_ford算法
由于我们会遍历所有的边,所以边的存储方式没有什莫限制,所以这里用最简单的结构体
void bellman_ford()
{
memset(dist, 0x3f, sizeof(dist));
dist[1] = 0;
for(int i = 1; i <= n; i ++)
{
memcpy(last, dist, sizeof(dist));
for(int j = 1; j <= m; j ++)
{
int a = res[j].a, b = res[j].b, c = res[j].c;
dist[b] = min(dist[b], last[a] + c); // 存在重边
}
}
}
具体步骤:
for n 次
for 所有边 a,b,w (松弛操作) // a, b, w 是一条从 a 到 b 且长度为 w 的一条边
dist[b] = min(dist[b],back[a] + w)
注意:last[] 数组是上一次迭代后 dist[] 数组的备份,
由于是每个点同时向外出发,因此需要对 dist[] 数组进行备份,
若不进行备份会因此发生串联效应,影响到下一个点
该算法有两个需要注意的地方,一是, 外层的 n 次循环是有实际意义的,如果改为循环 k 次,实际是求在经过最多不超过 k 条边的情况下,从 起始点到达某点的最短距离,这也就是为什莫,明明是spfa在时间复杂度等各方面优于bellman_ford的情况下,依旧采用bellman_ford,因为它可以限制边数。 二是, last数组,为了防止串联,那么什么是串联
更新从 1 点到 2 点,距离为dist[2] = 1,如果我们不用last数组备份一下,那么我们在更新 3 点的时候,由于此时 2 点 已经不是无穷大,会通过 2 点更新 dist[3] = 2, 但实际上经过一条边到达 3 点的最短距离是 dist[2] = 3,所以需要备份
那为什莫可以解决负权边呢 ? 因为我们每次更新所有的边,不会出现,不会出现dijkstra中,已经确定的点不会被更新,所以即使出现负权边导致前边的点距离变短,我们也会更新。
那spfa比bellman_ford优化在哪里
具体步骤:
for n 次
for 所有边 a,b,w (松弛操作) // a, b, w 是一条从 a 到 b 且长度为 w 的一条边
dist[b] = min(dist[b],back[a] + w)
我们仔细观察会发现,我们每次会更新会更新所有的边,这个复杂度就有些高了,
dist[b] = min(dist[b],back[a] + w), 我们发现如果dist[b] 想要被更新变小,只有在back[a]变小的前提下,才有可能,所以我们可以采用宽搜的方式,每次将已经更新的点放入队列中,不断更新,直至队列为空。
int spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(!q.empty())
{
int t = q.front();
q.pop();
st[t] = false;
// h[a] 是以 a 为起点的这条边,h[a]存的是边的序号
for(int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return dist[n];
}
好了,就到这里