图
基本术语
图G由两个集合V和E组成,记为,V代表图中顶点的集合,E代表顶点之间的关系。将顶点的规模记为,边集的规模记做。
-
邻接(adjacency)顶点与顶点间的关系
-
关联(incidence)顶点与边的关系
-
无向图(undigraph):邻接顶点u和v的次序无关系,则(u,v)为无向边。若图中都为无向边则为无向图。
-
有向图(digraph):邻接顶点u和v的有头有尾,且u为尾(tail),v为头(head),则(u,v)为有向边。若图中都为有向边,则为有向图。
-
混合图(mixed graph):一个图中既有有向边,又有无向边。
无向图可以用有向图表示。
路径:连续的边的端点构成的顶点序列。
- 简单路径:路径中不含重复的顶点
- 简单环路:除了起点和终点,其余顶点都不相同的环路。
- 欧拉环路:经过所有的边一次且恰好一次的环路
- 哈密尔顿环路:经过每一个顶点一次且恰好一次的环路
图的存储
邻接矩阵
使用二维数组表示顶点之间的相邻关系。设是有n个顶点的图,顶点序号依次为0,1,···,n-1,则邻接矩阵可以表示为:
arc\left [ i,j \right ]= \left\{\begin{matrix} 1 & \left ( v_{i},v_{j} \right ) \in E\cup \left \langle v_{i},v_{j} \right \rangle \in E \\ 0 & others \end{matrix}\right关联矩阵
使用二位数字表示顶点和边的关系。设是有n个顶点,e条边的图
顶点类
给出顶点类的一种实现方法,并未进行严格封装。其中status
、dTime
、fTime
、parent
、priority
要进行重点分析。
typedef enum {UNDISCOVERED, DISCOVERED, VISITED} VStatus; |
顶点操作
对于任意的顶点i,枚举出其所有的邻接顶点neighbor:
int nextNbr(int i, int j){ //表示若已经枚举到邻居j,则转向下一个邻居。采用逆序查找的方式,时间复杂度为O(n) |
如果改用邻接矩阵来查找,耗时将会降低到degree+1
顶点插入
在邻接矩阵中,顶点插入对应于增加一列和一行,并且在对应的边集中增加其关联的边,在顶点集中增加该顶点。
int insert(Tv const & vertex){ |
- 在每个列向量的尾部增加一个单元,初始化为NULL;
- 生成一个新的行向量,每个元素为一个边记录,总数为n,所有边引用都初始化为NULL。此处行向量的长度在原来的基础上增加1。
- 顶点集增加一个顶点元素。
这里未对新的边进行实质性操作。
顶点删除
与顶点插入类似,需要删除顶点及其关联边,并返回该顶点信息。
Tv remove(int i){ |
边类
对于Edge类也没有进行严格封装。
typedef enum {UNDTERMINED, TREE, CROSS, FORWARD, BACKWARD} EStatus; |
边操作
bool exists(int i, int j){ |
判断边是否合法,如果合法可以将该边对应的数据取出,且该边的其他状态也可以直接返回,其时间复杂度为。
边插入
使用邻接矩阵,将需要插入的边封装成一个边信息,再将地址链接到邻接矩阵对应的地方即可。
void insert(Te const & edge, int w, int i, int j){ |
边删除
在邻接矩阵中删除一条边只需要将边插入的过程反过来。查询邻接矩阵,对应到一条边记录,删除边记录,使其查询时对应为空,返回删除边的信息。
Te remove(int i, int j){ |
GraphMatrix
GraphMatrix派生于Graph类。首先构造顶点集和边集,其中顶点集是顶点构成的向量;边集相当于由边向量构成的矩阵,每个边向量长度为n,因此恰好构成邻接矩阵
template <typename Tv, typename Te> class GraphMatrix : public Graph<Tv, Te>{ |
邻接矩阵表示优缺点
- 优点:
- 直观,易于理解和实现
- 适用范围广泛,尤其适用于稠密图(dense graph)
- 判断两点之间是否存在联边只需要的时间
- 获取顶点(出/入)度数只需要的时间
- 添加或删除边后更新度数也只需的时间
- 良好的扩展性(scalability):
- Vector良好的空间控制策略
- 可“透明处理”空间溢出的情况
- 缺点:
- 消耗空间数与边数无关,总是会消耗的空间
- 如平面图,其边数,此时空间利用率约为
- 消耗空间数与边数无关,总是会消耗的空间
算法
广度优先遍历
过程
- 访问顶点s
- 依次访问s所有尚未访问的邻接顶点
- 依次访问2步骤中的顶点的尚未访问的邻接顶点
- …
- 直达没有尚未访问的邻接顶点
此算法会逐层访问顶点,灰色线条表示各邻接顶点之间可能会有的关系,但此算法会忽略这种关系。这种广度遍历是树的层次遍历的推广。
算法
template <Typename Tv, TypenameTe> |
遍历的起点是某个预先指定的顶点v。每个顶点都会经历从UNDISCOVERED状态到DISCOVERED状态,最后到VISITED状态,由此构成其生命周期。每个顶点都会入队和出队一次且仅一次,因此外层while循环的时间复杂度为,内层for循环是对顶点邻居的扫描,时间复杂度为,因此总体的时间消耗为,时间复杂度为,但在实际操作中,内层for循环的n很小,所以总体的时间消耗接近于。对于邻居u的处理方式为:
template <Typename Tv, TypenameTe> |
并不是没幅图都只包含一个联通域,在有多个连通域的时候,从任何一个起点s出发,未必能够抵达其它的连通域,要使得bfs搜索足以覆盖整幅图而不是其中一个特定的连通域,需要使用while循环对BFS进行封装。
template <Typename Tv, TypenameTe> |
最短路径性
在树的结构中,相对于树根节点,任何一个节点v都对应于一条唯一的通路,这条路径的长度称作顶点v的深度,于是我们可以进而对所有的顶点自上而下按照它们的深度进行等价类划分,在每一个等价类中的所有顶点,所具有的深度指标都是彼此相等的。而树的层次遍历也相当于按照这一指标非降的次序,将所有的顶点逐一枚举出来,这样一个遍历的过程也可以转换为图结构的遍历。
图与树不同之处在于,从起始顶点s出发可能有多条路径都最后通往同一个顶点而且可能出现分叉。实际上只需考察顶点之间的最短通路,并且将这两个顶点之间的距离取作这条最短通路的长度。而在起始顶点相对固定的情况下,可以将s在这个记号中省掉,直接简称之为顶点v所对应的距离。
深度优先遍历
过程:
- 访问顶点s
- if s的邻居尚未被访问,访问s尚未被访问的邻居,任取其一,递归执行DFS(u)
- else 返回
此过程等效于树的先序遍历,DFS会构造出原图的一颗支撑树。其过程为:
按照图中的箭头方向,从红色到白色到黄色到蓝色,每次改变颜色都是因为处于else语句中。
算法:
template <typename Tv, typename Te> |
在内部对u进行处理时,没有使用队列的方式,而是涉及到递归调用。
template <typename Tv, typename Te> |
无向图例子
例如对当前的无向图:
最终会得到一个支撑树:
与BFS(v)类似,在有多个连通域的时候,需要将DFS(v)用while循环封装起来
template <typename Tv, typename Te> |
在有向图中,情况更为复杂。一旦出现BACKWARD边,则表明图中出现了环路。
括号引理
顶点活动期:active[u] = (dTime[u], fTime[u])
嵌套引理:给定有向图及其任一DS森林,则
- u是v的后代,if and only if active[u]$\subseteq $active[v]
- u是v的祖先,if and only if activate[u]$\supseteq $activate[v]
- u与v无关,if and only if actiavte[u]active[v] =
祖先的活跃期必定包含后代的活跃期。借助时间标签可以在的时间内得出两个节点之间的祖先关系,若没有时间标签则需要从起点一直追溯到终点。