算法导论小结(11)-单源最短路径问题

By:             潘云登

Date:          2009-7-29

Email:         intrepyd@gmail.com

Homepage: http://blog.csdn.net/intrepyd

Copyright: 该文章版权由潘云登所有。可在非商业目的下任意传播和复制。

对于商业目的下对本文的任何行为需经作者同意。


写在前面

1.          本文内容对应《算法导论》(2)》第24章。

2.          主要介绍了两种求解单源最短路径的算法:Bellman-Ford算法和Dijkstra算法。

3.          希望本文对您有所帮助,也欢迎您给我提意见和建议。


最短路径问题

在最短路径问题中,给出的是一个带权有向图G=(V, E),加权函数wE->R为从边到实型权值的映射。路径p=<v0, v1, …, vk>的权是指其组成边的所有权值之和w(p)。可以定义从uv间的最短路径的权为δ(u, v)=min{w(p)}。如果从uv不可达,则δ(u, v)为∞。一条最短路径既不能包含负权回路,也不会包含正权回路。由于最短路径问题具有最优子结构性质,即最短路径的子路径是最短路径,因此可以利用动态规划或贪心算法的思想进行求解。单源最短路径问题,在给定图G=(V, E)的情况下,希望找出从某个给定源顶点sV到每个顶点vV的最短路径。

     通常不仅希望算出最短路径的权,而且希望得到最短路径上的顶点。因此,可以对每个顶点v,设置其前趋顶点π[v],以便使源于顶点v的前趋链表沿着从sv的最短路径的相反方向排列。由π的值导出的前趋子图Gπ=(Vπ, Eπ)定义为,Vπ={vV: π[v]NULL}{s},即顶点集VπG中所有具有非空前趋的顶点集合加上源点s;且Eπ={(π[v], v)E: vVπ-{s}},即有向边集EπVπ中的顶点的π值导出的边集。在单源最短路径问题得解后,Gπ就是最短路径树,包含了从源点ss可达的每个顶点之间的一条最短路径。


松弛操作

对每个顶点vV,可以设置一个属性d[v],用来描述从源点sv的最短路径上权值的上界,称为最短路径估计。在初始化时,对所有vV,有π[v]=NULL,对vV-{s},有d[s]=0以及d[v]=∞。在松弛一条边(u, v)的过程中,要测试是否可以通过u,对迄今找到的道v的最短路径进行改进;如果可以改进的话,则更新d[v]和前趋π[v]。松弛操作是单源最短路径算法中改变最短路径和前趋的唯一方式。

void initalize_single_source(adjlist *graphic, int s)

{

    int i;

 

    for(i=0; i<graphic->vertex_number; i++)

    {

                  graphic->distance[i] = INFINITE;

                  graphic->ancestor[i] = -1;

    }

    graphic->distance[s] = 0;

}

 

void relax(adjlist *graphic, int u, int v, int weight)

{

    if(graphic->distance[v] > graphic->distance[u]+weight)

    {

                  graphic->distance[v] = graphic->distance[u]+weight;

                  graphic->ancestor[v] = u;

    }

}


Bellman-Ford

 

算法

Bellman-Ford算法能在存在负权边的情况下,解决单源最短路径问题,并且可以返回一个布尔值,表明图中是否存在一个从源点可达的负权回路,这是它优于Dijkstra算法的地方。Bellman-Ford算法:首先,对各个顶点的最短路径估计d和前趋π进行初始化;然后,执行|V|-1次循环,每次循环中,利用松弛操作对所有边的端点的最短路径估计d和前趋π进行更新;最后,通过判断各顶点的最短路径估计是否收敛,表明是否存在负权回路。由于第二步的循环需要(|V|-1)|E|次松弛操作,Bellman-Ford算法的总运行时间为O(VE)

         在算法开始前,最短路径树中仅包含源点s。第一趟循环过后,与s相邻的结点被加入到最短路径树中。在所有顶点可达的情况下,至多需要|V|-1次循环,便可将所有顶点加入到树中。由于后加入的顶点与其前趋结点之间可能存在负权边,因此,已经存在于树中的路径仍然可能被更新。换句话说,在不存在负权回路的情况下,路径(s, v)至多需要|V|-1次松弛操作,即可收敛到最短路径的权d[v]=δ(s, v)。这是因为在|V|-1次循环后,不会再有新结点引入负权边,导致路径(s, v)被更新。这也是检查负权回路是否存在的依据。

int bellman_ford(adjlist *graphic, int s)

{

    int i, j;

    lnode *temp_lnode=NULL;

 

    initalize_single_source(graphic, s);

    for(i=0; i<graphic->vertex_number-1; i++)

    {

                  for(j=0; j<graphic->vertex_number; j++)

                  {

                        temp_lnode = graphic->vlist[j].link;

                        while(temp_lnode != NULL)

                        {

                                      relax(graphic, j, temp_lnode->index, temp_lnode->weight);

                                      temp_lnode = temp_lnode->next;

                        }

                  }

    }

    for(j=0; j<graphic->vertex_number; j++)

    {

                  temp_lnode = graphic->vlist[j].link;

                  while(temp_lnode != NULL)

                  {

                        if(graphic->distance[temp_lnode->index]

                               > graphic->distance[j]+temp_lnode->weight)

                            return 0;

                        temp_lnode = temp_lnode->next;

                  }

    }

    return 1;

}

Bellman-Ford算法的不足在于,每次循环过程中存在许多冗余的松弛操作。一种改进的SPFA(Shortest Path Faster Algorithm)算法基于以下观察:只有那些在前一趟循环中更新了最短路径估计的顶点,才可能引起它们的邻接点的最短路径估计的改变。因此,SPFA算法用一个队列来存放被成功松弛的顶点。初始时,源点s入队。当队列不为空时,取出队首顶点, 对它的邻接点进行松弛。如果某个邻接点松弛成功,且该邻接点不在队列中,则将其入队。经过有限次的松弛操作后,队列将为空,算法结束。但是,只有在不存在负权回路的时候,SPFA算法才能正常工作。


Dijkstra

 

算法

Dijkstra算法比Bellman-Ford算法的运行时间要低,但它要求所有边的权值非负。Dijkstra算法中设置了一个顶点集合S,从源点s到集合中的顶点的最终最短路径的权值均已确定。算法反复选择具有最短路径估计的顶点uV-S,并将u加入S中,对u的所有出边进行松弛。由于总是在V-S中选择“最近”的顶点插入集合S中,可以说Dijkstra算法使用了贪心策略。可以想象,在Dijkstra算法的执行过程中,最短路径估计沿着以s为根的最短路径树向下传播。由于不存在负权边,最短路径树中的边一旦确定就不再改变。可以使用最小堆构建的最小优先队列(同Prim算法),存储集合V-S中的顶点。Dijkstra算法需要|V|次运行时间为O(lg V)heap_extract_min操作和|E|次运行时间为O(lg V)heap_decrease_key操作,因此,总的运行时间为O((V+E) lg V),如果所有顶点都从源点可达的话,则为O(E lg V)

void dijkstra(adjlist *graphic, int s)

{

    int i, d;

    heap h;

    vhnode *temp_vhnode=NULL;

    lnode *temp_lnode=NULL;

 

    initalize_single_source(graphic, s);

    init_heap(&h, graphic->vertex_number);

    for(i=0; i<graphic->vertex_number; i++)

    {

                  h.array[i].index = i;

                  h.array[i].key = graphic->distance[i];

                  h.array[i].ancestor = graphic->ancestor[i];

    }

    build_min_heap(&h);

 

    while(h.heap_size > 0)

    {

                  temp_vhnode = heap_extract_min(&h);

                  /*S <- SU{temp_vnode}*/

                  temp_lnode = graphic->vlist[temp_vhnode->index].link;

                  while(temp_lnode != NULL)

                  {

                        d = graphic->distance[temp_lnode->index];

                        relax(graphic, temp_vhnode->index,

                                    temp_lnode->index, temp_lnode->weight);

                        if(d != graphic->distance[temp_lnode->index])

                                      heap_decrease_key(&h,

                                             heap_search(&h, temp_lnode->index),

                                             graphic->distance[temp_lnode->index]);

                        temp_lnode = temp_lnode->next;

                  }

    }

    free_heap(&h);

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值