【邓俊辉版数据结构】之图总结(二)

关电脑的时候忘记保存,之前的广度优先遍历都被清空了,现在不想重新敲一遍,来日有时间再补。

深度优先搜索

        深度优先搜索(Depth-First Search,DFS)选取下一顶点的策略,可概括为:

                优先选取最后一个被访问到的顶点的邻居

        于是,以顶点s为基点的DFS搜索,讲首先访问顶点s,再从s所有尚未访问到的邻居中任取其一,并以之为基点,递归地执行DFS搜索。故各顶点被访问到地次序,类似于树地先序遍历;而各顶点被访问完毕的次序,类似于树的后序遍历。

template <typename Tv,typename Te>  //深度优先搜索DFS算法(全图)
void Graph<Tv ,Te>::dfs(int s)
{
    //assert:0 <= s < n
    reset();
    int clock = 0;
    int v = s;
    //初始化
    do //逐一检查所有顶点
        if(UNDISCOVERED == status(v))   //一旦遇到尚未发现的顶点
        DFS(v,clock);//即从该顶点出发启动一次dfs
    while(s != (v = (++v % n)));  //按序号检查,故不漏不重
}
template <typename Tv,typename Te>  //深度优先搜索DFS算法单个连通域
void Graph<Tv,Te>::DFS(int v,int& clock)
{
    //assert : 0 <= v <n
     dTime(v) = ++clock;
     status(v) = DISCOVERD;//发现当前顶点v
     for(int u = firstNbr(v); -1 < u;u = nextNbr(v,u)) //枚举v的所有邻居u
     switch(status(u))  //并视其状态分别处理
     {
     case UNDISCOVEED:  //u尚未发现,意味着支撑树可在此拓展
        status(v,u) = TREE;
        parent(u) = v;
        DFS(u,clock);
     break;
     case DISCOVERED: //U已被发现但尚未访问完毕,应属被后代指向的祖先
         status(v,u) = BACKNARD;
     break;
     default://u已访问完毕(VISTED,有相图),则视承袭关系分为前向边或跨边
        status(v,u) = (dTime(v) < dTime(u)) ? FORWARD : CROSS;break;
     }
     status(v) = VISITED;
     fTime(v) = ++clock;//至此,当前顶点v方告访问完毕
}

    算法的实质功能,由于算法DFS ()递归地完成,都先讲当前节点v标记为DISCOVERED(已发现)状态,再逐一核对其各邻居u都状态,再逐一核对其各邻居u的状态并做相应处理。待其所有邻居均已处理完毕之后,将顶点v置为VISITED(访问完毕)状态,便可回溯。

    若顶点u尚处于UNDISCOVERED(未发现)状态,则将边(v,u)归类为树边(tree edge)。并将v记作u的父节点。此后,便可将u作为当前顶点,继续递归地遍历。

    若顶点u处于DISCOVERED状态。则意味着在此处发现一个有向环路。此时,在DFS遍历树种u必为v的祖先。故应将边(v,u)归类为后向边(back edge)。

    这里为每个顶点v都记录了被发现的和访问完成的时刻。对应的时间区间[dTime(v),fTime(v)]均作v的活跃期(active duration)。实际上,任意顶点v和u之间是否存在祖先/后代的“血缘”关系,完全取决于二者的活跃期是否包含(习题【6-12】)。

     对于有向图。顶点u还可能处于VISITED状态。此时,只要比对v与u的活跃期,即可判定在DFS树中v是否为u的祖先。若是,则边(v,u)应归类为前向边(forward edge);否则,二者必然来自于相互独立的两个分支。边(v,u)应归类为跨边(cross edge)。

    DFS(s)返回后。所有访问过的顶点通过parent[] 指针依次联接。从整体上给出了顶点s所属连通或可达分量的一棵遍历树。称作深度优先搜索树或DFS树(DFS tree)。与BFS搜索一样。此时若还有其它的连通或可达分量。则可以其中任何顶点为基点,再次启动DFS搜索。

    最终,经各次DFS搜索生成的一系列DFS树,构成了DFS森林(DFS forest)

实例

    下图针对含7个顶点和10条边的某有向图。给出了DFS搜索的详细过程。请留意观察顶点时间标签的设置。顶点状态的演变。边的分类和结果。以及DFS树(森林)的生长过程。

     

     最终结果如图所示,为包含两棵DFS树的一个DFS森林。可以看出,选出不同的起始基点,生成的DFS树(森林)也可能各异

如上图,如从D开始搜索,则DFS森林可能是如图u所示。

复杂度

     除了原图本身,深度优先搜索算法所使用的空间,主要消耗于各顶点的时间标签和状态标记以及各边的分类标记。二者累计不超过0(n)  + 0(e) = 0(n + e)。 当然,如采用以上代码的直接递归实现方式,操作系统为维护运行栈还需消耗一定量的空间——尽管这部分增量在渐进意义下还不足以动摇以上结论。为此,不妨按照之前的做法,通过显式地引入并维护一个栈结构,将DFS算法改写为迭代版本。

     时间方面,首先需要花费0(n + e)时间对所有顶点和边地状态复位。不计对子函数DFS()的调用,dfs()本身对所有顶点的枚举共需0(n)时间,不计DFS()之间相互的递归调用。每个顶点,每条边只在子函数DFS()的某以递归实例中耗费0(1)时间。故累计亦不过0( n + e)时间,综合而言,深度优先搜索算法也可在0(n + e)时间内完成。

应用

     深度优先搜索无疑是最为重要的图遍历算法。基于DFS的框架,可以导出和建立大量的图算法。以英雄营救公主的故事为例。为寻找从迷宫入口(起始顶点)至公主所在位置(目标顶点)的通路,可将迷宫内不同位置之间的联接关系标识为一张图,并将问题转化为起点和终点之间的可达性判定,从而可利用DFS算法便捷地加以解决。并非如此,一旦找到通路,则不仅可以顺利抵达终点与公主会合,还能沿着这条通路安全返回。当然。与广度优先搜索一样,深度优先搜索也可用作连通分量地分解,或者有向无环图地判定。

    下面仅以拓扑排序和双连通域分解为例,对DFS模式地应用做更为具体地介绍。

拓扑排序

    以教材地编写这一实际问题为例,首先,作者可借助有向图结构,整理出相关知识点之间地依赖关系  ,比如,因向量是散列表和查找表地基础知识点,故从Vector发出两条边分别指向Hashing 和Search Table;同理 ,查找表是二叉搜索树地基础知识。故也从前者引出一条边指向后者;...:注入此类,那么,如何将这些知识点串联为一份教学计划。以保证在整个授课进程中,每堂课的基础知识点均在此前业已讲授呢?

     实际上,许多应用问题,都可转化和描述为这一标准形式:给定描述某一实际应用的有向图,如何在与该图“相容’的前提下,将所有顶点排成一个线性序列。

    此处的”相容“,准确的含义是:” 每一顶点都不会通过边,指向其在此序列中的前驱顶点。这样的一个线性序列,称作原有有向图的一个拓扑排序。

有向无环图

    那么拓扑排序是否必然存在?若存在,又是否唯一?

    不含环路 的有向图——有向无环图——一定存在拓扑排序吗?答案是肯定的。有向无环图的拓扑排序必然存在;反之亦然。这是因为,有向无环图对应于偏序关系,而拓扑排序则对应于全序关系。在顶点数目有限时,与任一偏序相容的全序必然存在。‘

     任一有限偏序集,必有极值元素(尽管未必唯一);类似地,任一有向无环图,也必包含入度为零地顶点,否则,每个顶点都至少有一条入边,意味着要么顶点有无穷各,要么包含环路。

     于是,只要将入度为0的顶点m(及其关联边)从图G中取出。则剩余的G‘依然是有向无环图。故其拓扑排序也必然存在。从递归的角度来,一旦得到了G’的拓扑排序,只需将m作为最大顶点插入,即可得到G的拓扑排序。如此,我们已经得到了一个拓扑排序的算法(习题【6-18】)

     下图给出了该算法的一个实例。以下,将转而从BFS搜索入手,给出了另一拓扑排序算法

算法

      同理,有限偏序集中必然存在极小元素(同样,未必唯一),该元素作为顶点。出度必然为零。在对有向无环图的DFS搜索中,首先因访问完成而转至VISITED状态的顶点 m ,也必然具有这一性质;反之亦然。

      进一步地,根据DFS搜索的特性,顶点m(及其关联边)对此后的搜索过程将不起任何作用。于是,下一转换至VISITED状态的顶点可等效地理解为是,从图中剔除顶点m(及其关联边)之后的出度为零者——在拓扑排序中,该顶点应为顶点m的前驱。由此可见,DFS搜索过程中各顶点被标记为VISITED的次序,恰好(按逆序)给出了原图的一个拓扑排序。

      此外,DFS搜索善于检测环境的特性,恰好可以用来判别输入是否为有向无环图。具体地,搜索过程中一旦发现后向边,即可终止算法并报告"因非DAG而无法拓扑排序"

实现

      基于DFS搜索框架地拓扑排序算法,可实现如下代码

template <typename Tv,typename Te>  //基于DFS地拓扑排序算法
Stack<Tv>* Graph<Tv,Te>::tSort(int s)
{
    //assert:0 <= s <n
    reset();
    int clock = 0;
    int v = s;
    Stack<Tv>* S = new Stack<Tv>;//用栈记录排序顶点
    do
    {
        if(UNISCOVERED ==  status(v))
            if(!TSort(v,clock ,clock,S))
        {
            //clock并非必须
            while(!S->empty()) //任一连通城(亦整图)非DAG
                S->pop();break;//
        }
    }
    while (s!= (v = (++v % n)))
        return S;//若输入为DAG,则S内各顶点自顶向底排序;否则(不存在拓扑排序),S空
}
template <typename Tv,typename Te> //基于DFS的拓扑排序算法(单趟)
bool Graph(Tv,Te)::TSort(int v,int& clock,Stack<Tv>* S)
{

    //assert:0 <= v <n
    dTime(v) = ++clock;
    status(v) = DISCOVERED;//发现顶点v
    for (int u = firstNbr(v); -1 < u;u = nextNbr(v,u)) //枚举v的所有邻居u
    switch(status(u)) //并视u的状态分别处理
    {
    case UNDISCOVERED:
        parent(u) = v;
        status(v,u) = TREE;
        if(!TSort(u,clock,S)) //从顶点u处出发深入搜索
             return false;
         break;
    case DISCOVERED:
        status(v,u) = BACKWARD;//一旦发现后向边(非DAG),则
        return false;//不必深入,故返回报告
    default://VISITED(digraphs only)
        status(v,u) = (dTime(v) < dTime(u)) ? FORWARD : CROSS;
        break;
     }
     status(v) = VISITED;
     S->push(vertex(v));//顶点被标记为VISITED时,随即入栈
     return true;// v及其后代可以拓扑排序 
}

       相对于标准的DFS搜索算法,这里增设了一个栈结构,一旦某个顶点被标记为VISITED状态,便随即令其入栈。如此,当搜索终止时,所有顶点即按照被访问完毕的次序——亦即拓扑排序的次序——在栈中自顶向下排序。

实例

        下图以含6个顶点和7条边的有向无环图为例,给出了以上算法的执行过程,共分三步迭代,分别对应于起始于顶点C,B,A的三趟DFS搜索。请留意观察,各顶点的入栈次序。

   

     另外,对照图中的结果可见,因多个极大,极小元素(入度,出度为零顶点)并存而导致拓扑排序的不唯一性并为消除,而是转由该算法对每趟DFS起点的选择策略决定。

复杂度     

      这里仅额外引入的栈,规模不超过顶点信息总数0(n)。总体而言,空间复杂度与基本的深度优先搜索算法同样,仍为0(n + e)。该算法的递归跟踪过程与标准DFS搜索完全一致,且各递归实例自身的执行时间依然保持0(1)。故总体运行时间0(n + e)。为与基本的DFS搜索算法做对比上述代码保留了之前DFS的通用框架,但并非所有操作都与拓扑排序直接相关。因此通过精简代码,还可以进一步优化(习题[6-19])

2.双连通域分解

关节点与双连通域

      考查无向图G。若删除顶点v后G所包含的连通域增多,则v称作切割节点(cut vertex)或关节点(articulation point)。如下图的C即是一个关节点——它的剔除将导致连通域增加两块。反之,不含任何节点的图称作双连通图。任一无向图都可视作由若干个极大的双连通组合而成,这样的每一子图都称作原图的一个双连通域(bi - connected component)。例如图中的无向图。可分解为如图(b)所示的三个双连通域。

    较之其它顶点,关节点更为重要。在网络系统中它们对应于网关。决定子网之间能否连通。故在资源总量有限的前提下,找出关节点并重点予以保障,是提高系统整体稳定性和鲁棒性的基本策略。

蛮力算法

     那么,如何才能找到图中的关节点呢?

     由其定义,可之际导出蛮力算法大致如下:首先,通过BFS或DFS搜索统计出图G所含连通域的数目;然后逐一枚举每个顶点v,暂时将其从图G中删出,并再次通过搜索统计图G\{v}所含连通域的数目。于是,顶点v是关节点。当且仅当图G\{v} 包含的连通域多于图G。

     这一算法需执行n趟搜索,耗时0(n(n + e))。如此低的效率无法令人满意。

     以下将介绍基于DFS搜索的另一算法。它不仅效率更高,而且可同时对原图做双连通域分解。

可行算法

     在原无向图的DFS树中,关节点的真子树只可能通过后向边与C的真祖先连通。因此,在DFS搜索过程记录并更新各顶点v所能(经由后向边)连通的最高祖先(highest connected ancestor,HCA)hac[v] 。即可及时认定关节点,并报告对应的双连通域

实现  

template <typename Tv,typename Te>
void  Graph<Tv,Te>::bcc(int s) //基于DFS的BCC分解算法
{
    reset();
    int clock = 0;
    int v = s;
    Stack<int> S; //栈S用以记录已访问的顶点
    do 
        if(UNDISCOVERED == status(v)) 
        {
            //一旦发现未发现的顶点(新连通分量)
            BCC(v,clock,S);//即从该顶点出发启动一次BCC
            S.pop();//遍历返回后,弹出栈中最后一个顶点———当前连通域的起点                           
        
        }
        while (s != (v = (++v % n)));
}
#define hca(x) (fTime(x)) //利用此处闲置的fTime[]充当hca[]
template <typename Tv,typename Te> //顶点类型,边类型
void Graph<Tv,Te>::BBC(int v,int& clock,Stack<int>& S) 
{
    //assert:0 <= v < n
    hca(v) = dTime(v) = ++clock;
    status(v) = DISCOVERED;
    s.push(v);//顶点v被发现并入栈
    for(int u = firstNbr(v); -1 < u;u = nextNbr(v,u)) //枚举v的所有邻居u
      switch(status(u))
      {
          case UNIDISCOVERED;
          parent(u) = v;
          status(v,u) = TREE;
          BCC(u,clock,S);//从顶点u处深入
          if(hca(u) < dTime(v))  //遍历返回后,若发现u(通过后向边)可指向v的真祖先
            hca(v) = min(hca(v),hca(u));//则v亦必如此
          else
          {
              //否则,以v为关节点(u以下即是一个BCC,且其中顶点此时正集中于栈S的顶部
              while( v != S.pop());//依次弹出当前BCC中的节点,亦可根据实际需求转存至其它结构
              S.push(v);//最后一个顶点(关节点)重新入栈——总计至多两次
          }
          break;
          case DISCOVERED:
            status(v,u) = BACKARD;
            if(u != parent(v))
                hca(v) = min(hca(v),dTime(u));//更新hca[v]——越小越亮
            break;
          default://VISITED(digraphs only)
              status(v,u) = (dTime(v) < dTime(u)) ? FORWARD :CROSS;
              break;
      }
      status(v) = VISITED;//对v的访问结束
}

       由于处理的是无向图,故DFS搜索在顶点v的还在u返回之后,通过比较hca[u]与dTime[v]的大小,即可判断v是否关节点,这里将闲置的fTime[]用作 hca[].故hca[u] >= dTime[v],则说明后向边与v的真祖先连通。故v为关节点。既然栈S存有搜索过的顶点,与该关节点相对应的双连通域内的顶点,此时应集中存放于S顶部。故可依次挪出这些顶点。v本身必然最后挪出,作为多个连通域的联接枢纽,它应重新进栈。

       反之若hca[u] < dTime[v]。则意味着u可经由后向边连通至v的真祖先。果真如此则这一性质对v同样适用。故有必要将hca[v]。更新为hca[v]与hca[u]之间的更小者。

       当然,每遇到一条后向边(v,u)。也需要及时地将hca[v]。更新为hca[v]与dTime[u]之间地更小者,以保证hca[v]能够始终记录顶点v可经由后向边向上连通地最高祖先。 

复杂度

     与基本地DFS搜索算法相比,这里只增加了一个规模0(n)地辅助栈,故整体空间复杂度仍为0(n + e)。时间方面,尽管同一顶点v可能多次入栈,但每一次重复入栈都对应于某一新法线地双连通域。与之对应地必有至少另一顶点出栈且不再入栈。因此,这类重复入栈操作不会超过n次,入栈操作累计不超过2n次,故算法地整体时间复杂度仍然是0(n + e).

优先级搜索

     以上图搜索应用虽各具特点,但其基本框架却颇为相似,总体而言,都需通过迭代逐一发现各顶点,将其纳入遍历树中并做相应处理,同时根据应用问题地需求,适时给出解答,各算法在功能上地差异,主要体现为每一步迭代中对新顶点地选取策略不同。比如,BFS搜索会优先考查更早被发现地顶点,而DFS搜索恰好相反,会优先最后被发现的顶点。

     每一选取策略都等效于,给所有顶点赋予不同的优先级,而且随着算法的推进不断调整;而每一步迭代所选取的顶点都是当时的优先级最高者,按照这种理解,包括BFS和DFS在内的几乎所有图搜索,都可纳入统一的框架。鉴于优先级在其中所扮演的关键角色,故亦称作优先级搜索(priority-first search,PFS),或最佳优先搜索(best-first search ,BFS)。

      为落实以上理解,图ADT提供了priority()接口,以支持对顶点优先级数(priority number)的读取和修改。在实际应用中,引导优化的指标往往对应于某种有限的资源或成本(如光纤长度,通讯带宽等),故不妨约定优先级数大(小)顶点的优先级越低(高)。相应地,在算法地初始化阶段(如代码中地reset())。通常都将顶点地优先级数统一置为最大(比如INT_MAX)——或等价地,优先级最低。

基本框架

template<typename Tv,typename Te> template<typename PU> //优先级搜索(全图)
void Graph<Tv,Te>::pfs(int s,PU prioUpdater)
{
    //assert:0 <= s < n
    reset(); 
    int v = s;//初始化
    do  //逐一检查所有顶点
        if(UNDISCOVERED == status(v)) //一旦遇到尚未发现地顶点
        PFS(v,prioupdater);//即从该顶点出发启动依次PFS
    while(s != (v = (++v % n)));按序号检查,故不漏不重
}
template <typename Tv,typename Te>
template <typename PU>  //顶点类型,边类型优先级更新器
void Graph<Tv,Te>::PFS(int s,PU prioUpdater)
{
    //优先级搜索(单个连通域)
    priority(s) = 0;status(s) = VISITED;
    parents(S) = -1;//初始化,起点s加至PFS树中
    while(1)
    {
        //将下一顶点和边加至PFS树中
        for(int w = firstNbr(s);- 1 < w;w = nextNbr(s,w)) //枚举s的所有邻居
         prioUpdater(this,s,w);//更新顶点w的优先级及其父顶点
        for(int shortest = INT_MAX,w = 0;w < n;w++)
        if(status(w) == UNDISCOVERED) //从尚未加入遍历树的顶点中
          if(shortest = priority(w)) //选出下一个
        {
            shortest = priority(w);
            s = w;
        }//优先级最高的顶点s
        if (VISITED == status(s))
            break;
        status(s) = VISITED;
        status(parent(s),s) = TREE;//将s及其与父顶点的联边加入遍历树
    }
    
}//通过定义具体的优先级更新策略prioUpdater,即可实现不同的算法功能

复杂度

      PFS搜索由两重循环构成,其中内层循环又含有并列的两个循环,若采用邻接表实现方式。同时假定prioUpdater()只需常数时间,则前一内循环的累计时间应取决于所有顶点的出度总和,即0(e);后一内循环固定迭代n次,累计0(n2)时间。两项合计总体复杂度为0(n2)。

最小支撑树

支撑树

如图所示,连通图G的某一无环连通子图若覆盖G中所有的顶点,则称作G的一棵支撑生成树(spanning tree)

就保留原图中边的数目而言,支撑树既是“禁止环路”前提下的极大子图,也是“保持连通”前提下的最小子图。在实际应用中,原图往往对应于由一组可能相互联接(边)的成员(顶点)构成的系统,而支撑树则对应于该系统最经济的联接方案。确切地,尽管,同一幅图可能有多棵支撑树,但由于其中地顶点总和均为n,故其采用地边数也均为n - 1.

最小支撑树

      若图G为一带权网络,则每一棵支撑树地成本(cost)即为其所采用各边权重地总和,在G的所有支撑树种,成本最低者称作最小支撑树(minimum spanning tree,MST)

      聚类分析,网络架构设计,VLSL布线设计等诸多实际应用问题,都可转化并描述为最小支撑树的构造问题,在这些应用种,边的权重大多对应于某种可量化的成本,因此作为对应优化问题的基本模型,最小支撑树的价值不言而喻。另外,最小支撑树构造算法也可为一些NP问题提供足够快速,足够接近的近似解法。正因为受到来自众多应用和理论领域的需求推动,最小支撑树的构造算法也发展的较为成熟。

歧义性

      尽管同一带权网络通常有多棵支撑树。但总数毕竟有限。故必有最低的总体成本。然而,总体成本最低的支撑树却未必唯一。以包含三个顶点的完全图为例,若三条边的权重相等,则其中任意两条边都构成总体成本最低的一棵支撑树。

       更一般的例子如图所示,对应于左侧的带权网络,有两棵树的总体成本均达到最低(44)。故严格来说,此类支撑树应称作极小支撑树。当然,通过强制附加某种次序即可消除这种歧义性,故不妨仍称之为最小支撑树。

蛮力算法

      由最小支撑树的定义,可直接导出蛮力算法大致如下:逐一考查G的所有支撑树并统计其成本,从而挑选出其中的最低者。然后根据CayLey公式,由n个互异顶点组成的完全图供有n 的(n-2)次方棵支撑树,即便忽略掉构造所有支撑树所需的成本,就需要0(n n-2)时间。

      事实上基于PFS搜索框架,并采用适当的顶点优先级更新策略,即可得出如下高效的最小支撑树构造算法。

Prim算法

      为更好的理解这一算法的原理,以下先从最小支撑树的性质入手,为简化起见,不妨假定个边堵塞权重互异。实际上,为将最小支撑树的以下性质及其构造算法的正确性等结论推广到允许多边等权的退化情况,还需要补充更为严格的分析与证明。

割与极短跨越边

     图G = (V;E)中,顶点集V的任一平凡子集U及其补集V \U 都构成G的一个割(cut),记作(U:V\U)。若边uv满足uU且vU,则称作该割的一条跨越边(crossing edge)。因此类 边联接于V及其补集之间,故亦形象地称作该割的一座桥(bridge)。

     Prim算法的正确性基于以下事实:最小支撑树总是会采用联接每一割的最短跨越边。

贪心迭代

     由以上性质,可基于贪心策略导出一个迭代式算法,每一次迭代之前,假设已经得到最小支撑树Tk = (Vk; Ek),其中Vk包含K割顶点,Ek包含K- 1条边,于是,若将Vk及其补集视作原图的一个割,则在找到该割的最短跨越边ek = (vk, uk)(vk属于Vk且uk属于Vk)之后,即可将Tk扩展为一棵更大的子树Tk+1 = (Vk+1; Ek+1),其中Vk+1 = Vk  {uk},Ek+1 = Ek  {ek}。 最初的T1不含边而仅含单个顶点,故可从原图的顶点中任意选取。

 实现  

      以上Prim算法完全可以纳入优先搜索算法框架,为此,每次由Tk扩充至Tk+1 时,可以将Vk之外每个顶点u到Vk的距离视作u的优先级数。如此,每一最短跨越边ek对应的顶点uk都会因拥有最小的优先级数(即最高的优先级)而自然地被选中。

template <typename Tv,typename Te> struct PrimPU //针对Prim算法的顶点优先级更新器
{
   virtual void operator()(Graph<Tv,Te>* g,int uk,inrt v) 
   {
       if(g->status(v) == UNDISCOVERED) //对于uk每以尚未被发现的邻接顶点v
         if(g->priority(v) > g->weight(uk,v)) //按Prim策略做松弛
       {
           g->priority(v) = g->weight(uk,v)  //更新优先级(数)
           g->parent(v) = uk;//更新父节点
       }
   }
}

      那么,uk和ek加入Tk之后,应如何快速更新Vk+1以外顶点的优先级数呢?实际上,与uk互不 关联的顶点都无需考虑,故只需遍历uk的每一邻居v,若边ukv的权重小于v当前的先级数,则将 后者更新为前者。这一思路可具体落实为如代码6.8所示的优先级更新。

 复杂度  

      不难看出,以上顶点优先级更新器只需常数的运行时间,故由之前对优先级搜索算法性能的分析结论,以上Prim算法的时间复杂度为0(n2)。作为PFS搜索的特例,Prim算法的效率也可借助优先级队列进一步提高。

3.最短路径

      若以带权图来表示真实的通讯,交通,物流或社交网络,则各边的权重可能代表信道成本,交通运输费用或交往程度。此时我们经常关心的一类问题,可以概括为:

     给定带权网络G = (V,E)。以及源点(source) s属于V,对于所有的其它顶点v,s到v的最短通路有多长?该通路由哪些边构成?

最短路径树

    单调树

    如图所示,设顶点s到v的最短路径为p。于是对于该路径上的任一顶点u,若其在p上对应的前缀为a,则 a也必是s到u的最短路径。否则,若从s到u还有另一严格更短的路径t,则易见p不可能是s到v的最短路径

歧义性

    较之最小支撑树,最短路径的歧义性更难处理。首先,即使各边权重互异,从s到v的最短路径也未必唯一。另外,当存在非正权重的边,并导致某个环路的总权值非正时,最短路径甚至无从定义。因此以下不妨假定,带权网络G内各边权重均大于零。在如下图所示的任意带权网络中,考查从源点到其余顶点的最短路径(若有多条,任选其一)。于是由以上单调性,这些路径的并集必然不含任何(有向)回路,这就意味着。它们应如图(b)和图(c)所示,构成所谓的最短路径树。

猜你喜欢

转载自blog.csdn.net/qq_39218906/article/details/86596008