zoukankan      html  css  js  c++  java
  • 图的遍历(搜索)

    与其他数据结构一样,图也需要进行遍历操作,来访问各个数据点,以及后续对顶点和边进行操作。相对于树来说,图的结构更为复杂。

    图的遍历,可以理解为将非线性结构转化为半线性结构的过程。我们知道,树就是一种半线性结构,经遍历而确定的边类型中,最为重要的类型就是树边,所有的树边与顶点一起构成了原始图的一颗支撑树(森林),称作遍历树(traversal tree)。

    因为图中的顶点间,可能存在多条通路,所以不仅要对边设置各种状态,对于顶点也需要动态地设置多种状态,以避免重复访问同一个顶点多次。图的遍历更加强调对于处于特定状态顶点的甄别和查找,所以也称为图搜索。大部分的图搜索算法都可以在O(n+e)的时间内完成,因为每个顶点和每条边都必须访问,这已经是最优结果。

    图的搜索策略主要有三种,广度优先搜索(bfs),深度优先搜索(dfs),优先级搜索(pfs)。不同搜索策略的区别,可以体现为边分类的结果不同,以及所得遍历树的结构差异。决定因素在于,每一步迭代按照何种策略来选取下一个访问的顶点。通常,下一步访问都选取某个已经访问到的顶点的邻接顶点,同一顶点所有邻接顶点的优先级可以根据实际情况确定。各种搜索策略的不同在于,存在多个顶点的时候,下一步选择哪个顶点的邻接顶点。下面分别介绍这三种搜索方法的策略以及简单应用。

    广度优先搜索

    策略:越早被访问到的节点,其邻居越被优先的访问。

    引入波峰集的概念,在所有已访问到的顶点中,仍有邻居尚未访问的,构成波峰集。搜索过程也可以理解为,反复从波峰集中寻找最早被访问到的顶点v,若它的邻居已经全部被访问到,将其逐出波峰集;否则,随意选出一个尚未访问到的邻居,并将它加入波峰集。

    仔细回想,广度优先的策略,与二叉树的层次遍历是相同的。所以可以借鉴二叉树层次遍历的方法,用一个辅助队列来实现图的广度优先搜索:

     1 template<typename Tv, typename Te> void Graph<Tv, Te>::bfs(int s)//以s为起点的广度优先搜索
     2 {
     3     reset(); int clock = 0; int v = s;
     4     do
     5         if (stasus(v) == UNDISCOVERED)//遇到未发现的顶点
     6             BFS(v, clock);//执行一次BFS
     7     while (s != (v = (++v%n)));
     8 }
     9 template<typename Tv, typename Te> void Graph<Tv, Te>::BFS(int v)
    10 {
    11     queue<int> Q;
    12     status(v) = DISCOVERED; Q.push(v);
    13     while (!Q.empty())
    14     {
    15         int v = Q.front(); Q.pop(); //取出最前方的点
    16         for(int u = firstNbr(v); u > -1; u = nextNbr(v, u))//枚举v的所有邻居u(按点编号从后向前)
    17             if (status(u) == UNDISCOVERED)
    18             {
    19                 status(u) = DISCOVERED;
    20                 Q.push(u);
    21                 type(v, u) = TREE; parent(u) = v;//引入树边拓展支撑树并确定父子关系
    22             }
    23             else
    24             {
    25                 type(v, u) = CROSS;//跨边
    26             }
    27         status(v) = VISITED;
    28     }
    29 }

    可以看到,把边简单地分成了两类:树边和跨边。若当前节点的邻居为UNDISCOVERED,则将边加入到支撑树中,并将点的状态设置为DISCOVERED,存入辅助队列之中,改写父子关系。否则,将边归为跨边(CROSS)。当前节点的所有邻居都已经被检查状态后,该节点的访问完成,并取出队列中最前面的点,继续这一过程,直到辅助队列中没有顶点,即全部顶点均已被访问完毕,算法结束。

    深度优先搜索

    策略:优先选取最后一个访问到的顶点的邻居。

    因此,各顶点被访问到的次序,类似于树中的先序遍历,但完成访问的次序,类似于后序遍历。实现代码如下:

     1 template<typename Tv, typename Te> void Graph<Tv, Te>::dfs(int s)//以s为起点的深度优先搜索
     2 {
     3     reset(); int clock = 0; int v = s;
     4     do
     5         if (stasus(v) == UNDISCOVERED)//遇到未发现的顶点
     6             DFS(v, clock);//执行一次BFS
     7     while (s != (v = (++v%n)));//做到不重不漏
     8 }
     9 template<typename Tv, typename Te> void Graph<Tv, Te>::DFS(int v, int& clock)//递归实现
    10 {
    11     dTime(v) = ++clock; status(v) = DISCOVERED;//发现的时间
    12     for (int u = firstNbr(v); u > -1; u = nextNbr(v, u))
    13         switch (status(u))
    14         {
    15         case UNDISCOVERED:type(u, v) = TREE; parent(u) = v; DFS(u, clock); break;
    16         case DISCOVERED:type(u, v) = BACKWARD; break;//有向环路,u必为v的祖先,故为后向边
    17         default://u已经访问完毕(visited,有向图),通过比较承袭关系区分前向边和跨边
    18             type(u, v) = (dTime(v) < dTime(u)) ? FORWARD : CROSS; break;//u的发现时间晚,为前向边
    19         }
    20     status(v) = VISITED; fTime(v) = ++clock;//访问结束的时间
    21 }

    把边分为四类:树边,前向边,后向边,跨边。如果u发现了但是还没有访问完毕,说明u是v的祖先,因此定义为后向边;如果u已经是被访问完毕的,就要分开讨论:如果u的发现时间比v还要晚,说明u的层次比v要低,定义为前向边,如果u的发现时间比v早,说明u和v属于不同的分支,定义为跨边。几类边的定义,对于处理一些问题是很有帮助的,比如双连通域分解、拓扑排序等。

    深度优先搜索的策略体现在,发现了一个UNDISCOVERED状态的邻居,就以这个邻居为起点继续递归地进行搜索。一个重要之处在于,用dTime和fTime来表示一个顶点被发现和被访问完毕的时间,一个顶点的活跃期即为dTime----fTime,这可以给我们判断两个顶点是否有血缘关系提供方便。可以证明,两个顶点存在“”祖先-后代”关系,当且仅当两个顶点的活跃期为包含关系。

    算法运行过后,通过parent指针可以给出起始顶点可达域的遍历树,这些树构成了DFS森林。

    这里用了递归的方法,实际上很容易改成迭代方法。与BFS类似,这里使用一个辅助堆栈,需要添加的一些操作是,发现未访问的顶点,就把这个顶点入栈,每次循环检查栈顶的顶点,如果邻居均访问完成,就出栈,取出下一个顶点,直到栈中已经没有顶点。

    深度优先搜索的应用(一)  拓扑排序

    如果一个线性序列,每一个顶点都不会通过边,指向其在此序列中的前驱顶点,那么这个线性序列,称作原有向图的一个拓扑排序(topological sorting)。

    可以证明,有向无环图必然存在拓扑排序,且拓扑排序未必唯一。任一有向无环图必然存在入度为0的顶点,否则这个图将包含环路。这样就产生了得到一个拓扑排序的方法:只要将入度为0的顶点m以及相关联的边从图G中取出,则剩余的G'依然是一个有向无环图,递归下去,直到所有的点都被去掉,则按照次序,即可组成原图的一个拓扑排序。

    另一种思路,可以通过深度优先搜索的方法。对应上面的方法,图中也必然存在出度为0的顶点,而这个顶点在深度优先搜索中会被首先转换为VISITED。与第一种方法类似,将访问完毕的顶点m以及关联边去掉,递归下去,下一个出度为0的顶点应当为m的前驱。由此可见,DFS中各顶点被标记为VISITED的次序,正好逆序地给出了一个原图的拓扑排序,实现代码如下:

     1 template<typename Tv, typename Te> stack<Tv>* Graph<Tv, Te>::tSort(int s)
     2 {
     3     reset(); int clock = 0; int v = s;
     4     stack<Tv>* S = new Stack<Tv>;
     5     do {
     6         if(status(v)==UNDISCOVERED)
     7             if (!TSort(v, clock, S))
     8             {
     9                 while (!S->empty())
    10                     S->pop(); break;//任一连通域非DAG,直接返回
    11             }
    12     } while (s != (v = ( ++v % n ) ) );
    13     return S;
    14 }
    15 template<typename Tv, typename Te> bool Graph<Tv, Te>::TSort(int v, int& clock, stack<Tv>* S)
    16 {                                                                  //基于DFS的拓扑排序(单次)
    17     dTime(v) = ++clock; status(v) = DISCOVERED;
    18     for (int u = firstNbr(v); u > -1; u = nextNbr(v, u))
    19         switch (status(u))
    20         {
    21         case UNDISCOVERED:parent(u) = v; type(v, u) = TREE;
    22             if (!TSort(u, clock, S)) return false;//若从u出发,u及其后代不能拓扑排序,返回
    23             break;
    24         case DISCOVERED:type(v, u) = BACKWARD; return false;//出现后向边直接退出
    25         default:
    26             type(v, u) = (dTime(v) < dTime(u)) ? FORWARD : CROSS;
    27             break;
    28         }
    29     status(v) = VISITED; S->push(vertex(v));//返回时,顶点按照被访问的次序也就是拓扑排序的次序,在栈中自顶向下
    30     return true;
    31 }

    这里可以看到,主函数里面执行了一个判别操作,当结束执行函数的时候,栈中仍然有顶点,说明拓扑排序不存在。单次搜索过程中,一旦存在后向边,这个连通域必然存在一个环路,那么也就不存在拓扑排序,可以直接退出本次执行。当主函数执行完毕时,栈中的次序即为一个拓扑排序。

    深度优先搜索的应用(二)  双连通域分解

    对于一个无向图,如果删除一个顶点v后,原图中包含的连通域增多,称v是一个切割节点或关节点。不含任何关节点的图,称为双连通图。任一无向图都可以视为若干个极大的双连通子图组合而成,每一个这样的子图都称为原图的一个双连通域(bi-connected component)。

    讨论什么样的节点可能是关节点。DFS树中的叶节点不可能是关节点,因为删除它不会造成任何影响。如果根节点包含两个分支,那么根节点必然是关节点。对于内部节点,如果删除这个节点后,导致一颗真子树与其真祖先无法连通,那么该节点必然是关节点,反之则不是关节点。

    考虑前面DFS算法中定义的边,后向边是与其祖先相联的,因此,在DFS过程中,只要随时更新每个顶点所能连通的最高祖先(highest connected ancestor,hca),就能判断关节点,并获得双连通域,实现代码如下:

     1 template<typename Tv, typename Te> void Graph<Tv, Te>::bcc(int s)
     2 {
     3     reset(); int clock = 0; int v = s; stack<int> S;
     4     do {
     5         if (status(v) == UNDISCOVERED)
     6         {
     7             BCC(v, clock, S);
     8             S.pop(); break;//任一连通域非DAG,直接返回
     9         }
    10     } while (s != (v = (++v % n)));
    11 }
    12 template<typename Tv, typename Te> void Graph<Tv, Te>::BCC(int v, int& clock, stack<int>& S)
    13 {
    14     hca(v) = dTime(v) = ++clock; status(v) = DISCOVERED; S.push(v);
    15     for (int u = firstNbr(v); u > -1; u = next(v, u))
    16         switch (status(u))
    17         {
    18         case UNDISCOVERED:parent(u) = v; type(v, u) = TREE; BCC(u, clock, S); 
    19             if (hca(u) < dTime(v))//u可以通过后向边指向v的真祖先
    20                 hca(v) = min(hca(v), hca(u));
    21             else//否则,u无法通过后向边与v的祖先相连,v为关节点,u以下即为一个bcc
    22             {
    23                 while (v != S.top()) S.pop();//依次弹出栈中当前bcc中的节点
    24             }
    25             break;
    26         case DISCOVERED:type(v, u) = BACKWARD;
    27             if (u != parent(v)) hca(v) = min(hca(v), dTime(u));//更新hca(v)
    28             break;
    29         //default://visited(仅对于有向图)
    30         //    type(v, u) = (dTime(v) < dTime(u)) ? FORWARD : CROSS;
    31         //    break;
    32         }
    33     status(v) = VISITED;
    34 }

    深度优先搜索的过程中,随时更新节点的最高连通祖先。如果节点UNDISCOVERED,那么遍历返回后,如果hca(u)比他父亲的发现时间小,那么更新父亲的hca;否则,说明无法通过后向边与祖先连接,弹出关节点v之前的节点。如果节点为DISCOVERED状态,此边为后向边,更新hca(v)。

  • 相关阅读:
    css计数器
    使用area标签模仿a标签
    移动端判断触摸的方向
    简单圆形碰撞检测
    冒泡排序和二分查找算法
    基本数据类型float和double的区别
    HTML5-form表单
    代码块以及它们的执行顺序
    反射
    Excel表格的导入导出
  • 原文地址:https://www.cnblogs.com/lustar/p/7212352.html
Copyright © 2011-2022 走看看