1.4图
与树一样,图也是非线性结构。图论有很多广泛的应用背景,有很多经典的算法。
1.4.1 图的基本概念
图可表示给G=(V,E),其中V是顶点(vertice)集合,顶点通常由名字或标号来表示;E是边(edge)的集合,E中的每条边都是V中某一堆顶点间的连接,表示为e=(vi,vj)。顶点数记为|E|.边数较少的图称为稀疏图,边数较多的图称为密集图。含由最多条边的图称为完全图。
若图的边限定为从一个顶点指向另一个顶点,则便成为有向边,否则称为无向边。无向边可看成是一堆方向相反的有向边。 若图中所含的边均是有向边,则图称为有向图。若图中所含的边均是无向边,则称为无向图。如果途中所含有向边,又含无向边,则将每条无向边转换为一对方向相反的有向边,图变为有向图。所有不同顶点间都是边相连的图称为完全图。
如果无向边e=(vi,vj)∈E,则称顶点vi和vj是邻接的,他们互称为邻接点,边e与顶点vi和vj相关联。
1.4.2 图的存储及基本操作
1.邻接矩阵法
对图G=(V,E),|V|=n,图的邻接矩阵是一个二维数组A[n][n] ,定义如下:A[i][j]={0,(vi,vj) 相反的一面1,(vi,vj)∈E}
带权图的邻接矩阵定义如下:
A[i][j]=A[i][j]={0,(vi,vj) 相反的一面1,(vi,vj)∈E,W为边(vi,vj)的权值}
2.邻接表法
邻接表法中即用到数组也用到链表。
对每个Vi∈V,使用一个单链表保存Vi的所有邻接点,每个邻接点保存在表的一个结点中。表结点结构定义为
顶点序号 | 指针 |
---|
带权图邻接表表结点的结构定义为
定点序号 | 权值 | 指针 |
---|
对于无向图,若(vi,vj)∈E,则包括Vj的结点出现在vi对应的邻接点表中,同样地,包括vi的结点出现在vi对应的邻接点表中。每条边在邻接表中保存两次。所有邻接点表中的结点数之和为图顶点个数的两倍。顶点vi对应的邻接点表中的结点个数即是vi的度。
对于有向图,顶点vi对应的邻接点表中的结点个数是Vi的出度,求其入度值时需要遍历所有邻接点表。
3.邻接多重表,十字链表。
无向图中的各条边在其邻接图中都被保存两次,而在邻接多重表中只保存一次,这对要求处理每一条的应用十分有利。
在邻接多重表中,每各结点保存一条边的信息,结点结构定义如下:
mark | vertex1 | link1 | vertex2 | link2 |
---|
其中,vertex1和vertex2分别是边所依附的两个顶点,link1指向下一条依附于顶点vertex1的边,link2指向下一条依附于顶点vertex2的边,对该边的处理结果可由mark来标记。
像邻接表一样,邻接多重表使用一堆数组保存图中所有顶点的信息。每个数组单元包括两个域:vertex和first,其中vertex保存每个顶点的信息,first指向第一条依附于vertex的边。
类似地,有向图可采用十字链表表示。十字链表中每个结点保存一条有向边的信息,结点结构定义为
mark | vertex1 | link1 | vertex2 | link2 |
---|
其中,vertex1和vertex2分别是边的始点和终点,link1指向下一条以vertex1为始点的边,link2指向下一条以vertex2以终点的边。
同样地,使用一维数组保存顶点信息,数组单元的结构为
vertex | firin | firout |
---|
其中firout指向以vertex为始点的第一条边,firin指向以vertex为终点的第一条边。
十字链表结合了邻接表和逆灵界表的特点,即方便求顶点的出度,也方便求顶点的入度。
1.4.3 图的遍历
图由顶点及边组成,它的遍历也有两大类:一是遍历图中的所有顶点,二是遍历图中的所有边。这里主要讨论第一类遍历情况,即不重复地列出图中所有的顶点。有两种主要遍历策略: 深度优先搜索(DFS)和广度优先搜索(BFS)。
1.深度优先搜索
深度优先搜索过程中,每输出一个顶点后,递归地访问并输出它的所有未被输出的邻接点。
从图G=(V,E)的任一顶点v1开始,输出顶点vi。将vi作为当前顶点,寻找当前顶点的邻接点vj,并进行如下判断:
若目前尚未输出vj,则输出vj.vj成为新的当前顶点,继续这个过程。
若vj已输出,则检查vi的另一个邻接点。
若vi的所有邻接点均已输出,则退回到vi之前的顶点。
深度优先搜索过程是一个递归的过程,程序实现时使用栈记录遍历路径,当遍历一个顶点时,顶点入栈;当某顶点的所有邻接点均已访问过,则退栈,栈顶顶点又作为当前顶点,继续遍历过程。当栈空时,检查是否已遍历了图中的所有顶点。若是,图为连通图,否则,图是不联通的,此时得到图的一个连通分量。选择另一个尚未遍历的连通分量中的任一顶点作为当前顶点,重新开始遍历过程。深度优化搜索算法可以得到图的各个连通分量。
直观地来看,深度优先搜索过程从某顶点v开始,沿着一条路径一直遍历下去,直到不能走到一个尚未访问的顶点时,再沿原路径返回到可找到为访问顶点的"岔路口"。
当图中顶点数大于1时,图的深度优先搜索序列有多个。 选择不同的顶点出发,或是选择某顶点的邻接点时,都会存在多种不可能性。 树的遍历,迷宫问题的求解等均采用深度优先搜索策略。
2.广度优先搜索
广度优先搜索过程是一个迭代的过程 ,类似于二叉树层序遍历过程。
从图G=(V,E)的任一顶点vi开始,将顶点vi入队列。当队列不空时,循环执行:出队列,并输出该顶点,标记该顶点已输出,将该顶点所有尚未输出的邻接点入队列。
直观地来看,广度优先搜索过程从某顶点V开始,按照于V由近及远的次序来遍历。先是遍历于v一步之遥的各顶点,然后是遍历与v一步之遥的各顶点,然后是遍历与v两部之遥的各顶点,以此类推。
与深度优先搜索过程类似,当图中顶点个数大于1时候,广度优先搜索序列也有多个。遍历过程中,也需要一个一维数组为顶点做标记。广度优先搜索算法也可以得到图的连通分量。
与深度优先搜索过程中使用栈不同,广度优先搜索过程中使用队列来暂存那些等待输出的顶点。
1.最小(代价)生成树
1)最小生成树的定义与特点
图的深度优先搜索过程中经过的所有边成为深度优先生成树,广度优先过程中经过的所有边称为广度优先生成树。
实际上,连通图的生成树是包含图中所有顶点及最少边数的连通子图。若图G=(V,E)中顶点数|V|=n,则G的生成树的边数为n-1。这是使得G连通的最少边数。
若连通图G=(V,E)的边数|E|>|V|-1,表明图中存在回路,生成树不唯一。|E|=|V|-1时,图本身既是生成树。若图不连通,每个连通分量的生成树一个生成森林。
对带权连通图G=(V,E),可计算生成树所含边的权值之和,称为生成树的代价。
若带权连通图G=(V,E)存在多棵生成树,则不同生成树的代价可能不同,其中代价最小的生成树称为最小(代价)生成树,简称为最小生成树。最小生成树中所含边的权值之和,小于等于图中任何一颗生成树所含边的权值之和。最小的生成树可能不唯一。
2)普里姆算法
普里姆(Prim)算法是求最小生成树的经典算法之一,算法相当简洁。普里姆算法的具体过程如下:
初始化
设T是图G的最小生成树,U是最小生成树的顶点集合,设由顶点V1开始,T=⭕/,U={v1};
在所有u∈U,v∈V-U的边(u,v)∈E中选一条权值最小的边(ui,uj),将vj加入U中,U=Uu{uj},将边(ui,uj)加入T中,T=TU{(ui,uj)};
重复 (2),直到U=V时结束。
Prim算法的时间复杂度为O(n2),适合求边稀疏图的最小生成树。
3)克鲁斯卡尔算法
克鲁斯卡尔算法(Kruskal)算法是求最小神成熟的另一个经典算法。
初始时,最小生成树为只含n个顶点的非连通图T=(V,{}),图中每个顶点自成液体个连通分量。在E中选择权值最小的边,若该边依附的两个顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去该边而选择下一条权值最小的边。以此类推,直到T中所有顶点都在同一连接分量上为止。
克鲁斯卡尔算法至多对e条边各扫描一次,以堆来保存图中所有的边,则每次选择权值最小的边时仅需O(log2e)的时间。生成树T的每个连通分量可堪称是一个等价类,将新边加入T的过程类似于两个等价类合并为一条等价类的过程,其时间复杂度为O(log2e),故克鲁斯卡尔算法的时间复杂度为O(elog2e)。
对于图G,广度优先搜索基于“由近及远”的遍历策略,可以得到从某一点到另一点的“最近路径”。
对于带权图G=(V,E),路径长度定义为路径所含边的权值之和,而不再是路径所含的边数。两个顶点Vi,vj之间路径长度最小的路径称为两顶点间的最短路径。
最短路径问题有两个:一是单源最短路径,二是所有顶点间的最短路径。
1)单源最短路径
求带权图的单源最短路径的算法称为迪杰斯特拉(Dijkstra)算法,它按照路径长度不减的次序产生最短路径。
Dijkstra算法的基本思想是:把图中的所有顶点分成两个集合,令S表示已求出最短路径的顶点集合,其余尚未确定最短路径的顶点组成另一个记号个V-S。初始时,S中仅含有源点。 按最短路径查高度不减的次序逐个把第二个集合V-s中的顶点加入到S中,不断扩大已求出最短路径的顶点集合,直到从源点出发可以到达的所有顶点都在S中为止。
为图中每个顶点定义一个距离值dist,分两种情况:S集合中顶点对应的距离值dist就是从源点到此顶点的最短路径长度;第二个集合V-S中顶点对应的距离值dist就是从源点到此顶点的最短路径长度;第二个集合V-s中顶点对应的距离值是从源点到此顶点,且路径中仅包含S中的顶点为中间顶点的最短路径的长度。
如果dist值确实变小了,则以变化后的值替代原来的值,如果dist值没有变小,则保持原值不变。
dist[i]={弧上的权值,若从源点V0到Vi有弧
{∞,否则
Dijkstra算法的步骤如下:
初始化
设用带权的邻接矩阵cost表示带权有向图G=(V,E);
cost[i][j] 为弧(ui,uj)上的权值;
S为已找到从源点V0出发的最短路径的终点集合,初始时只含有源点Vo;
从Vo出发到图中其余各顶点Vi的距离值为:
dist[i] =cost[vo][vi] A vi∈V
最后得到的A(n)[i][j]就是从顶点Vi到J的最短路径的长度。
拓扑排序
不存在回路的有向图称为有向无环图,简称为DAG图。
在有向图中,以顶点表示活动,有向边表示活动之间的优先关系,这样的有向图称为顶点表示活动的网络,简称为AOV网络。
在AOV网中不允许出现回路,因为如果有回路,就表示某个活动是以自己为先决条件的,即存在一个活动Vi,vi即是其本身的前驱,又是其后继,这显然是矛盾的。因此AOV网是有向无环图。
拓扑排序可以安排AOV网中的各个活动,它把AOV网张各个顶点按照它们之间的先后关系拍成一个线性序列,这个序列成为拓扑有序序列。在AOV网中,如果从顶点Vi到顶点Vj存在有向路径,则在拓扑有序序列中那个,Vi必定排在Vj的前面;如果从顶点vi到顶点uj,没有有向路径,则在拓扑有序序列中,Vi于Vj的先后次序可以任意。
使用广度优先搜索算法求AOV网中所有顶点的入度值;
(1)初始化-----记录AOV网中所有顶点的入度值
(2)选一个入度为0(没有前驱)的顶点,输出它
(3)从图中删除该顶点和以它为尾的所有的弧。
重复步骤(2),(3),直至输出全部顶点;或者还没有输出全部顶点,但以找不到入度为0的顶点为止。第一种情况表示拓扑排序已完成,第二种情况表明原有向图中含有回路。
AOV网的拓扑有序序列可能不唯一。
在带权的有向图中,用顶点表示事件,用弧度表示活动,权表示活动持续的时间,这样组成的网称为以边表示活动的网,简称AOE网。在AOE网中,通常只有一个入度为O的点和一个出度为O的点,这是因为一个工程只有一个开始完成点。入度为O的点称为起始点或源点,出度为O的点称为结束点或汇点。
一个工程中的某些子活动是可以并行进行的;从源点到汇点最长路径的长度,即该路径上所有活动持续时间之和,就是完成整个工程所需的最少时间。把从源点到汇点具有最大长度的路径称为关键路径。关键路径上的所有活动称为关键活动。
在AOE网中,从源点Vi到任意顶点Vi的最长路径长度叫做时间Vi的最早发生时间。