• 图的遍历


    记录遍历状态

    对于图结构来说,图的遍历和树的遍历有类似之处,树结构的遍历从根结点出发,图结构的遍历从某一结点出发。出发之后,按照某种手法无重复地访问所有的结点,这也是后续解决图的连通性、拓扑排序和关键路径的预备知识。
    由于在图结构中,任意顶点都有可能与其他顶点相互邻接,因此如果没有对已走过的路径进行记录的话,很有可能会由于结点的重复访问而无法遍历所有顶点。因此我们需要一种手法记录访问过的顶点,一种直接而有效的手法是使用一个 visited[n] 数组,先将其每一个元素初始化为 0,当我访问了第 i 个顶点时,就将 visited[i] 的值赋值为 1,表示已经访问过。当我访问某一个顶点时,可以通过 visited 数组来确定我接下来是否要从这个顶点往下走。

    DFS

    深度优先搜索

    深度优先搜索( Depth First Serarch )我们之前是接触过的,在迷宫问题(栈实现)和树结构的递归遍历法中,我们用其思想实现了一些功能,现在我们来详细谈一谈。所谓 DFS,我称之为视角放在路径的手法,思想是通过对某一条路径的顶点的挖掘,从而试探出一条可行的路径。当我使用递归或者通过修饰后的栈结构,可以实现回溯的效果,以获取全部的路径。

    算法流程

    对于一个连通图,DFS 的遍历过程为:

    1. 选择图中的某个顶点出发,并访问该顶点;
    2. 找出刚访问过的顶点的第一个未被访问的邻接点并访问;
    3. 以该顶点为新顶点,步骤 2 直至刚访问过的顶点没有未被访问的邻接点;
    4. 返回前一个访问过的且仍有未被访问的邻接点的顶点,找出该顶点的下一个未被访问的邻接点,并访问该顶点;
    5. 重复上述 2、3、4 步骤直至所有顶点访问完毕。

    模拟遍历

    例如上图的连通图,我们选择顶点 A 出发,访问该顶点:

    选取 A 的邻接点 B,访问该结点:

    选取 B 的邻接点 D,访问该结点:

    选取 D 的邻接点 H,访问该结点:

    选取 H 的邻接点 E,访问该结点:

    由于 E 顶点的所有邻接点都遍历完了,因此需要回溯。回溯需要 4 次回到顶点 A,并访问下一个邻接点 C:

    选取 C 的邻接点 F,访问该结点:

    选取 F 的邻接点 G,访问该结点:

    遍历完毕,顺序为 A->B->D->H->E->C->F->G。其深度优先生成树为:

    代码实现

    由于 DFS 需要涉及到回溯问题,因此我们想到使用递归来实现。

    邻接矩阵 DFS

    int visited[MAXV] = { 0 };
    void DFS(MGraph g, int v)    //v 表示当前所在的顶点
    {
        cout << v << " ";    //输出顶点
        visited[v] = 1;    //标记已访问
        for (int i = 1; i <= g.n; i++)    //使用循环控制回溯
        {
    	if (g.edges[v][i] == 1 && visited[i] == 0)
    	{
    	    DFS(g, i);    //当前顶点与 i 顶点邻接且未被访问,递归搜索
    	}
        }
    }
    

    邻接表 DFS

    int visited[MAXV] = { 0 };
    void DFS(AdjGraph* G, int v)    //从 v 顶点开始深度遍历
    {
        ArcNode* ptr;
    
        cout << v << " ";    //输出顶点
        visited[v] = 1;    //标记已访问
        ptr = G->adjlist[v].firstarc;
        while (ptr)    //沿着邻接表搜索路径
        {
    	if (visited[ptr->adjvex] == 0)
    	{
    	    DFS(G, ptr->adjvex);    //当前顶点与 i 顶点邻接且未被访问,递归搜索
    	}
    	ptr = ptr->nextarc;    //继续访问邻接表
        }
    }
    

    时间复杂度

    当我们遍历一个连通图时,对图中每个顶点至多调用一次 DFS 函数,并且当这个顶点被访问过之后,由于被标志成已被访问,就不再从它出发进行搜索。因此,遍历图的过程实质上是对每个顶点查找其邻接点的过程,其时间复杂度则取决于使用的存储结构。当用邻接矩阵描述图结构时,查找每个顶点的邻接点的时间复杂度为 O(n2),其中 n 为图结构中的顶点数。当以邻接表做图的存储结构时,查找邻接点的时间复杂度为 O(e),e 为图结构的边数。由此当以邻接表做存储结构时,深度优先搜索遍历图的时间复杂度为 O(n + e)

    BFS

    广度优先搜索

    广度优先搜索( Breadth First Serarch )我们之前也有接触过,在迷宫问题(队列实现)和树结构的层序遍历法中,我们用其思想实现了一些功能,现在我们来详细谈一谈。所谓 BFS,我称之为视角放在整个图结构的手法,思想是通过从某个顶点向外扩散,从而囊括所有顶点的手法。当我借助队列结构时,可以实现该算法。

    算法流程

    对于一个连通图,DFS 的遍历过程为:

    1. 选择图中的某个顶点出发,并访问该顶点;
    2. 依次访问 v 的每个未曾访问过的邻接点 i;
    3. 分别从这些邻接点出发,依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问;
    4. 重复步骤 3,直至图中所有己被访问的顶点的邻接点全部访问到。

    模拟遍历

    例如上图的连通图,我们选择顶点 A 出发,访问该顶点:

    访问顶点 A 的所有邻接点:

    访问顶点 B 的所有邻接点:

    访问顶点 C 的所有邻接点:

    访问顶点 D 的所有邻接点:

    接着访问剩下的顶点,由于所有的顶点都被访问过了,因此没有新顶点入队列。遍历顺序为:A->B->C->D->E->F->G,广度优先生成树为:

    代码实现

    由于 BFS 需要涉及到回溯问题,因此我们想到使用递归来实现。
    广度优先搜索遍历图结构其实和树结构的层序遍历是一个玩意,尽可能先对横向进行搜索。设 x 和 y 是两个相继被访问过的顶点,若当前是以 X 为出发点进行搜索,则在访问 x 的所有未曾被访问过的邻接点之后,紧接着是以 y 为出发点进行横向搜索,并对搜索到的 y 的邻接点中尚未被访问的顶点进行访问。也就是说,先访问的顶点其邻接点亦先被访问,因此我们需要一个队列来辅助实现算法。

    邻接矩阵 BFS

    void BFS(MGraph g, int v)
    {
        int front, rear;    //头指针与尾指针
        int point[MAXV];    //构造队列结构(可以是非循环队列)
    
        front = 0;
        point[front] = v;    //顶点 v 入队列
        rear = visited[v] = 1;    //标记顶点已访问
        while (front != rear)    //队列不为空,搜索继续
        {
    	for (int i = 1; i <= g.n; i++)    //遍历表头顶点的邻接点
    	{
    	    if (g.edges[point[front]][i] == 1 && visited[i] == 0)
    	    {
    	        point[rear++] = i;    //顶点 i 入队列
    		visited[i] = 1;    //标记顶点已访问
    	    }
            }
    	cout << point[front++] << " ";    //输出顶点
    }
    

    邻接表 BFS

    void BFS(AdjGraph* G, int v)    //顶点 v 开始广度遍历
    {
        int front, rear;    //头指针与尾指针
        int a_que[MAXV];   //构造队列结构(可以是非循环队列
        ArcNode* ptr;
    
        front = 0;
        a_que[front] = v;    //顶点 v 入队列
        visited[v] = rear = 1;    //标记顶点已访问
        while (front != rear)    //队列不为空,搜索继续
        {
            ptr = G->adjlist[a_que[front]].firstarc;
    	while (ptr)    //遍历表头顶点的邻接点
    	{
    	    if (visited[ptr->adjvex] == 0)
    	    {
    	        a_que[rear++] = ptr->adjvex;    //顶点 i 入队列
    		visited[ptr->adjvex] = 1;    //标记顶点已访问
    	    }
    	    ptr = ptr->nextarc;
    	}
    	cout << point[front++] << " ";    //输出顶点
        }
    }
    

    时间复杂度

    对于 BFS,每个顶点至多进一次队列,因此遍历图的过程实质上是通过边找邻接点的过程。因此广度优先搜索遍历图的时间复杂度和深度优先搜索遍历相同,两种遍历方法的不同之处仅仅在于对顶点访问的顺序不同。

    实例:六度空间

    情景需求

    输入样例

    10 9
    1 2
    2 3
    3 4
    4 5
    5 6
    6 7
    7 8
    8 9
    9 10
    

    输出样例

    1: 70.00%
    2: 80.00%
    3: 90.00%
    4: 100.00%
    5: 100.00%
    6: 100.00%
    7: 100.00%
    8: 90.00%
    9: 80.00%
    10: 70.00%
    

    情景解析

    这道题表面上看好像不好懂,但在本质上是一个有限的图结构遍历,即我们可能在遍历到某些顶点的时候就需要提前结束了。基于这一点,我们反应过来用 BFS 做比较方便一点,因为 BFS 的流程可以抽象为一层一层地往外探测,这与我们的思路是符合的。
    那么接下来我们读题,根据题意,我们需要找到关系网在 6 层以内的结点。那也就是说,我们需要去记录某个顶点,它对于初始顶点来说是第几层的关系网。为了方便理解,我展示的做法是去修改记录结点信息的数组的结构体为:

    typedef struct
    {
    	int level;    //表示结点相对目标顶点的距离
    	int v;
    }AVertex;
    

    即多开一个成员来记录层次的信息。那么层次的信息怎么操作?首先我们先初始化为 0,接下来每进行一轮 BFS,添加入队列的顶点就从它的上一层继承层数,可以用这行代码实现:

    a_que[rear].level = a_que[front].level + 1;
    

    解决了层次的确定问题,剩下的就是我们喜闻乐见的建图和遍历的事情啦。

    伪代码

    代码实现

    int getCount(AdjGraph* G, int v)
    {
        int visited[MAXV] = { 0 };
        space a_que[MAXV] = { 0 };
        int front, rear;
        int count = 1;
        ArcNode* ptr;
    
        front = rear = 0;
        a_que[rear].v = v;    //目标顶点 v 入队列
        a_que[rear++].level = 0;    //初始化层数为 0
        visited[v] = 1;
        while (front != rear)
        {
    	if (a_que[front].level == 6)
    	{
    	    break;    //表头顶点在第 6 层,结束 BFS
    	}
    	ptr = G->adjlist[a_que[front].v].firstarc;
    	while (ptr)
    	{
    	    if (visited[ptr->adjvex] == 0)
    	    {
    	        a_que[rear].v = ptr->adjvex;
    		a_que[rear++].level = a_que[front].level + 1;    //继承上一个顶点的层数
    		visited[ptr->adjvex] = 1;
    		count++;
    	    }
    	    ptr = ptr->nextarc;
            }
    	front++;
        }
        return count;
    }
    

    扩充资料

    六度空间理论(数学领域的猜想)

    实例:判断 DFS 序列合法性

    左转我另一篇博客——PTA习题解析——判断DFS序列的合法性

    参考资料

    《大话数据结构》—— 程杰 著,清华大学出版社
    《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社

  • 相关阅读:
    VMWare上的ubuntu系统安装VMWare Tools(图文)
    Ubuntu添加新分区
    emacs入门
    SQL UNION 操作符
    eclipse安装其他颜色主题包
    mysql左连接
    不能用notepad++编辑器编写python
    ImportError: No module named simplejson.scanner
    运行 python *.py 文件出错,如:python a.py
    doc命令大全(详细版)
  • 原文地址:https://www.cnblogs.com/linfangnan/p/12784323.html
Copyright © 2020-2023  润新知