思路:如果要让任意两点i
、j
之间的距离变短,只能引入第三个点k
,通过这个顶点k
中转即i->k->j
,才可能缩短i到j的路程。Floyd算法就是每次增加一个允许通过的中转点,来求所有顶点的最短距离。
核心代码:
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(a[i][k] < INF && a[k][j] < INF && a[i][k]+a[k][j] < a[i][j])
a[i][j] = a[i][k] + a[k][j];
这段代码的基本思想是:最开始只允许经过1号顶点进行中转,接下来只允许经过1号和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短距离。
时间复杂度 O ( N 3 ) O(N^3) O(N3),可以处理有负权边的图,不能处理负权回路。
用来求指定一个点(源点)到其他顶点的距离。
算法基本思想:每次找到离源点最近的一个顶点,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的路径。步骤如下:
将所有的顶点分为两部分:已知最短路径的顶点集合P和未知最短路径的顶点集合Q。最开始,已知最短路径的顶点集合P中只有源点一个顶点。用一个book数组来记录哪些点在集合P中,为1表示在P中,为0表示在Q中。
设置源点s到自己的最短路径为0,即dis[s] = 0
,若存在有源点能直接到达的顶点i,则把dis[i]
设为e[s][i]
,同时把所有其他(源点不能直接到达的)顶点的最短路径设为 ∞ ∞ ∞。
在集合Q的所有顶点中选择一个离源点s最近的顶点u(即dis[u]最小)加入到集合P,并考察所有以点u为起点的边,对每一条边进行松弛操作。
例如,存在一条从u到v的边,那么可以通过将边u->v添加到尾部来拓展一条从s到v的路径,这条路径的长度是dis[u]+e[u][v]
。如果这个值比目前已知的dis[v]
的值要小,就可以更新dis[v]=dis[u]+e[u][v]
。
重复第3步,如果集合Q为空,算法结束,最终dis数组中的值就是源点到所有顶点的最短路径。
算法时间复杂度 O ( N 2 ) O(N^2) O(N2),找最近的那个顶点的时间复杂度 O ( N ) O(N) O(N),使用堆优化可以把这部分降到 O ( l o g N ) O(logN) O(logN),不能处理负权边,使用邻接表存储可以优化。
代码:
void dijkstra()
{
//需要把集合Q中的n-1个顶点拿到P中
for(int i = 1; i <= n-1; i++)
{
int minx = INF;
int u;
for(int j = 1; j <= n; j++)
{
if(book[j] == 0 && dis[j] < minx)
{
u = j;
minx = dis[j];
}
}
book[u] = 1;
for(int v = 1; v <= n; v++)
{
if(dis[u] + a[u][v] < dis[v])
dis[v] = dis[u] + a[u][v];
}
}
}
核心代码只有四行,并可以完美解决带有负权边的图,还可以用来检测是否有负权回路
for(int k = 1; k <= n-1; k++) //进行n-1轮松弛
for(int i = 1; i <= m; i++) //枚举每一条边
if(dis[u[i]] != INF && w[i] != INF && dis[u[i]] + w[i] < dis[v[i]]) //尝试对每一条边进行松弛
dis[v[i]] = dis[u[i]] + w[i];
最多进行n-1
轮松弛。因为在一个含有n
个顶点的图中,任意两点之间的最短路最多包含n-1条边。时间复杂度 O ( N M ) O(NM) O(NM)。
Bellman-Ford算法还可以用来检测一个图是否含有负权回路,如果在进行n-1轮松弛之后,仍然存在
//尝试对每一条边进行松弛
if(dis[u[i]] != INF && w[i] != INF && dis[u[i]] + w[i] < dis[v[i]])
dis[v[i]] = dis[u[i]] + w[i];
的情况,说明在n-1
轮松弛之后仍然可以继续松弛成功,那么此图必然存在负权回路。检测代码如下:
for(int k = 1; k <= n-1; k++)
for(int i = 1; i <= m; i++)
if(dis[u[i]] + w[i] < dis[v[i]])
dis[v[i]] = dis[u[i]] + w[i];
int flag = 0;
for(int i = 1; i <= m; i++)
{
if(dis[u[i]] + w[i] < dis[v[i]])
{
flag = 1;
break;
}
}
if(flag)
printf("此图含有负权回路\n");
优化:如果已经松弛完毕,提前跳出循环,使用check变量来标记本轮是否发生了变化
for(int k = 1; k <= n-1; k++)
{
int check = 0;
for(int i = 1; i <= m; i++)
{
if(dis[u[i]] + w[i] < dis[v[i]])
{
dis[v[i]] = dis[u[i]] + w[i];
check = 1;
}
}
if(!check)
break;
}
在每实施一次松弛操作后,就会有一些顶点已经求得其最短路,此后这些顶点的最短路的估计值就会一直保持不变,不再受后续松弛操作的影响,但是上面代码还会继续判断是否需要松弛,这里浪费了时间。
这启发:每次仅对最短路估计值发生变化的顶点的所有出边执行松弛操作。
思路:每次选取队首顶点u
,对顶点u
的所有出边进行松弛操作,例如有一条u->v
的边,如果dis[u]+e[u][v]
v
不在当前队列中,就将顶点v
放入队尾。需要注意的是,同一个顶点同时在队列中出现多次毫无意义,所以需要一个数组来判重(判断哪些顶点已经在队列中)。在对顶点u
的所有边松弛完毕后,就将顶点u
出队。家下来不断从队列中取出新的队首顶点再进行如上操作,直至队列为空。
借助队列实现,代码如下(这里使用数组实现的邻接表):
#include
#include
#include
#include
using namespace std;
const int N = 51;
const int INF = 99;
int u[N], v[N], w[N], book[N];
int head, tail, dis[N], n, m;
int Queue[N], next[N], first[N];
void bellman_ford_()
{
while(head < tail)
{
int t = Queue[head];
int k = first[t];
while(k != -1)
{
if(dis[u[k]] + w[k] < dis[v[k]])
{
dis[v[k]] = dis[t] + w[k];
if(book[v[k]] == 0)
{
Queue[tail] = v[k];
tail++;
book[v[k]] = 1;
}
}
k = next[k];
}
book[Queue[head]] = 0;
head++;
}
}
int main()
{
memset(book, 0, sizeof(book));
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
{
dis[i] = INF;
first[i] = -1;
}
for(int i = 1; i <= m; i++)
{
scanf("%d%d%d", &u[i], &v[i], &w[i]);
next[i] = first[u[i]];
first[u[i]] = i;
}
dis[1] = 0;
head = tail = 0;
Queue[tail++] = 1;
book[1] = 1;
bellman_ford_();
for(int i = 1; i <= n; i++)
printf("%d ", dis[i]);
return 0;
}
/*
5 7
1 2 2
1 5 10
2 3 3
2 5 7
3 4 4
4 5 5
5 3 6
*/
需要说明:
book[Queue[head]] = 0;
这句的理解
通常一个顶点出队后不会再重新进入队列,而这里一个顶点很可能在出队列之后再次放入队列,也就是当一个顶点的最短路程估计值变小之后,需要对其所有的出边进行松弛,但是如果这个顶点的最短路程估计值再次变小,仍需要对其所有的出边再次进行松弛,这样才能保证相邻顶点的最短路程估计值同步更新。
Bellman-Ford队列优化如何判断一个图是否有负环?
某个点进入队列的次数超过n次。
Floyd | Dijkstra | Bellman-Ford | 队列优化的Bellman-Ford | |
---|---|---|---|---|
空间复杂度 | O ( N 2 ) O(N^2) O(N2) | O(N^2),使用邻接表、堆优化 O ( M ) O(M) O(M) | O ( M ) O(M) O(M) | O ( M ) O(M) O(M) |
时间复杂度 | $O(N^3) | O ( N 2 ) O(N^2) O(N2),使用邻接表、堆优化最少 O ( ( M + N ) l o g N ) O((M+N)logN) O((M+N)logN) | O ( N M ) O(NM) O(NM) | 最 坏 O ( N M ) 最坏O(NM) 最坏O(NM) |
适用情况 | 稠密图 | 稠密图 | 稀疏图,和边关系密切 | 稀疏图,和边关系密切 |
有负权边 | 可以 | 不可以 | 可以 | 可以 |
判定是否存在负权回路 | 不能 | 不能 | 可以判定 | 可以判定 |