关于最短路径的描述请参考维基百科Shortest Path
简单总结一下算法导论上描述的计算从单一节点源到图中每一节点的最短路径算法,Bellman-Ford算法及其优化版本spfa,以及对权重非负的图的Dijkstra算法。
初始化节点源
设节点 v 到节点源 s 的距离为 v.d 求最短路径之前进行如下初始化操作
INITIALIZE_SINGLE_SOURCE(G, s )
for each veterx v∈G.V
v.d = INF
v.p = NIL
s.d = 0
松弛
RELAX(u,v)
if v.d>u.d+w(u,v)
v.d=u.d+w(u,v)
v.p=u
在介绍最短路径算法之前先简单介绍一些最短路的性质,但这里不做严格证明,证明详见算法导论。
三角不等式: δ(s,v)≤δ(s,u)+w(u,v)
环路:
最短路径首先是不允许有负环的,因为如果有负环,那在负环路径上的节点可以绕着负环无穷多圈从而 δ(s,v)=−∞ 。那么可不可以有正环呢,答案是否定的,可以用反证法证明,如果有正环,那么显然去掉正环后的路径的权值肯定比带正环的路径更小。所以最短路径是不包含环路的,而且单源节点到所有节点的最短路径构成一颗最短路径树。
子路径性质:最短路的子路径也是最短路,即如果路径 s⇝v 的最短路经过节点 u ,则路径 p:s⇝u 是 s⇝u 的最短路,这可以用“剪贴法”证明一下。
收敛性质:给定一个带权重的有向图, G(V,E) ,设路径 p:s⇝u→v 为定源节点 s 到 v 的一条最短路径,并且在进行松弛操作以前已经,按INITIALIZE_SINGLE_SOURCE(G, s )进行初始化,且有 u.d=δ(s,u) ,则在松弛操作RELAX(u,v)之后, v.d=δ(s,v)
路径松弛性质:设 G 中 s 到任意节点 vk 的最短路径为 p=<v0,...,vk−1,vk> ,并且按算法INITIALIZE_SINGLE_SOURCE(G, s )进行初始化,则在进行一系列松弛操作,包括对边 (v0,v1),...,(vi−1,vi),..,(vk−1,vk) 所列次序进行松弛操作以后有 vk.d=δ(s,vk) .这可以用归纳法证明一下。
这些性质是后续介绍的算法的基础,特别是其中的松弛操作。
伪代码:
Bellman-Ford(G,s)
INITIALIZE_SINGLE_SOURCE(G, s )
for i=1 to V-1
for each edge ∈G.E
RELAX(u,v)
for each edge (u,v) ∈G.E
ifv.d>u.d+w(u,v)
return FALSE
return TRUE
时间复杂度 O(VE) ,该算法可以求出给定源节点 s ,到任意节点的最短路径,并在图中存在负环是返回 false
但是该算法时间复杂度太大,通常我们使用它的优化版本。可以看到Bellman-ford算法,对每条边都松弛 V−1 遍,但我们可以看到其中大部分时候所做的都是无用功,所以可以用一个队列来优化,减少不必要的松弛
伪代码
spfa(G,s)
INITIALIZE_SINGLE_SOURCE(G, s )
Q.inqueue(s)
inq[s] = TRUE
whle !Q.empty()
vertex u = Q.pop()
for each edge (u,v)∈ AdjEdge[u]
if RELAX(u,v) ∧ inq[v] ==FALSE
Q.inque(v)
if ++ v.cnt>|V|
return FALSE
return TRUE
c++代码
bool spfa(int s)
{
// memset(cnt,0,sizeof(cnt[0])*nv);//记录数组,用于检测负环真正使用时若题目中告诉没有负环可不用
memset(d,INF,sizeof(d[0])*nv);
memset(inq,false,sizeof(inq[0])*nv);
d[s] = 0;
memset(p,-1,sizeof(p[0])*nv);
queue<int> q;
q.push(s);
inq[s] = true;
while(!q.empty())
{
int u = q.front();q.pop();
inq[u] = false;
for(int i=first[u] ; i!=-1; i = nt[i])//用边集数组储存
{
Edge &e = edges[i] ;
//松弛
if(d[e.to]>d[u]+e.w)
{
d[e.to] = d[u]+e.w;
p[e.to] = u;
if(!inq[e.to])
{
q.push(e.to);
inq[e.to] = true;
//if(++cnt[e.to]>nv)return false;
}
}
}
}
return true;
}
可以“感受”到该算法的复杂度会低于Bellman的算法,实际证明出来是 O(kE) ,一般来说,k是一个比|V|低很多的常数。用边集数组储存也可以优化内存
这个算用于计算权重为正数的SP的时候会优于SPFA,用一般的二叉堆储存只需 O(ElgV) 而用斐波那契堆只需 O(VlgV) 。
算法描述
用一个优先队列储存所有节点,每次取出 v.d 最小的节点,松弛他的邻接顶点,依次下去直到队列为空。
伪代码
INITIALIZE_SINGLE_SOURCE(G, s )
Q(v)
while !Q.empty()
vertex u = Q.pop()
for each edge (u,v)∈ AdjEdge[u]
RELAX(u,v)
c++代码
void Dijkstra(int s)
{
memset(d,INF,sizeof(d[0])*nv);
d[s] = 0;
memset(p,-1,sizeof(p[0])*nv);
memset(vis,fasle,sizeof(vis))
priority_queuevector,greater > q;//pair默认先比较第一个分量,{d,s}
q.push(pii(0,s));
vis[s] = true;
while(!q.empty())
{
int u = q.top().second;q.pop();
if(vis[u])continue;
vis[u] = true;
for(int i = first[u] ; i!=-1 ; i = n[i])
{
Edge &e = edges[i];
if(d[e.to]>d[u]+e.w)
{
d[e.to] = d[u]+e.w;
//p[e.to] = G[u][i]//保存边
p[e.to] = u;//最短路径树中的父节点
q.push(pii(d[e.to],e.to));
}
}
}
}
因为STL里面的priority_queue,不支持对 e.to 的优先级实时更新,所以采用每次更新节点后重新放入的方式。为了防止多次重复访问采用用一个标记数组标记的做法记录是否访问过。
对于一般的有负权重的最短路问题最好使用spfa,而只有正权重的图,使用Dijkstra往往更快。而对于一般的有向无环图直接使用BFS加上松弛已经足够。
附上一个代码测试题
HDU 2544 最短路