图的遍历方法和图的几个典型算法
图的遍历
-
遍历定义——从已给的连通图中某一顶点出发,沿着一边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算。
-
遍历实质——找每个顶点的邻接点的过程。
-
图的特点——图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回了曾经访问过的顶点。
- 怎样避免重复
解决思路——设置辅助数组 v i s i t e d [ n ] visited[n] visited[n],用来标记没个被访问过的顶点。- 初始状态 v i s i t e d [ i ] 为 0 visited[i]为0 visited[i]为0。
- 顶点 i i i被访问,改 v i s i t e d [ i ] visited[i] visited[i]为1,防止被多次访问。
- 图常用的遍历
- 深度优先搜索(Depth_First Search——DFS)
- 广度优先搜索(Breadth_First Search——BFS)
深度优先搜索
深搜例子:
- 方法:
- 在访问图中某一起始顶点 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为止。
- 接着,退回一步,退到前一次刚访问过的顶点看是否还有其它没有被访问的邻接顶点。
- 如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;
- 如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。
例子:
- 邻接矩阵表示的无向图深度遍历实现:
- 采用邻接矩阵表示图的深度优先搜索遍历——算法实现
void DFS(AMGraph G,int v){ //图G为邻接矩阵类型
cout<<v; visited[v]=true; //访问第v个顶点
for(int w=0;w<G.vexnum;w++) //依次检查邻接矩阵v所在的行
if(G.arc[v][w]!=0&&(!visited[w]))
DFS(G,w);
// w是v的邻接点,如果w为访问,则递归调用DFS
}
- DFS算法效率分析
- 用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描所在行,时间复杂度为 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)。
- 结论:
- 稠密图适于在邻接矩阵上进行深度遍历
- 稀疏图适于在邻接表上进行深度遍历
广度优先搜索
广搜例子:
- 方法:
- 从图的某一结点出发,首先依次访问该结点的所有邻接点 V i 1 , V i 2 , . . . , V n V_i{_1},V_i{_2},...,V_n Vi1,Vi2,...,Vn再按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点
- 重复此过程,直至所有顶点均被访问为止。
例:
// 按广度优先非递归遍历连通图
void BFS(Graph G int v){
cout<<v; visited[v]=true; //访问第v个顶点
InitQueue(Q); //辅助队列Q初始化,置空
EnQueue(Q,v); //v进队
while(!QueueEmpty(Q)) { //队列非空
DeQueue(Q,u); //对头元素出对并置为u
for(int w=FirstAdjVex(G,u);w>0;w=NextAdjVex(G,u,w))
if(!visited[w]){ // w为u的尚未访问的邻接顶点
cout<<w; visited[w]=true; EnQueue(Q,w); //w进队
} // if
} // while
} // BFS
- BFS算法效率分析
- 如果使用邻接矩阵,则BFS对于每一个被访问到的顶点,都要循环检测矩阵中的整整一行( n n n个元素),总的时间代价 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)。
DFS和BFS算法效率比较
- 空间复杂度相同,都是 O ( n ) O(n) O(n)(借用了堆栈或队列)
- 时间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关。
图的应用
最小生成树
- 生成树——所有顶点均由边连接在一起,但不存在回路的图
- 一个图可以有许多生成树
- 所有生成树具有以下共同特点
- 生成树的顶点个数与图的顶点个数相同;
- 生成树是图的极小连通子图,去掉一条边则非连通;
- 一个有n个顶点的连通图的生成树有n-1条边;
- 在生成树中再加一条边必然形成回路;
- 生成树中任意两个顶点间的路径是唯一的。
- 含n个顶点n-1条边的图不一定是生成树。
例:
- 无向图的生成树
- 最小生成树定义——给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树。
最小生成树的典型应用
- 构造最小生成树——MST性质
构造最小生成树的算法很多,其中多数算法都利用了MST的性质。
MST性质:设
N
=
(
V
,
E
)
N=(V,E)
N=(V,E)是一个连通网,
U
U
U顶点集
V
V
V的一个非空子集。若边
(
u
,
v
)
(u,v)
(u,v)是一条具有最小权值的边,其中
u
∈
U
,
v
∈
V
−
U
u∈U,v∈V-U
u∈U,v∈V−U,则必存在一棵包含边
(
u
,
v
)
(u,v)
(u,v)的最小生成树。
-
MST性质解释:
在生成树的构造过程中,图中n个顶点分属两个集合:- 已落在生成树上的顶点集;
- 尚未落在生成树上的顶点集
接下来则应在所有连通U中顶点的边中选取权值最小的边。
图示:
构造最小生成树方法一:普里姆(Prime)算法
- 算法思想——加结点法
- 设 N = ( V , E ) N=(V,E) N=(V,E)是连通网, T E TE TE是 N N N上最小生成树中边的集合。
- 初始化 U = u 0 , ( u 0 ∈ V ) , T E = U={u_0},(u_0∈V),TE={} U=u0,(u0∈V),TE=。
- 在所有 u ∈ U , v ∈ V − U u∈U,v∈V-U u∈U,v∈V−U的边 ( u , v ) ∈ E (u,v)∈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 = ( V , T E ) T=(V,TE) T=(V,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中所有顶点都在同一连通分量上为止。
例子:
- 最小生成树不一定唯一
两种算法比较
算法名 | 普里姆算法 | 克鲁斯卡尔算法 |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | O ( n 2 ) O(n^2) O(n2)( n n n为顶点数) | O ( e l o g e ) O(eloge) O(eloge)( e e e为边数) |
适应范围 | 稠密图 | 稀疏图 |
最短路径
- 典型用途:交通网络问题——从甲地到乙地之间是否有公路相连?在有多条通路的情况下,哪一条路最短?
交通网络用有向网来表示:- 顶点——表示地点
- 弧——表示两个地点有路连通
- 弧上的权值——表示两地点之间的距离、交通费或途中所花费的时间等。
如何能够使一个地点到另一个地点的运输时间最短或运费最省?这就是一个求两个地点间的最短路径问题。
- 问题抽象
在有向网中A点(源点)到达B点(终点)的多条路径中,寻找一条各边权值之和最小的路径,即最短路径。
最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n-1条边。
- 第一类问题:两点间最短路径——Dijkstra(迪杰斯特拉)算法
- 第二类问题:某源点到其他各点最短路径——Floyd(弗洛伊德)算法
求单源最短路径——Dijkstra(迪杰斯特拉)算法
- 算法思想:
1.初始化:先找到从源点
v
0
v_0
v0到各终点
v
k
v_k
vk的直达路径
(
v
0
,
v
k
)
(v_0,v_k)
(v0,vk),即通过一条弧到达的路径。
2.选择:从这些路径中找出一条长度最短的路径
(
v
0
,
u
)
(v_0,u)
(v0,u)。
3.更新:然后对其余各条路径进行适当调整。
若在图中存在弧
(
u
,
v
k
)
(u,v_k)
(u,vk),且
(
v
0
,
u
)
+
(
u
,
v
k
)
<
(
v
0
,
v
k
)
(v_0,u)+(u,v_k)<(v_0,v_k)
(v0,u)+(u,vk)<(v0,vk),则以路径
(
v
0
,
u
,
v
k
)
(v_0,u,v_k)
(v0,u,vk)代替
(
v
0
,
v
k
)
(v_0,v_k)
(v0,vk)。
在调整后的各条路径中,在找长度最短的路径,以此类推。
-
迪杰斯特拉(Dijkstra)算法:按路径长度递增次序产生最短路径
-
1、把V分成两组:
- (1) S S S:已求出最短路径的顶点的集合。
- (2) T = V − S T=V-S T=V−S:尚未确定最短路径的顶点集合。
-
2、将 T T T中顶点按最短路径递增的次序加入到 S S S中
保证:- (1)从源点 v 0 v_0 v0到 S S S中各顶点的最短路径长度都不大于从 v 0 v_0 v0到 T T T中任何顶点的最短路径长度。
- (2)每个顶点对应一个距离值:
- S中顶点:从 v 0 v_0 v0到此顶点的最短路径长度。
- T中顶点:从 v 0 v_0 v0到此顶点的只包括 S S S中顶点作中间顶点的最短路径长度。
例子:
- 时间复杂度: O ( n 3 ) O(n^3) O(n3)
求多源最短路径——Floyd(弗洛伊德)算法
-
求所有顶点间的最短路径:
方法一:每次以一个顶点为源点,重复执行Dijkstra算法n次——时间复杂度: O ( n 3 ) O(n^3) O(n3)
方法二:弗洛伊德(Floyd)算法——时间复杂度: O ( n 3 ) O(n^3) O(n3) -
算法思想:
- 逐个顶点试探
- 从 v i v_i vi到 v j v_j vj的所有可能存在的路径中
- 选出一条长度最短的路径
例子:
拓扑排序
针对特殊的图——有向无环图
- 有向无环图——无环的有向图,简称:DAG图(Directed Acycline Graph)
-
AOV网:
用一个有向图表示一个工程的各子工程及其相互制约的关系,其中以顶点表示活动,弧表示活动之间的制约关系,称这种有向图为顶点表示活动的网,简称AOV网(Activity On Vertex)。 -
AOE网:
用一个有向网表示一个工程的各子工程及其相互制约的关系,以弧表示活动,以顶点表示活动的开始或结束事件,称这种有向图为边表示活动的网,简称AOE网(Activity On Edge)
- AOV网的特点:
- 若从 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网,我们关心两个问题:——求解关键路径问题
- (1)完成整项工程至少需要多少时间?
- (2)哪些活动是影响工程进度的关键?
-
关键路径——路径长度最长的路径
-
路径长度——路径上各活动持续时间之和。
-
如何确定关键路径,需要定义4个描述量:
- v e ( v j ) ve(v_j) ve(vj)——表示事件 v j v_j vj的最早发生时间。eg: v e ( v 1 ) = 0 , v e ( v 2 ) = 30 ve(v_1)=0,ve(v_2)=30 ve(v1)=0,ve(v2)=30
- v l ( v j ) vl(v_j) vl(vj)——表示事件 v j v_j vj的最迟发生时间。eg: v l ( v 4 ) = 165 vl(v_4)=165 vl(v4)=165
- e ( i ) e(i) e(i)——表示活动 a i a_i ai的最早开始时间。eg: e ( a 3 ) = 30 e(a_3)=30 e(a3)=30
- l ( i ) l(i) l(i)——表示活动 a i a_i ai的最迟开始时间。eg: l ( a 3 ) = 120 l(a_3)=120 l(a3)=120
l ( i ) − e ( i ) l(i)-e(i) l(i)−e(i)——表示完成活动 a ( i ) a(i) a(i)的时间余量。eg: l ( 3 ) − e ( 3 ) = 90 l(3)-e(3)=90 l(3)−e(3)=90
关键活动——关键路径上的活动,即 l ( i ) = = e ( i ) l(i)==e(i) l(i)==e(i)。(即 l ( i ) − e ( i ) = = 0 l(i)-e(i)==0 l(i)−e(i)==0)的活动。
例:
- 关键路径的求解步骤
关键路径:
l
−
e
=
=
0
l-e==0
l−e==0的点