【程序人生】数据结构杂记(六)

说在前面

个人读书笔记

图的概念

图结构是描述和解决实际应用问题的一种基本而有力的工具。

所谓的图(graph),可定义为G=(V,E)G = (V, E)。其中,集合VV中的元素称作顶点(vertex);集合EE中的元素分别对应于VV中的某一对顶点(u,v)(u, v)表示它们之间存在某种关系,故亦称作边(edge)

一种直观显示图结构的方法是,用小圆圈或小方块代表顶点,用联接于其间的直线段或曲线弧表示对应的边。

从计算的需求出发,我们约定VVEE均为有限集。

无向图、有向图及混合图

若边(u,v)(u, v)所对应顶点uuvv的次序无所谓,则称作无向边(undirected edge),例如表示同学关系的边。
反之若uuvv不对等,则称(u,v)(u, v)为有向边(directed edge),例如描述企业与银行之间的借贷关系,或者程序之间的相互调用关系的边。
如此,无向边(u,v)(u, v)也可记作(v,u)(v, u),而有向的(u,v)(u, v)(v,u)(v, u)则不可混淆。这里约定,有向边(u,v)(u, v)uu指向vv,其中uu称作该边的起点(origin)或尾顶点(tail),而vv称作该边的终点(destination)或头顶点(head)

EE中各边均无方向,则GG称作无向图。若EE中只含有向边,则GG称作有向图。若EE同时包含无向边和有向边,则GG称作混合图。

相对而言,有向图的通用性更强,因为无向图和混合图都可转化为有向图:
每条无向边(u,v)(u, v)都可等效地替换为对称的一对有向边(u,v)(u, v)(v,u)(v, u)

对于任何边e=(u,v)e = (u, v)称顶点uuvv彼此邻接(adjacent),互为邻居;而它们都与边ee彼此关联(incident)

在这里插入图片描述
在无向图中,与顶点vv关联的边数,称作vv的度数(degree),记作deg(v)deg(v)
以上图中无向图为例,顶点{ A, B, C, D }的度数为{ 2, 3, 2, 1 }。

对于有向边e=(u,v)e = (u, v)ee称作uu的出边(outgoing edge)、vv的入边(incoming edge)vv的出边总数称作其出度(out-degree),记作outdeg(v)outdeg(v);入边总数称作其入度(in-degree),记作indeg(v)indeg(v)
在上图中的有向图中,各顶点的出度为{ 1, 3, 1, 1 },入度为{ 2, 1, 2, 1 }。

简单图

联接于同一顶点之间的边,称作自环(self-loop)。在某些特定的应用中,这类边可能的确具有意义——比如在城市交通图中,沿着某条街道,有可能不需经过任何交叉路口即可直接返回原处。
不含任何自环的图称作简单图(simple graph)

通路和环路

所谓路径或通路(path),就是由m+1m + 1个顶点与mm条边交替而成的一个序列,且对任何0<i<=m0 < i <= m都有ei=(vi1,vi)e_i = (v_{i-1} , v_i )。也就是说,这些边依次地首尾相联其中沿途边的总数mm,亦称作通路的长度

尽管通路上的边必须互异,但顶点却可能重复。沿途顶点互异的通路,称作简单通路

特别地,对于长度m>=1m >= 1的通路,若起止顶点相同(即v0=vmv_0 = v_m),则称作环路(cycle),其长度也取作沿途边的总数。同样,尽管环路上的各边必须互异,但顶点却也可能重复。反之,若沿途除v0=vmv_0 = v_m外所有顶点均互异,则称作简单环路。

特别地,经过图中各边一次且恰好一次的环路,称作欧拉环路(Eulerian tour)——其长度恰好等于图中边的总数。经过图中各顶点一次且恰好一次的环路,称作哈密尔顿环路(Hamiltonian tour)。

不含任何环路的有向图,称作有向无环图

带权网络

图不仅需要表示顶点之间是否存在某种关系,有时还需要表示这一关系的具体细节。以铁路运输为例,可以用顶点表示城市,用顶点之间的联边表示对应的城市之间是否有客运铁路联接;同时,往往还需要记录各段铁路的长度、承运能力,以及运输成本等信息。

为适应这类应用要求,需通过一个权值函数,为每一边ee指定一个权重(weight),比如wt(e)wt(e)即为边ee的权重。各边均带有权重的图,称作带权图(weighted graph)或带权网络(weightednetwork),有时也简称网络(network),记作G(V,E,wt())G(V, E, wt())

图ADT的实现方法

邻接矩阵

邻接矩阵(adjacency matrix)是图ADT(Abstract Data Type)最基本的实现方式,使用方阵A[n][n]A[n][n]表示由nn个顶点构成的图,其中每个单元,各自负责描述一对顶点之间可能存在的邻接关系,故此得名。
在这里插入图片描述
对于无权图,存在(不存在)从顶点uuvv的边,当且仅当A[u][v]=1(0)A[u][v] =1(0)。上图中(a)(a)(b)(b)即为无向图和混合图的邻接矩阵实例。

对于带权网络,如上图中(c)(c)所示,矩阵各单元可从布尔型改为整型或浮点型,记录所对应边的权重。对于不存在的边,通常统一取值为无穷大或0。

邻接表

在这里插入图片描述
以如上图中(a)(a)所示的无向图为例,只需将如上图中(b)(b)所示的邻接矩阵,逐行地转换为如上图中(c)(c)所示的一组列表,即可分别记录各顶点的关联边(或等价地,邻接顶点)。这些列表,也因此称作邻接表(adjacency list)。

图遍历算法

无论采用何种策略和算法,图的遍历都可理解为,将非线性结构转化为半线性结构的过程

经遍历而确定的边类型中,最重要的一类即所谓的树边,它们与所有顶点共同构成了原图的一棵支撑树(森林),称作遍历树(traversal tree)。

以遍历树为背景,其余各种类型的边,也能提供关于原图的重要信息,比如其中所含的环路等。

图中顶点之间可能存在多条通路,故为避免对顶点的重复访问,在遍历的过程中,通常还要动态地设置各顶点不同的状态,并随着遍历的进程不断地转换状态,直至最后的“访问完毕”。

图的遍历更加强调对处于特定状态顶点的甄别与查找,故也称作图搜索(graph search)。

广度优先搜索

各种图搜索之间的区别,体现为边分类结果的不同,以及所得遍历树(森林)的结构差异。
其决定因素在于,搜索过程中的每一步迭代,将依照何种策略来选取下一接受访问的顶点。

通常,都是选取某个已访问到的顶点的邻居。同一顶点所有邻居之间的优先级,在多数遍历中不必讲究。因此,实质的差异应体现在,当有多个顶点已被访问到,应该优先从谁的邻居中选取下一顶点

比如,广度优先搜索(breadth-first search, BFS)采用的策略,可概括为:
越早被访问到的顶点,其邻居越优先被选用

于是,始自图中顶点ss的BFS搜索,将首先访问顶点ss;再依次访问ss所有尚未访问到的邻居;再按后者被访问的先后次序,逐个访问它们的邻居;…;如此不断。

在所有已访问到的顶点中,仍有邻居尚未访问者,构成所谓的波峰集(frontier)(用队列实现)。于是,BFS搜索过程也可等效地理解为:
反复从波峰集中找到最早被访问到顶点v,若其邻居均已访问到,则将其逐出波峰集;否则,随意选出一个尚未访问到的邻居,并将其加入到波峰集中。

不难发现,若将上述BFS策略应用于树结构,则效果等同于层次遍历。波峰集内顶点的深度始终相差不超过一,且波峰集总是优先在更浅的层次沿广度方向拓展。

在这里插入图片描述
仿照树的层次遍历,这里也借助队列QQ,来保存已被发现,但尚未访问完毕的顶点

因此,任何顶点在进入该队列的同时,都被随即标记为DISCOVERED(已发现)状态。

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

若顶点uu尚处于UNDISCOVERED(未发现)状态,则令其转为DISCOVERED状态,并随即加入队列QQ

实际上,每次发现一个这样的顶点uu都意味着遍历树可从vvuu拓展一条边。于是,将边(v,u)(v, u)标记为树边(tree edge),并按照遍历树中的承袭关系,将vv记作uu的父节点。

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

BFS()遍历结束后,所有访问过的顶点通过parent[]指针依次联接,从整体上给出了原图某一连通或可达域的一棵遍历树,称作广度优先搜索树,或简称BFS树(BFS tree)。

实例:
在这里插入图片描述不难看出,BFS(s)将覆盖起始顶点ss所属的连通分量或可达分量,但无法抵达此外的顶点

而上层主函数bfs()的作用,正在于处理多个连通分量或可达分量并存的情况。具体地,在逐个检查顶点的过程中,只要发现某一顶点尚未被发现,则意味着其所属的连通分量或可达分量尚未触及,故可从该顶点出发再次启动BFS(),以遍历其所属的连通分量或可达分量。

如此,各次BFS()调用所得的BFS树构成一个森林,称作BFS森林(BFS forest)。

深度优先搜索

深度优先搜索(Depth-First Search, DFS)选取下一顶点的策略,可概括为:
优先选取最后一个被访问到的顶点的邻居

于是,以顶点ss为基点的DFS搜索,将首先访问顶点ss;再从ss所有尚未访问到的邻居中任取其一,并以之为基点,递归地执行DFS搜索。

故各顶点被访问到的次序,类似于树的先序遍历;而各顶点被访问完毕的次序,则类似于树的后序遍历。

在这里插入图片描述
每一递归实例中,都先将当前节点vv标记为DISCOVERED(已发现)状态,再逐一核对其各邻居uu的状态并做相应处理。待其所有邻居均已处理完毕之后,将顶点vv置为VISITED(访问完毕)状态,便可回溯。

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

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

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

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

DFS(s)返回后,所有访问过的顶点通过parent[]指针依次联接,从整体上给出了顶点ss所属连通或可达分量的一棵遍历树,称作深度优先搜索树或DFS树(DFS tree)。

与BFS搜索一样,此时若还有其它的连通或可达分量,则可以其中任何顶点为基点,再次启动DFS搜索。最终,经各次DFS搜索生成的一系列DFS树,构成了DFS森林(DFS forest)。

实例:
在这里插入图片描述
在这里插入图片描述
最终结果如上图中(t)(t)所示,为包含两棵DFS树的一个DFS森林。可以看出,选用不同的起始基点,生成的DFS树(森林)也可能各异。如本例中,若从D开始搜索,则DFS森林可能如上图中(u)(u)所示。

深度优先搜索的应用——拓扑排序

问题描述:
在这里插入图片描述

给定描述某一实际应用(上图中(a)(a))的有向图(上图中(b)(b)),如何在与该图“相容”的前提下,将所有顶点排成一个线性序列(上图中(c)(c))。

此处的“相容”,准确的含义是:
每一顶点都不会通过边,指向其在此序列中的前驱顶点。

这样的一个线性序列,称作原有向图的一个拓扑排序。
同一有向图的拓扑排序未必唯一,一个有向图也未必一定存在拓扑排序。
有向无环图一定存在拓扑排序。

思路一:
任一有向无环图必包含入度为零的顶点。否则,每个顶点都至少有一条入边,意味着要么顶点有无穷个,要么包含环路。
于是,只要将入度为0的顶点mm(及其关联边)从图GG中取出,则剩余的GG'依然是有向无环图,故其拓扑排序也必然存在。从递归的角度看,一旦得到了GG'的拓扑排序,只需将mm作为最大顶点插入,即可得到GG的拓扑排序。
在这里插入图片描述

思路二:
有向无环图的DFS搜索过程中各顶点被标记为VISITED的次序,恰好(按逆序)给出了原图的一个拓扑排序。
在这里插入图片描述
在这里插入图片描述

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

最小支撑树

连通图GG的某一无环连通子图TT若覆盖GG中所有的顶点,则称作GG的一棵支撑树或生成树(spanning tree)。
在这里插入图片描述就保留原图中边的数目而言,支撑树既是“禁止环路”前提下的极大子图,也是“保持连通”前提下的最小子图。在实际应用中,原图往往对应于由一组可能相互联接(边)的成员(顶点)构成的系统,而支撑树则对应于该系统最经济的联接方案。确切地,尽管同一幅图可能有多棵支撑树,但由于其中的顶点总数均为n,故其采用的边数也均为n - 1

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

尽管同一带权网络通常都有多棵支撑树,但总数毕竟有限,故必有最低的总体成本。然而,总体成本最低的支撑树却未必唯一

Prim算法

假定各边的权重互异

G=(V;E)G = (V; E)中,顶点集VV的任一非平凡子集UU及其补集V\U都构成GG的一个割(cut),记作(U : V\U)。若边uvuv满足uu属于UU,且vv不属于UU,则称作该割的一条跨越边(crossing edge)。

因此类边联接于VV及其补集之间,故亦形象地称作该割的一座桥(bridge)。

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

在这里插入图片描述
否则,如上图(a)(a)所示假设uvuv是割(U : V\U)的最短跨越边,而最小支撑树TT并未采用该边。于是由树的连通性,如图(b)(b)所示在TT中必有至少另一跨边stst联接该割(有可能s=us = ut=vt =v,尽管二者不能同时成立)。同样由树的连通性,TT中必有分别联接于uussvvtt之间的两条通路。由于树是极大的无环图,故倘若将边uvuv加至TT中,则如图(c)(c)所示,必然出现穿过uvtu、v、tss的唯一环路。接下来,只要再删除边stst,则该环路必然随之消失。

经过如此的一出一入,若设TT转换为TT',则TT’依然是连通图,且所含边数与TT相同均为n1n - 1。这就意味着,TT'也是原图的一棵支撑树。就结构而言,TT'TT的差异仅在于边uvuv和边stst,故二者的成本之差就是这两条边的权重之差。不难看出,边stst的权重必然大于身为最短跨越边的uvuv,故TT'的总成本低于TT——这与TT总体权重最小的前提矛盾。

由以上性质,可基于贪心策略(当前看来是最好的选择)导出一个迭代式算法。

实例:
在这里插入图片描述在这里插入图片描述

最短路径

若以带权图来表示真实的通讯、交通、物流或社交网络,则各边的权重可能代表信道成本、交通运输费用或交往程度。此时我们经常关心的一类问题可以概括为:
给定带权网络G=(V,E)G = (V, E),以及源点(source)ss属于VV,对于所有的其它顶点vvssvv的最短通路有多长?该通路由哪些边构成?
在这里插入图片描述

设顶点ssvv的最短路径为pp。于是对于该路径上的任一顶点uu,若其在pp上对应的前缀为σ\sigma,则σ\sigma也必是ssuu的最短路径(之一)。否则,若从ssuu还有另一严格更短的路径τ\tau,则易见pp不可能是ssvv的最短路径。

即便各边权重互异,从ssvv的最短路径也未必唯一。
当存在非正权重的边,并导致某个环路的总权值非正时,最短路径甚至无从定义。

最短路径不含任何(有向)回路。

Dijkstra算法

该算法与Prim算法仅有一处差异:
考虑的是uk+1u_{k+1}ss的距离,而不再是其到TkT_k的距离。

实例:
在这里插入图片描述

结语

如果您有修改意见或问题,欢迎留言或者通过邮箱和我联系。
手打很辛苦,如果我的文章对您有帮助,转载请注明出处。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章