一、图
1.1 图
由顶点【vertex】的有穷非空集合和顶点之间的边【edge】的集合组成的数据结构称为图【Graph】,通常表示为
G
=
(
V
,
E
)
G = (V, E)
G=(V,E)其中,
G
G
G表示一个图,
V
V
V是图
G
G
G中顶点的集合,
E
E
E是图
G
G
G中顶点之间边的集合。顶点一般用于表示数据对象,而边表示了数据对象之间的关系。
若顶点 v i v_i vi和 v j v_j vj之间的边没有方向,则称这条边为无向边,表示为 ( v i , v j ) (v_i, v_j) (vi,vj),而当图的任意两个顶点之间的边都是无向边,则称图为无向图;若边有方向,则称这条边为有向边,也成为弧,表示为 ⟨ v i , v j ⟩ \lang v_i, v_j \rang ⟨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
j
)
(v_i, v_j)
(vi,vj)依附于顶点
v
i
v_i
vi和
v
j
v_j
vj;
在有向图中,对于顶点
v
i
v_i
vi和
v
j
v_j
vj,若存在有向边
⟨
v
i
,
v
j
⟩
\lang v_i, v_j \rang
⟨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
⟩
\lang v_i, v_j \rang
⟨vi,vj⟩依附于顶点
v
i
v_i
vi和
v
j
v_j
vj。
在无向图中,对于顶点
v
v
v,依附于其的边数称为该顶点的度,通常记为
D
(
v
)
D(v)
D(v);
而在有向图中,对于顶点
v
v
v,指向其的弧的数量称为该顶点的入度,而由其指向其他顶点的弧的数量称为该顶点的出度,分别记为
I
D
(
v
)
ID(v)
ID(v)与
O
D
(
v
)
OD(v)
OD(v),并定义该顶点的度
D
(
v
)
=
I
D
(
v
)
+
O
D
(
v
)
D(v) = ID(v) + OD(v)
D(v)=ID(v)+OD(v)。
在无向图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E)中,若存在一个顶点序列
v
p
,
v
1
,
v
2
,
.
.
.
,
v
m
,
v
q
v_p, v_1, v_2, ...,v_m, v_q
vp,v1,v2,...,vm,vq,且
(
v
p
,
v
1
)
,
(
v
1
,
v
2
)
,
.
.
.
,
(
v
m
,
v
q
)
∈
E
(
G
)
(v_p, v_1), (v_1, v_2), ..., (v_m, v_q) \in E(G)
(vp,v1),(v1,v2),...,(vm,vq)∈E(G),则称为顶点
v
p
v_p
vp到
v
q
v_q
vq的一条路径;
在有向图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E)中,若存在一个顶点序列
v
p
,
v
1
,
v
2
,
.
.
.
,
v
m
,
v
q
v_p, v_1, v_2, ...,v_m, v_q
vp,v1,v2,...,vm,vq,且
⟨
v
p
,
v
1
⟩
,
⟨
v
1
,
v
2
⟩
,
.
.
.
,
⟨
v
m
,
v
q
⟩
∈
E
(
G
)
\lang v_p, v_1\rang, \lang v_1, v_2\rang, ..., \lang v_m, v_q\rang \in E(G)
⟨vp,v1⟩,⟨v1,v2⟩,...,⟨vm,vq⟩∈E(G),则称为顶点
v
p
v_p
vp到
v
q
v_q
vq的一条有向路径。
若路径之间的各点均不相同,称为简单路径;若路径之间的两端点
v
1
v_1
v1和
v
m
v_m
vm重合,则称为简单回路。
在无向图中,若从相异的顶点
v
i
v_i
vi到
v
j
v_j
vj有路径,则称顶点
v
i
v_i
vi到
v
j
v_j
vj是连通的,若无向图中任意一对顶点都是连通的,则称此图是连通图,非连通图的极大连通子图称为连通分量;
在有向图中,若对于相异的顶点
v
i
v_i
vi到
v
j
v_j
vj,都存在从
v
i
v_i
vi到
v
j
v_j
vj与从
v
j
v_j
vj到
v
i
v_i
vi的有向路径,则称顶点
v
i
v_i
vi到
v
j
v_j
vj是强连通的,若有向图中任意一堆顶点都是强连通的,则称此图是强连通图,非强连通图的极大强连通子图称为强连通分量。
1.2 图的邻接矩阵描述
将图的各顶点之间的邻接关系使用一维数组储存图的顶点,二维数组,也称邻接矩阵储存顶点之间的邻接关系。对于图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E),邻接矩阵有
e
d
g
e
[
i
]
[
j
]
=
{
1
,
(
i
,
j
)
∈
E
o
r
⟨
i
,
j
⟩
∈
E
0
,
o
t
h
e
r
w
i
s
e
edge[i][j] = \left\{\begin{aligned} &1, &&(i, j) \in E\ or\ \lang i, j \rang \in E \\ &0, &&otherwise\\ \end{aligned}\right.
edge[i][j]={1,0,(i,j)∈E or ⟨i,j⟩∈Eotherwise其数据结构描述为
typedef struct{
VertexData verlist[NumVertices];
EdgeData edge[NumVertices][NumVertices];
int n, e;
} MTGraph;
在无向图中,邻接矩阵的主对角线为0且为对称矩阵。
1.3 图的邻接表描述
对于无向图,对于其每一个顶点,将与该顶点相邻的顶点链成一个单链表,形成顶点的邻接表,再将所有的邻接表的指针和顶点信息组成一维数组;而对于有向图,其邻接表为将与邻接于顶点的顶点链成的链表,形成正邻接表;或将与邻接到顶点的顶点链成链表,形成逆邻接表。邻接表的数据结构描述为
typedef struct node{
int adjvex;
struct node *next;
} EdgeNode;
typedef struct{
VertexData vertex;
EdgeNode *firstedge;
} VertexNode;
typedef struct{
VertexNode vexlist[NumVertices];
int n, e;
} AdjGraph;
此外,也可以在无向图的边结点中使用指向上一个和下一个边的指针,形成邻接多重表;或在有向图的弧结点中使用分别指向邻接于与邻接到该顶点的指针,形成十字链表。
相比之下,邻接矩阵适合稠密图,而邻接表适合稀疏图。
二、图的遍历
2.1 深度优先遍历
设图
G
G
G的初始状态是所有的顶点都未被访问,那么在
G
G
G中任选一个顶点
v
v
v作为源点,则深度优先搜索的步骤为:
-1.访问
v
v
v,并标记;
-2.考察与
v
v
v相邻的顶点
w
w
w,若
w
w
w未被标记,则将
w
w
w作为源点,递归的深度优先搜索,直到与
v
v
v连通的顶点均被标记;
-3.访问未被标记的点,并作为源点进行深度优先搜索,直到图的所有顶点被标记。
深度优先搜索尽可能对纵深方向上进行搜索,在搜索的过程中,根据访问顺序给顶点进行编号,称为深度优先编号,而得到的顶点序列称为深度优先【Depth First Search,DFS】序列。图的所有顶点与搜索过程经过的边构成了一个森林,称为生成森林。
2.2 广度优先遍历
设图
G
G
G的初始状态是所有的顶点都未被访问,那么在
G
G
G中任选一个顶点
v
v
v作为源点,则广度优先搜索的步骤为:
-1.访问
v
v
v,并标记,将
v
v
v放入集合
V
V
V;
-2.依次考察所有与
V
V
V中顶点相邻的未标记集合
w
1
,
w
2
,
.
.
.
w
t
w_1, w_2, ...w_t
w1,w2,...wt形成新的
V
V
V,依次访问集合
V
V
V的顶点并标记;
-3.迭代2,直到与
v
v
v连通的顶点均被标记;
-4.访问未被标记的点,并作为源点进行广度优先搜索,直到图的所有顶点被标记。
广度优先搜索尽可能对横向方向上进行搜索,在搜索的过程中,根据访问顺序给顶点进行编号,称为广度优先编号,而得到的顶点序列称为广度优先【Breadth First Search,BFS】序列。图的所有顶点与搜索过程经过的边构成了生成森林。
2.3 图与树
生成森林分为先深生成森林与先广生成森林,其在树的树边基础上增加了回退边,分别对应图搜索经过的边与剩余的边。
连通而无环路的无向图称为开放树,当取任意一个顶点作为根都可以得到一棵树。
2.4 最小生成树
设
G
=
(
V
,
E
)
G = (V, E)
G=(V,E)是一个无向连通网,
E
E
E的每一条边
(
u
,
v
)
(u, v)
(u,v)都存在权值
c
(
u
,
v
)
c(u, v)
c(u,v),那么
G
G
G的生成树上各边权值之和称为生成树的代价。
在图
G
G
G的所有生成树中,代价最小的生成树称为最小生成树。
设
U
U
U是
V
V
V的一个非空子集,若
(
u
,
v
)
(u, v)
(u,v)是一条具有最小权值的边,且
u
∈
U
u \in U
u∈U,
v
∈
V
−
U
v \in V - U
v∈V−U,则必存在一颗包含边
(
u
,
v
)
(u, v)
(u,v)的最小生成树。此条性质称为最小生成树【Minimum Spanning Tree,MST】性质。
考察MST性质的正确性,考虑最小生成树
T
T
T不包含边
(
u
,
v
)
(u, v)
(u,v),那么必然有另一条边
(
u
′
,
v
′
)
(u', v')
(u′,v′)连接
U
U
U与
V
−
U
V - U
V−U,当使用边
(
u
,
v
)
(u, v)
(u,v)代替
(
u
′
,
v
′
)
(u', v')
(u′,v′)就形成了新的生成树
T
′
T'
T′,而边
(
u
′
,
v
′
)
(u', v')
(u′,v′)的权高于
(
u
,
v
)
(u, v)
(u,v),故生成树
T
T
T并不是最小生成树。
因此在构造最小生成树的过程中,连接顶点集合应在边集合中选择权值最小的边,这便是最小生成树算法的基本思想。
2.5 普里姆算法
普里姆【Prim】算法用于构造最小生成树,其基本思想为在非连通分量中寻找权值最短的边。对于图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E),其算法步骤形如
1.从顶点集合
V
V
V中任去一顶点
v
0
v_0
v0,初始化
U
=
{
v
0
}
U = \{v_0\}
U={v0},并令边集
T
E
=
{
}
TE = \{\}
TE={};
2.对于顶点
v
∈
V
−
U
v \in V-U
v∈V−U,
u
∈
U
u \in U
u∈U,取权值最小的边
(
u
,
v
)
(u, v)
(u,v)加入
T
E
TE
TE,并将
v
v
v加入
U
U
U;
3.迭代2,直到
U
=
V
U = V
U=V,那么
T
=
(
U
,
T
E
)
T = (U, TE)
T=(U,TE)就是
G
G
G的一颗最小生成树。
普里姆算法以顶点为基准,其时间性能为
O
(
p
2
)
O(p^2)
O(p2)。
2.6 克鲁斯卡尔算法
克鲁斯卡尔【Kruskal】算法用于构造最小生成树,其基本思想为寻找权值最短的边并判断其连通性。对于图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E),其算法步骤形如
1.初始化集合
U
=
V
U = V
U=V,边集
T
E
=
{
}
TE = \{\}
TE={};
2.考察
E
E
E中未被标记的最短边
(
u
,
v
)
(u, v)
(u,v),若顶点
u
u
u、
v
v
v属于
T
T
T的不同分量,并加入
T
E
TE
TE,否则丢弃;考察后标记该边;
3.迭代2,直到
T
T
T的连通分量为1,那么
T
=
(
U
,
T
E
)
T = (U, TE)
T=(U,TE)就是
G
G
G的一颗最小生成树。
克鲁斯卡尔算法以边为基准,其时间性能为
O
(
q
l
o
g
q
)
O(qlogq)
O(qlogq)。
三、图的拓扑结构
3.1 图的双连通性
设
G
=
(
V
,
E
)
G = (V, E)
G=(V,E)是一个连通的无向图,若存在
v
,
w
,
a
∈
V
v, w, a \in V
v,w,a∈V,
v
≠
w
≠
a
v \ne w \ne a
v=w=a,使得
v
v
v与
w
w
w之间的任意路径均包括
a
a
a,称
a
a
a为图的关节点。当单连通图删去关节点及其相邻的边后,图的一个连通分量将分割成两个及以上。
设
G
=
(
V
,
E
)
G = (V, E)
G=(V,E)是一个连通的无向图,若对于
V
V
V中相异的任意三元组
v
,
w
,
a
v, w, a
v,w,a,都在
v
v
v与
w
w
w之间存在一条不包括
a
a
a的路径,称
G
G
G是双连接图。双连接图没有关节点。
若生成树的根有两株及以上子树,则根必为(第一类)关节点;而生成树的非叶顶点
v
v
v,其子树的节点均没有指向
v
v
v的祖先的回退边,则
v
v
v是(第二类)关节点。
考虑无向图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E),其关节点的求取算法步骤形如
1.计算树的先深编号
d
f
n
[
v
]
dfn[v]
dfn[v],形成深度优先生成树
S
=
(
V
,
T
)
S = (V, T)
S=(V,T);
2.计算按后序计算顶点的
l
o
w
[
v
]
low[v]
low[v],形如
l
o
w
[
v
]
=
m
i
n
{
d
f
n
[
v
]
,
d
f
n
[
w
]
,
l
o
w
[
y
]
}
low[v] = min\{dfn[v], dfn[w], low[y]\}
low[v]=min{dfn[v],dfn[w],low[y]}其中
(
v
,
w
)
(v, w)
(v,w)是回退边,
y
y
y是
v
v
v的子结点;
3.若
v
v
v是根且有两个及以上的子结点,为(第一类)关节点;若
v
v
v存在某个子结点
y
y
y,存在
l
o
w
[
y
]
≥
d
f
n
[
v
]
low[y] \ge dfn[v]
low[y]≥dfn[v],则为(第二类)关节点。
3.2 拓扑排序算法
在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,称为顶点表示活动【Activity On Vertex,AOV】网。AOV网的弧表示活动之间的制约关系,且不能出现回路。
AOV网的拓扑排序的基本思想为访问没有前驱顶点,并删除该顶点与以其为尾的弧。其实质是广度优先搜索算法。
3.3 关键路径算法
在一个表示工程的有权有向图中,用顶点表示事件,用弧表示活动,权表示代价,称为边表示活动【Activity On Edge,AOE】网。AOE网应该是无环的,并且有唯一的入度为0的源点与出度为0的汇点。
在AOE网中,由于活动可以并行,故完成活动的最小代价是源点到汇点的最大路径长度,称为关键路径。其算法步骤形如
1.从源点
v
1
v_1
v1出发,使用拓扑排序算法计算其他顶点事件最早发生的时间
V
E
(
k
)
=
m
a
x
{
V
E
(
j
)
+
a
c
t
(
⟨
j
,
k
⟩
)
}
VE(k) = max\{VE(j) + act(\lang j, k \rang)\}
VE(k)=max{VE(j)+act(⟨j,k⟩)}并令
V
E
(
1
)
=
0
VE(1) = 0
VE(1)=0;
2.从汇点
v
n
v_n
vn出发,使用逆拓扑排序算法计算其他顶点事件最迟发生的时间
V
L
(
j
)
=
m
i
n
{
V
L
(
k
)
−
a
c
t
(
⟨
j
,
k
⟩
)
}
VL(j) = min\{VL(k) - act(\lang j, k \rang)\}
VL(j)=min{VL(k)−act(⟨j,k⟩)}并令
V
L
(
n
)
=
V
E
(
n
)
VL(n) = VE(n)
VL(n)=VE(n);
3.计算活动
⟨
v
j
,
v
k
⟩
\lang v_j, v_k \rang
⟨vj,vk⟩的最早开始时间
E
=
V
E
(
j
)
E = VE(j)
E=VE(j)与最迟开始时间
L
=
V
L
(
k
)
−
a
c
t
(
⟨
j
,
k
⟩
)
L = VL(k) - act(\lang j, k \rang)
L=VL(k)−act(⟨j,k⟩),那么
E
=
L
E = L
E=L的活动即为关键活动,所有关键活动的集合即为关键路径。
3.4 最短路径算法
在一个AOE网中,从一个顶点到达另一个顶点之间路径长度最短的路径称为最短路径,其长度称为最短路径长度。
迪科斯彻【Dijkstra】算法是一种按路径长度的递增次序,逐步产生最短路径的贪心算法。其基本思想为求出两顶点之间长度最短的一条路径,并参照其求出其他顶点的最短路径,直到从顶点到其他所有顶点的最短路径全部求出。
考虑带权有向图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E),以
v
1
v_1
v1为原点,使用数组
D
D
D记录
v
1
v_1
v1到
v
i
v_i
vi的最短路径长度,
P
P
P记录
v
1
v_1
v1到
v
i
v_i
vi的最短路径上经过的顶点,
C
C
C记录顶点之间的权,其算法步骤形如
1.将
V
V
V分为最短路径已经确定的顶点集合
S
S
S与未确定的顶点集合
V
−
S
V - S
V−S,并初始化
S
=
v
0
S = {v_0}
S=v0,
P
[
i
]
=
1
P[i] = 1
P[i]=1,
D
[
i
]
=
C
[
1
]
[
j
]
D[i] = C[1][j]
D[i]=C[1][j];
2.在
V
−
S
V - S
V−S选取顶点
w
w
w,使得
w
w
w与
v
0
v_0
v0的距离最小,即
D
[
w
]
=
m
i
n
{
D
[
i
]
∣
i
∈
V
−
S
}
D[w] = min\{D[i]|i \in V-S\}
D[w]=min{D[i]∣i∈V−S}从而,
v
0
v_0
v0到达
w
w
w仅通过
S
S
S的点,且是一条最短路径,并将
w
w
w加入
S
S
S;
3.更新
D
[
v
]
,
v
∈
V
−
S
D[v], v \in V - S
D[v],v∈V−S,有
D
[
v
]
=
m
i
n
{
D
[
v
]
,
D
[
w
]
+
C
[
w
]
[
v
]
}
D[v] = min\{D[v], D[w] + C[w][v]\}
D[v]=min{D[v],D[w]+C[w][v]}且
P
[
v
]
=
w
P[v] = w
P[v]=w;
4.迭代2-3,直到
S
=
V
S = V
S=V,
D
D
D便记录了最短路径长度,
P
P
P记录了最短路径。
弗洛伊德【Floyd】算法是一种系统的在原路径中加入顶点并调整的动态规划算法。其基本思想为两顶点之间的最短路径,及在两顶点之间增加一个顶点得到的依次三个顶点之间的最短路径相同。
考虑带权有向图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E),
C
C
C记录顶点之间的权,
P
P
P存储了两点的最短距离,
A
A
A存储了两点之间的迭代过程路径长度,并有迭代公式
A
0
[
i
]
[
j
]
=
C
[
i
]
[
j
]
A
k
[
i
]
[
j
]
=
m
i
n
{
A
k
−
1
[
i
]
[
j
]
,
A
k
−
1
[
i
]
[
k
]
+
A
k
−
1
[
k
]
[
j
]
}
,
0
≤
k
≤
n
−
1
A^0[i][j] = C[i][j] \\ A^{k}[i][j] = min\{ A^{k-1}[i][j], A^{k-1}[i][k] + A^{k-1}[k][j]\}, 0 \le k \le n - 1
A0[i][j]=C[i][j]Ak[i][j]=min{Ak−1[i][j],Ak−1[i][k]+Ak−1[k][j]},0≤k≤n−1那么算法步骤形如
1.初始化
A
0
[
i
]
[
j
]
=
C
[
i
]
[
j
]
A^0[i][j] = C[i][j]
A0[i][j]=C[i][j],
k
=
1
k = 1
k=1;
2.使用迭代公式
A
k
[
i
]
[
j
]
=
m
i
n
{
A
k
−
1
[
i
]
[
j
]
,
A
k
−
1
[
i
]
[
k
]
+
A
k
−
1
[
k
]
[
j
]
}
A^{k}[i][j] = min\{ A^{k-1}[i][j], A^{k-1}[i][k] + A^{k-1}[k][j]\}
Ak[i][j]=min{Ak−1[i][j],Ak−1[i][k]+Ak−1[k][j]};
3.迭代n次2,并令
P
[
i
]
[
j
]
=
A
n
[
i
]
[
j
]
P[i][j] = A^{n}[i][j]
P[i][j]=An[i][j]。
使用
D
[
i
]
[
j
]
D[i][j]
D[i][j]表示结点的最短距离,那么称
E
(
k
)
=
m
a
x
{
d
[
i
]
[
k
]
∣
i
∈
V
}
E(k) = max\{d[i][k]|i \in V\}
E(k)=max{d[i][k]∣i∈V}称为顶点
k
k
k的偏心度,具有最小偏心度的顶点称为图的中心点。