L
O
A
D
I
N
G


辅助函数

  1. FirstNeighbor(G,x):

    • 求图G中顶点x的第一个邻接点,若有则返回顶点号。

    • 若x没有邻接点或图中不存在x,则返回-1。

  2. NextNeighbor(G,x,y):

    • 假设图G中顶点y是顶点x的一个邻接点,返回除y之外,顶点x的下一个邻接点的顶点号
    • 若y是x的最后一个邻接点,则返回-1。

邻接矩阵表示法

假设图 G 使用邻接矩阵表示,G.matrix[i][j] 为 1 表示顶点 i 和顶点 j 之间有边,为 0 表示没有边。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define MAX_VERTEX_NUM 100

typedef struct {
int numVertices;
int matrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM];
} Graph;

// 返回顶点v的第一个邻接顶点
int FirstNeighbor(Graph G, int v) {
for (int i = 0; i < G.numVertices; ++i) {
if (G.matrix[v][i] == 1) {
return i;
}
}
return -1; // 没有邻接顶点
}

// 返回顶点v的邻接顶点w之后的下一个邻接顶点
int NextNeighbor(Graph G, int v, int w) {
for (int i = w + 1; i < G.numVertices; ++i) {
if (G.matrix[v][i] == 1) {
return i;
}
}
return -1; // 没有更多的邻接顶点
}

邻接表表示法

如果图 G 使用邻接表表示,G.adjList 是一个包含每个顶点的邻接顶点列表的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <vector>

#define MAX_VERTEX_NUM 100

typedef struct {
int numVertices;
std::vector<int> adjList[MAX_VERTEX_NUM];
} Graph;

// 返回顶点v的第一个邻接顶点
int FirstNeighbor(Graph G, int v) {
if (!G.adjList[v].empty()) {
return G.adjList[v][0];
}
return -1; // 没有邻接顶点
}

// 返回顶点v的邻接顶点w之后的下一个邻接顶点
int NextNeighbor(Graph G, int v, int w) {
for (size_t i = 0; i < G.adjList[v].size(); ++i) {
if (G.adjList[v][i] == w) {
if (i + 1 < G.adjList[v].size()) {
return G.adjList[v][i + 1];
}
break;
}
}
return -1; // 没有更多的邻接顶点
}

FirstNeighbor(Graph G, int v):

  • 邻接矩阵版本:遍历顶点 v 的所有可能邻接顶点(从 0 到 G.numVertices - 1),如果找到一个顶点 i 与顶点 v 有边(G.matrix[v][i] == 1),则返回这个顶点 i。如果没有找到,则返回 -1。
  • 邻接表版本:直接返回顶点 v 的邻接顶点列表的第一个元素。如果列表为空,则返回 -1。

NextNeighbor(Graph G, int v, int w):

  • 邻接矩阵版本:从顶点 w + 1 开始,遍历顶点 v 的所有可能邻接顶点,找到第一个与顶点 v 有边的顶点 iG.matrix[v][i] == 1),返回这个顶点 i。如果没有找到,则返回 -1。
  • 邻接表版本:在顶点 v 的邻接顶点列表中,找到顶点 w,然后返回其后的下一个顶点。如果 w 是最后一个邻接顶点或没有更多邻接顶点,则返回 -1。

广度优先遍历

概要

广度优先遍历(Breadth-First-Search, BFS)要点:

  1. 找到与一个顶点相邻的所有顶点
  2. 标记哪些顶点被访问过
  3. 需要一个辅助队列

BFS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool visited[MAX_VERTEX_NUM]; // 访问标记数组

// 广度优先遍历
void BFS(Graph G, int v) { // 从顶点v出发,广度优先遍历图G
visit(v); // 访问初始顶点v
visited[v] = TRUE; // 对v做已访问标记
Enqueue(Q, v); // 顶点v入队列Q

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入列
}
}
}
}

执行顺序解析:

  1. 初始化邻接顶点 wint w = FirstNeighbor(G, v);,返回顶点 v 的第一个邻接顶点 w

  2. 检查是否有邻接顶点w >= 0

    • 如果 w 为负数(例如,-1),表示没有邻接顶点,跳出循环。
    • 如果 w 为非负数,表示有邻接顶点,进入循环体。
  3. 检查顶点 w 是否已访问if (!visited[w])

    • 检查 visited[w] 是否为 false,即顶点 w 是否尚未被访问。
  4. 访问尚未访问的邻接顶点 w:

    • 如果未被访问,执行以下步骤:

      1
      2
      visited[w] = TRUE; // 对w做已访问标记
      EnQueue(Q, w); // 顶点w入队
      • 调用 visit(w) 函数,对顶点 w 进行访问(例如,打印顶点或进行其他处理)。
      • visited[w] 设为 TRUE,标记顶点 w 已被访问。
      • 将顶点 w 入队列 Q,以便在之后的 BFS 阶段继续处理。
  5. 更新邻接顶点 w 为下一个邻接顶点w = NextNeighbor(G, v, w);

    • 调用 NextNeighbor(G, v, w) 函数,返回顶点 v 的下一个邻接顶点,更新 w 的值。
  6. 重复步骤 2 到 5:

    • 继续循环,检查新的 w 是否非负,并按照上述步骤处理新的邻接顶点。

遍历序列的可变性

邻接表表示的BFS序列不唯一

同一个图的邻接矩阵表示方式唯一,因此广度优先遍历序列唯一
同一个图邻接表表示方式不唯一,因此广度优先遍历序列不唯一

上述算法存在的问题

不能遍历完非连通图

如果是非连通图,则无法遍历完所有结点。

要解决不能遍历非连通图的问题,可以在调用 BFS 之前或之后添加一个遍历所有顶点的循环,对于尚未访问的顶点,启动一次新的 BFS。这确保了图中所有连通分量都被遍历。

对于无向图,调用BFS函数的次数=连通分量数

1
2
3
4
5
6
7
8
9
10
11
12
// 遍历整个图,处理非连通图的情况
void BFSTraverse(Graph G) {
for (int i = 0; i < G.Vexnum; ++i) {
visited[i] = false; // 初始化访问标记数组
}
InitQueue(Q); ///初始化辅助队列Q
for (int i = 0; i < G.Vexnum; ++i) {
if (!visited[i]) {
BFS(G, i); // 对尚未访问的顶点调用BFS
}
}
}

复杂度分析

  1. 空间复杂度:最坏情况,辅助队列大小为 O(|V|)
  2. 时间复杂度
    • 邻接矩阵存储的图:访问 V 个顶点需要O(|V|)的时间。查找每个顶点的邻接点都需要O(|V|)的时间,而总共有|V|个顶点,时间复杂度= O(|V|^2^)
      • 总的时间复杂度为 O(|V|^2^),忽略低阶
    • 邻接表存储的图:访问 V 个顶点需要O(|V|)的时间,查找各个顶点的邻接点共需要O(2E)的时间(E为所有边数)
      • 总的时间复杂度为:时间复杂度= O(|V|+|E|)

广度优先生成树:

广度优先生成树

  • 广度优先生成树由广度优先遍历过程确定。
  • 由于邻接表的表示方式不唯一,因此基于邻接表的广度优先生成树也不唯一
  • 对非连通图的广度优先遍历,可得到广度优先生成森林

总结

深度优先遍历

概要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool visited[MAX_VERTEX_NUM]; // 访问标记数组

void DFS(Graph G, int v) {
visit(v); // 访问顶点v
visited[v] = true; // 设已访问标记

// 从顶点v出发,深度优先遍历图G
for (int w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w)) {
// 检测v的所有邻接点
if (!visited[w]) {
DFS(G, w); // 递归调用DFS
}
}
}

和之前一样,会出现无法遍历非连通图的问题

需要加上

1
2
3
4
5
6
7
8
9
10
11
12
13
void DFSTraverse(Graph G) {
// 初始化访问标记数组
for (int v = 0; v < G.vexnum; ++v) {
visited[v] = false;
}

// 对图中的每个顶点执行 DFS
for (int v = 0; v < G.vexnum; ++v) {
if (!visited[v]) {
DFS(G, v);
}
}
}

复杂度和BFS一样。

  • 同一个图的邻接矩阵表示方式唯一,因此深度优先遍历序列唯一,深度优先生成树也唯一
  • 同一个图邻接表表示方式不唯一,因此深度优先遍历序列不唯一,深度优先生成树也不唯一

总结

最小生成树

生成树:

连通图的生成树是包含图中全部顶点的一个极小连通子图。

若图中顶点数为n,则它的生成树含有n-1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路

生成树

最小生成树(最小代价树):

对于一个带权连通无向图G = (V, E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同

设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(Minimum-Spanning-Tree, MST)

最小生成树可能有多个,但边的权值之和总是唯一且最小的

最小生成树的边数 = 顶点数 - 1

砍掉一条则不连通,增加一条边则会出现回路

如果一个连通图本身就是一棵树,则其最小生成树就是它本身

只有连通图才有生成树,非连通图只有生成森林

求最小生成树的两种方法

Prim算法

Kruskal算法

Prim算法(普里姆):从某一个顶点开始构建生成树;每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。时间复杂度: O(V2)适合用于边稠密图。

Kruskal算法(克鲁斯卡尔):每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选)直到所有结点都连通。时间复杂度: O(lEllog2lEl )适合用于边稀疏图。

Prim 算法(普里姆):

算法思想:

从某一个顶点开始构建生成树;

每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。

时间复杂度:O(|V|^2),适合用于边稠密图


文章作者: loyeh
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 loyeh !
评论
  目录