1 图的基本概念
图是描述不同事物之间的关系的一个自然的模型。
图中每个结点(也称为顶点)既可能有前驱结点也可能有后继结点,且个数不限。用图可以表达复杂的关系。
1.1 图
图G是由V和E两个集合组成的二元组,记为G=(V,E),其中V是顶点的非空有限集,E是V中顶点对,即边的有限集。通常,也将图G的顶点集和边集分别记为V(G)和E(G)。E(G)可以是空集,此时图G中只有顶点而没有边。
1.2 有向图
若图G中的每一条边都是有方向的,则称G为有向图。在有向图中,一条有向边是顶点的有序对。有向图中的有向边常用带箭头的线段来表示
例如,下图的G1是一个有向图,该图的顶点集和边集分别为V(G1)={1,2,3,4},E(G1)={(1,2),(1,3),(2,4),(3,2),(4,3)}
1.3 无向图
若图G中每条边是没有方向的,则称G为无向图。无向图中的边表示图中顶点的无序对。因此,在无序图中(u,v)和(v,u)表示同一条边。
例如,下图的G2是一个无向图,它的顶点集和边集分别为 V(G2)={1,2,3,4,5},E(G2)={(1,2),(1,4),(2,3),(2,5),(3,4),(3,5)}。
1.4 完全图
完全图在上述规定下,图G的顶点数和边数e满足下述关系:
若G是无向图,则0≤e≤(n-1)/2;
若G是有向图,则0≤e≤n(n-1)。
恰好有n(n-1)/2条边的无向图称为完全无向图,恰好有n(n-1)条边的有向图称为完全有向图。显然,完全图具有最多的边数,任意一对顶点间均有边相连。
1.5 关联
若(u,v)是一条无向边,则称顶点u和v互为接点,或称u和v相接。并称边(u,v)关联于顶点u和v,或称边(u,v)与顶点u和v相关联。
若(u,v)是一条有向边,则称v是u的邻接顶点;并称边(u,v)关联于顶点u和v,或称边(u,v)与顶点u和v相关联。
1.6 顶点的度
例如,下图G1的G2中顶点2的度为3,图G1中顶点2的入度为2,出度为1,度为3。
1.7 子图
上图G1的的子集如下
1.8 路
路在无向图G中,若存在一个顶点序列u(1),u(2),…,u(m),则称该项点序列为顶点u()和u(m)之间的一条路径。其中,u(1)称为该路径的起点,u(m)称为该路径的终点,这条路径所包含的边数m-1称为该路径的长度。
若图G是有向图,则路径也是有向的,其中每条边均为有向边。
1.9 简单路
简单路若一条路径上除起点和终点可能相同外,其余顶点均不相同,则称此路径为一条简单路径。
1.10 回路
回路起点和终点相回的简单路径称为简单回路或简单环或圈。
例如,下图的G1
顶点序3,2,4,3组成一条长度为3的简单回路。
1.11 有根图
有根图在一个有向图中,若从一个顶点v有路径可以到达图中其他所有顶点,则称此有向图为有图,v称为该有根图的根。例如,下图的G1为有根图,顶点1为G1的根。
1.12 连通图
连通图在无向图G中,若从顶点u到顶点v有条路径,则称顶点u和v在图G中是连通的。若V(G)中任意两个不同的顶点u和v都是连通的则称G为连通图。例如,下图中的G2为一个连通图。
1.13 连通分支
连通分支无向图C的极大连通子图称为G的连通分支。
任何连通图只有一个连通分支即其自身,而非连通的无向图有多个连通分支。
例如,下图中的图有两个连通分支H1和H2。
1.14 强连通分支
在有向图G中,若对于V(G)中任意两个不同的顶点u和v,都存在从u到v及从v到u的路径,则称G是强连通图。有向图G的极大强连通子图称为G的强连通分支。
显然,强连通图只有一个强连通分支,即其自身。
非强连通的有向图有多个强连通分支。
1.15 赋权图和网络
若无向图的每条边都带一个权,则称相应的图为赋权无向图。
同理,若有向图的每条边都带一个权,则称相应的图为赋权有向图。
通常权是具有某种实际意义的数,如两个顶点之间的距离、耗费等。
赋权无向图和赋权有向图统称为网络。下图就是网络的一个例子。
2 抽象数据类型图
抽象数据类型(Abstract Data Type,ADT)。
无向图和有向图差别仅在于无向图中的边是顶点的无序对,而有序图中的边是顶点的有序对。
定义ADT图的基本运算是以有向图为基本模型的。
ADT图支持以下的基本运算。
(1) Graphinit(n):创建n个孤立顶点的图。
(2)GraphExist(i,j,G):判断图G中是否存在边(i,j) 。
(3) GraphEdges(G):返回图G的边数。
(4)GraphVertices(G):返回图G的顶点数。
(5) GraphAdd(i,j,G):在图G中加入边(i,j)。
(6)GraphDelete(i,j,G):删除图G的边(i,j)。
(7) Degree(i,G):返回图G中顶点i的度数。
(8)OutDegree(i,G):返回图G中顶点i的出度。
(9)InDegree(i,G):返回图G中顶点i的入度。
3 图的表示法
3.1 邻接矩阵表示法
在图的邻接矩阵表示法中,用一个二维数组,即图的邻接矩阵来存储图中各边的信息。
图G的邻接矩阵A是一个布尔矩阵,定义为
下图G1和G2
它们的邻接矩阵A1和A2
解释A1
1到1无边,所以第一行第一列为0
1到2有边,所以第一行第二列为1
1到3有边,所以第一行第三列为1
1到4无边,所以第一行第4列为0
依此类推。
当图G是一个网络时,其邻接矩阵可定义为
例如:下图为网络
其网络的邻接矩阵图,如下
用邻接矩阵表示一个有n个顶点的有向图时,所需空间为W(n^2)。
输入邻接矩阵和查看一遍邻接矩阵都要O(n^2)时间。
当图的边数远远小于n^2时,用邻接表来表示图会更有效。
3.2 邻接表表示法
用邻接表表示图G=(V,E) 时, 对每个顶点,将它的所有邻接顶点存放在一个表中, 这个表称为顶点i的邻接表。将每个顶点的邻接表存储在图G的邻接表数组中。
下图G1和G2
它们的邻接表分别为
3.3 紧缩邻接表表示法
紧缩邻接表将图G的每个顶点的邻接表紧凑地存储在两个一维数组List和h中。
例如,G1和G2的紧缩邻接表分别如下图。
4 用邻接矩阵实现图
图可分为赋权有向图、赋权无向图、有向图和无向图。
有向图可看成不带权的赋权有向图。
4.1 用邻接矩阵实现赋权有向图
结构定义如下。
其中,数组a存储赋权有向图的邻接矩阵;NoEdge是赋权有向图在邻接矩阵中的无边标记;n是赋权有向图的顶点数;e是赋权有向图的边数。
4.1.1 函数GrapHinit(n,noEdge)
创建一个有n个弧立顶点的赋权有向图。
4.1.2 函数GraphEdges(G)和GraphVertices(G)
分别返回赋权有向图G的边数和顶点数。
4.1.3 函数GrapExist(i,j,G)
判断当前赋权有向图G中的边(i,j)是否存在。
4.1.4 函数GrapAdd(i,j,w,G)
在赋权有向图G中加入边权为w的边(i,j)。
4.1.5 函数GraphDelete(i,j,G)
删除赋权有向图G中的边(i,j)。
4.1.6 函数OutDegree(i,G)
返回赋权有向图G中顶点i的出度。
4.1.7 函数InDegree(i,G)
返回赋权有向图G中顶点i的入度。
4.1.8 函数GraphOut(G)
输出赋权有向图G的邻接矩阵。
4.2 用邻接矩阵实现赋权无向图
函数GrapAdd(i,j,w,G)
函数GraphDelete(i,j,G)
删除边(i,j)时,还应同时删除边(j,i)。
4.3 用邻接矩阵实现有向图
用邻接矩阵实现无向图时,每条边的边权规定为1,边权为0表示无边。
函数Graphinit(n)创建一个有n个孤立顶点的有向图。
4.4 用邻接矩阵实现无向图
用邻接矩阵实现有向图和用邻接矩阵实现赋权有向图类似每条边的边权也规定为1。将无向图G的每一条边(i,j)用两条有向边(i,j)he(j,i)来替代。
函数GrapAdd(i,j,G)
函数GraphDelete(i,j,G)
删除边(i,j)时,还应同时删除边(j,i)。
5 用邻接表实现图
5.1 用邻接表实现有向图
定义如下。
其中,v是边的另一个顶点;next是邻接表的指针,指向邻接表的下一个结点。函数Newnode创建一个新的邻接表结点。
用邻接表实现有向图的结构定义如下。
其中,数组adj存储有向图的邻接表;n是有向图的顶点数;e是有向图的边数。
5.1.1 函数Graphinit(n)
创建一个用邻接表实现的有n个孤立顶点的有向图。
5.1.2 函数GraphEdges(G)和GraphVertices(G)
分别返回有向图G的变数和顶点数。
5.1.3 函数GrapExist(i,j,G)
判断当前有向图G的边(i,j)是否存在。
5.1.4 函数GrapAdd(i,j,G)
通过在顶点i的邻接表的表首插入顶点j来实现向有向图中加入一条有向边(i,j)的操作。
5.1.5 函数GraphDelete(i,j,G)
删除有向图G中的边(i,j)。
5.1.6 函数OutDegree(i,G)
通过计算顶点i的邻接表长,返回有向图G中顶点i的出度。
5.1.7 函数InDegree(i,G)
返回有向图G中顶点i的入度。
5.1.8 函数GraphOut(G)
输出有向图G的邻接表。
5.2 用邻接表实现无向图
函数GraphAdd(i,j,G)
在无向图G中加入有向边(i,j)时,还应同时加入有向边(j,i)。
函数GraphDelete(i,j,G)
在图G中删除边(i,j)时,还应同时删除边(j,i)。
5.3 用邻接表实现赋权有向图
赋权有向图相应的邻接表结点类型定义如下。
其中,v是边的另一个顶点;w是边权;next是邻接表指针,指向邻接表的下一个结点。函数NewLWNode创建一个新的邻接表结点。
用邻接表实现赋权有向图的结构定义如下。
其中数组adj存储赋权有向图的邻接表;n是赋权有向图的顶点数;e是赋权有向图的边数。
5.3.1 函数Graphinit(n)
创建一个用邻接表实现的有n个孤立顶点的赋权有向图。
5.3.2 函数GraphEdges(S)和GraphVertices(G)
分别返回赋权有向图G的变数和顶点数。
5.3.3 函数GraphExist(i,j,G)
判断赋权有向图G中的边(i,j)是否存在。
5.3.4 函数GraphAdd(i,jw,G)
通过在顶点i的邻接表的表首插入顶点j,来实现向赋权有向图中加入一条边权为w的有向边(i,j)。
5.3.5 函数GraphDelete(i,j,G)
删除赋权有向图G中的边(i,j)。
5.3.6 函数OutDegree(i,G)
通过计算顶点i的邻接表长,返回赋权有向图G中顶点i的出度。
5.2.6 函数InDegree(i,G)
返回赋权有向图G中顶点i的入度。
5.2.7 函数GraphOut(G)
输出赋权有向图G的邻接表。
5.4 用邻接表实现赋权无向图
将一个赋权无向图G当成一个赋权有向图来处理,即将赋权无向图G的每一条权值为w的边(i,j),用两条权值为w的赋权有向边(i,j)和(j,i)替代。
函数GraphDelete(i,j,G)在图G中删除边(i,j)时,还应同时删除边(j,i)。
6 图的遍历
广度优先搜索和深度优先搜索就是访问图的所有顶点,即遍历一个图的两个重要方法。用这两种方法都可以访问到与任意给定图的一个顶点有路相连的所有顶点。
6.1 广度优先搜索
以一个顶点为起始顶点,由近及远,依次访问和顶点i有路相通,且路径长度为1,2……的顶点。
广度优先搜索算法bfs可描述如下。
上述算法适用于各种类型的图。
用邻接矩阵实现的无向图中广度优先搜索算法bfs可实现如下。
调用函数bfs一次只能遍历图的一个连通分支。
当图G是连通图时,只要调用一次bfs就可遍历图的所有顶点。
当图有多个连通分支时,必须对每一个连通分支调用一次bfs。
改进算法bfs如下。
用邻接矩阵实现图时,广度优先遍历所需的搜索时间为O(n^2)。
用邻接表实现图时,广度优先遍历所需的搜索时间为O(n+e)。其中,n为图G的顶点数;e为图G的变数。
6.2 深度优先搜索
深度优先搜索策略遍历一个图类似于数的前序遍历。
深度优先搜索总是尽可能地先沿纵深方向进行搜索,所以称为深度优先搜索。
用邻接矩阵实现的无向图G中的深度优先搜索算法dfs可实现如下。
用邻接表实现有向图G中的深度优先搜索算法dfs可实现如下。
调用函数dfs一次只能遍历图的一个连通分支。
当图G是连通图时,只要调用一次dfs就可遍历图的所有顶点。
当图有多个连通分支时,必须对每一个连通分支调用一次dfs。
用深度优先搜素方式遍历图G的算法如下。
深度优先搜索算法dfs和广度优先搜索算法bfs具有相同的时间复杂性。
用邻接矩阵实现图时,深度优先遍历所需的搜索时间为O(n^2)。
用邻接表实现图时,深度优先遍历所需的搜索时间为O(n+e)。其中,n为图G的顶点数;e为图G的变数。
7 最短路径
谈论在一个赋权有向图上寻找最短路径的问题。最短路径问题可分为单源最短路径和所有顶点对之间的最短路径两大类。
7.1 单源最短路径
给定一个赋权有向图,其中每条边的权都是一个非负实数。另外,还给定一个顶点,称为源。现在要计算从源到图G的其他所有顶点的最短路径长度。这里路径的长度是指路上各边权之和。这个问题通常称为单源最短路径问题。
解单源最短路径的一个常用算法是Dijkstra算法。其基本思想是,设置一个顶点集合S并不断地进行贪心选择来扩充这个集合。
Dijkstra算法可描述如下。其中,输入的赋权有向图,顶点s是源,dist[v]表示当前从源s到顶点v的最短特殊路径长度,prev[v]表示从源s到顶点c的最短特殊路径上顶点v的前驱顶点。
在用邻接矩阵实现赋权有向图中,单源最短路径问题的Dijkstra算法可实现如下。
其中,函数ListdDelMin(L,dist)返回表L中具有最小dist值得顶点v,并将顶点v从表L中删除。
Dijkstra算法求出源顶点间最短路径长度。
Dijkstra算法是应用贪心算法的典型例子。
Dijkstra算法的计算复杂性:如果用赋权邻接矩阵实现赋权有向图,Dijkstra算法的主循环需要O(n)时间。这个循环需要执行n-1次,完成循环需要O(n^2)时间。算法的其余部分所需要的时间不超过O(n ^2)。
7.2 Bellman-Ford最短路径算法
Bellman-Ford最短路径算法是解决Dijkstra算法遇到赋权有向图权为负数的问题的方法。
Bellman-Ford最短路径算法是对Dijkstra算法适当修改得出来的。
Bellman-Ford最短路径算法遇到有一个从源可达的负权圈,判定该问题无解。若不存在从源可达的负权圈,算法将正确地计算从源到其他各顶点间的最短路径。
实现Bellman-Ford算法如下。
当图的顶点数为n,边数为e时,算法在最坏情况下需要O(ne)时间。
7.3 所有顶点对之间的最短路径
每次以一个顶点为源,重复执行Dijkstra算法n次。这样就可以求得所有顶点对之间的最短路径。这样做所需的计算时间为O(n^3)。
所有顶点对之间最短路径的较直接的Floyd算法。
用邻接矩阵实现的赋权有向图中,求所有顶点对之间最短路径的Floyd算法可实现如下。
Floyd算法计算时间为O(n^3)。
8 无圈有向图
一个不含圈的有向图称为无圈有向图(DAG)。DAG是有向图的特殊情况,而树又是DAG的特殊情况。
8.1 拓扑排序
DAG可以用来表示偏序关系。集合S上的偏序关系是指满足下述二元关系R:
对于一个有向图G=(V,E)中所有顶点确定一个线性序 ord:
使得对所有
有
这个过程就称对有向图G的拓扑排序。
当G是一个DAG时,对G进行深度优先遍历,各顶点的后序编号给出G的顶点的反拓扑排序。
具体算法描述如下。算法中用邻接表实现给定的DAG。
反拓扑排序算法的计算时间为O(n+e)。其中,n为给定DAG的定点数,e为给定DAG的边数。
8.2 DAG的最短路径
可利用DAG的无圈性,解决关于DAG的多源最短路径问题,具体算法可描述如下。
计算时间为O(n+e)。
8.3 DAG的最长路径
解决DAG的多源最长路径问题,算法如下。
计算时间为O(n+e)。
8.4 DAG所有顶点对之间的最短路径
解决关于DAG的所有顶点对之间的最短路径问题的动态规划算法,具体描述如下。
算法的计算时间为O(ne)。
9 最小支撑树
设G=(V,E)是一个无向连通赋权图。E中每条边(u,v)的权为a[u][v]。若G的一个子图G’是一棵包含G的所有顶点的树,则称G’为G的支撑树。支撑树上各边的总和称为该支撑树的权。在G的所有支撑树中,权值最小的支撑树称为G的最小支撑树。
9.1 最小支撑树性质
用贪心算法涉及策略可设计出构造最小支撑树的有效算法。构造最小支撑树的Prim算法和Kruskal算法都可以看成应用贪心算法涉及策略的典型例子。这两个进行贪心选择的方式不同,但都利用了下面的最下支撑树性质。
9.2 Prim算法
对于下图中的连通赋权图,
按Prim算法选取边的过程如下图。
用邻接矩阵实现的赋权无向图G中构造一棵最小支撑树的Prim算法实现如下。
算法Prim计算时间为O(n^2)。
9.3 Kruskal算法
构造最小支撑树的另一个常用算法是 Kruskal算法。
对于下图中的连通赋权图,
按Kruskal算法选取边的过程如下。
存储每条边的结构定义如下。
函数EDGE(u,v,w)构造一条为w的边(u,v)。
函数Edges(a,G)抽取图G的所有边到赋权边数组a中,并返回图G的变数。
利用并查集UFset可实现Kruskal算法如下。
Kruskal算法计算时间为O(elonge)。
10 图匹配
设G=(V,E)是一个无向图。若顶点集合V可分割为连个互不相交的子集,并且图中每条边(i,j)所关联的两个顶点i和j分属于这两个不同的顶点集,则称图G为一个二分图。
在学校的教务管理中,排课表是一项例行工作。一般情况下,每位教师可胜任多门课程的学,而每个学期只讲授一门所胜任的课程;每学期的一门课程只需一位教师讲授。可以用一二分图来表示教师与课程的这种关系。教师和课程都是图的顶点,边(t,c)表示教师t胜任课程c。下图为表示5位教师和5门课程之间关系的二分图。
总结
(1)表示复杂非线性关系的数据结构图,以及图的一般操作和表示图的数据结构。
(2)图的邻接矩阵表示及其实现方法、图的邻接表表示及其实现方法,以及图的紧缩邻接表实现方法。
(3)图的广度优先搜索和深度优先搜索算法。
(4)单源最短路径问题的Dijkstra算法、有负权边的单源最短路径问题的Bellman-Ford算法和所有顶点对之间最短路径问题的Floyd算法。
(5)最小支撑树问题是另一个经典的图论问题。构造最小支撑树的Prim算法和Kruskal算法。Prim算法和Kruskal算法的计算时间分别为O(n^2)和O(elonge)。
(6)二分图的概念及其相关的图匹配问题。