6.1 图的基本概念
6.1.1 图的定义
图G由顶点集V和边集 E组成,记为G=(V,E),其中 V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点之间的关系(边)集合。若V={v?,v?,...,vn},则用|M表示图G中顶第6章 点的个数,E={(u,v) | uεV,vεV},用|E|表示图G中边的条数。
1.有向图
若E是有向边(也称弧)的有限集合,则图G为有向图。弧是顶点的有序对,记为<v,w>,其中 v,w是顶点,v称为弧尾,w称为弧头,<v,w>称为从v到 w的弧,也称v邻接到 w。
图6.1(a)所示的有向图G1可表示为
2.无向图
若E是无向边(简称边)的有限集合,则图G为无向图。边是顶点的无序对,记为(v,w)或(w,v)。可以说w和v互为邻接点。边(v,w)依附于w和v,或称边(v,w)和v,w相关联。
图6.1(b)所示的无向图G2可表示为
3.简单图、多重图
一个图G若满足:①不存在重复边;②不存在顶点到自身的边,则称图G为简单图。图6.1中G1和G2均为简单图。若图G中某两个顶点之间的边数大于 1条,又允许顶点通过一条边和自身关联,则称图G为多重图。多重图和简单图的定义是相对的。本书中仅讨论简单图。
4.完全图(也称简单完全图)
对于无向图,|E|的取值范围为0到n(n-1)2,有n(n-1)2条边的无向图称为完全图,在完全图中任意两个顶点之间都存在边。对于有向图,|E|的取值范围为0到n(n-1),有n(n-1)条弧的有向图称为有向完全图,在有向完全图中任意两个顶点之间都存在方向相反的两条弧。图 6.1中G?为无向完全图,而G3为有向完全图。
5.子图
设有两个图G=(V,E)和G=(V,E),若V是V的子集,且E是E的子集,则称G'是G的子图。若有满足V(G)=V(G)的子图G,则称其为G的生成子图。图6.1中G3为G1的子图。
6.连通、连通图和连通分量
在无向图中,若从顶点v到顶点 w有路径存在,则称v和 w是连通的。若图G中任意两个顶点都是连通的,则称图G为连通图,否则称为非连通图。无向图中的极大连通子图称为连通分量,在图 6.2(a)中,图G?有3个连通分量如图6.2(b)所示。假设一个图有n个顶点,若边数小于n-1,则此图必是非连通图;思考,若图是非连通图,则最多可以有多少条边?
7.强连通图、强连通分量
在有向图中,若有一对顶点v和w,从v到w和从w到v之间都有路径,则称这两个顶点是强连通的。若图中任意一对顶点都是强连通的,则称此图为强连通图。有向图中的极大强连通子图称为有向图的强连通分量,图G1的强连通分量如图6.3所示。思考,假设一个有向图有n个顶点,若是强连通图,则最少需要有多少条边?
8.生成树、生成森林
连通图的生成树是包含图中全部顶点的一个极小连通子图。若图中顶点数为 n,则它的生成树含有n-1条边。包含图中全部顶点的极小连通子图,只有生成树满足这个极小条件,对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。在非连通图中,连通分量的生成树构成了非连通图的生成森林。图G2的一个生成树如图6.4所示。
9.顶点的度、入度和出度
在无向图中,顶点v的度是指依附于顶点v的边的条数,记为 TD(v)。在图6.1(b)中,每个 顶点的度均为3。无向图的全部顶点的度之和等于边数的2倍,因为每条边和两个顶点相关联。
在有向图中,顶点v的度分为入度和出度,入度是以顶点v为终点的有向边的数目,记为ID(v);而出度是以顶点v为起点的有向边的数目,记为 OD(v)。在图 6.1(a)中,顶点 2的出度为2、入度为1。顶点v的度等于其入度与出度之和,即 TD(v)=ID(v)+OD(v)。有向图的全部顶点的入度之和与出度之和相等,并且等于边数,这是因为每条有向边都有一个起点和终点。
10.边的权和网
在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。这种边上带有权值的图称为带权图,也称网。
11.稠密图、稀疏图
边数很少的图称为稀疏图,反之称为稠密图。稀疏和稠密本身是模糊的概念,稀疏图和稠密图常常是相对而言的。一般当图G满足|E|<|V|log|V|时,可以将G视为稀疏图。
12.路径、路径长度和回路
顶点v,到顶点vq之间的一条路径是指顶点序列Vp,Vi1,...,Vim,当然关联的边也可理解为路径的构成要素。路径上的边的数目称为路径长度。第一个顶点和最后一个顶点相同的路径称为回路或环。若一个图有n个顶点,且有大于n-1条边,则此图一定有环。
13.简单路径、简单回路
在路径序列中,顶点不重复出现的路径称为简单路径。除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
14.距离
从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。若从u到v根本不存在路径,则记该距离为无穷(~)。
15.有向树
一个顶点的入度为0、其余顶点的入度均为1的有向图,称为有向树。
6.2 图的存储及基本操作
6.2.1 邻接矩阵法
所谓邻接矩阵存储,是指用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(即各顶点之间的邻接关系),存储顶点之间邻接关系的二维数组称为邻接矩阵。
对带权图而言,若顶点 v;和 y,之间有边相连,则邻接矩阵中对应项存放着该边对应的权值,若顶点V;和V;不相连,则通常用0或来代表这两个顶点之间不存在边:
有向图、无向图和网对应的邻接矩阵示例如图6.5所示。
图的邻接矩阵存储结构定义如下:
cpp
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //顶点对应的数据类型
typedef int EdgeType; //边对应的数据类型
typedef struct{
VertexType vex[MaxVertexNum];//顶点表
EdgeType edge[MaxVertexNum] [MaxVertexNum];//邻接矩阵,边表
int vexnum,arcnum; //图的当前顶点数和边数
}MGraph;
6.2.2 邻接表法
当一个图为稀疏图时,使用邻接矩阵法显然会浪费大量的存储空间,而图的邻接表法结合了顺序存储和链式存储方法,大大减少了这种不必要的浪费。
所谓邻接表,是指对图G中的每个顶点 v;建立一个单链表,第i个单链表中的结点表示依附于顶点 v;的边(对于有向图则是以顶点 v;为尾的弧),这个单链表就称为顶点 v的边表(对于有向图则称为出边表)。边表的头指针和顶点的数据信息采用顺序存储,称为顶点表,所以在邻接表中存在两种结点:顶点表结点和边表结点,如图 6.6所示。
顶点表结点由两个域组成:顶点域(data)存储顶点 v,的相关信息,边表头指针域(firstarc)指向第一条边的边表结点。边表结点至少由两个域组成:邻接点域(adjvex)存储与头结点顶点v,邻接的顶点编号,指针域(nextarc)指向下一条边的边表结点。
无向图和有向图的邻接表的实例分别如图6.7和图6.8所示。
图的邻接表存储结构定义如下:
cpp
#define MaxVertexNum 100
typedef struct ArcNode{
int adjvex;
struct ArcNode *nextarc;
InfoType info; //网的边权值
}ArcNode;
typedef struct VNode{ //顶点表结点
VertexType data;
ArcNode *firstarc; //指向第一条依附该顶点的弧的指针
}VNode,Adjlist[MaxVertexNum];
typedef struct{
Adjlist vertices;
int vexnum, arcnum;
}ALGraph;
6.3 图的遍历4
6.3.1广度优先搜索
广度优先搜索(Breadth-First-Search,BFS)类似于二叉树的层序遍历算法。基本思想是:首先访问起始顶点v,接着由v出发,依次访问v的各个未访问过的邻接顶点w1,w2,...,wi,然后依次访问w1,w1,...,wi,的所有未被访问过的邻接顶点;再从这些访问过的顶点出发,访问它们所有未被访问过的邻接顶点,直至图中所有顶点都被访问过为止。若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为始点,重复上述过程,直至图中所有顶点都被访问到为止。Dijkstra单源最短路径算法和Prim最小生成树算法也应用了类似的思想。
换句话说,广度优先搜索遍历图的过程是以v为起始点,由近至远依次访问和v有路径相通且路径长度为 1,2,...的顶点。广度优先搜索是一种分层的查找过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有往回退的情况,因此它不是一个递归的算法。为了实现逐层的访问,算法必须借助一个辅助队列,以记忆正在访问的顶点的下一层顶点。
下面通过实例演示广度优先搜索的过程,给定图G如图6.11所示。
假设从顶点a开始访问,a先入队。此时队列非空,取出队头元素a,因为b,c与a邻接且未被访问过,于是依次访问b,c,并将 b,c依次入队。队列非空,取出队头元素 b,依次访问与b邻接且未被访问的顶点d,e,并将d,e入队(注意:a与b也邻接,但a已置访问标记,所以不再重复访问)。此时队列非空,取出队头元素 c,访问与 c邻接且未被访问的顶点f,g,并将f,g入队。此时,取出队头元素 d,但与d邻接且未被访问的顶点为空,所以不进行任何操作。继续取出队头元素 e,将 h入队列......最终取出队头元素h后,队列为空,从而循环自动跳出。遍历结果为 abcdefgh
从上例不难看出,图的广度优先搜索的过程与二叉树的层序遍历是完全一致的,这也说明了图的广度优先搜索遍历算法是二叉树的层次遍历算法的扩展。
1.BFS算法的性能分析
无论是邻接表还是邻接矩阵的存储方式,BFS算法都需要借助一个辅助队列Q,n个项点均需入队一次,在最坏的情况下,空间复杂度为O(V)。
遍历图的过程实质上是对每个顶点查找其邻接点的过程,耗费的时间取决于所采用的存储结构。采用邻接表存储时,每个顶点均需搜索(或入队)一次,时间复杂度为 OQVD,在搜索每个顶点的邻接点时,每条边至少访问一次,时间复杂度为O(E),总的时间复杂度为 O(V+E)。采用邻接矩阵存储时,查找每个顶点的邻接点所需的时间为O(V),总时间复杂度为O(V^2)。
2.广度优先生成树
在广度遍历的过程中,我们可以得到一棵遍历树,称为广度优先生成树,如图 6.12 所示。需要注意的是,同一个图的邻接矩阵存储表示是唯一的,所以其广度优先生成树也是唯一的,但因为邻接表存储表示不唯一,所以其广度优先生成树也是不唯一的。
6.3.2深度优先搜索
与广度优先搜索不同,深度优先搜索(Depth-First-Search,DFS)类似于树的先序遍历。如其名称中所暗含的意思一样,这种搜索算法所遵循的策略是尽可能"深"地搜索一个图。
它的基本思想如下:首先访问图中某一起始顶点 v,然后由v出发,访问与v邻接且未被访问的任意一个顶点w1,再访问与 w1,邻接且未被访问的任意一个顶点 w2......重复上述过程。当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该点开始继续上述搜索过程,直至图中所有顶点均被访问过为止。
以图 6.11的无向图为例,深度优先搜索的过程:首先访问a,并置a访问标记;然后访问与a邻接且未被访问的顶点 b,置b访问标记;然后访问与b邻接且未被访问的顶点 d,置d访问标记。此时d已没有未被访问过的邻接点,所以返回上一个访问的顶点b,访问与其邻接且未被访问的顶点e,置e访问标记,以此类推,直至图中所有顶点都被访问一次。遍历结果为 abdehcfg。
1.DFS算法的性能分析
DFS算法是一个递归算法,需要借助一个递归工作栈,所以其空间复杂度为O(V)。遍历图的过程实质上是通过边查找邻接点的过程,因此两种遍历方式的时间复杂度都相同,不同之处仅在于对顶点访问顺序的不同。采用邻接矩阵存储时,总时间复杂度为O(V^2)。采用邻接表存储时,总的时间复杂度为O(V+E)。
2.深度优先的生成树和生成森林
与广度优先搜索一样,深度优先搜索也会产生一棵深度优先生成树。当然,这是有条件的,即对连通图调用 DFS才能产生深度优先生成树,否则产生的将是深度优先生成森林,如图 6.13 所示。与 BFS类似,基于邻接表存储的深度优先生成树是不唯一的。
6.4 图的应用
6.4.1 最小生成树
一个连通图的生成树包含图的所有顶点,并且只含尽可能少的边。对于生成树来说,若砍去它的一条边,则会使生成树变成非连通图;若给它增加一条边,则会形成图中的一条回路。
对于一个带权连通无向图 G,生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同。权值之和最小的那棵生成树称为G的最小生成树(Minimum-Spanning-Tree,MST)。
不难看出,最小生成树具有如下性质:
- 若图G中存在权值相同的边,则 G的最小生成树可能不唯一,即最小生成树的树形不唯一。当图G中的各边权值互不相等时,G的最小生成树是唯一的;若无向连通图G的边数比顶点数少1,即G本身是一棵树时,则G的最小生成树就是它本身。
- 虽然最小生成树不唯一,但其对应的边的权值之和总是唯一的,而且是最小的。
- 最小生成树的边数为顶点数减1。
1.Prim算法
Prim 算法构造最小生成树的过程如图 6.15 所示。初始时从图中任取一顶点(如顶点 1)加入树 T,此时树中只含有一个顶点,之后选择一个与当前T中顶点集合距离最近的顶点,并将该顶点和相应的边加入T,每次操作后T中的顶点数和边数都增 1。以此类推,直至图中所有的顶点都并入T,得到的T就是最小生成树。此时T中必然有n-1条边。
Prim 算法的时间复杂度为 O(V^2),不依赖于E,因此它适用于求解边稠密的图的最小生成树。虽然采用其他方法能改进Prim算法的时间复杂度,但增加了实现的复杂性。
2.Kruskal算法
与 Prim算法从顶点开始扩展最小生成树不同,Kruskal算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法。
Kruskal 算法构造最小生成树的过程如图6.16 所示。初始时为只有n个顶点而无边的非连通图T={V,{}},每个顶点自成一个连通分量。然后按照边的权值由小到大的顺序,不断选取当前未被选取过且权值最小的边,若该边依附的顶点落在T中不同的连通分量上(使用并查集判断这两个顶点是否属于同一棵集合树),则将此边加入 T,否则舍弃此边而选择下一条权值最小的边。以此类推,直至T中所有顶点都在一个连通分量上。
在 Kruskal算法中,最坏情况需要对因条边各扫描一次。通常采用堆(见第7章)来存放边的集合,每次选择最小权值的边需要 O(logE)的时间;算法的总时间复杂度为O(ElogE),不依赖于V,因此 Kruskal算法适合于边稀疏而顶点较多的图。
6.4.2 最短路径
1. Dijkstra算法求单源最短路径问题
例如,对图6.17中的图应用 Dijkstra算法求从顶点1出发至其余顶点的最短路径的过程,如表 6.2所示。算法执行过程的说明如下。
6.4.3 有向无环图描述表达式
有向无环图:若一个有向图中不存在环,则称为有向无环图,简称 DAG图。
有向无环图是描述含有公共子式的表达式的有效工具。例如表达式
可以用上一章描述的二叉树来表示,如图 6.20 所示。仔细观察该表达式,可发现有一些相同的子表达式(c+d)和(c+d)*e,而在二叉树中,它们也重复出现。若利用有向无环图,则可实现对相同子式的共享,从而节省存储空间,图6.21所示为该表达式的有向无环图表示。
6.4.4 拓扑排序
AOV网:若用有向无环图表示一个工程,其顶点表示活动,用有向边<Vi,Vj>表示活动V;必须先于活动 V;进行的这样一种关系,则将这种有向图称为顶点表示活动的网络,简称 AOV网。在 AOV网中,活动 Vi是活动 Vj的直接前驱,Vj是 Vi的直接后继,这种前驱和后继关系具有传递性,且任何活动 Vi不能以它自己作为自己的前驱或后继。
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
- 每个顶点出现且只出现一次。
- 若顶点A在序列中排在顶点B的前面,则在图中不存在从B到A的路径。
或定义为:拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点 A到顶点B的路径,则在排序中B出现在A的后面。每个AOV网都有一个或多个拓扑排序序列。
图 6.22 所示为拓扑排序过程的示例。每轮选择一个入度为0的顶点并输出,然后删除该顶点和所有以它为起点的有向边,最后得到拓扑排序的结果为{1,2,4,3,5}。
6.4.5 关键路径
在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网。AOE网和 AOV网都是有向无环图,不同之处在于它们的边和顶点所代表的含义是不同的,AOE 网中的边有权值;而AOV网中的边无权值,仅表示顶点之间的前后关系。
AOE网具有以下两个性质:
- 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
- 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。
在 AOE 网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始;也仅有一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。