一、图的基本概念
(一)图的定义
图(graph):图 G 由两个集合 V(vertex)和 E(edge)组成,记为 G=(V, E) ,其中 V 是顶点的有限集合,记为 V(G) ,E是连接V中两个不同顶点(顶点队)的边的有限集合,记为 E(G) 。
有向图(digraph):在图 G 中,如果表示边的顶点对(或序偶)是有序的,则称 G 为有向图。用 < i , j > 表示从顶点 i 到顶点 j 的一条边。
无向图(undigraph):在图 G 中,如果 E(G) 是对称的,则用 ( i , j ) 代替这两个顶点对,表示顶点 i 与顶点 j 的一条无向边,称 G 为无向图。
(二)图的基本术语
端点和邻接点(endpoint & adjacent):在一个无向图中,若存在一条边 ( i , j ),则称顶点 i 和顶点 j 为该边的两个端点,并称它们互为邻接点,边 ( i , j ) 和顶点 i 、j 关联。
顶点的度、入度和出度(degree, indegree & outdegree):在无向图中,一个顶点所关联的边的数目称为该顶点的度。在有向图中,顶点的度又分为入度和出度,以顶点 j 为终点的边数目称为该顶点的入度,以顶点 i 为起点的边数目,称为该顶点的出度。
完全图(completed graph):若无向图中的每两个顶点之间都存在着一条边,有向图中的每两个顶点之间都存在着方向相反的两条边,则称此图为完全图。
稠密图和稀疏图(dense graph & sparse graph):当一个图接近完全图时,称为稠密图。相反,当一个图含有较少的边数时,称为稀疏图。
子图(subgraph):设有两个图 G = ( V, E ) 和 G' = ( V', E' ) ,若 V' 是 V 的子集,且 E' 是 E 的子集,则称 G' 是 G 的子图。
路径和路径长度(path & path length):在一个图 G = ( V, E ) 中,从顶点i到顶点j的一条路径 ( i, i1, i2, …, im, j)。其中,所有的 (ix,iy) ∈ E(G),或者 <ix,iy> ∈E(G),路径长度是指一条路径上经过的边的数目。若一条路径上除开始点和结束点可以相同外,其余顶点均不相同,则称此路径为简单路径。
回路或环(cycle):若一条路径上的开始点与结束点为同一个顶点,则此路径被称为回路或环。开始点与结束点相同的简单路径被称为简单回路或简单环。
连通、连通图和连通分量(connect, connected graph & connected component):在无向图中,若从顶点 i 到顶点 j 有路径,则称顶点 i 和 j 是连通的。若图中任意两个顶点都连通,则称为连通图,否则称为非连通图。无向图 G 中的极大连通子图称为 G 的连通分量。任何连通图的连通分量只有一个,即本身,而非连通图有多个连通分量。
强连通图和强连通分量(strongly connected graph & strongly connected component):若图 G 中的任意两个顶点 i 和 j 都连通,即从顶点 i 到 j 和从顶点 j 到 i 都存在路径,则称图G是强连通图。有向图G中的极大强连通子图称为G的强连通分量。强连通图只有一个强连通分量,即本身。非强连通图有多个强连通分量。
权、带权图和网(weight, weighted graph & net):图中每一条边都可以附带有一个对应的数值,这种与边相关的数值称为权。边上带有权的图称为带权图,也称作网。
二、图的存储结构和基本运算算法
(一)邻接矩阵(adjacency matrix)存储方法
设 G = ( V, E ) 是含有n(n>0)个顶点的图,各顶点的编号为 0~(n-1) ,则 G 的邻接矩阵数组 A 是 n 阶方阵,其定义如下:
对于无向图:
对于有向图:
图的完整邻接矩阵类型的声明如下:
#define MAXV<最大顶点个数>
#define INF 32767 //定义∞
//定义顶点的类型
typedef struct{
int no; //顶点的编号
InfoType info; //顶点的其他信息
} VertexType;
//定义完整的图邻接矩阵类型
typedef struct{
int edges[MAXV][MAXV]; //邻接矩阵数组
int n,e; //顶点数,边数
VertexType vexs[MAXV]; //存放顶点信息
} MatGraph;
对于含有 n 个顶点的图采取邻接矩阵存储时,存储空间都为 ,适合于存储边数较多的稠密图。无向图的邻接矩阵数组一定是一个对称矩阵,可以只存储上(或下)三角部分的元素。
在需要提取边权值的算法中通常采用邻接矩阵存储结构。
(二)邻接表(adjacency list)存储方法
图的邻接表是一种顺序与链式存储相结合的存储方法。
对于含有 n 个顶点的图,每个顶点建立一个单链表,第 i(0 ≤ i ≤ n-1)个单链表中的结点表示关联于顶点 i 边(对有向图是以顶点 i 为起点的边),也就是将顶点 i 的所有邻接点(对有向图是出边邻接点)链接起来,其中每个结点表示一条边的信息。
图的完整邻接表存储类型的声明如下:
//边结点的类型
typedef struct ANode{
int adjvex; //该边的邻接点编号
struct ANode *nextarc; //指向下一条边的指针
int weight; //该边的相关信息,如权值
} ArcNode;
//邻接表的头结点类型
typedef struct Vnode{
InfoType info; //顶点的其他信息
ArcNode *firstarc; //指向第一个头结点
} VNode;
//完整的图邻接表类型
typedef struct{
VNode adjlist[MAXV]; //邻接表的头结点数组
int n,e; //图中的顶点数和边数
} AdjGraph;
对于含有 n 个顶点和e条边的图采取邻接表存储方式时,存储空间为 或 ,对于边数数目较少的稀疏图,邻接表比邻接矩阵更节省存储空间。
在需要提取某个顶点的所有邻接点的算法中,通常采用邻接表存储结构。
另外,所谓逆邻接表就是每个顶点链接的是指向该顶点的边。
(三)图的基本运算算法
CreateGraph(&g):创建图,由相关数据构造一个图 g 。
DestroyGraph(&g):销毁图,释放为图 g 分配的存储空间。
DispGraph(g):输出图,显示图 g 的顶点和边信息。
三、图的遍历
(一)深度优先遍历(DFS)
深度优先遍历的过程是从图中的某个初始点 v 出发,首先访问初始点 v ,然后选择一个与顶点 v 相邻且没被访问过的顶点 w ,以 w 为初始顶点,再从它出发进行深度优先遍历,直到图中与顶点 v 邻接的所有顶点都被访问过为止,显然这个遍历过程是一个递归过程。
以以上有向图G3为例,从顶点0开始进行深度优先遍历,可以获得以下访问序列:0 1 2 4 3,0 3 2 4 1。
(二)广度优先遍历(BFS)
广度优先遍历的过程是首先访问初始点 v ,接着访问顶点 v 的所有未被访问过的邻接点 v1、v2、…、vt ,然后按照 v1、v2、…、vt 的次序访问每一个顶点的所有未被访问过的邻接点,以此类推,直到图中所有和初始点 v 有路径相通的顶点都被访问过为止。
以以上有向图G3为例,从顶点2出发的深度优先访问序列是2 1 0 3 4。
四、生成树和最小生成树
(一)生成树基本概念
生成树(spanning tree):一个连通图的生成树是一个极小连通子图,其中含有图中的全部顶点和构成一棵树的 (n-1) 条边。
最小生成树(minimal spanning tree):在图的所有生成树中,边上的权值之和最小的树称为图的最小生成树。
深度优先生成树(DFS tree):由深度优先遍历得到的生成树称为深度优先生成树。
广度优先生成树(BFS tree):由广度优先遍历得到的生成树称为广度优先生成树。
(二)普里姆(Prim)算法
普里姆算法是一种构造性算法,用于构造最小生成树。T = ( U, TE ) 是 G 的最小生成树,其构造过程如下:
(1)初始化 U={v} 。v 到其他顶点的所有边为候选边;
(2)重复以下步骤 n-1 次,使得其他 n-1 个顶点被加入到U中:
①从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
②考察当前V-U中的所有顶点j,修改候选边:若(j,k)的权值小于原来和顶点k关联的候选边,则用(k,j)取代后者作为候选边。
对于一个的带权连通图,以 0 作为起始点,用普里姆算法构造最小生成树的过程如下:
(三)克鲁斯卡尔(Kruskal)算法
克鲁斯卡尔算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法。T = ( U, TE ) 是 G 的最小生成树,其构造过程如下:
(1)置 U 的初值等于 V(即包含有G中的全部顶点),TE 的初值为空集(即图 T 中每一个顶点都构成一个连通分量)。
(2)将图G中的边按权值从小到大的顺序依次选取:
①若选取的边未使生成树T形成回路,则加入 TE;
②否则舍弃,直到TE中包含 (n-1) 条边为止。
对于一个的带权连通图,用克鲁斯卡尔算法构造最小生成树的过程如下:
五、最短路径
(一)狄克斯特拉(Dijkstra)算法
问题描述:给定一个带权有向图G与源点v,求从v到G中其他顶点的最短路径,并限定各边上的权值大于或等于0。(单源最短路径问题)
求解思路:设 G = ( V, E )是一个带权有向图,把图中顶点集合 V 分成两组:第1组为已求出最短路径的顶点集合(用 S 表示),第2组为其余未求出最短路径的顶点集合(用 U 表示)。
(1)初始化: S 只包含源点即 S = { v }, v 的最短路径为 0 。U 包含除 v 外的其他顶点,U 中顶点 i 距离为边上的权值(若 v 与 i 有边 < v, i > )或 ∞(若 i 不是 v 的出边邻接点)。
(2)从 U 中选取一个距离 v 最小的顶点 u ,把 u 加入 S 中(该选定的距离就是 v→u 的最短路径长度)。
(3)以 u 为新考虑的中间点,修改 U 中各顶点 j 的最短路径长度:若从源点 v→j( j ∈ U)的最短路径长度(经过顶点 u )比原来最短路径长度(不经过顶点 u )短,则修改顶点 j 的最短路径长度。
(4)重复步骤(2)和(3)直到所有顶点都包含在 S 中。
以如上带权有向图为例,采用 Dijkstra 算法求从顶点 0 到其他顶点的最短路径,过程如下:
(二)弗洛伊德(Floyd)算法
问题描述:对于一个各边权值均大于零的有向图,对每一对顶点 i ≠ j ,求出顶点 i 与顶点 j 之间的最短路径和最短路径长度。(多源最短路径问题)
求解思路:假设有向图 G = ( V, E ) 采用邻接矩阵存储。设置一个二维数组 A 用于存放当前顶点之间的最短路径长度,分量 A [ i ][ j ] 表示当前顶点 i → j 的最短路径长度。
(1)用表示 i → j 的路径上所经过的顶点编号不大于 k 的最短路径长度。
(2)初始时,有。
(3)若已经求出,则考虑顶点 k,求从 i → j 的最短路径经过编号为k顶点的情况:。
以如上带权有向图为例,采用 Floyd 算法求解任意两点的最短路径,过程如下:
六、拓扑排序
拓扑序列(topological sequence):设 G = ( V, E ) 是一个具有 n 个顶点得有向图,V 中的顶点序列 v1, v2, …, vn 称为一个拓扑序列,若 < vi, vj > 是图中的一条边或者从顶点 vi 到顶点 vj 有路径,则在该序列中顶点 vi 必须排在顶点 vj 之前。
拓扑排序(topological sort):在一个有向图中找一个拓扑序列的过程称为拓扑排序。
顶点表示活动的网(activity on vertex network,AOV网):用顶点表示活动,用有向边表示活动之间优先关系的有向图称为顶点表示活动的网。
对上图表示课程之间先后关系的的课程AOV网进行拓扑排序,可得到拓扑序列 C1 → C3 → C2 → C4 → C7 → C6 → C5 ,C2 → C7 → C1 → C3 → C4 → C5 → C6 ,还可以得到其他拓扑序列。
拓扑排序的过程如下:
(1)从有向图中选择一个没有前驱(即入度为 0)的顶点并且输出它。
(2)从图中删去该顶点,并且删去从该顶点发出的全部有向边。
(3)重复上述两步,直到剩余的图中不再存在没有前驱的顶点为止。