数据结构(C++)——图的遍历算法:广度优先搜索、深度优先搜索、优先级搜索算法

图的遍历算法

图的遍历都可以理解为,将非线性结构转化为半线性结构的过程。经遍历而确定的边类型中,最重要的一类即所谓的树边,它们与所有顶点共同构成了原图的一棵支撑树(森林),称作遍历树(traversal tree)。

 

 

广度优先搜索(BFS)

广度优先搜索(breadth-first search, BFS)采用的策略,可概括为:

    越早被访问到的定点,其邻居越优先被选用

 

于是,始自图中顶点s的BFS搜索,将首先访问顶点s;再依次访问s所有尚未访问到的邻居;再按后者被访问的先后次序,逐个访问它们的邻居;…;如此不断。在所有已访问到的顶点中,仍有邻居尚未访问者,构成所谓的波峰集(frontier)。

    广度优先搜索类似与树的层次遍历。

 

广度优先搜索算法实现:(BFS算法)

template <typename Tv, typename Te> //广度优先搜索BFS算法(全图)
void Graph<Tv, Te>::bfs(int s) { //assert: 0 <= s < n
    reset(); int clock = 0; int v = s; //初始化
    do //逐一检查所有顶点
        if (UNDISCOVERED == status(v)) //一旦遇到尚未发现的顶点
            BFS(v, clock); //即从该顶点出发启动一次BFS
    while (s != (v = (++v % n))); //按序号检查,故不漏不重
}

template <typename Tv, typename Te> //广度优先搜索BFS算法(单个连通域)
void Graph<Tv, Te>::BFS(int v, int& clock) { //assert: 0 <= v < n
    Queue<int> Q; //引入辅助队列
    status(v) = DISCOVERED; Q.enqueue(v); //初始化起点
    while (!Q.empty()) { //在Q变空之前,不断
        int v = Q.dequeue(); dTime(v) = ++clock; //取出队首顶点v
        for (int u = firstNbr(v); -1 < u; u = nextNbr(v, u)) //枚举v的所有邻居u
            if (UNDISCOVERED == status(u)) { //若u尚未被发现,则
                status(u) = DISCOVERED; Q.enqueue(u); //发现该顶点
                status(v, u) = TREE; parent(u) = v; //引入树边拓展支撑树
            } else { //若u已被发现,或者甚至已访问完毕,则
                status(v, u) = CROSS; //将(v, u)归类于跨边
            }
        status(v) = VISITED; //至此,当前顶点访问完毕
    }
}

算法的实质功能,由子算法BFS()完成。对该函数的反复调用,即可遍历所有连通或可达域。

 

仿照树的层次遍历,这里也借助队列Q,来保存已被发现,但尚未访问完毕的顶点。因此,任何顶点在进入该队列的同时,都被随即标记为DISCOVERED(已发现)状态。

BFS()的每一步迭代,都先从Q中取出当前的首顶点v;再逐一核对其各邻居u的状态并做相应处理;最后将顶点v置为VISITED(访问完毕)状态,即可进入下一步迭代。

若顶点u尚处于UNDISCOVERED(未发现)状态,则令其转为DISCOVERED状态,并随即加入队列Q。实际上,每次发现一个这样的顶点u,都意味着遍历树可从v到u拓展一条边。于是,将边(v, u)标记为树边(tree edge),并按照遍历树中的承袭关系,将v记作u的父节点。

若顶点u已处于DISCOVERED状态(无向图),或者甚至处于VISITED状态(有向图),则意味着边(v, u)不属于遍历树,于是将该边归类为跨边(cross edge)。

 

 

实例:

下图给出了一个含8个顶点和11条边的有向图,起始于顶点S的BFS搜索过程。请留意观察辅助队列(下方)的演变,顶点状态的变化,边的分类与结果,以及BFS树的生长过程。

《数据结构(C++)——图的遍历算法:广度优先搜索、深度优先搜索、优先级搜索算法》

 

 

深度优先搜索(DFS)

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

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

 

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

 

深度优先搜索算法实现:(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) = DISCOVERED; //发现当前前顶点v
    for (int u = firstNbr(v); -1 < u; u = nextNbr(v, u)) //枚举v的所有邻居u
        switch (status(u)) { //并视其状态分别处理
            case UNDISCOVERED: //u尚未发现,意味着支撑树可在此拓展
                status(v, u) = TREE; parent(u) = v; DFS(u, clock); break;
            case DISCOVERED: //u已被发现但尚未访问完毕,应属被后代指向的祖先
                status(v, u) = BACKWARD; break;
            default: //u已访问完毕(VISITED,有向图),则视承袭关系分为前向边或跨边
                status(v, u) = (dTime(v) < dTime(u)) ? FORWARD : CROSS; break;
        }
    status(v) = VISITED; fTime(v) = ++clock; //至此,当前顶点v方告访问完毕
}

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

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

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

 

 

实例:

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

《数据结构(C++)——图的遍历算法:广度优先搜索、深度优先搜索、优先级搜索算法》《数据结构(C++)——图的遍历算法:广度优先搜索、深度优先搜索、优先级搜索算法》

 

 

优先级搜索

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

  

优先级搜索算法的基本框架实现:

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; parent(s) = -1; //初始化,起点s加至PFS树中
    while (1) { //将下一顶点和边加至PFS树中
        for (int w = firstNbr(s); -1 < w; w = nextNbr(s, w)) //枚举s的所有邻居w
            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搜索的基本过程和功能与常规的图搜索算法一样,也是以迭代方式逐步引入顶点和边,最终构造出一棵遍历树(或者遍历森林)。如上所述,每次都是引入当前优先级最高(优先级数最小)的顶点s,然后按照不同的策略更新其邻接顶点的优先级数。

这里借助函数对象prioUpdater,使算法设计者得以根据不同的问题需求,简明地描述和实现对应的更新策略。具体地,只需重新定义prioUpdater对象即可,而不必重复实现公共部分。比如,此前的BFS搜索和DFS搜索都可按照此模式统一实现

    原文作者:数据结构之图
    原文地址: https://blog.csdn.net/amoscykl/article/details/81395595
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞