图的基本概念和术语
- 图的定义
G = ( V , E ) V (Vertex):顶点(数据元素)的有穷非空集合; E (Edge):边的有穷集合。 \begin{aligned} &G=(V,E) \\ &V\text{(Vertex):顶点(数据元素)的有穷非空集合;} \\ &E\text{(Edge):边的有穷集合。} \end{aligned} G=(V,E)V(Vertex):顶点(数据元素)的有穷非空集合;E(Edge):边的有穷集合。 - 有向图:图中每条边都是有方向的,有向图的边也称作“弧”
- 无向图:图中每条边都是无方向的
- 完全图:图中任意两个顶点之间都有一条边相连。完全图分为有向完全图和无向完全图,如下图示
若图中有 n n n个顶点,无向完全图共有 C n 2 = n ( n − 1 ) 2 C_n^2=\frac{n(n-1)}{2} Cn2=2n(n−1)条边,而有向完全图共有 2 C n 2 = n ( n − 1 ) 2C_n^2=n(n-1) 2Cn2=n(n−1)条边。 - 稀疏图:有很少边或弧的图( e < n log n e<n\log n e<nlogn), e e e为边或弧的数目, n n n为顶点数目
- 稠密图:有较多边或弧的图
- 网:边/弧带权的图
- 邻接:有边/弧相连的两个顶点之间的关系。在无向图中,存在 ( V i , V j ) (V_i,V_j) (Vi,Vj),则称 V i V_i Vi和 V j V_j Vj互为邻接点;同样,在有向图中,存在 < V i , V j > <V_i,V_j> <Vi,Vj>,则称 V i V_i Vi邻接到 V j V_j Vj, V j V_j Vj邻接于 V i V_i Vi。
- 关联(依附):边/弧与顶点之间的关系。存在 ( V i , V j ) (V_i,V_j) (Vi,Vj)/ < V i , V j > <V_i,V_j> <Vi,Vj>,则称该边关联于 V i V_i Vi和 V j V_j Vj。
- 顶点的度:与某顶点 v v v相关联的边/弧的数目,记为TD( v v v)。在有向图中,顶点的度等于该顶点的入度和出度之和。顶点 v v v的入度是以 v v v为终点的有向边的条数,记作ID( v v v);而顶点 v v v的出度是以 v v v为始点的有向边的条数,记作OD( v v v)。
- 路径:接续的边构成的顶点序列。
- 路径长度:路径上的边/弧数目,或权值之和。
- 回路(环):第一个顶点和最后一个顶点相同的路径。
- 简单路径:除路径起点和终点可以相同外,其他顶点均不相同的路径。
- 简单回路(简单环):除路径起点和终点相同外,其余顶点均不相同的路径。
- 连通图/强连通图:在无/有向图
G
=
(
V
,
{
E
}
)
G=(V,\{E\})
G=(V,{E})中,若对任意两个顶点
v
,
u
v,u
v,u都存在从
v
v
v到
u
u
u的路径,则称
G
G
G是连通图/强连通图。
- 权与网:图中边或弧所具有的相关系数称为权。带权的图称为网。
- 子图:设有两个图
G
=
(
V
,
{
E
}
)
G=(V,\{E\})
G=(V,{E}),
G
1
=
(
V
1
,
{
E
1
}
)
G_1=(V_1,\{E_1\})
G1=(V1,{E1}),若
V
1
⊆
V
V_1\subseteq V
V1⊆V,
E
1
⊆
E
E_1\subseteq E
E1⊆E,则称
G
1
G_1
G1是
G
G
G的子图。
- 极大连通子图:若某子图 G 1 G_1 G1是 G G G的连通子图,若 G G G的任一不在 G 1 G_1 G1的顶点加入 G 1 G_1 G1,得到的子图不再连通,则称 G 1 G_1 G1为 G G G的极大连通子图。
- 连通分量:无向图
G
G
G的极大连通子图称为
G
G
G的连通分量。
- 极大强连通子图:若某子图 G 1 G_1 G1是 G G G的强连通子图,若 G G G的任一不在 G 1 G_1 G1的顶点加入 G 1 G_1 G1,得到的子图不再强连通,则称 G 1 G_1 G1为 G G G的极大连通子图。
- 强连通分量:有向图
G
G
G的极大强连通子图称为
G
G
G的强连通分量。
- 极小连通子图:若某子图 G 1 G_1 G1是 G G G的连通子图,若删除 G 1 G_1 G1的任一条边,得到的子图不再连通,则称 G 1 G_1 G1为 G G G的极小连通子图。
- 生成树:包含无向图 G G G所有顶点的极小连通子图。
- 生成森林:对非连通图,由各个连通分量的生成树组成的集合。
图的存储结构
- 数组表示法:邻接矩阵
- 链表存储结构:邻接表、邻接多重表、十字链表
邻接矩阵表示法
表示方法:建立一个顶点表(一维数组),记录顶点信息,再建立一个邻接矩阵(二维数组)表示各个顶点之间的关系。
设图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)有
n
n
n个顶点,则顶点表 Vtxs[
n
n
n ]为
| $i$ | $0$| $1$ | $2$|...|$n-1$|
|:--------:|:---------:|:---------:|:---------:|:---------:|:---------:|
| Vtxs[$i$ ]| $v_1$|$v_2$ |$v_3$ |**...** |$v_n$ |
而图的邻接矩阵arcs[$n$][$n$]定义为:$$
\text{arcs}[i][j]=
\begin{cases}1,\quad &
\text{如果}(v_i,v_j)/<v_i,v_j>\in E,\\
0, \quad & \text{否则}.
\end{cases}
$$
无向图的邻接矩阵表示法
特点1:无向图的邻接矩阵是对称的。
特点2:顶点
v
i
v_i
vi的度是第
i
i
i行(列)中1的个数。
特别地,完全图的邻接矩阵对角线元素为0,其余为1。
有向图的邻接矩阵表示法
在有向图的邻接矩阵中,
第
i
i
i行的含义:以顶点
v
i
v_i
vi为尾的弧(即出度边);
第
i
i
i列的含义:以顶点
v
i
v_i
vi为头的弧(即入度边)。
特点1:有向图的邻接矩阵可能是不对称的。
特点2:顶点
v
i
v_i
vi的出度是第
i
i
i行元素之和;顶点
v
i
v_i
vi的入度是第
i
i
i列元素之和;所以顶点
v
i
v_i
vi的度=第
i
i
i行元素之和 + 第
i
i
i列元素之和。
网(即有权图)的邻接矩阵表示法
arcs
[
i
]
[
j
]
=
{
w
i
j
,
如果
(
v
i
,
v
j
)
/
<
v
i
,
v
j
>
∈
E
,
∞
,
无边弧
.
\text{arcs}[i][j]= \begin{cases}w_{ij},\quad & \text{如果}(v_i,v_j)/<v_i,v_j>\in E,\\ \infty, \quad & \text{无边弧}. \end{cases}
arcs[i][j]={wij,∞,如果(vi,vj)/<vi,vj>∈E,无边弧.
邻接矩阵表示法优缺点
优点:
- 直观、简单、好理解
- 方便检查任意一对顶点之间是否存在边
- 方便找任一顶点的所有邻接点(有边直接相连的顶点)
- 方便求得任一顶点的度
缺点:
- 不利于增加和删除节点
- 浪费空间,存稀疏图时有大量无效元素
- 统计稀疏图总边数时浪费时间
邻接表表示法(链表)
邻接表表示方法:
- 顶点存储:按编号顺序将顶点数据存储在一维数组中
- 关联同一顶点的边(以顶点为尾的弧)的存储:用线性链表存储
无向图邻接表表示法
特点:
- 邻接表不唯一
- 若无向图有 n n n个节点, e e e条边,则其邻接表需要 n n n个头节点和 2 e 2e 2e个表节点。适合存储稀疏图。
- 无向图中节点 v i v_i vi的度为第 i i i个存储节点关联边的单链表的节点数。
- 计算节点的度容易
有向图邻接表表示法
有向图邻接表的表示方法与无向图的表示方法类似,但只将以对应节点为尾的弧存储在单链表中。如下图示
特点:
- 若无向图有 n n n个节点, e e e条边,则其邻接表需要 n n n个头节点和 e e e个表节点。
- 顶点 v i v_i vi的出度为第 i i i个单链表的节点数。
- 顶点 v i v_i vi的入度为全部单链表中邻接点域值为 i − 1 i-1 i−1的节点的个数
- 找出度容易,找入度难
有向图邻接表的表示方法还可以用逆邻接表法,即单链表存储的是以对应节点为头的弧。
邻接矩阵和邻接表的关系
- 联系
- 邻接表中每个链表对应邻接矩阵中的一行,链表中节点个数等于邻接矩阵一行中非零元素的个数。
- 区别
- 对于任一确定的无向图,其邻接矩阵是唯一的(行列号和顶点编号一致), 但邻接表不唯一(链接次序与顶点编号无关)。
- 邻接矩阵空间复杂度为 O ( n 2 ) \Omicron(n^2) O(n2),邻接表空间复杂度为 O ( n + e ) \Omicron(n+e) O(n+e)。
- 用途:邻接矩阵多用于稠密图,邻接表多用于稀疏图。
十字链表
- 十字链表(Orthogonal List)是解决有向图的邻接表存储方式中求节点的度困难的问题,它是有向图的另一种链式存储结构,可以看成是有向图的邻接表和逆邻接表的结合。
- 有向图中每一条弧对应十字链表中一个弧节点,同时有向图中每一个顶点在十字链表中对应有一个顶点节点。
邻接多重表
- 邻接多重表解决无向图邻接表存储方式中,每条边要存储两次的问题。
图的遍历
定义
- 从给定的连通图中某一顶点除法,沿着一些边访遍图中所有顶点,且每个顶点仅被访问一次,就叫做图的遍历,它是图的基本操作。
- 遍历的实质:寻找每个顶点的邻接点的过程。
遍历可能存在的问题及解决思路
- 图中可能存在回路,且图的任一顶点都可能与其他顶点相通,访问完某个顶点后可能沿着某些边又回到曾经访问过的顶点。
- 解决思路:设置辅助数组visited[n],用来标记被访问过的顶点
- 初始状态,辅助数组visited中所有值为0。
- 顶点i被访问,则将visited[i]设置为1,防止被多次访问。
深度优先搜索(Depth First Search, DFS)
遍历方法:
-
在访问图中某一起始点 v v v后,由 v v v出发,访问它的任一邻接点 w 1 w_1 w1;
-
再从 w 1 w_1 w1出发,访问与 w 1 w_1 w1邻接但还没被访问过的顶点 w 2 w_2 w2;
-
然后再从 w 2 w_2 w2出发,重复类似的访问过程,…,如此进行下去,直到所有邻接顶点都被访问过的顶点 u u u为止。
-
接着,退回一步,退到前一次刚访问过的顶点,看其是否由未被访问过的邻接点:
- 如果有,则访问此顶点,再由此顶点出发,进行与先前类似的访问过程。
- 如果没有,则再回退一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。
-
连通图的深度优先遍历的思想类似于树中先序遍历。
-
算法时间复杂度
- 用邻接矩阵表示的图,遍历图中每一顶点都要完整扫描每一顶点所在的行,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
- 用邻接表表示的图,虽然有 2 e 2e 2e个表节点,但依靠辅助数组,只需扫描 e e e个数组即可完成遍历,再加上访问 n n n个头节点的时间,时间复杂度为 O ( n + e ) O(n+e) O(n+e)。
-
空间复杂度:借用堆栈, O ( n ) O(n) O(n)。
广度优先搜索(Breadth First Search, BFS)
遍历方法:
- 从图的某一顶点 v i v_i vi出发,首先依次访问该顶点的所有邻接点 v i 1 , v i 2 , . . . , v i n v_{i_1},v_{i_2},...,v_{i_n} vi1,vi2,...,vin,再按这些邻接点的被访问的先后次序访问与它们邻接的所有未被访问的顶点。
- 重复此过程,直到所有顶点都被访问为止。
- 连通图的广度优先遍历的思想类似于树中的层次遍历。
- 算法时间复杂度:同DFS。
- 空间复杂度:借用队列, O ( n ) O(n) O(n)。
生成树
生成树的概念及特点
- 生成树:所有顶点都由边连在一起,但不存在回路的图。
- 一个图可以有许多棵不同的生成树。
- 所有生成树均有以下特点:
- 生成树的节点个数与图的顶点个数相同。
- 生成树树的极小连通子图,去掉任一条边则不再连通。
- 一个n个顶点的连通图的生成树有n-1条边。
- 在生成树中再加一条边必形成回路。
- 生成树中任意两点间的路径唯一。
- 含n个顶点n-1条边的图不一定是生成树。
无向图的生成树的构建
设图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)是一个连通图,当从图的任一顶点出发遍历
G
G
G时,将边集
E
(
G
)
E(G)
E(G)分成两个集合
T
(
G
)
T(G)
T(G)和
B
(
G
)
B(G)
B(G)。其中,
T
(
G
)
T(G)
T(G)是遍历是经过的边的集合,
B
(
G
)
B(G)
B(G)是未经过的边的集合。显然
G
1
=
(
V
,
T
)
G_1=(V,T)
G1=(V,T)是
G
G
G的极小连通子图,即
G
1
G_1
G1是
G
G
G的生成树。
最小生成树
定义:给定一个无向网络,在该网的所有生成树中,使得各边所有权值之和最小的生成树称为该网的最小生成树(Minimum Spanning Tree,MST),也叫最小代价生成树。
最小生成树的性质
- 构造最小生成树的算法很多利用MST的性质。
- 设 N = ( V , E ) N=(V,E) N=(V,E)是一个连通网, U U U是顶点集 V V V的一个非空子集。设 u ∈ U , v ∈ V − U u\in U,v\in V-U u∈U,v∈V−U若边 ( u , v ) (u,v) (u,v)是一条具有最小权值的边,则必存在一棵包含 ( u , v ) (u,v) (u,v)的最小生成树。
- 性质解释:在生成树的构造过程中,图中
n
n
n个顶点分属两个不同的集合:
- 已落在生成树上的顶点集: U U U
- 尚未落在生成树上的顶点集: V − U V-U V−U
- 接下来应该在所有连通
U
U
U中顶点和
V
−
U
V-U
V−U中顶点的边中选取权值最小的边。
构造最小生成树——Prim算法
- 算法思想
- 设 N = ( V , E ) N=(V,E) N=(V,E)是一个连通网, T E TE TE是 N N N上最小生成树中边的集合。
- 初始令 U = { u 0 } U=\{u_0\} U={u0}, u 0 ∈ V u_0\in V u0∈V, U = { } U=\{\} U={}。
- 在所有 u ∈ U u\in U u∈U, v ∈ V − U v\in V-U v∈V−U构成的边 ( u , v ) ∈ E (u,v)\in E (u,v)∈E中,找出一条代价(权值)最小的边 ( u 0 , v 0 ) (u_0,v_0) (u0,v0)。
- 将 ( u 0 , v 0 ) (u_0,v_0) (u0,v0)加入 T E TE TE,并同时将 v 0 v_0 v0加入 U U U。
- 重复上两步骤,直至 U = V U=V U=V,此时 T = ( U , T E ) T=(U,TE) T=(U,TE)为 N N N的最小生成树。
构造最小生成树——Kruskal算法
- 算法思想
- 设 N = ( V , E ) N=(V,E) N=(V,E)是一个连通网,令最小生成树为只有 n n n个顶点而无边的非连通图 T = ( V , { } ) T=(V,\{\}) T=(V,{}),且每个顶点自成一个连通分量。
- 在 E E E中选取代价最小的边,若该边依附的顶点落在 T T T中不同的连通分量上(即不能形成环),则将此边加入 T T T中;否则,舍弃此边,选取下一条代价最小的边。
- 依次类推,直至 T T T中所有顶点都在同一连通分量上为止。
两种算法比较
算法 | Prim算法 | Kruskal算法 |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | O ( n 2 ) O(n^2) O(n2) ( n n n为顶点数) | O ( e log e ) O(e\log e) O(eloge) ( e e e为边数) |
适应范围 | 稠密图 | 稀疏图 |
最短路径问题
- 问题定义:在有向网中,A点(源点)到达B点(重点)的所有路径中,寻找一条各边权值之和最小的路径,即是最短路径问题。
- 特点:最短路径与最小生成树不同,路径上不一定包含 n n n个顶点,也不一定包含 n − 1 n-1 n−1条边。
- 两类问题
- 第一类问题:两点间最短路径,使用Dijkstra算法。
- 第二类问题:某源点到其他各点的最短路径,使用Floyd算法。
Dijkstra最短路径算法
-
单源最短路径算法。
-
算法步骤:
- 初始化:初始时,令已检测的点集为 S = ∅ S=\varnothing S=∅,图中其余未检测的点集为 T = { v i } , i = 0... n − 1 T=\{v_i\},i=0...n-1 T={vi},i=0...n−1。定义一个距离辅助数组 D [ n ] D[n] D[n]记录源点到其他各点的最短路径值, D [ 1... n − 1 ] D[1...n-1] D[1...n−1]值为 ∞ \infty ∞, D [ 0 ] = 0 D[0]=0 D[0]=0。
- 选择: T T T中选择出令 D [ j ] D[j] D[j]最小的顶点 v j v_j vj加入 S S S,并将其从 T T T中移去。
- 更新:遍历
v
j
v_j
vj在
T
T
T中的所有邻接顶点:
若在 T T T中存在 v j v_j vj的邻接点 v k v_k vk,使得 w e i g h t ( < v j , v k > ) + D [ j ] < D [ k ] weight(<v_j,v_k>)+D[j] < D[k] weight(<vj,vk>)+D[j]<D[k],则用 w e i g h t ( < v j , v k > ) + D [ j ] weight(<v_j,v_k>)+D[j] weight(<vj,vk>)+D[j] 的值更新 D [ k ] D[k] D[k]。 - 重复步骤2,3,直至 T = ∅ , S = V T=\varnothing, S=V T=∅,S=V。
- 结束时, D [ i ] D[i] D[i]即为源点到 v i v_i vi的最短路径值。
-
上述算法只能计算到源点到各个顶点的最短路径距离,但不能得到该最短路径。若要获得最短路径,则需要定义一个记录前驱顶点的辅助数组 p r e d e c e s s o r predecessor predecessor。在满足更新条件的时候,还要将被更新的顶点的前驱记录在数据中。例如,满足更新条件 w e i g h t ( < v j , v k > ) + D [ j ] < D [ k ] weight(<v_j,v_k>)+D[j] < D[k] weight(<vj,vk>)+D[j]<D[k]的同时,记录被更新的 v k v_k vk的前驱: p r e d e c e s s o r [ k ] = p r e d e c e s s o r [ j ] predecessor[k] = predecessor[j] predecessor[k]=predecessor[j]。取某个顶点的最短路径时,只需沿着该顶点往前寻找前驱,直到到达源点。
Floyd最短路径算法
- 计算所有顶点间的最短路径,多源最短路径算法。
- 算法思想:逐个点试探,从 v i v_i vi到 v j v_j vj的所有可能存在的路径中,选出一条长度最短的路径。
- 算法步骤
- 初始时设置一个 n n n阶方阵,令其对角元素为0,若存在弧 < v i , v j > <v_i,v_j> <vi,vj>,则方阵中对应 [ i , j ] [i,j] [i,j]元素为权值,否则为 ∞ \infty ∞。
- 逐步在原直接路径中增加中间节点,若加入中间节点后路径变短,则修改之;否则维持原值。所有顶点试探完毕,算法结束。
有向无环图及其应用
基本概念
- 有向无环图:无环的有向图,简称DAG(Directed Acycline Graph)图。
- 有向无环图通常用来描述一个工程或者系统的进行过程。(通常把计划、施工、生产、程序流程等当成是一个工程。)
- 一个工程可以分成若干个子工程,只要完成了这些子工程,就可以导致整个工程的完成。
AOV网
- 用一个有向图表示一个工程的各子工程及其相互制约的关系。其中以顶点表示活动,弧表示活动之间的优先制约关系,称这种有向图为顶点表示活动的网,简称AOV网(Activity On Vertex network)。
- 特点:
- 若从 i i i到 j j j有一条有向路径,则 i i i是 j j j的前驱, j j j是 i i i的后继。
- 若 < i , j > <i,j> <i,j>是网中有向边,则 i i i是 j j j的直接前驱, j j j是 i i i的直接后继。
- AOV网中,不允许存在回路,因为如果有回路存在,则表明某项活动以自身为先决条件,显然这是荒谬的。
- 问题:如何判断AOV网中是否存在回路?
拓扑排序
- 在AOV网没有回路的前提下,将全部活动排列称一个线性序列,使得若AOV网中有弧 < i , j > <i,j> <i,j>存在,则在这个序列中, i i i一定排在 j j j的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序。
- 方法步骤:
- 在有向图中选一个没有前驱的顶点且输出之。
- 在图中删除该顶点和所有以它为尾的弧。
- 重复上面两步,直至全部顶点均已输出;或者当图中不存在无前驱的顶点为止。
- 检测AOV网中是否存在环的方法:对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV网必定不存在环。
AOE网
- 用一个有向图表示一个工程的各子工程及其相互制约的关系。其中以弧表示活动,顶点表示活动的开始或结束时间,称这种有向图为边表示活动的网,简称AOE网(Activity On Edge network)。
- 解决关键路径问题。
- 把工程计划表示为边表示活动的网络,用顶点表示时间,弧表示活动,弧的权值表示活动持续时间。
- 事件表示在它之前的活动已经结束,在它之后的活动可以开始。