对一个带权有向图G=(V,E),给定一个源顶点S,找出S到图中其他顶点v的最短路径即单源最短路径问题。该问题还有很多变体,像单终点最短路径、单对顶点最短路径、每对顶点间的最短路径等等。
最短路径问题是具有最优子结构的:一对顶点间的最短路径包含了该路径上的顶点间的最短路径。直观上理解,如果该路径上的两个顶点间的路径pij不是最短路径,那么用这两个顶点间的最短路径代替pij,那么就会出现一条更短的路径,与前面所说的最短路径矛盾。(具体证明参见算法导论P358)。
需要说明的是负权值边和松弛技术。Dijkstra算法是不允许图中存在负权边的,否则无法得到正确的结果。而Bellman-ford算法就允许图中存在负权边,而且该算法可以检测图中是否存在负权回路。两种算法都用到了松弛技术。即对边(u,v),如果通过u到达v比当前找到的到v的最短路径还短,那么就更新d[v]、parent[v]。通过松弛,可以减小最短路径估计。
Bellman-ford算法:
因为图中任意两个顶点的最短路径最多包含|V|-1条边,所以至多对每条边进行|V|-1次松弛后就会得到任意两个顶点间的实际最短路径。如果还能通过松弛降低最短路径估计,那么就可以断定图中存在负权回路,因为如果从s到v的路径中包含负权回路,那么s到v的最短路径长度就是负无穷了。可以这样理解,第i(i>=1)次松弛得到的是源点s到每个顶点vV的路径长度为i的最短路径,第|V|-1次松弛得到的就是长度为|V|-1的最短路径。不过,显然不是每个顶点到s的最短路径长度都是|V|-1,所以对每条边都进行|V|-1次松弛操作是没有必要的。Bellman-ford的时间复杂度为O(VE)。可以对该算法进行简单的优化,如果本次循环并未对任何一条边进行松弛,那么可以判定已经得到了最终结果,退出循环。
template< typename VertexType, typename CostType >
bool Graph<VertexType, CostType>::bellman_ford( int source, vector<int> &distance )//序号为source的节点作为源点.
{
initialize_single_source( source, distance );
for( int i = 1; i < vex_num; ++i )//最远的点经过vertex_num-1条边也到了.
{
for( vector< Vertex<VertexType, CostType> >::iterator
iter = vertex_table.begin();
iter != vertex_table.end();
++iter )
{
int begin = iter->serial_number;
Edge<VertexType, CostType> *edge = iter->first_adj;
while( edge != NULL )
{
int end = edge->end_vertex;
relax( begin, end, distance );
edge = edge->next_adj;
}
}
}
for( vector< Vertex<VertexType, CostType> >::iterator
iter = vertex_table.begin();
iter != vertex_table.end();
++iter )
{
int begin = iter->serial_number;
Edge<VertexType, CostType> *edge = iter->first_adj;
while( edge != NULL )
{
int end = edge->end_vertex;
if( distance[end-1] > distance[begin-1] + edge->cost )
return false;
edge = edge->next_adj;
}
}
return true;
}
这个实现的时间复杂度为O(v(v+e))。
Dijkstra算法:
算法将图中的顶点分为两类,一类是已经找到最短路径,记为S,初始状态为空集,另一类就是没找到的,记为Q,初始状态包含图中的所有顶点。Q中的每个顶点维护一个distance变量,表示该顶点到源点s的最短路径估计。每次循环选择Q中具有最小最短路径估计的一个顶点m,将它踢出Q,加入S。然后,松弛m的每条邻接边。直至Q为空。
该算法和广度优先搜索、prim算法有相似之处。因为广搜可以看成是对无权图的单源最短路径算法。对比一下,广搜是每搜完一个顶点后通过它的所有邻接边搜它的所有邻接点,dijkstra是每选出一个顶点后松弛它的所有邻接边。另外,广搜中已经遍历过的顶点相当于已经找到了最短路径,类似于dijkstra中的集合S。和prim算法的相似之处在于二者都将图中的顶点分成两个集合Q,S,每次都是从集合Q中选择具有最小性质的顶点,然后加入到S中,并对它的邻接点进行更新。另外,二者都使用了贪心思想:每次从Q中选出具有最小性质的顶点。
该算法的一个限制是不能有负权边。
代码地址:点击打开链接