• 最短路径 之 Floyd 算法
• 最短路径 之 Dijkstra 算法
Bellman算法差不多是Floyd算法和Dijkstra算法的结合体。
核心代码
// Bellman-Ford
dis[1] = 0;
for (int k = 1; k < n; k++)
for (int i = 1; i <= m; i++)
dis[v[i]] = min(dis[v[i]], dis[u[i]] + w[i]);
Bellman算法需要用邻接表来储存图。u[i], v[i], w[i]分别表示第i条边的起点、终点和权值。dis数组表示指定顶点(这里是1)到其余各个顶点的最短路径。
上面代码最后一行表示看看能否通过u[i] –> v[i]这条边,使得1号顶点到v[i]号顶点的距离变短,即1号顶点到u[i]号顶点的距离(dis[u[i]])加上u[i] –> v[i]这条边(权值为w[i])的值是否比1号顶点到v[i]号顶点的距离(dis[v[i]])要小。这一点非常像Dijkstra算法的“松弛”操作。
再回过头来看Floyd算法,它的三重循环k, i, j表示i号顶点经过前k个顶点到j号顶点的最短路径。Bellman的二重循环k, i和这个类似,表示进行k轮得到1号顶点最多经过k条边到达其余顶点的最短路径。
由于最短路径不包含回路,所以Bellman算法最多松弛n-1轮,如果在这之后仍然能够松弛则说明这个图里面包含负权回路。因此Bellman算法可以用来判断图中是否存在负权回路,这是它优于Dijkstra和Floyd之处。
判断是否存在负权回路只需要在循环完n-1轮后再加下列代码即可。
bool flag = false;
for (int i = 1; i <= m; i++)
if ( dis[v[i]] > dis[u[i]] + w[i] )
flag = true;
这样Bellman算法的时间复杂度是O(NM),比Dijkstra算法还要高。但事实上在很多情况下Bellman算法经常会在未达到n-1轮松弛前就已经求得了最短路径,之后的每一次循环都不会有松弛操作。因此可以用一个布尔变量来标记dis是否发生了变化,如果没有发生变化就可以跳出循环。
// Bellman-Ford
for (int k = 1; k < n; k++)
{
bool check = false;
for (int i = 1; i <= m; i++)
if (dis[v[i]] > dis[u[i]] + w[i])
{
dis[v[i]] = dis[u[i]] + w[i];
check = true;
}
if(!check) break;
}
分析Bellman算法的流程,发现它有很大的时间都浪费在了循环中判断是否需要松弛。但是在每一次实施松弛后,就已经有一些顶点完成了最短路径的计算,在此之后它们的值都不会再发生变化。因此我们就可以每次仅对最短路径估计值发生变化的顶点的所有出边进行松弛。
队列优化的Bellman算法
它的操作方法是这样的:
每次选取队首顶点u,对顶点u的每一条出边进行松弛。如果松弛成功且这条边的终点不在队列中,就把该顶点放入队列中。由于一个顶点同时出现在队列中没有意义,所以可以用一个数组book来判重。顶点u松弛完毕后就将u出队。如此循环直至队空。
这里需要用到链表,我们用数组来模拟一下。
核心代码
// Queue-optimized Bellman-Ford
/*
first[u[i]]表示顶点u[i]的第一条边的编号,next[i]表示“编号为i的边”的“下一条边”的编号
q是C++ STL中的队列模板
*/
dis[1] = 0;
book[1] = 1;
q.push(1);
while(!q.empty())
{
int cur = q.front();
for (int k = first[cur]; k != -1; k = next[k])
if (dis[v[k]] > dis[u[k]] + w[k])
{
dis[v[k]] = dis[u[k]] + w[k];
if(!book[k])
{
book[k] = 1;
q.push(k);
}
}
book[cur] = 0;
q.pop();
}
队列优化的Bellman算法在形式上类似于广度优先搜索,但不同之处在于广搜的时候一个顶点出队后通常不会再次入队。
当一个顶点入队超过n次,那么就可以判断这个图里面存在负环。
队列优化的Bellman算法关键在于只有那些在前一遍松弛成功的顶点才能引起与它们相连顶点的最短路径估计值的变化。