图 G 由顶点集 V 和边集 E 组成,记为
G
=
(
V
,
E
)
G=(V, E)
G=(V,E),其中 V(G) 表示图 G 中顶点的有限非空集;E(G) 表示图 G 中顶点之间的关系(边)的集合
若
V
=
{
v
1
,
v
2
,
.
.
.
,
v
n
}
V = \{ v_1, v_2, ..., v_n \}
V={v1,v2,...,vn},则用
∣
V
∣
| V |
∣V∣ 表示图 G 中顶点的个数,也称图 G 的阶,
E
=
{
(
u
,
v
)
∣
u
∈
V
,
v
∈
V
}
E = \{ ( u,v ) | u \in V, v \in V \}
E={(u,v)∣u∈V,v∈V},用
∣
E
∣
| E |
∣E∣ 表示图 G 中边的条数
线性表、树都可以为空,但图不能为空
有向图
若 E 是有向边(也称弧)的有限集合,则图 G 为有向图
弧是顶点的有序对,记为
⟨
v
,
w
⟩
\left \langle v,w \right \rangle
⟨v,w⟩,其中 v, w 是顶点,v 称为弧尾,w称为弧头,
⟨
v
,
w
⟩
\left \langle v,w \right \rangle
⟨v,w⟩ 称为从顶点 v 到顶点 w 的弧,也称 v 邻接到 w,或 w 邻接自 v
无向图
若 E 是无向边(简称边)的有限集合,则图 G 为无向图
边是顶点的无序对,记为
(
v
,
w
)
( v,w )
(v,w) 或
(
w
,
v
)
( w,v )
(w,v),因为
(
v
,
w
)
=
(
w
,
v
)
( v,w )=( w,v )
(v,w)=(w,v),其中 v, w 是顶点,可以说顶点 w 和顶点 v 互为邻接点,边
(
v
,
w
)
( v,w )
(v,w) 依附于顶点 w 和 v,或者说边
(
v
,
w
)
( v,w )
(v,w) 和顶点 w, v 相关联
简单图
一个图 G 若满足:① 不存在重复边;② 不存在顶点到自身的边,则称图 G 为简单图
数据结构中仅讨论简单图
多重图
若图 G 中某两个点之间的边数多于一条,又允许顶点通过同一条边和自己关联,则 G 为多重图
完全图(也称简单完全图)
对于无向图,
∣
E
∣
| E |
∣E∣ 的取值范围是
0
0
0 到
n
(
n
−
1
)
/
2
n(n-1)/2
n(n−1)/2,有
n
(
n
−
1
)
/
2
n(n-1)/2
n(n−1)/2 条边的无向图称为无向完全图,在完全图中任意两个顶点之间都存在边
对于有向图,
∣
E
∣
| E |
∣E∣ 的取值范围是
0
0
0 到
n
(
n
−
1
)
n(n-1)
n(n−1),有
n
(
n
−
1
)
n(n-1)
n(n−1) 条边的无向图称为有向完全图,在有向完全图中任意两个顶点之间都存在方向相反的两条弧
子图
设有两个图
G
=
(
V
,
E
)
G=(V, E)
G=(V,E) 和
G
′
=
(
V
,
′
E
′
)
G'=(V,' E')
G′=(V,′E′),若
V
′
V'
V′ 是
V
V
V 的子集,若
E
′
E'
E′ 是
E
E
E 的子集,则称
G
′
G'
G′ 是
G
G
G 的子图
若有满足
V
′
(
G
)
=
V
(
G
)
V'(G) = V(G)
V′(G)=V(G) 的子图
G
′
G'
G′ ,则称其为
G
G
G 的生成子图
连通、连通图和连通分量
在无向图中,若从顶点 v 到顶点 w 有路径存在,则称 v 和 w 是连通的
若图 G 中任意两个顶点都是连通的,则称图 G 为连通图,否则称为非连通图
无向图中的极大连通子图称为连通分量
n 个顶点的连通图最少有 n-1 条边
强连通图、强连通分量
在有向图中,若从顶点 v 到顶点 w 和顶点 w 到顶点 v之间都有路径,则称这两个顶点是强连通的
若图中任何一对顶点都是强连通的,则称此图为强连通图
有向图中的极大强连通子图称为有向图的强连通分量
n 个顶点的连通图最少有 n 条边
生成树、生成森林
连通图的生成树是包含图中全部顶点的一个极小连通子图
若图中顶点数为 n,则它的生成树含有 n-1 条边
对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会变形成一个回路
在非连通图中,连通分量的生成树构成了非连通图的生成森林
顶点的度、入度和出度
图中每个顶点的度定义为以该顶点为一个端点的边的数目
对于无向图,顶点 v 的度是指依附于该顶点的边的条数,记为 TD(v)
在具有 n 个顶点、e 条边的无向图中,
∑
i
=
1
n
T
D
(
v
i
)
=
2
e
\sum_{i=1}^{n}TD(v_i) = 2e
∑i=1nTD(vi)=2e,即无向图的全部顶点的度的和等于边数的两倍,因为每条边和两个顶点相关联
对于有向图,顶点 v 的度分为入度和出度,入度是以顶点 v 为终点的有向边的数目,记为
I
D
(
v
)
ID(v)
ID(v);而出度是以顶点 v 为起点的有向边的条数,记为
O
D
(
v
)
OD(v)
OD(v)。顶点 v 的度等于其入度和出度之和,即
T
D
(
v
)
=
I
D
(
v
)
+
O
D
(
v
)
TD(v) = ID(v) + OD(v)
TD(v)=ID(v)+OD(v)
在具有 n 个顶点、e 条边的无向图中,
∑
i
=
1
n
I
D
(
v
i
)
=
∑
i
=
1
n
O
D
(
v
i
)
=
e
\sum_{i=1}^{n}ID(v_i) = \sum_{i=1}^{n}OD(v_i) = e
∑i=1nID(vi)=∑i=1nOD(vi)=e,即有向图的全部顶点的入度之和与出度之和相等,并且等于边数。这是因为每条有向边都有一个起点和终点
边的权和网
在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值
这种边上带有权值的图称为带权图,也称网
稠密图、稀疏图
边数很少的图称为稀疏图,反之称为稠密图。稀疏和稠密本身是模糊的概念,稀疏图和稠密图常常是相对而言的
一般当图 G 满足
∣
E
∣
<
∣
V
∣
l
o
g
∣
V
∣
|E| < |V| log|V|
∣E∣<∣V∣log∣V∣ 时,可以将 G 视为稀疏图
路径、路径长度和回路
顶点
v
p
v_p
vp 到顶点
v
q
v_q
vq 之间的一条路径是指顶点序列
v
p
,
v
i
1
,
v
i
2
,
.
.
.
,
v
i
m
,
v
q
v_p, v_{i_1}, v_{i_2}, ..., v_{i_m}, v_q
vp,vi1,vi2,...,vim,vq,当然,关联的边也可以理解为路径的构成要素
结点数为
n
n
n 的图
G
=
(
V
,
E
)
G = (V, E)
G=(V,E) 的邻接矩阵
A
A
A 是
n
×
n
n×n
n×n 的。将
G
G
G 的顶点编号为
v
1
,
v
2
,
.
.
.
,
v
n
v_1, v_2, ..., v_n
v1,v2,...,vn。若
(
v
i
,
v
j
)
∈
E
(v_i, v_j) \in E
(vi,vj)∈E,则
A
[
i
]
[
j
]
=
1
A[i][j] = 1
A[i][j]=1,否则
A
[
i
]
[
j
]
=
0
A[i][j] = 0
A[i][j]=0
A
[
i
]
[
j
]
=
{
1
,
若
(
v
i
,
v
j
)
或
⟨
v
i
,
v
j
⟩
是
E
(
G
)
中
的
边
0
,
若
(
v
i
,
v
j
)
或
⟨
v
i
,
v
j
⟩
不
是
E
(
G
)
中
的
边
A[i][j] = \begin{cases} 1,若(v_i,v_j)或\left \langle v_i,v_j \right \rangle是E(G)中的边 \\ 0,若(v_i,v_j)或\left \langle v_i,v_j \right \rangle不是E(G)中的边\end{cases}
A[i][j]={1,若(vi,vj)或⟨vi,vj⟩是E(G)中的边0,若(vi,vj)或⟨vi,vj⟩不是E(G)中的边
对于带权图而言,若顶点
v
i
v_i
vi 和
v
j
v_j
vj 之间有边相连,则邻接矩阵中对应项存放着该边对应的权值,若顶点
v
i
v_i
vi 和
v
j
v_j
vj 不相连,则用
∞
\infty
∞来代表这两个顶点之间不存在边
A
[
i
]
[
j
]
=
{
w
i
j
,
若
(
v
i
,
v
j
)
或
⟨
v
i
,
v
j
⟩
是
E
(
G
)
中
的
边
0
或
∞
,
若
(
v
i
,
v
j
)
或
⟨
v
i
,
v
j
⟩
不
是
E
(
G
)
中
的
边
A[i][j] = \begin{cases} w_{ij},若(v_i,v_j)或\left \langle v_i,v_j \right \rangle是E(G)中的边 \\ 0或\infty,若(v_i,v_j)或\left \langle v_i,v_j \right \rangle不是E(G)中的边\end{cases}
A[i][j]={wij,若(vi,vj)或⟨vi,vj⟩是E(G)中的边0或∞,若(vi,vj)或⟨vi,vj⟩不是E(G)中的边
⑥ 设图
G
G
G 的邻接矩阵为
A
A
A,
A
n
A^n
An 的元素
A
n
[
i
]
[
j
]
A^n[i][j]
An[i][j] 等于由顶点
i
i
i 到顶点
j
j
j 的长度为
n
n
n 的路径的数目
6.2.2 邻接表法
当一个图为稀疏图时,使用邻接表法,大大减少了空间的浪费
所谓邻接表,是指对图
G
G
G 的每个顶点
v
i
v_i
vi 建立一个单链表,第
i
i
i 个单链表中的结点表示依附于顶点
v
i
v_i
vi 的边(对于有向图则是以顶点
v
i
v_i
vi 为尾的弧),这个单链表就称为顶点
v
i
v_i
vi 的边表(对于有向图则称出边表)。边表的头指针的顶点的数据信息采用顺序存储(称为顶点表),所以在邻接表中存在两种结点:顶点表结点和边表结点
① 若
G
G
G 为无向图,则所需的存储空间为
O
(
∣
V
∣
+
2
∣
E
∣
)
O(|V|+2|E|)
O(∣V∣+2∣E∣);若
G
G
G 为有向图,则所需的存储空间为
O
(
∣
V
∣
+
∣
E
∣
)
O(|V|+|E|)
O(∣V∣+∣E∣)。前者的倍数 2 是由于无向图中,每条边在邻接表中出现了两次
② 对于稀疏图,采用邻接表表示将极大地节省存储空间
③ 在邻接表中,给定一顶点,能很容易地找出它的所有邻边,因为只需要读取它的邻接表。在邻接矩阵中,相同的操作则需要扫描一行,花费的时间为
O
(
n
)
O(n)
O(n)。但是,若要确定给定的两个顶点间是否存在边,则在邻接矩阵中可以立刻查到,而在邻接表中则需要在相应结点对应的边表中查找另一结点,效率较低
④ 若
G
G
G 为无向图,则结点的度为该节点边表的长度;若
G
G
G 为有向图,则结点的出度为该节点边表的长度,但计算入度则需要遍历全部的邻接表。因此,也有人采用逆邻接表的存储方式来加速求解给定顶点的入度
bool visited[MAX_VERTEX_NUM];//访问标记数组voidBFSTraverse(Graph G){//对图G进行广度优先遍历for(int i =0; i < G.vexnum; i++){
visited[i]= false;//访问标记数组初始化}//forInitQueue(Q);//初始化辅助队列Qfor(int i =0; i < G.vexnum; i++){//从0号顶点开始遍历if(!visited[i]){//对每个连通分量调用一次BFSBFS(G, i);//vi未被访问过,从vi开始BFS}//if}//for}voidBFS(Graph G,int v){//从顶点v出发,广度优先遍历图Gvisit(v);//访问初始顶点v
visited[v]= true;//对v左已访问标记EnQueue(Q, v);while(!isEmpty(Q)){DeQueue(Q, v);//顶点v出队列for(int w =FirstNeighbor(G, v); w >=0; w =NextNeighbor(G, v, w)){//检测v所有邻接点if(!visited[w]){//w为v的尚未访问的邻接顶点visit(w);//访问顶点w
visited[w]= true;//对w做已访问标记EnQueue(Q, w);//顶点w如队列}//if}//for}//while}
BFS算法的性能分析
无论邻接表还是邻接矩阵的存储方式,BFS算法都需要借助一个辅助队列
Q
Q
Q,
n
n
n 个顶点均需入队一次,在最坏的情况下,空间复杂度为
O
(
∣
V
∣
)
O(|V|)
O(∣V∣)
采用邻接表存储方式时,每个顶点均需搜索一次(或入队一次),故时间复杂度为
O
(
∣
V
∣
)
O(|V|)
O(∣V∣),在搜索任意顶点的邻接点时,每条边至少访问一次,故时间复杂度为
O
(
∣
E
∣
)
O(|E|)
O(∣E∣),故算法总的时间复杂度为
O
(
∣
V
∣
+
∣
E
∣
)
O(|V|+|E|)
O(∣V∣+∣E∣)
采用邻接矩阵存储方式时,查找每个顶点的邻接点所需的时间为
O
(
∣
V
∣
)
O(|V|)
O(∣V∣),故算法总的时间复杂度为
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2)
BFS算法求解单源最短路径问题
若图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E) 为非带权图,定义从顶点
u
u
u 到 顶点
v
v
v 的最短路径
d
(
u
,
v
)
d(u,v)
d(u,v) 为从
u
u
u 到
v
v
v 的任何路径中最少的边数;若从
u
u
u 到
v
v
v 没有通路,则
d
(
u
,
v
)
=
∞
d(u,v) = \infty
d(u,v)=∞
voidBFS_MIN_Distance(Graph G,int u){for(int i =0; i < G.vexnum; i++){
d[i]= MAX;//初始化路径长度}
visited[u]= true;
d[u]=0;EnQueue(Q, u);while(!isEmpty(Q)){//BFS算法主过程DeQueue(Q, u);//队头元素u出队for(int w =FirstNeighbor(G, u); w >=0; w =NextNeighbor(G, u, w)){if(!visit[w]){//w为u的尚未访问的邻接顶点
visited[w]= true;//设已访问标记
d[w]= d[u]+1;//路径长度加1EnQueue(Q, w);//顶点w入队}//if}//while}}
GENERIC_MST(G){
T =NULL;while T未形成一颗生成树;do 找到一条最小代价边(u,v)并且加入T后不会产生回路;
T = T∪(u,v);}
Prim算法
Prim(普里姆)算法的执行非常类似于寻找图的最短路径的Dijkstra算法
Prim算法构造最小生成树的过程:
初始时,从图
G
G
G 中任取一顶点加入树
T
T
T,此时树中只含有一个顶点
之后,从图
G
G
G 中选择一个与当前
T
T
T 中顶点集合距离最近的顶点,并将该顶点和相应的边加入
T
T
T,每次操作后
T
T
T 中的顶点数和边数都增加
1
1
1
以此类推,直至图中所有的顶点都并入
T
T
T,得到的
T
T
T 就是最小生成树,此时
T
T
T 中必然有
n
−
1
n-1
n−1 条边
Prim算法的步骤:
假设
G
=
{
V
,
E
}
G = \{ V,E \}
G={V,E} 是连通图,其最小生成树
T
=
(
U
,
E
T
)
T=(U,E_T)
T=(U,ET),
E
T
E_T
ET 是最小生成树中边的集合
初始化:向空树
T
=
(
U
,
E
T
)
T=(U,E_T)
T=(U,ET) 中添加图
G
=
(
V
,
E
)
G=(V,E)
G=(V,E) 中的任一顶点
u
0
u_0
u0,使
U
=
{
u
0
}
U =\{u_0\}
U={u0},
E
T
=
∅
E_T=\varnothing
ET=∅
循环(重复下列操作直至
U
=
V
U=V
U=V):从图
G
G
G 中选择满足
{
(
u
,
v
)
∣
u
∈
U
,
v
∈
V
−
U
}
\{ (u,v) | u \in U, v \in V-U \}
{(u,v)∣u∈U,v∈V−U} 且具有最小权值的边
(
u
,
v
)
(u,v)
(u,v),加入树
T
T
T,置
U
=
U
∪
{
v
}
U = U \cup \{v\}
U=U∪{v},
E
T
=
E
T
∪
{
(
u
,
v
)
}
E_T = E_T \cup \{(u,v)\}
ET=ET∪{(u,v)}
voidPrim(G, T){
T = ∅;//初始化空树
U ={w};//添加任一顶点wwhile((V - U)!= ∅){//若树中不含全部顶点
设(u,v)是使 u∈U与v∈(V-U),且权值最小的边;
T = T∪{(u,v)};//边归入树
U = U∪{v};//顶点归入树}}
需要辅助数组min_weight[n] 和 adjvex[n]
min_weight[n] 表示从当前树
T
T
T 中的顶点到下标为n的顶点的最小权值,0表示已加入树
T
T
T
Prim算法的时间复杂度为
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2),不依赖于
∣
E
∣
|E|
∣E∣,因此它适用于求解边稠密的图的最小生成树
Kruskal算法
Kruskal(克鲁斯卡尔)算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法
Kruskal算法构造最小生成树的过程:
初始时,为只有
n
n
n 个顶点而无边的非连通图
T
=
{
V
,
{
}
}
T = \{ V, \{\}\}
T={V,{}},每个顶点自成一个连通分量
然后,按照边的权值由大到小的顺序,不断选取当前未被选取过且权值最小的边,若该边衣服的顶点落在
T
T
T 中不同的连通分量上,则将此边加入
T
T
T,否则舍弃此边而选择下一条权值最小的边
以此类推,直至
T
T
T 中所有顶点都在一个连通分量上
Kruskal算法的步骤:
假设
G
=
(
V
,
E
)
G = ( V,E )
G=(V,E) 是连通图,其最小生成树
T
=
(
U
,
E
T
)
T=(U,E_T)
T=(U,ET)
初始化:
U
=
V
U =V
U=V,
E
T
=
∅
E_T=\varnothing
ET=∅。即每个顶点构成一棵独立的树,
T
T
T 此时是一个仅含
∣
V
∣
|V|
∣V∣ 个顶点的森林
循环(重复下列操作直至
T
T
T 是一棵树):按
G
G
G 的边的权值递增顺序依次从
E
−
E
T
E-E_T
E−ET中选择一条边,若这条边加入
T
T
T 后不构成回路,则将其加入
E
T
E_T
ET,否则舍弃,直到
E
T
E_T
ET 中含有
n
−
1
n-1
n−1条边
voidKruskal(V,T){
T = V;//初始化树T,仅含顶点
numS = n;//连通分量数while(numS >1){//若连通分量数大于1
从E中取出权值最小的边(u,v);if(v和u属于T中不同的连通分量){
T = T∪{(v,u)};//将此边加入生成树中
numS--;//连通分量数减1}}}
需要借助堆排序和并查集
采用堆排序来存放边的集合
由于生成树
T
T
T 中的所有边可视为一个等价类,因此每次添加新的边的过程类似于求解等价类的过程,由此可以采用并查集的数据结构来描述
T
T
T
typedefstruct Edge {int a, b;//边对应两个端点的下标int weight;//边的权值};voidMST_Kruskal(Graph G, Edge* edges,int*parent){heap_sort(edges);//对边进行堆排序Initial(parent);//初始化并查集for(int i =0; i < G.arcnum; i++){int a_root =Find(parent, edges[i].a);//查找顶点a在并查集中的根结点int b_root =Find(parent, edges[i].b);//查找顶点a在并查集中的根结点if(a_root != b_root){//如果顶点a和顶点b不在同一个集合(连通分量)中Union(parent, a_root, b_root);//连接两个顶点,合并两个连通分量}}}
Kruskal算法的时间复杂度为
O
(
∣
E
∣
l
o
g
∣
E
∣
)
O(|E|log|E|)
O(∣E∣log∣E∣) (堆排序
O
(
l
o
g
∣
E
∣
)
O(log|E|)
O(log∣E∣)),因此Kruskal算法适适合于边稀疏而顶点较多的图
6.4.2 最短路径
当图时带权图时,把从一个顶点
V
0
V_0
V0 到图中其余任意一个顶点
v
i
v_i
vi 的一条路径(可能不止一条)所经过边上的权值之和,定义为该路径的带权路径长度,把带权路径长度最短的那条路径称为最短路径
求解最短路径的算法通常都依赖于一种性质,即两点之间的最短路径也包含了路径上其他顶点间的最短路径。带权有向图
G
G
G 的最短路径问题一般可分为两类:一是单源最短路径,即求图中某一顶点到其他各顶点的最短路径,可通过Dijkstra(迪杰斯特拉)算法求解;二是求没对顶点间的最短路径,可通过Floyd(弗洛伊德)算法来求解
Dijkstra算法求单源最短路径问题
Dijkstra算法设置一个集合
S
S
S 记录已求得的最短路径的顶点,初始时把源点
v
0
v_0
v0 放入
S
S
S,集合
S
S
S 每并入一个新顶点
v
i
v_i
vi,都要修改源点
v
0
v_0
v0 到集合
V
−
S
V-S
V−S 中顶点当前的最短路径长度值
还设置了两个辅助数组:
dist[]:记录从源点
v
0
v_0
v0 到其他各顶点当前的最短路径长度,它的初态为:若从
v
0
v_0
v0 到
v
i
v_i
vi 有弧,则dist[i]为弧上的权值;否则置dist[i]为
∞
\infty
∞
path[]:path[i]表示从源点到顶点i之间的最短路径的前驱结点。在算法结束时,可根据其值追溯得到源点
v
0
v_0
v0 到顶点
v
i
v_i
vi 的最短路径
假设从顶点
0
0
0 出发,即
v
0
=
0
v_0 = 0
v0=0,集合
S
S
S 最初只包含顶点
0
0
0,邻接矩阵
a
r
c
s
arcs
arcs 表示带权有向图,Dijkstra 算法的步骤如下(不考虑对 path[] 的操作)
1)初始化:集合
S
S
S 初始为
{
0
}
\{0\}
{0},
d
i
s
t
[
]
dist[]
dist[] 的初始值
d
i
s
t
[
i
]
=
a
r
c
s
[
0
]
[
i
]
,
i
=
1
,
2
,
.
.
.
,
n
−
1
dist[i]=arcs[0][i], i = 1,2,...,n-1
dist[i]=arcs[0][i],i=1,2,...,n−1
2)从顶点集合
V
−
S
V-S
V−S 中选出
v
j
v_j
vj,满足
d
i
s
t
[
j
]
=
M
i
n
{
d
i
s
t
[
i
]
∣
v
i
∈
V
−
S
}
dist[j] = Min\{dist[i] | v_i \in V-S\}
dist[j]=Min{dist[i]∣vi∈V−S},
v
j
v_j
vj 就是当前求得的一条从
v
0
v_0
v0 出发的最短路径的终点,令
S
=
S
∩
{
j
}
S = S \cap \{j\}
S=S∩{j}
3)修改从
v
0
v_0
v0 出发到集合
V
−
S
V-S
V−S 上任一顶点
v
k
v_k
vk 可达的最短路径长度:若
d
i
s
[
j
]
+
a
r
c
s
[
j
]
[
k
]
<
d
i
s
t
[
k
]
dis[j] + arcs[j][k] < dist[k]
dis[j]+arcs[j][k]<dist[k],则更新
d
i
s
t
[
k
]
=
d
i
s
t
[
j
]
+
a
r
c
s
[
j
]
[
k
]
dist[k] = dist[j] + arcs[j][k]
dist[k]=dist[j]+arcs[j][k]
4)重复 2)~3)操作共
n
−
1
n-1
n−1 次,直到所有的顶点都包含在
S
S
S 中
voidDijkstra(Graph G,int v){//v为起始顶点int s[G.vexnum];int path[G.vexnum];int dist[G.vexnum];for(int i =0; i < G.vexnum; i++){//初始化,上述步骤第1)步
dist[i]= G.edge[v][i];
s[i]=0;if(G.edge[v][i]< MAX)
path[i]= v;else
path[i]=-1;}
s[v]=1;
path[v]=-1;for(int i =0; i < G.vexnum; i++){int min = MAX;int u;for(int j =0; j < G.vexnum; j++){//上述步骤第2)步if(s[j]==0&& dist[j]< min){
min = dist[j];
u = j;}}
s[u]=1;for(int j =0; j < G.vexnum; j++){//上述步骤第3)步if(s[j]==0&& dist[u]+ G.edge[u][j]< dist[j]){
dist[j]= dist[u]+ G.edges[u][i];
path[j]= u;}}}}
时间复杂度为
O
(
∣
V
∣
2
)
O(|V|^2)
O(∣V∣2)
边上有负权值时,Dijkstra算法并不适用
Floyd算法求各顶点之间最短路径问题
Floyd算法的基本思想是:
递推产生一个
n
n
n 阶方阵序列
A
(
−
1
)
,
A
(
0
)
,
A
(
1
)
,
.
.
.
,
A
(
k
)
,
.
.
.
,
A
(
n
−
1
)
A^{(-1)}, A^{(0)}, A^{(1)}, ..., A^{(k)}, ..., A^{(n-1)}
A(−1),A(0),A(1),...,A(k),...,A(n−1),其中
A
(
k
)
[
i
]
[
j
]
A^{(k)}[i][j]
A(k)[i][j] 表示从顶点
v
i
v_i
vi 到顶点
v
j
v_j
vj 的路径长度,
k
k
k 表示绕行第
k
k
k 个顶点的运算步骤
初始时,对于任意两个顶点
v
i
v_i
vi 和
v
j
v_j
vj,若它们之间存在边,则以此边上的权值作为它们之间的最短路径长度;若它们之间不存在边,则以
∞
\infty
∞作为它们的最短路径长度
以后逐步尝试在原路径中加入顶点
k
(
k
=
0
,
1
,
.
.
.
,
n
−
1
)
k(k=0,1,...,n-1)
k(k=0,1,...,n−1)作为中间顶点。若增加中间顶点后,得到 的路径比原来的路径长度减少了,则以此新路径代替原路径
算法描述如下:
定义一个
n
n
n 阶方阵序列
A
(
−
1
)
,
A
(
0
)
,
A
(
1
)
,
.
.
.
,
A
(
n
−
1
)
A^{(-1)}, A^{(0)}, A^{(1)}, ..., A^{(n-1)}
A(−1),A(0),A(1),...,A(n−1),其中
A
(
−
1
)
[
i
]
[
j
]
=
a
r
c
[
i
]
[
j
]
A^{(-1)}[i][j] = arc[i][j]
A(−1)[i][j]=arc[i][j]
A
(
k
)
[
i
]
[
j
]
=
M
i
n
{
A
(
k
−
1
)
[
i
]
[
j
]
,
A
(
k
−
1
)
[
i
]
[
k
]
+
A
(
k
−
1
)
[
k
]
[
j
]
}
,
k
=
0
,
1
,
.
.
.
,
n
−
1
A^{(k)}[i][j] = Min\{ A^{(k-1)}[i][j], A^{(k-1)}[i][k] + A^{(k-1)}[k][j] \}, k=0,1,...,n-1
A(k)[i][j]=Min{A(k−1)[i][j],A(k−1)[i][k]+A(k−1)[k][j]},k=0,1,...,n−1
式中,
A
(
0
)
[
i
]
[
j
]
A^{(0)}[i][j]
A(0)[i][j] 是从顶点
v
i
v_i
vi 到
v
j
v_j
vj、中间顶点是
v
0
v_0
v0 的最短路径长度,
A
(
k
)
[
i
]
[
j
]
A^{(k)}[i][j]
A(k)[i][j] 是从顶点
v
i
v_i
vi 到
v
j
v_j
vj 、中间顶点的序号不大于
k
k
k 的最短路径长度
Floyd算法是一个迭代的过程,每迭代一次,在从
v
i
v_i
vi 到
v
j
v_j
vj 的最短路径上就多考虑了一个顶点;经过
n
n
n 次迭代后,所得到的
A
(
n
−
1
)
[
i
]
[
j
]
A^{(n-1)}[i][j]
A(n−1)[i][j] 就是
v
i
v_i
vi 到
v
j
v_j
vj 的最短路径长度,即方阵
A
(
n
−
1
)
A^{(n-1)}
A(n−1) 就保存了任意一对顶点之间的最短路径长度
voidFloyd(Graph G){int A[G.vexnum][G.vexnum];for(int i =0; i < G.vexnum; i++){//初始化for(int j =0; j < G.vexnum; j++){
A[i][j]= G.Edge[i][j];}//for j}//for ifor(int k =0; k < G.vexnum; k++){//迭代for(int i =0; i < G.vexnum; i++){for(int j =0; j < G.vexnum; j++){if(A[i][j]> A[i][k]+ A[k][j]){
A[i][j]= A[i][k]+ A[k][j];}//if}//for j}//for i}//for k}
Floyd算法时间复杂度为
O
(
∣
V
∣
3
)
O(|V|^3)
O(∣V∣3)
Floyd算法允许图中有带负权值的边,但不允许有包含带负权值的边组成的回路
Floyd算法同样适用于带权无向图,因为带权无向图可视为权值相同往返二重边的有向图
也可以用单源最短路径算法来解决每对顶点之间的最短路径问题。轮流将每个顶点作为源点,并且在所有边权值均非负时,运行一次Dijkstra算法,其时间复杂度为
O
(
∣
V
∣
2
)
⋅
∣
V
∣
=
O
(
∣
V
∣
3
)
O(|V|^2) \cdot |V| = O(|V|^3)
O(∣V∣2)⋅∣V∣=O(∣V∣3)
AOV 网:若用DAG网表示一个工程,其顶点表示活动,用有向边
⟨
V
i
,
V
j
⟩
\left \langle V_i, V_j \right \rangle
⟨Vi,Vj⟩ 表示活动
V
i
V_i
Vi 必须先于活动
V
j
V_j
Vj 进行的这样一种关系,则将这种有向图称为顶点表示活动的网络,记为AOV 网
在 AOV 网中,活动
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
1
v_1
v1 到顶点
v
k
v_k
vk 的最长路径长度。事件
v
k
v_k
vk 的最早发生时间决定了所有从
v
k
v_k
vk 开始的活动能够开工的最早时间。可用下面的递推公式来计算:
v
e
(
源
点
)
=
0
ve(源点) = 0
ve(源点)=0
v
e
(
k
)
=
M
a
x
{
v
e
(
j
)
+
W
e
i
g
h
t
(
v
j
,
v
k
)
}
ve(k) = Max \{ve(j) + Weight(v_j, v_k)\}
ve(k)=Max{ve(j)+Weight(vj,vk)},
v
k
v_k
vk 为
v
j
v_j
vj 的任意后继,
W
e
i
g
h
t
(
v
j
,
v
k
)
Weight(v_j, v_k)
Weight(vj,vk) 表示
⟨
v
j
,
v
k
⟩
\left \langle v_j, v_k \right \rangle
⟨vj,vk⟩ 上的权值
计算
v
e
(
)
ve()
ve() 值时,按从前往后的顺序进行,可以在拓扑排序的基础上计算:
① 初始时,令
v
e
[
1...
n
]
=
0
ve[1...n] = 0
ve[1...n]=0
② 输出一个入度为 0 的顶点
v
j
v_j
vj 时,计算它所有直接后继顶点
v
k
v_k
vk 的最早发生时间, 若
v
e
[
j
]
+
W
e
i
g
h
t
(
v
j
,
v
k
)
>
v
e
[
k
]
ve[j] + Weight(v_j, v_k) > ve[k]
ve[j]+Weight(vj,vk)>ve[k],则
v
e
[
k
]
=
v
e
[
j
]
+
W
e
i
g
h
t
(
v
j
,
v
k
)
ve[k] = ve[j] + Weight(v_j, v_k)
ve[k]=ve[j]+Weight(vj,vk)。以此类推,直至输出全部顶点
事件
v
k
v_k
vk 的最迟发生时间
v
l
(
k
)
vl(k)
vl(k)
它是指在不推迟整个工程完成的前提下,即保证它的后续事件
v
j
v_j
vj 在其最迟发生时间
v
l
(
j
)
vl(j)
vl(j) 能够发生时,改时间最迟必须发生的时间。可用下面的递推公式来计算:
v
l
(
汇
点
)
=
v
e
(
汇
点
)
vl(汇点) = ve(汇点)
vl(汇点)=ve(汇点)
v
l
(
k
)
=
M
i
n
{
v
l
(
j
)
−
W
e
i
g
h
t
(
v
k
,
v
j
)
}
vl(k) = Min \{ vl(j) - Weight(v_k, v_j) \}
vl(k)=Min{vl(j)−Weight(vk,vj)},
v
k
v_k
vk 为
v
j
v_j
vj 的任意前驱
计算
v
l
(
)
vl()
vl()值 时,按从后往前的顺序进行,可以在逆拓扑排序的基础上计算(增设一个栈以记录拓扑序列,拓扑排序结束后从栈顶至栈底便为逆拓扑有序序列):
① 初始时,令
v
l
[
1...
n
]
=
v
e
[
n
]
vl[1...n] = ve[n]
vl[1...n]=ve[n]
② 栈顶顶点
v
j
v_j
vj 出栈,计算其所有直接前驱顶点
v
k
v_k
vk 的最迟发生时间,若
v
l
[
j
]
−
W
e
i
g
h
t
(
v
k
,
v
j
)
<
v
l
[
k
]
vl[j] - Weight(v_k, v_j) < vl[k]
vl[j]−Weight(vk,vj)<vl[k],则
v
l
[
k
]
=
v
l
[
j
]
−
W
e
i
g
h
t
(
v
k
,
v
j
)
vl[k] = vl[j] - Weight(v_k, v_j)
vl[k]=vl[j]−Weight(vk,vj)。以此类推,直至输出全部栈中顶点
活动
a
i
a_i
ai 的最早开始时间
e
(
i
)
e(i)
e(i)
它是指该活动弧的起点所表示的事件的最早发生时间
若边
⟨
v
k
,
v
j
⟩
\left \langle v_k, v_j \right \rangle
⟨vk,vj⟩ 表示活动
a
i
a_i
ai,则有
e
(
i
)
=
v
e
(
k
)
e(i) = ve(k)
e(i)=ve(k)
活动
a
i
a_i
ai 的最迟开始时间
l
(
i
)
l(i)
l(i)
它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差
若边
⟨
v
k
v
j
⟩
\left \langle v_k v_j \right \rangle
⟨vkvj⟩ 表示活动
a
i
a_i
ai,则有
l
(
i
)
=
v
l
(
j
)
−
W
e
i
g
h
t
(
v
k
,
v
j
)
l(i) = vl(j) - Weight(v_k, v_j)
l(i)=vl(j)−Weight(vk,vj)
一个活动
a
i
a_i
ai 的最迟开始时间
l
(
i
)
l(i)
l(i) 和其最早开始时间
e
(
i
)
e(i)
e(i) 的差额
d
(
i
)
=
l
(
i
)
−
e
(
i
)
d(i) = l(i) - e(i)
d(i)=l(i)−e(i)
它是指该活动完成的时间余量,即在不增加完成整个工程所需总时间的情况下,活动
a
i
a_i
ai 可以拖延的时间
若一个活动的时间余量为零,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以称
l
(
i
)
−
e
(
i
)
=
0
l(i) - e(i) = 0
l(i)−e(i)=0 即
l
(
i
)
=
e
(
i
)
l(i) = e(i)
l(i)=e(i) 的活动
a
i
a_i
ai 是关键活动
求关键路径
算法步骤如下:
1)从源点出发,令
v
e
(
源
点
)
=
0
ve(源点) = 0
ve(源点)=0,按拓扑有序求其余顶点的最早发生时间
v
e
(
)
ve()
ve()
2)从汇点出发,令
v
l
(
汇
点
)
=
v
e
(
汇
点
)
vl(汇点) = ve(汇点)
vl(汇点)=ve(汇点),按逆拓扑有序求其余顶点的最迟发生时间
v
l
(
)
vl()
vl()
3)根据各顶点的
v
e
(
)
ve()
ve() 值求所有弧的最早开始时间
e
(
)
e()
e()
4)根据各顶点的
v
l
(
)
vl()
vl() 值求所有弧的最迟开始时间
l
(
)
l()
l()
5)求 AOE 网中所有活动的差额
d
(
)
d()
d(),找出所有
d
(
)
=
0
d()=0
d()=0 的活动构成关键路径