G
N
I
D
A
O
L

【数据结构-图】图的常用算法

手工模拟图的各大常用算法。

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); 				// 初始化辅助数列 Q 
	
// 考虑到非连通图,为防止一次调用 BFS 函数访问不到其他连通分量,检查一遍 visited 即可继续访问未访问的顶点 
// 考虑到非强连通图,有些顶点也是访问不到的,因此检查一遍 visited 即可继续访问未访问的顶点 
	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);		// 顶点 v 入队 
	
	while (!isEmpty(Q)){// 队列非空时 
		DeQueue(Q, v);		// 顶点 v 出队
		// 依次访问 v 的邻接点
		// FirstNeighbor(G,v):求图 G 中顶点 v 的第一个邻接点
		// NextNeighbor(G,v,w):图 G 中顶点 w 是顶点 v 的一个邻接点,返回除 w 外顶点 v 的下一个邻接点 
		for (int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)){
			if (visited[w] == FALSE){	// 若顶点 w 尚未访问 
				visit(w);			// 访问顶点
				visited[w] = TRUE;	// 设置该顶点为已访问过
				EnQueue(Q, w);		// 顶点 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;		// 初始化各顶点的访问情况为未访问 
	}
// 考虑到非连通图,为防止一次调用 DFS 函数访问不到其他连通分量,检查一遍 visited 即可继续访问未访问的顶点 
// 考虑到非强连通图,有些顶点也是访问不到的,因此检查一遍 visited 即可继续访问未访问的顶点 
	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;	// 设置该结点为已访问过 
	// 依次访问 v 的邻接点
	// FirstNeighbor(G,v):求图 G 中顶点 v 的第一个邻接点
	// NextNeighbor(G,v,w):图 G 中顶点 w 是顶点 v 的一个邻接点,返回除 w 外顶点 v 的下一个邻接点 
	for (int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)){
		if (visited[w] == FALSE)	// 若顶点 w 尚未访问 
			DFS(G, w);
	} 
}

2 最短路径问题

2.1 BFS 算法(求无权图的单源最短路径)

【核心思想】

  • 注意到可以将无权图转换为根为顶点 i 的生成树。又因为广度优先生成树的高度一定小于等于深度优先生成树,所以对于广度优先生成树来说,它的根到其他顶点的距离一定是最短的。

【核心代码】

  • 新增加两个数组:dist 和 path
bool visited[MAX_VERTEX_NUM];	// 标记顶点的访问情况 
int dist[MAX_VERTEX_NUM];		// 记录从源点(V0)到该点(Vi)的最短路径长度
int path[MAX_VERTEX_NUM];		// 路径上的前驱(存储到达 Vi 的前一个结点编号)
Queue Q;						// 辅助队列 

void BFSTraverse (Graph G){
	for (v = 0; v < G.vexnum; v++){
		visited[v] = FALSE;		// 初始化各顶点的访问情况为未访问 
		dist[v] = 999999;		// 初始化路径长度 
		path[v] = -1;			// 初始化路径上的顶点 v 的前驱顶点 
	}
	InitQueue(Q); 				// 初始化辅助数列 Q 
	
// 考虑到非连通图,为防止一次调用 BFS 函数访问不到其他连通分量,检查一遍 visited 即可继续访问未访问的顶点 
// 考虑到非强连通图,有些顶点也是访问不到的,因此检查一遍 visited 即可继续访问未访问的顶点 
	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;		// 从初始顶点 v 开始遍历,它的最短路径长度设置为 0 
	visited[v] = TRUE;	// 设置该顶点为已访问过
	EnQueue(Q, v);		// 顶点 v 入队 
	
	while (!isEmpty(Q)){// 队列非空时 
		DeQueue(Q, v);		// 顶点 v 出队
		// 依次访问 v 的邻接点
		// FirstNeighbor(G,v):求图 G 中顶点 v 的第一个邻接点
		// NextNeighbor(G,v,w):图 G 中顶点 w 是顶点 v 的一个邻接点,返回除 w 外顶点 v 的下一个邻接点 
		for (int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)){
			if (visited[w] == FALSE){	// 若顶点 w 尚未访问 
				dist[w] = dist[v] + 1;	// 路径长度加 1 
				path[w] = v; 			// 标记到达顶点 w 需要从顶点 v 过来 
				visited[w] = TRUE;		// 设置该顶点为已访问过
				EnQueue(Q, w);			// 顶点 w 入队 
			}
		}
	}
}

2.2 Dijkstra 算法(求带权图的单源最短路径)

【性质】

  • 时间复杂度:O(|V|2)
  • 基于贪心策略
  • 不适用于带负权值的图
  • 不适用于带负权回路的图

以下面有向图为例:

image

step0. 初始状态

  • final 数组:标记各顶点是否已找到最短路径
V0 V1 V2 V3 V4
True False False False False
  • dist 数组:记录从源点(V0)到该点(Vi)的最短路径长度
V0 V1 V2 V3 V4
0 10 5
  • path 数组:路径上的前驱(存储到达 Vi 的前一个结点编号)
V0 V1 V2 V3 V4
-1 0 -1 -1 0

step1. 第一轮

image

上一步的表格:

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 的最短路径已确定。

  • final 数组:
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。

  • dist 数组:
V0 V1 V2 V3 V4
0 8 14 7 5
  • path 数组:
V0 V1 V2 V3 V4
-1 4 4 4 0

step2. 第二轮

image

上一步的表格:

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 的最短路径已确定。

  • final 数组:
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。

  • dist 数组:
V0 V1 V2 V3 V4
0 8 13 7 5
  • path 数组:
V0 V1 V2 V3 V4
-1 4 3 4 0

step3. 第三轮

image

上一步的表格:

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 的最短路径已确定。

  • final 数组:
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。

  • dist 数组:
V0 V1 V2 V3 V4
0 8 9 7 5
  • path 数组:
V0 V1 V2 V3 V4
-1 4 1 4 0

step4. 第四轮

image

上一步的表格:

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 的最短路径已确定。

  • final 数组:
V0 V1 V2 V3 V4
True True True True True

【2】检查所有邻接自 Vi = V2 的点(对应 dist = 13,path = 3),若其 final 值为 False,则对比 step2 中的 dist 信息,如果找到的路径比当前的信息还小,则更新其 dist 和 path 信息。

已经找不到其他未访问结点,算法结束,以下为最终结果:

  • dist 数组:
V0 V1 V2 V3 V4
0 8 9 7 5
  • path 数组:
V0 V1 V2 V3 V4
-1 4 1 4 0

【应试】快速解答

image

image

2.3 Floyd 算法(求带权图的各顶点之间最短路径)

【性质】

  • 时间复杂度:O(|V|3)
  • 适用于带负权值的图
  • 不适用于带负权回路的图

【注】可以使用单源最短路径算法解决每对顶点最短路径问题,例如可使用 Dijkstra 算法,轮流将每个结点当成源点,时间复杂度也为 O(|V|3)

【核心代码】

for (int k = 0; k < n; k++){  // 加入 Vk 作为中转站
  for (int i = 0; i < n; i++){  // 遍历整个邻接权值矩阵,i 为行,j 为列
    for (int j = 0; j < n; j++){
        if (A[i][j] > A[i][k] + A[k][j]){  // 从顶点 i 到顶点 j,经过 Vk 中转站的路径更短
            A[i][j] = A[i][k] + A[k][j];   // 更新路径长度
            path[i][j] = k;                // 记录中转顶点的编号
        }
    }
  }
}

以下面有向图为例:

image

step0. 初始状态(不允许在其他顶点中转)

  • A(-1)(从目前来看,各顶点之间的最短路径长度)
V0 V1 V2
V0 0 6 13
V1 10 0 4
V2 5 0
  • path(-1)(两个顶点之间的中转点)
V0 V1 V2
V0 -1 -1 -1
V1 -1 -1 -1
V2 -1 -1 -1

step1. 第一轮(允许在顶点 V0 中转)

image

上一步中可以发现,如果加入了 V0 中转,则有:

A-1[2][1] = ∞ > A-1[2][0] + A-1[0][1] = 11

所以应改为:

  • A(0)(从目前来看,各顶点之间的最短路径长度)
V0 V1 V2
V0 0 6 13
V1 10 0 4
V2 5 11 0
  • path(0)(两个顶点之间的中转点)
V0 V1 V2
V0 -1 -1 -1
V1 -1 -1 -1
V2 -1 0 -1

step2. 第二轮(允许在顶点 V1 中转/加入顶点 V1 进行中转)

image

上一步中可以发现,如果在加入了 V0 中转的基础上,又加入了 V1 中转,则有:

A0[0][2] = 13 > A0[0][1] + A0[1][2] = 10

所以应改为:

  • A(1)(从目前来看,各顶点之间的最短路径长度)
V0 V1 V2
V0 0 6 10
V1 10 0 4
V2 5 11 0
  • path(1)(两个顶点之间的中转点)
V0 V1 V2
V0 -1 -1 1
V1 -1 -1 -1
V2 -1 0 -1

step3. 第三轮(允许在顶点 V2 中转/加入顶点 V2 进行中转)

image

上一步中可以发现,如果在加入了 V0、V1 中转的基础上,又加入了 V2 中转,则有:

A1[1][0] = 13 > A1[1][2] + A1[2][0] = 9

所以应改为:

  • A(2)(从目前来看,各顶点之间的最短路径长度)
V0 V1 V2
V0 0 6 10
V1 9 0 4
V2 5 11 0
  • path(2)(两个顶点之间的中转点)
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;
  • 以此类推。

较复杂的例子

image

如果要找从 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 算法手工构造最小生成树的过程如下:

image

3.2 Kruskal 算法

若要判断最小生成树唯一,最好使用 Kruskal 算法。

使用 Kruskal 算法手工构造最小生成树的过程如下:

image

---EOF---
posted @ 2022-08-02 10:58  漫舞八月(Mount256)  阅读(125)  评论(0编辑  收藏  举报