与Dijkstra算法一样,我们定义一幅加权有向图的结构如下:
//带权有向图
struct EdgeWeightedDigraph
{
size_t V; //顶点数
size_t E; //边数
map<int, forward_list<tuple<int, int, double>> adj; //改进后的邻接表,tuple存储的是边集
}
Bellman-Ford算法
在加权有向图的最短路径求解算法中,Dijkstra算法只能处理所有边的权值都是非负的图(是否有环不影响求解),而基于拓扑顺序的算法虽然能在线性时间内高效处理负权重图,但仅局限于无环图。为此还需要一个更为普遍的最短路径求解算法:能够处理负权重图,也能处理有环的情况。
Bellman-Ford算法是求含负权重图的单源最短路径的一种算法。其原理为连续进行松弛,对于含有V个顶点的加权有向图,在每次松弛时把每条边都更新一下,若在V-1次松弛后还能更新,则说明图中有负权重环,因此无法得出结果,否则就完成。
vector<int> Bellman-Ford(EdgeWeightedDigraph &g)
{
vector<int> edge(g.V);
//定义并初始化dis[]
vector<double> dis(g.V, DBL_MAX);
dis.at(0) = 0.0;
//进行V-1次松弛
for (size_t i = 0; i < g.V-1; ++i) //松弛计数
{
for (auto ite = g.adj.cbegin(); ite != g.adj.cend(); ++ite)
{
for (const auto &e : (*ite).second) //松弛操作
{
if (dis.at(get<0>(e)) + get<2>(e) < dis.at(get<1>(e)))
{
dis.at(get<1>(e)) = dis.at(get<0>(e)) + get<2>(e);
edge.at(get<1>(e)) = get<0>(e);
}
}
}
}
//判断是否存在负权重环
for (auto ite = g.adj.cbegin(); ite != g.adj.cend(); ++ite)
{
for (const auto &e : (*ite).second)
{
if (dis.at(get<0>(e)) + get<2>(e) < dis.at(get<1>(e)))
{
cerr << "含有负权重环,无解\n";
vector<int> tmp;
return tmp;
}
}
}
return edge;
}
性能
朴素的Bellman-Ford算法实现非常简单,在每一轮迭代中都会放松E条边,共进行V轮迭代,因此时间复杂度为O(VE)。这种实现在实际应用中并不常见,因为它的效率不高,而且我们只需要对Bellman-Ford算法稍作修改就能大幅提高在一般场景下的运行时间。
SPFA算法
分析Bellman-Ford算法,最外层循环(迭代次数)V-1实际上是算法是否有解的上限,因为需要的迭代遍数等于最短路径树的高度。如果不存在负权重环,平均情况下的最短路径树的高度应该远远小于V-1,在此情况下,多余最短路径树高的迭代遍数就是时间上的浪费,由此,可以依次来实施优化。
实际上,在任意一轮中许多边的松弛都不会成功:只有上一轮中的dis[]值发生变化的顶点指出的边才能够改变其他dis[]的值。即,从算法执行的角度来说,如果某一轮迭代中松弛操作未执行,说明此次迭代所有的边都没有被松弛,因此可以证明:至此后,边集中所有的边都不需要再被松弛,从而可以提前结束迭代过程。
为了实现这样的优化,我们可以用队列来记录松弛操作被成功执行的顶点。同时还需要一个向量mark[]来指示顶点是否已经存在于队列中,以防止将顶点重复插入队列。
vector<int> SPFA(EdgeWeightedDigraph &g)
{
vector<int> edge(g.V);
queue<int> q;
vector<int> mark(g.V, 0);
//定义并初始化dis[]
vector<double> dis(g.V, DBL_MAX);
dis.at(0) = 0.0;
int v = (*g.adj.cbegin()).first;
q.push(v);
mark.at(v) = 1;
int cnt = 0;
while (!q.empty())
{
v = q.front();
q.pop();
mark.at(v) = 0;
//松弛操作
for (const auto &e : g.adj.at(v))
{
int w = get<1>(e);
if (dis.at(v) + get<2>(e) < dis.at(w))
{
dis.at(w) = dis.at(v) + get<2>(e);
edge.at(w) = v;
if (mark.at(w) == 0)
{
q.push(w);
mark.at(w) = 1;
}
}
if (++cnt % g.V == 0)
{
cerr << "存在负权重环,无解\n";
vector<int> tmp;
return tmp;
}
}
}
return edge;
}
性能
SPFA算法是Bellman-Ford算法的改进,一般情况下其路径长度的比较次数的数量级为O(E+V) 。但如果加权有向图中存在负权重环,由于每次都会有边被松弛,因而不可能提前终止外层循环。这对应了最坏情况,其时间复杂度仍旧为O(VE) 。