手工模拟图的各大常用算法。
1 图的遍历算法
1.1 BFS 算法(广度优先遍历)
【性质】
- 空间复杂度:O(|V|)(需要一个辅助队列)
- 采用邻接表的时间复杂度:O(|V|+|E|)
- 采用邻接矩阵的时间复杂度:O(|V|2)
- 遍历方法与树的层序遍历相同(不再给出手工模拟算法的过程)
- 非递归算法
- 用邻接矩阵存储的图,其广度优先生成树唯一;用邻接表存储的图,其广度优先生成树不唯一
【核心思想】
- 首先访问起始顶点 v
- 由 v 出发,依次访问 v 的各个未访问的邻接顶点 w1,w2,…,wi(将它们依次存入辅助队列)
- 由这些顶点 w1,w2,…,wi 出发(将它们依次出列),再依次访问它们的未访问的邻接顶点
【核心代码】
bool visited[MAX_VERTEX_NUM];
Queue Q;
void BFSTraverse (Graph G){
for (v = 0; v < G.vexnum; v++){
visited[v] = FALSE;
}
InitQueue(Q);
for (v = 0; v < G.vexnum; v++){
if (visited[v] == FALSE)
BFS(G, v);
}
}
void BFS (Graph G, int v){
visit(v);
visited[v] = TRUE;
EnQueue(Q, v);
while (!isEmpty(Q)){
DeQueue(Q, v);
for (int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)){
if (visited[w] == FALSE){
visit(w);
visited[w] = TRUE;
EnQueue(Q, w);
}
}
}
}
1.2 DFS 算法(深度优先遍历)
【性质】
- 空间复杂度:O(|V|)(需要一个辅助递归工作栈)
- 采用邻接表的时间复杂度:O(|V|+|E|)
- 采用邻接矩阵的时间复杂度:O(|V|2)
- 遍历方法与树的先序遍历相同(不再给出手工模拟算法的过程)
- 递归算法
- 用邻接矩阵存储的图,其深度优先生成树唯一;用邻接表存储的图,其深度优先生成树不唯一
【核心思想】
- 访问起始顶点 v
- 由顶点 v 出发,访问与其邻接的未访问的第一个顶点 w1
- 再访问与 w1 邻接的未访问顶点 w11
- 不断重复以上过程,直到不能再向下访问时,回退到最近被访问的顶点,继续访问它的其它邻接顶点
【核心代码】
bool visited[MAX_VERTEX_NUM];
void DFSTraverse (Graph G){
for (v = 0; v < G.vexnum; v++){
visited[v] = FALSE;
}
for (v = 0; v < G.vexnum; v++){
if (visited[v] == FALSE)
DFS(G, v);
}
}
void DFS (Graph G, int v){
visit(v);
visited[v] = TRUE;
for (int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)){
if (visited[w] == FALSE)
DFS(G, w);
}
}
2 最短路径问题
2.1 BFS 算法(求无权图的单源最短路径)
【核心思想】
- 注意到可以将无权图转换为根为顶点 i 的生成树。又因为广度优先生成树的高度一定小于等于深度优先生成树,所以对于广度优先生成树来说,它的根到其他顶点的距离一定是最短的。
【核心代码】
bool visited[MAX_VERTEX_NUM];
int dist[MAX_VERTEX_NUM];
int path[MAX_VERTEX_NUM];
Queue Q;
void BFSTraverse (Graph G){
for (v = 0; v < G.vexnum; v++){
visited[v] = FALSE;
dist[v] = 999999;
path[v] = -1;
}
InitQueue(Q);
for (v = 0; v < G.vexnum; v++){
if (visited[v] == FALSE)
BFS(G, v);
}
}
void BFS_Min_Dist (Graph G, int v){
dist[v] = 0;
visited[v] = TRUE;
EnQueue(Q, v);
while (!isEmpty(Q)){
DeQueue(Q, v);
for (int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)){
if (visited[w] == FALSE){
dist[w] = dist[v] + 1;
path[w] = v;
visited[w] = TRUE;
EnQueue(Q, w);
}
}
}
}
2.2 Dijkstra 算法(求带权图的单源最短路径)
【性质】
- 时间复杂度:O(|V|2)
- 基于贪心策略
- 不适用于带负权值的图
- 不适用于带负权回路的图
以下面有向图为例:
step0. 初始状态
V0 | V1 | V2 | V3 | V4 |
---|
True | False | False | False | False |
- dist 数组:记录从源点(V0)到该点(Vi)的最短路径长度
- path 数组:路径上的前驱(存储到达 Vi 的前一个结点编号)
step1. 第一轮
上一步的表格:
| V0 | V1 | V2 | V3 | V4 |
---|
final | True | False | False | False | False | dist | 0 | 10 | ∞ | ∞ | 5 | path | -1 | 0 | -1 | -1 | 0 |
【1】找到上一步中:final 数组还未确定的最短路径,且在 dist 数组中最小的顶点 Vi = V4,令 final[i] = True,表示从 V0 到 Vi 的最短路径已确定。
V0 | V1 | V2 | V3 | V4 |
---|
True | False | False | False | True |
【2】检查所有邻接自 Vi = V4 的点,若其 final 值为 False,则对比 step0 中的 dist 信息,如果找到的路径比当前的信息还小,则更新其 dist 和 path 信息。
【2.1】对于 V1(原本 dist = 10, path = 0):final 值为 False,从 V0–>V4–>V1 的路径长度为 5+3=8 < 10,所以需要更新其 dist = 8(表示从 V0 到 V1 的路径长度为 8,以下类似),path = 4(表示这条路径是从 V4 结点过来的,以下类似);
【2.2】对于 V2(原本 dist = ∞, path = -1):final 值为 False,从 V0–>V4–>V2 的路径长度为 5+9=14 < ∞,所以需要更新其 dist = 14,path = 4;
【2.3】对于 V3(原本 dist = ∞, path = -1):final 值为 False,从 V0–>V4–>V3 的路径长度为 5+2=7 < ∞,所以需要更新其 dist = 7,path = 4。
step2. 第二轮
上一步的表格:
| V0 | V1 | V2 | V3 | V4 |
---|
final | True | False | False | False | True | dist | 0 | 8 | 14 | 7 | 5 | path | -1 | 4 | 4 | 4 | 0 |
【1】找到上一步中:final 数组还未确定的最短路径,且在 dist 数组中最小的顶点 Vi = V3,令 final[i] = True,表示从 V0 到 Vi 的最短路径已确定。
V0 | V1 | V2 | V3 | V4 |
---|
True | False | False | True | True |
【2】检查所有邻接自 Vi = V3 的点(对应 dist = 7,path = 4),若其 final 值为 False,则对比 step1 中的 dist 信息,如果找到的路径比当前的信息还小,则更新其 dist 和 path 信息。
【2.1】对于 V0:final 值为 True。
【2.2】对于 V2(原本 dist = 14,path = 4):final 值为 False,从 V0–>V4–>V3–>V2 的路径长度为 7+6=13 < 14,所以需要更新其 dist = 13,path = 3。
step3. 第三轮
上一步的表格:
| V0 | V1 | V2 | V3 | V4 |
---|
final | True | False | False | True | True | dist | 0 | 8 | 13 | 7 | 5 | path | -1 | 4 | 3 | 4 | 0 |
【1】找到上一步中:final 数组还未确定的最短路径,且在 dist 数组中最小的顶点 Vi = V1,令 final[i] = True,表示从 V0 到 Vi 的最短路径已确定。
V0 | V1 | V2 | V3 | V4 |
---|
True | True | False | True | True |
【2】检查所有邻接自 Vi = V1 的点(对应 dist = 8,path = 4),若其 final 值为 False,则对比 step2 中的 dist 信息,如果找到的路径比当前的信息还小,则更新其 dist 和 path 信息。
【2.1】对于 V2(原本 dist = 13,path = 3):final 值为 False,从 V0–>V4–>V1–>V2 的路径长度为 8+1=9 < 13,所以需要更新其 dist = 9,path = 1。
【2.2】对于 V4:final 值为 True。
step4. 第四轮
上一步的表格:
| V0 | V1 | V2 | V3 | V4 |
---|
final | True | True | False | True | True | dist | 0 | 8 | 9 | 7 | 5 | path | -1 | 4 | 1 | 4 | 0 |
【1】找到上一步中:final 数组还未确定的最短路径,且在 dist 数组中最小的顶点 Vi = V2,令 final[i] = True,表示从 V0 到 Vi 的最短路径已确定。
V0 | V1 | V2 | V3 | V4 |
---|
True | True | True | True | True |
【2】检查所有邻接自 Vi = V2 的点(对应 dist = 13,path = 3),若其 final 值为 False,则对比 step2 中的 dist 信息,如果找到的路径比当前的信息还小,则更新其 dist 和 path 信息。
已经找不到其他未访问结点,算法结束,以下为最终结果:
【应试】快速解答
2.3 Floyd 算法(求带权图的各顶点之间最短路径)
【性质】
- 时间复杂度:O(|V|3)
- 适用于带负权值的图
- 不适用于带负权回路的图
【注】可以使用单源最短路径算法解决每对顶点最短路径问题,例如可使用 Dijkstra 算法,轮流将每个结点当成源点,时间复杂度也为 O(|V|3)
【核心代码】
for (int k = 0; k < n; k++){
for (int i = 0; i < n; i++){
for (int j = 0; j < n; j++){
if (A[i][j] > A[i][k] + A[k][j]){
A[i][j] = A[i][k] + A[k][j];
path[i][j] = k;
}
}
}
}
以下面有向图为例:
step0. 初始状态(不允许在其他顶点中转)
- A(-1)(从目前来看,各顶点之间的最短路径长度)
| V0 | V1 | V2 |
---|
V0 | -1 | -1 | -1 | V1 | -1 | -1 | -1 | V2 | -1 | -1 | -1 |
step1. 第一轮(允许在顶点 V0 中转)
上一步中可以发现,如果加入了 V0 中转,则有:
A-1[2][1] = ∞ > A-1[2][0] + A-1[0][1] = 11
所以应改为:
| V0 | V1 | V2 |
---|
V0 | -1 | -1 | -1 | V1 | -1 | -1 | -1 | V2 | -1 | 0 | -1 |
step2. 第二轮(允许在顶点 V1 中转/加入顶点 V1 进行中转)
上一步中可以发现,如果在加入了 V0 中转的基础上,又加入了 V1 中转,则有:
A0[0][2] = 13 > A0[0][1] + A0[1][2] = 10
所以应改为:
| V0 | V1 | V2 |
---|
V0 | -1 | -1 | 1 | V1 | -1 | -1 | -1 | V2 | -1 | 0 | -1 |
step3. 第三轮(允许在顶点 V2 中转/加入顶点 V2 进行中转)
上一步中可以发现,如果在加入了 V0、V1 中转的基础上,又加入了 V2 中转,则有:
A1[1][0] = 13 > A1[1][2] + A1[2][0] = 9
所以应改为:
| V0 | V1 | V2 |
---|
V0 | -1 | -1 | 1 | V1 | 2 | -1 | -1 | V2 | -1 | 0 | -1 |
由于没有其他的顶点,因此算法结束。
根据以上两个矩阵:
- 从 V0 到 V2,在矩阵 A 中获知最短路径为 10,在矩阵 path 中获知路径是 V0–>V1–>V2;
- 从 V1 到 V0,在矩阵 A 中获知最短路径为 2,在矩阵 path 中获知路径是 V1–>V2–>V0;
- 以此类推。
较复杂的例子
如果要找从 V0 到 V4 的最短路径:
- 在矩阵 A 中得知 V0 到 V4 的最短路径长度为 4,在矩阵 path 中得知从 V0 到 V4 需经过 V3;
- 在矩阵 A 中得知 V0 到 V3 的最短路径长度为 3,在矩阵 path 中得知从 V0 到 V3 需经过 V2;
- 在矩阵 A 中得知 V0 到 V2 的最短路径长度为 1,在矩阵 path 中得知从 V0 到 V2 不需经过顶点;
- 在矩阵 A 中得知 V2 到 V3 的最短路径长度为 2,在矩阵 path 中得知从 V2 到 V3 需经过 V1;
- 在矩阵 A 中得知 V1 到 V3 的最短路径长度为 1,在矩阵 path 中得知从 V1 到 V3 不需要经过顶点;
- 最终,从 V0 到 V4 的最短路径是 4,路径为 V0–>V2–>V1–>V3–>V4。
3 最小生成树
【性质】
- 图的各边权值不相等时,其最小生成树不唯一
- 最小生成树的权值是唯一的
- 最小生成树的边数为顶点数减 1
- 以下两种算法均基于贪心策略
3.1 Prim 算法
使用 Prim 算法手工构造最小生成树的过程如下:
3.2 Kruskal 算法
使用 Kruskal 算法手工构造最小生成树的过程如下:
---EOF---
|