790 likes | 953 Views
第六章 图 第一节 图的基本概念和基本操作 第二节 图的存储表示 第三节 图的遍历 第四节 最小生成树 第五节 最短路径 第六节 拓扑排序 本 章 小 结 实 训 思 考 与 习 题. 学习要求: 掌握图的基本概念和术语、图的存储结构、图的深度优先和广度优先搜索算法以及图的应用。 主要内容: 图是一种复杂的非线性数据结构。本章介绍了图的基本概念和术语、图的存储结构、图的深度优先和广度优先搜索算法、最小生成树的概念及构造算法和最短路径的概念。. 第六章 图.
E N D
第六章 图 第一节 图的基本概念和基本操作 第二节 图的存储表示 第三节 图的遍历 第四节 最小生成树 第五节 最短路径 第六节 拓扑排序 本 章 小 结 实 训 思 考 与 习 题
学习要求: 掌握图的基本概念和术语、图的存储结构、图的深度优先和广度优先搜索算法以及图的应用。 主要内容: 图是一种复杂的非线性数据结构。本章介绍了图的基本概念和术语、图的存储结构、图的深度优先和广度优先搜索算法、最小生成树的概念及构造算法和最短路径的概念。 第六章 图
图是一种比线性表和树更加复杂的数据结构。在线性表中,数据元素之间呈现一种线性关系,即每个元素只有一个直接前驱和一个直接后继;在树结构中,结点之间是一种层次关系,即每个结点只有一个直接前驱,但可有多个直接后继;而在图结构中,每个结点既可以有多个直接前驱,也可以有多个直接后继。前面讨论的线性表和树都可以看成是两种特殊的图。图是一种比线性表和树更加复杂的数据结构。在线性表中,数据元素之间呈现一种线性关系,即每个元素只有一个直接前驱和一个直接后继;在树结构中,结点之间是一种层次关系,即每个结点只有一个直接前驱,但可有多个直接后继;而在图结构中,每个结点既可以有多个直接前驱,也可以有多个直接后继。前面讨论的线性表和树都可以看成是两种特殊的图。 图结构可以描述各种复杂的数据对象,因此图的应用极为广泛,已渗透到诸如语言学、逻辑学、物理、化学、电讯工程、计算机科学以及数学的其他分支中。 本章首先介绍图的概念,然而讨论图在计算机中的表示方法以及有关图的几个典型应用。
第一节 图的基本概念和基本操作 一、图的定义和术语 (一)图的定义 图G(graph)是由两个集合V和E组成,记为G=(V,E)。其中,G表示一个图,V是顶点的有穷非空集合,E是V中顶点偶对(称为边)的有穷集合。通常,也将图G的顶点集和边集分别记为V(G)和E(G),E(G)可以是空集。若E(G)为空,则图G只有顶点而没有边。图6.1给出了一个图的示例,在该图中: 集合V={ v1,v2,v3,v4,v5}; 集合E={ (v1,v2),( v1,v4), (v2,v3),(v2,v5),(v3,v4),(v3,v5)}; 图6.1 无向图G1
(二)图的相关术语 1. 无向图 在图G中,如果每条边都是没有方向的,则称G为无向图,无向图中的边均是顶点的无序偶对,无序偶对通常用圆括号表示,即用(vi,vj)表示顶点vi和vj间相连的边。在无向图中,(vi,vj)和(vj,vi)表示同一条边。如图6.1所示,图G1是一个无向图。 2. 有向图 在图G中,如果每条边都是有方向的,则称G为有向图,有向图中的边均是顶点的有序偶对,有序偶对通常用尖括号表示,即用<vi,vj>表示从顶点vi到顶点vj的一条有向边。因此<vi,vj>和<vj,vi>表示不同的两条有向边。如图6.2所示,图G2是一个有向图。
图6.2 有向图G2 3. 顶点、边、弧、弧头和弧尾 数据元素vi称为顶点,P(vi,vj)表示在顶点vi和顶点vj之间有一条直接连线。如果是在无向图中,则称这条连线为边;如果是在有向图中,一般称这条连线为弧。边用顶点的无序偶对(vi,vj)来表示,称顶点vi和顶点vj互为邻接点,边(vi,vj)依附或关联于顶点vi与顶点vj。弧用顶点的有序偶对<vi,vj>来表示,有序偶对的第一个顶点vi被称为始点(或弧尾),在图中就是不带箭头的一端;有序偶对的第二个顶点vj被称为终点(或弧头),在图中就是带箭头的一端。
4. 无向完全图 在一个无向图中,如果任意两顶点之间都有一条边相连接,则称该图为无向完全图。可以证明,在一个含有n个顶点的无向完全图中,有n(n-1)/2条边。 5. 有向完全图 在一个有向图中,如果任意两顶点之间都有方向互为相反的两条弧相连接,则称该图为有向完全图。在一个含有n个顶点的有向完全图中,有n(n-1)条弧。 6. 稠密图、稀疏图 含有很多条边或弧的接近完全图的图,称为稠密图;反之,含有很少边或弧的图称为稀疏图。 7. 顶点的度、入度和出度 顶点的度(degree)是指依附于某顶点v的边或弧的条数,通常记为D(v)。在有向图中,要区分顶点的入度和出度的概念。顶点v的入度是指以顶点v为终点的弧的数目,记为ID(v);顶点v的出度是指以顶点v为始点的弧的数目,记为OD(v),有D(v)= ID(v)+ OD(v)。
在图6.1所示的无向图中,顶点v1的度为2,顶点v2的度为3;在图6.2所示的有向图中,顶点v1的度为3,其中,入度为1,出度为2。在图6.1所示的无向图中,顶点v1的度为2,顶点v2的度为3;在图6.2所示的有向图中,顶点v1的度为3,其中,入度为1,出度为2。 无论是有向图还是无向图,顶点数n、边数e和度数之间有如下关系: 8. 边的权、网络 图中每条边都可以附有一个对应的数值,这种与边有关的数值称为权(weight)。权可以表示从一个顶点到另一个顶点的距离或花费的代价。边上带权的图称为带权图,也称为网络(network),图6.3所示的是网络G3。 图6.3 网络G3
9. 路径、路径长度 在无向图G中,顶点vp到顶点vq之间的路径(path)是指顶点序列vp,vi1,vi2,…,vim,vq。其中,(vp,vi1), (vi1,vi2), …,(vim,vq)分别为图中的边。若G是有向图,则路径也是有向的,它由有向边<vp,vi1>,<vi1,vi2>,…,<vim,vq>组成。路径上边或弧的数目称为路径长度。 图6.1所示的无向图中,v1→v4,v1→v2→v3→v4和v1→v2→v5→v3→v4是从顶点v1到v4的三条路径,路径的长度分别为1,3和4。 10. 回路、简单路径和简单回路 第一个顶点和最后一个顶点相同的路径称为回路或环(cycle)。序列中顶点不重复出现的路径称为简单路径。除第一个顶点与最后一个顶点之外,其他顶点不重复出现的回路称为简单回路。 11. 子图 对于图G=(V,E),G′=(V′,E′),若存在V′是V的子集,E′是E的子集,则称图G′是图G的一个子图。 图6.4(a)和图6.4(b)所示是图6.1的两个子图。
12. 连通的、连通图和连通分量 在无向图中,如果从一个顶点vi到另一个顶点vj(i≠j)有路径,则称顶点vi和vj是连通的。如果图中任意两顶点都是连通的,则称该图为连通图。无向图的极大连通子图称为连通分量。 图6.4 无向图G1的两个子图
图6.1是一个连通图,图6.5(a)是非连通图。图6.5(a)有两个连通分量,如图6.5(b)所示。图6.1是一个连通图,图6.5(a)是非连通图。图6.5(a)有两个连通分量,如图6.5(b)所示。 图6.5 无向图及连通分量 13. 强连通图和强连通分量 对于有向图来说,若图中任意一对顶点vi和vj(i≠j)均有从一个顶点vi到另一个顶点vj的路径,也有从vj到vi的路径,则称该有向图是强连通图。有向图的极大强连通子图称为强连通分量。
图6.2中有两个强连通分量,如图6.6所示。 14. 生成树 所谓连通图G的生成树,是G的包含其全部n个顶点的一个极小连通子图。它必定包含且仅包含G的n-1条边。在生成树中添加任意一条原图中的边必定会产生回路,因为新添加的边使其所依附的两个顶点之间有了第二条路径。若生成树中减少任意一条边,则必然成为非连通的。 图6.6 图6.2的强连通分量
图6.7所示为图6.1的一棵生成树。 15. 生成森林 在非连通图中,由每个连通分量都可得到一个极小连通子图,即一棵生成树。这些连通分量的生成树就组成了一个非连通图的生成森林。 图6.5(b)所示为非连通图的连通分量构成的生成森林。 图6.7 一棵生成树
二、图的基本操作 图的基本操作有: 1CreatGraph(G):输入图G的顶点和边,建立图G的存储。 2DestroyGraph(G):释放图G占用的存储空间。 3GetVex(G,v):在图G中找到顶点v,并返回顶点v的相关信息。 4PutVex(G,v,value):在图G中找到顶点v,并将value值赋给顶点v。 5InsertVex(G,v):在图G中增添新顶点v。 6DeleteVex(G,v):在图G中,删除顶点v以及所有和顶点v相关联的边或弧。 7InsertArc(G,v,w):在图G中增添一条从顶点v到顶点w的边或弧。 8DeleteArc(G,v,w):在图G中删除一条从顶点v到顶点w的边或弧。 9DFSTraverse(G,v):在图G中,从顶点v出发深度优先遍历图G。 10BFSTraverse(G,v):在图G中,从顶点v出发广度优先遍历图G。
第二节 图的存储表示 由于图的结构比较复杂,任意两个顶点之间都可能存在联系,因此很难用顺序存储结构来存放图。如果用多重链表表示图,即由一个数据域和多个指针域组成的结点来表示图中的一个顶点,顶点之间的边或弧用指针关联起来,这是一种最简单的链式存储结构,但由于图中各顶点的度各不相同,这样结点的指针域不定长,会给算法设计带来很大的困难。在实际应用中,是根据具体的图和所定义的操作来设计适当的存储结构或表示方法的。下面介绍两种常用的存储方法:邻接矩阵表示法和邻接表表示法。 为了适合用C语言描述,从本节起假定顶点序号从0开始,即图G的顶点集的一般形式是V(G)={ v0 ,v1,v2,…,vn-1 }。 一、邻接矩阵 邻接矩阵是表示顶点之间相邻关系的矩阵。设G=(V,E)是具有n个顶点的图,则G的邻接矩阵是具有如下性质的n阶方阵:
若图G是网络,则邻接矩阵可定义为: 其中,wij表示边(vi,vj)或弧<vi,vj>上的权值;∞表示一个计算机允许的、大于所有边上权值的正整数。 图6.8(a)无向图的邻接矩阵表示如图6.8(b)所示。 图6.9(a)有向图的邻接矩阵表示如图6.9(b)所示。
图6.9 有向图及其邻接矩阵 图6.8 无向图及其邻接矩阵
图6.3所示网络的邻接矩阵表示如图6.10所示。 图6.10 邻接矩阵表示法 从图的邻接矩阵存储方法容易看出这种表示具有以下特点: 1无向图的邻接矩阵一定是一个对称矩阵。因此,在具体存放邻接矩阵时只需存放上(或下)三角矩阵的元素即可。 2对于无向图,邻接矩阵的第i行(或第i列)非零元素(或非∞元素)的个数正好是第i个顶点的度TD(vi)。 3对于有向图,邻接矩阵的第i行非零元素(或非∞元素)的个数正好是第i个顶点的出度OD(vi)。 4对于有向图,邻接矩阵的第i列非零元素(或非∞元素)的个数正好是第i个顶点的入度ID(vi)。
5用邻接矩阵方法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,这时所花费的时间代价很大。这是用邻接矩阵存储图的局限性。5用邻接矩阵方法存储图,很容易确定图中任意两个顶点之间是否有边相连。但是,要确定图中有多少条边,则必须按行、按列对每个元素进行检测,这时所花费的时间代价很大。这是用邻接矩阵存储图的局限性。 采用邻接矩阵表示法存储图时,除了使用一个一维数组来存储顶点信息,用一个二维数组存储顶点之间的边或弧,还需要明确指出图的顶点数和边(或弧)数以及图的类型。对应的结构说明如下: #define VEXTYPE int ∥顶点类型 #define ADJTYPE int ∥边的数值类型 #define MAXLEN 40 ∥最大顶点数 typedef struct { VEXTYPE vexs[MAXLEN]; ∥图中顶点的信息 ADJTYPE arcs[MAXLEN][MAXLEN] ∥邻接矩阵 int vexnum, arcnum; ∥顶点数和边数 } MGRAPH;
图的类型分四种:有向图,kind=1;无向图,kind=2;有向网,kind=3;无向网,kind=4。图的类型分四种:有向图,kind=1;无向图,kind=2;有向网,kind=3;无向网,kind=4。 【算法6.1】 建立一个有向网的邻接矩阵存储的算法。 #define MAX 10000∥设∞为MAX MGRAPH create_mgraph() { inti, j, k, h; char b, t; MGRAPH mg; mg.kind=3; printf ("请输入顶点数和边数:"); scanf("%d%d", &i, &j); mg.vexnum=i; mg.arcnum=j; for (i=0; i<mg.vexnum; i++) { printf("第%d个顶点信息:",i+1); scanf("%d", &mg.vexs[i]);} for (i=0; i<mg.vexnum; i++)
for (j=0; j<mg.vexnum; j++) mg.arcs[i][j]=MAX; for (k=1; k<=mg.arcnum; k++) { printf("第%d条边的起始顶点编号和终止顶点编号:\n", k); scanf("%d%d", &i, &j); while (i<1‖i>mg.vexnum‖j<1‖j>mg.vexnum) { printf("编号超出范围,重新输入:"); scanf("%d%d", &i, &j); } printf("此边的权值:"); scanf("%d", &h); mg.arcs[i-1][j-1]=h; } return mg; }
二、邻接表 邻接表表示法类似于树的孩子链表表示法,是图的一种链式存储结构。在邻接表中,对于图G中的每个顶点vi,把所有邻接于vi的顶点vj链成一个单链表,这个单链表被称为顶点vi的邻接表。邻接表中每个表结点均有两个域,一是邻接点域adjvex,用以存放与vi相邻接的顶点vj的序号j;二是链域next,用来将邻接表的所有表结点链在一起,如图6.11(a)所示。再为每个顶点vi的邻接表设置一个头结点,头结点包含两个域:一是顶点域vertex,用来存放顶点vi的名或其他有关信息;二是指针域firstedge,它是vi的邻接表的头指针,如图6.11(b)所示。如果用邻接表表示网,则还需要在结点中增加一个存放权值的域(weighth)。如图6.11(c)所示。 对于无向图而言,vi的邻接表中每个表结点都对应于与vi相关联的一条边;对于有向图来说,vi的邻接表中每个表结点都对应于以vi为始点射出的一条弧。因此,我们将无向图的邻接表称为边表,将有向图的邻接表称为出边表,将邻接表的表头向量称为顶点表。
有向图还有一种称为逆邻接表表示法,该方法为图中每个顶点vi建立一个入边表,入边表中的每个表结点均对应一条以vi为终点(即射入vi)的边。有向图还有一种称为逆邻接表表示法,该方法为图中每个顶点vi建立一个入边表,入边表中的每个表结点均对应一条以vi为终点(即射入vi)的边。 图6.9(a)所示的有向图,其逆邻接表表示如图6.14所示。 图6.14 有向图的逆邻接表
下面给出图的邻接表的结构说明: #defineVEXTYPE int #defineMAXLEN 40 typedef struct nodes ∥表结点结构 { int adjvex; ∥存放与表头结点相邻接的顶点在数组中的序号 struct nodes *next; ∥指向与表头结点相邻的下一个顶点的表结点 } EDGENODE typedef struct { VEXTYPE vertex; ∥存放图中顶点的信息
EDGENODE *firstedge; ∥指针指向对应的单链表中的表结点 } VEXNODE; typedef struct { VEXNODE adjlist[MAXLEN]; ∥邻接表表头向量 int vexnum, arcnum; ∥顶点数和边数 int kind; ∥图的类型 } ADJGRAPH 【算法6.2】 建立一个有向图邻接表的算法。 ADJGRAPH creat_adjgraph() { EDGENODE *p int i, s, d; ADJGRAPH adjg; adjg.kind=1; printf("请输入顶点数和边数:"); scanf("%d%d", &s, &d); adjg.vexnum=s; adjg.arcnum=d;
for (i=0; i<adjg.vexnum; i++) { printf("第%d个顶点信息:", i+1); scanf("%d", &adjg.adjlist[i].vertex); adjg.adjlist[i].firstedge=NULL; } for (i=0; i<adjg.arcnum; i++) { printf("第%d条边的起始顶点编号和终止顶点编号:", i+1); scanf("%d%d", &s, &d); while (s<1‖s>adjg.vexnum‖d<1‖d>adjg.vexnum) { printf("编号超出范围,重新输入:"); scanf("%d%d", &s, &d); } s--; d--; p=malloc(sizeof(EDGENODE)); p→adjvex=d; p→next=adjg.adjlist[s].firstedge; adjg.adjlist[s].firstedge=p; } return adjg; }
从算法中可以看到,与一个图对应的邻接表的存储结构可以不是唯一的。它主要反映在单链表中各结点的前后次序可以不同,并取决于建立邻接表的算法中结点是前插入还是后插入,以及各边的输入次序。从算法中可以看到,与一个图对应的邻接表的存储结构可以不是唯一的。它主要反映在单链表中各结点的前后次序可以不同,并取决于建立邻接表的算法中结点是前插入还是后插入,以及各边的输入次序。 从图的邻接表存储方法容易看出,图的邻接表表示法具有以下特点: 1若无向图G有n个顶点,e条边,则邻接表需n个表头结点和2e个表结点。显然,对于边很少的图,用邻接表比用邻接矩阵要节省存储空间。 2在无向图的邻接表中,顶点vi的度为第i个链表中表结点的个数。 3在有向图的邻接表中,一条弧对应一个表结点,表结点的数目和图中弧的数目相同。 4在有向图的邻接表中,求图中某个顶点vi的出度很容易,它就是第i个链表中的表结点的个数;若要求vi的入度,则必须扫描整个邻接表,为了方便求其入度,可以对有向图建立逆邻接表。
第三节 图的遍历 图的遍历是指从图中的任一顶点出发,对图中的所有顶点访问一次且只访问一次的过程。图的遍历是图的一种基本操作,是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。 图的遍历通常有深度优先搜索和广度优先搜索两种方式,它们对无向图和有向图都适用。
一、深度优先搜索 图的深度优先搜索(depth first search)类似于树的先序遍历。假设给定图G的初态是所有顶点均未曾访问过,在G中任选一顶点v为初始出发点(源点),则深度优先搜索可定义如下:首先访问出发点v,并将其标记为已访问过;然后依次从v出发搜索v的每个邻接点w。若w未曾访问过,则以w为新的出发点继续进行深度优先搜索,直至图中所有和源点v有路径相通的顶点(亦称为从源点可达的顶点)均已被访问为止;若此时图中仍有未访问的顶点,则另选一个尚未访问的顶点作为新的源点重复上述过程,直至图中所有顶点均已被访问为止。 以图6.15为例,进行图的深度优先搜索。假设从顶点v1出发进行搜索,在访问了顶点v1之后,选择邻接点v2。因为v2未曾访问,则从v2出发进行搜索。依次类推,接着从v4、v8、v5出发进行搜索。在访问了v5之后,由于v5的邻接点都已被访问,则搜索回到v8。同样,搜索继续回到v4、 v2直到v1,此时由于v1的另一个邻接点v3未被访问,则搜索又从v1到v3,再由此继续进行下去,得到的顶点访问序列为: v1 → v2 → v4 →v8→ v5 →v3→v6→v7。过程示意图如图6.15(b)所示。
按深度优先搜索遍历规则还可以得到v1→v3→v7→v6→v2→v5→v8→v4等多种序列。按深度优先搜索遍历规则还可以得到v1→v3→v7→v6→v2→v5→v8→v4等多种序列。 因为深度优先搜索遍历是递归定义的,故容易写出其递归算法。 【算法6.3】 以邻接矩阵作为图的存储结构下的深度优先搜索遍历算法。 int visited[MAXLEN]; void dfs (ADJGRAPH adjg, int v) ∥从第v个顶点出发递归深度优先搜索遍历图G,G以邻接矩阵表示 { int w; visited[v]=1; visitfunc(v);∥访问第v个顶点 for (w=firstadjvex(adjg, v); w; w=nextadjvex(adjg, v, w)) if (visited[w]) dfs(adjg, w); ∥对v的尚未访问的邻接顶点w递归调用dfs }
【算法6.4】 以邻接表作为图的存储结构下的深度优先搜索遍历算法。 void dfs(ADJGRAPH adjg, int v) { EDGENODE *p; int i; visited[v-1]=1; ∥标记vi已访问 v--; printf("%d", adjg.adjlist[v].vertex); p=adjg.adjlist[v].firstedge; ∥取vi边表的头指针 while (pNULL) ∥依次搜索vi的邻接点vj { if (visited[p→adjvex]==0)∥若vj尚未被访问过,则以vj为出发纵深访问 dfs(adjg, (p→adjvex)+1); p=p→next; } ∥找vi的下一个邻接点 }
二、广度优先搜索 广度优先搜索遍历(breadth first search)类似于树的层次遍历。设图G的初态是所有顶点均未访问过,在G中任选一顶点v为源点,则广度优先遍历可以定义为:首先访问出发点v,接着依次访问v的所有邻接点w1,w2,…,wt,然后再依次访问与w1,w2,…,wt邻接的所有未曾访问过的顶点,依此类推,直至图中所有和源点v有路径相通的顶点都已访问到为止,此时从v开始的搜索过程结束。若G是连通图,则遍历完成;否则,在图G中另选一个尚未访问的顶点作为新源点继续上述的搜索过程,直至G中所有顶点均已被访问为止。 以图6.15为例,进行广度优先搜索遍历,首先访问v1和v1的邻接点v2和v3,然后依次访问v2的邻接点v4和v5及v3的邻接点v6和v7,最后访问v4的邻接点v8。由于这些顶点的邻接点均已被访问,并且图中所有顶点都被访问,完成了图的遍历。得到的顶点访问序列为: v1→ v2 → v3→ v4→ v5→ v6→ v7→ v8。过程示意图如图6.15(c)所示。 按广度优先搜索遍历规则还可以得到v1→ v3→ v2 → v6→ v7→ v4→ v5→ v8等多种序列。
图6.15 连通图深度优先搜索和广度优先搜索过程示意图
用邻接表作为存储结构,广度优先搜索遍历的算法设计如下:用邻接表作为存储结构,广度优先搜索遍历的算法设计如下: 【算法6.4】 以邻接表作为存储结构下的广度优先搜索遍历算法。 void bfs(ADJGRAPH adjg, int vi) { int visited[MAXLEN]; int i, v; EDGENODE *p; LINKQUEUE que, *q; q=&que; initlinkqueue(q); for (i=0; i<adjg.vexnum; i++) visited[i]=0; visited[vi-1]=1; printf("%4d", adjlist[vi-1].vertex); enlinkqueue (q, vi); while (emptylinkqueue (q)) { v=dellinkqueue(q); v--;
p=adjg.adjlist[v].firstedge; while (pNULL) { if (visited[p→adjvex]==0) { visited[p→adjvex]=1; printf("%d", adjg.adjlist [p→adjvex].vertex); enlinkqueue(q,(p→adjvex)+1); } p=p→next; } }
第四节 最小生成树 前面已介绍过,对给定的连通图G,其极小连通子图是G的生成树。生成树中包含了连通图G中的所有的顶点和n-1条边。由于图的遍历序列不唯一,具有n个顶点的连通图G的生成树也就不一定是唯一的。 一、最小生成树的基本概念 如果无向连通图是一个网,那么,在它的所有生成树中,必有一棵树其边上的权值总和最小,我们称这棵生成树为最小生成树(也称最小代价生成树)。 最小生成树的概念可以应用到许多实际问题中。例如,有这样一个问题:构造一个造价最低的通信网络,把若干个城市联系在一起。在这若干个城市中,任意两个城市之间都可以建造通信线路,通信线路的造价依据城市间的距离的不同而不同。这时可以构造一个通信线路造价网络,在网络中,每个顶点表示一个城市,顶点之间的边表示城市之间可构造通信线路,每条边上的权值表示该条通信线路的造价,要想使总的造价最低,实际上就是寻找该网络的最小生成树。
构造最小生成树可以有多种算法,其中大多数构造算法都利用了最小生成树的性质(简称为MST性质):设G=(V,E)是一个连通网络,U是顶点集V的一个真子集。若(u,v)是G中所有的一个端点在U(u∈U)里、另一个端点不在U(即v∈V-U)里的边中,具有最小权值的一条边,则一定存在G的一棵最小生成树包括此边(u,v)。构造最小生成树可以有多种算法,其中大多数构造算法都利用了最小生成树的性质(简称为MST性质):设G=(V,E)是一个连通网络,U是顶点集V的一个真子集。若(u,v)是G中所有的一个端点在U(u∈U)里、另一个端点不在U(即v∈V-U)里的边中,具有最小权值的一条边,则一定存在G的一棵最小生成树包括此边(u,v)。 下面介绍两个利用MST性质构造最小生成的算法。 二、构造最小生成树的Prim算法 假设G=(V,E)为一连通网,其中V为网中所有顶点的集合,E为网中所有带权边的集合。设置两个新的集合U和T,其中集合U用于存放G的最小生成树中的顶点,集合T存放G的最小生成树中的边。令集合U的初值为U={u1}(假设构造最小生成树时,从顶点u1出发),集合T的初值为T={ }。
Prim算法的思想是:从所有u∈U、v∈V-U的边中,选取具有最小权值的边(u,v),将顶点v加入集合U中,将边(u,v)加入集合T中,如此不断重复,直到U=V时,最小生成树构造完毕。这时集合T中包含了最小生成树的所有边。 图6.16(a)所示的一个连通图,按照Prim算法思路,从顶点v1出发,该网的最小生成树的构造过程如图6.16(a)~(g)所示。 初始时,U={v1},V-U={v2,v3, v4, v5, v6, v7},T={ };在v1相关联的所有的边中,(v1, v2)的权值是最小的,因此取(v1, v2)为最小生成树的第一条边,如(b)所示,此时U={v1,v2},V-U={v3, v4,v5,v6,v7},T={( v1 v2)};在和v1、v2相关联的所有边中,(v2, v5)权值最小,取(v2, v5)为最小生成树的第二条边,如(c)所示;现U={v1,v2, v5},V-U={v3,v4, v6, v7},T={( v1,v2),(v2, v5)},在和v1、 v2、 v5相关联的所有边中,(v5, v4)的权值最小,取(v5, v4)为最小生成树的第三条边,如(d)所示,这样,U={v1, v2, v5, v4},V-U={v3, v6, v7},T={( v1, v2),( v2, v5),( v5, v4)};在所有和v1, v2, v5, v4相关联的边中,(v4, v6)为权
值最小的边,取(v4,v6)为最小生成树的第四条边,如(e)所示;这时U={v1,v2,v5,v4,v6},V-U={v3,v7},={(v1, v2),(v2, v5),(v5, v4),(v4, v6)};在所有和v1, v2, v5, v4, v6相关联的边中,(v4, v7)为权值最小的边,取(v4,v7)为权值最小的边,取(v4, v7)为最小生成树的第五条边,如(f)所示;这时U={v1, v2, v5, v4, v6,v7}, VU={v3};T={(v1, v2),(v2, v5),(v5, v4),(v4, v6),(v4, v7)};U中顶点和v3相关联的权值最小边为(v7, v3),取(v7, v3)为最小生成树的第六条边,如(g)所示;至此U=V。(g)即为最终得到的最小生成树。
(b) (a) (c) (d)
(e) (f) (g) 图6.16 Prim算法构造最小生成树的过程
三、构造最小生成树的Kruskal算法 Kruskal算法是一种按照网中边的权值递增的顺序构造最小生成树的方法。其基本思想是: 设无向连通网为G=(V,E),令G的最小生成树为T,其初态为T=(V,{ }),即开始时,最小生成树T由图G中的n个顶点构成,顶点之间没有一条边,此时T中各顶点各自构成一个连通分量。然后,按照边的权值由小到大的顺序,考察G的边集E中的各条边。若被考察的边的两个顶点属于T的两个不同的连通分量,则将此边作为最小生成树的边加入到T中,同时把两个连通分量连接为一个连通分量;若被考察边的两个顶点属于同一个连通分量,则舍去此边,以免造成回路。如此下去,当T中的连通分量个数为1时,此连通分量便为G的一棵最小生成树。 此算法思想可简单描述如下: T=(v, {}); while (T中所有边数e<n-1)
{ 从E中选取当前最短边(u,v); if (u,v)并入T之后不产生回路) 将边(u,v)并入T中; 从E中删去边(u,v); } 对于图6.16(a)所示的网,按照Kruskal方法构造最小生成树的过程如图6.17所示。在构造过程中,按照网中边的权值由小到大的顺序,不断选取当前未被选取的边集中权值最小的边。 依据生成树的概念,n个结点的生成树,有n-1条边,故反复上述过程,直到选取了n-1条边为止,就构成了一棵最小生成树。 (a) (b)
(d) (c) (e) (f) 图6.17 Kruskal算法构造最小生成树的过程
第五节 最短路径 图的最短路径问题用途很广,例如,交通网络中常常提出这样的问题:从甲地到乙地之间是否有公路连通?在有多条通路的情况下,哪一条通路最短? 交通网络可用带权有向图来表示。顶点表示城市名称,边表示两个城市有路连通,边上权值可表示两城市之间的距离、交通费或途中所花费的时间等。如何使一个城市到另一个城市的运输时间最短或运费最省,就是一个求两座城市间最短路径的问题。 在带权有向图中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。求解图的最短路径问题主要包括两个方面:求图中某一顶点(源点)到其余各顶点的最短路径和求图中每一对顶点之间的最短路径。这两方面的问题可分别采用迪杰斯特拉(Dijkstra)算法和弗洛伊德(Floyd)算法来实现。 注:最短路径与最小生成树不同,路径上不一定包含n个顶点。
一、从一个源点到其他各顶点的最短路径 给定一个带权有向图G=(V,E)与源点v,求从v到G中其他顶点的最短路径,并限定各边上的权值大于或等于0。 单源点最短路径是指:给定一个出发点(单源点)和一个有向网G=(V,E),求出源点到其他各顶点之间的最短路径。那么怎样求出单源点的最短路径呢?我们可以将源点到终点的所有路径都列出来,然后在里面选最短的一条即可。但是如果这样做,用手工方式可以,当路径特别多时,就显得特别麻烦,并且没有什么规律,因此不能用计算机算法来实现。 迪杰斯特拉(Dijkstra)在做了大量观察后,首先提出了一个按路径长度递增的次序产生各顶点的最短路径算法,我们称之为迪杰斯特拉算法。 (一)迪杰斯特拉算法的基本思想 迪杰斯特拉算法的基本思想是:设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为顶点集合(用S表示),存放已求出其最短路径的顶点,第二组为尚未确定最短路径的顶点集合是