算法和应用

需熟练掌握图对于各种问题的应用,在选择题和大题中都是考察重点。

DFS

深度优先搜索(Depth-First Search,简称 DFS)是一种用于图的遍历和搜索的算法,它从图中的一个起始顶点开始,尽可能深地访问顶点,直到无法继续前进,然后回溯到之前的顶点,继续深入其他分支。DFS 通常用递归或栈来实现,确保在深度方向上优先遍历。

以下是 DFS 算法的一般步骤:

  1. 从起始顶点开始,将其标记为已访问。
  2. 遍历与当前顶点相邻的未访问顶点,选择其中一个未访问顶点作为下一个深度遍历的目标,然后递归地进行 DFS。
  3. 如果无法继续深度遍历(即当前顶点没有未访问的邻居),则回溯到上一个顶点,继续遍历其他未访问的邻居。
  4. 重复步骤 2 和步骤 3,直到所有顶点都被访问。
int visited[MAX_VERTICES];

void dfs(int start, int vertices) {
    visited[start] = 1;
    printf("%d ", start);

    for (int i = 0; i < vertices; i++) {
        if (adjMatrix[start][i] == 1 && !visited[i]) {
            dfs(i, vertices);
        }
    }
}
void dfs(struct Graph* graph, int vertex, int* visited) {
    visited[vertex] = 1;
    printf("%d ", vertex);

    struct Node* temp = graph->adjacencyList[vertex];
    while (temp) {
        int adjVertex = temp->data;
        if (!visited[adjVertex]) {
            dfs(graph, adjVertex, visited);
        }
        temp = temp->next;
    }
}

void dfsTraversal(struct Graph* graph, int startVertex) {
    int* visited = (int*)malloc(graph->vertices * sizeof(int));
    for (int i = 0; i < graph->vertices; i++) {
        visited[i] = 0;
    }

    dfs(graph, startVertex, visited);

    free(visited);
}

BFS

广度优先搜索(Breadth-First Search,简称 BFS)是一种用于图的遍历和搜索的算法,它从图中的一个起始顶点开始,逐层地访问与该顶点相邻的顶点,然后再访问这些相邻顶点的邻居,以此类推。BFS 通常用队列来实现,确保按照广度优先的顺序访问顶点。

以下是 BFS 算法的一般步骤:

  1. 将起始顶点放入队列中,标记为已访问。
  2. 从队列中弹出一个顶点并访问它。
  3. 遍历该顶点的所有未被访问的邻居,将它们放入队列中,并标记为已访问。
  4. 重复步骤 2 和步骤 3,直到队列为空。
// 从 start 开始遍历图
void bfs(int start, int vertices) {
    enqueue(start);
    visited[start] = 1;

    while (front != -1) {
        int currentVertex = dequeue();
        printf("%d ", currentVertex);

        for (int i = 0; i < vertices; i++) {
            if (adjMatrix[currentVertex][i] == 1 && !visited[i]) {
                enqueue(i);
                visited[i] = 1;
            }
        }
        printf("\n");
    }
}
void bfs(struct Graph* graph, int startVertex) {
    int* visited = (int*)malloc(graph->vertices * sizeof(int));
    for (int i = 0; i < graph->vertices; i++) {
        visited[i] = 0;
    }

    int queue[graph->vertices];
    int front = -1, rear = -1;
    queue[++rear] = startVertex;
    visited[startVertex] = 1;

    printf("BFS traversal starting from vertex %d: ", startVertex);

    while (front != rear) {
        int currentVertex = queue[++front];
        printf("%d ", currentVertex);

        struct Node* temp = graph->adjacencyList[currentVertex];
        while (temp) {
            int adjVertex = temp->data;
            if (!visited[adjVertex]) {
                queue[++rear] = adjVertex;
                visited[adjVertex] = 1;
            }
            temp = temp->next;
        }
    }

    printf("\n");

    free(visited);
}

最小生成树

最小生成树(Minimum Spanning Tree,MST)是在一个连接所有顶点的无向图中,通过选择一部分边而形成的树,使得树的总权重(或成本)最小。

最小生成树满足以下特点:

  • 包含图中的所有顶点。
  • 通过选择一些边而形成一个树结构,没有环路。
  • 边的总权重最小。

prim 算法

Prim 算法的主要思想是从一个初始顶点开始,逐步扩展生成树,每次选择与当前生成树相邻且权重最小的边所连接的顶点,并将该顶点加入生成树。算法的运行过程可以概括为以下步骤:

  1. 选择一个起始顶点作为生成树的根节点。
  2. 初始化一个空的生成树,包含初始顶点。
  3. 将与生成树中的顶点相邻且权重最小的边的另一端顶点加入生成树,即选择一条最小权重边。
  4. 重复步骤 3,直到生成树包含了所有的顶点。
  5. 最终生成树即为原图的最小生成树。
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
2
2
2
2
2
2
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
2
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
2

kruskal 算法

Kruskal 算法是一种用于求解最小生成树(Minimum Spanning Tree,MST)的贪心算法。Kruskal 算法的基本思想是从边的权重最小的边开始,逐步构建生成树,确保不会形成环路。

以下是 Kruskal 算法的一般流程:

  1. 初始化一个空的生成树,开始时生成树中没有边。
  2. 将图中的所有边按照权重从小到大进行排序。
  3. 依次从排序后的边列表中选取边,加入生成树,但要确保添加该边不会形成环路。可以使用并查集来检查是否形成环路。
  4. 重复步骤 3,直到生成树包含了所有的顶点,即生成树中的边数等于顶点数减 1。
  5. 返回生成树作为最小生成树。
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
2
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
2
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
2
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
2
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
2
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
2
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
2
a
b
h
i
c
g
f
d
4
8
11
8
7
2
1
6
7
4
14
2

最短路径

djkstra 算法

Dijkstra 算法的基本思想是从起始顶点开始,逐步扩展已知的最短路径,直到到达所有其他顶点。算法维护一个集合,其中包含已知的最短路径,以及一个优先队列(最小堆)用于选择下一个要探索的顶点。算法的主要步骤如下:

  1. 初始化距离数组和集合:
    • 创建一个距离数组,用于存储从起始顶点到每个顶点的最短距离。初始时,距离数组中的距离值设置为无穷大,除了起始顶点的距离值为 0。
    • 创建一个空的集合,用于存储已知的最短路径。
  2. 将起始顶点添加到优先队列中,距离为 0。
  3. 重复以下步骤,直到优先队列为空:
    • a. 从优先队列中取出距离最短的顶点,将其添加到集合中。
    • b. 遍历该顶点的所有邻居,对于每个邻居顶点:
      • 如果该邻居顶点不在集合中(即尚未找到最短路径),则计算从起始顶点经过当前顶点到达邻居顶点的距离。如果这个距离小于目前已知的距离,则更新距离数组中的距离值。
      • 将邻居顶点添加到优先队列中,以备以后探索。
  4. 当优先队列为空时,Dijkstra 算法完成,距离数组中存储了从起始顶点到每个顶点的最短距离。
  5. 可以使用距离数组来重构最短路径。

Dijkstra_Animation

function Dijkstra(Graph, source):

    for each vertex v in Graph.Vertices:
        dist[v] ← INFINITY
        prev[v] ← UNDEFINED
        add v to Q
    dist[source] ← 0

    while Q is not empty:
        u ← vertex in Q with min dist[u]
        remove u from Q

        for each neighbor v of u still in Q:
            alt ← dist[u] + Graph.Edges(u, v)
            if alt < dist[v]:
                dist[v] ← alt
                prev[v] ← u

    return dist[], prev[]

floyed 算法

Floyd-Warshall 算法是一种用于求解图中所有顶点对之间最短路径的动态规划算法。它适用于有向图和带有权重的边,可以找到从一个顶点到另一个顶点的最短路径,并计算所有顶点对之间的最短路径。

以下是 Floyd-Warshall 算法的一般流程:

  1. 初始化距离矩阵:
    • 创建一个二维矩阵 dist,其中 dist[i][j] 表示从顶点 i 到顶点 j 的最短路径距离。
    • 初始时,将 dist[i][j] 设置为正无穷(表示无法直接从顶点 i 到顶点 j),但当 i 等于 j 时,将 dist[i][j] 设置为 0(表示从顶点 i 到自身的距离为 0)。
    • 对于图中的每一条边 (u, v),如果存在从顶点 u 到顶点 v 的直接路径,则将 dist[u][v] 设置为边的权重。
  2. 三重循环进行动态规划:
    • 对于每一对顶点 (i, j),遍历一个中间顶点 k(从 1 到顶点的数量),检查是否存在一条从顶点 i 经过顶点 k 到顶点 j 的路径距离更短。具体步骤如下:
      • 如果 dist[i][j] 大于 dist[i][k] + dist[k][j],则更新 dist[i][j]dist[i][k] + dist[k][j]
      • 这意味着通过中间顶点 k 可以找到一条更短的路径。
  3. 最终的距离矩阵 dist 包含了所有顶点对之间的最短路径距离。
  4. 如果需要,可以使用距离矩阵 dist 来重构具体的最短路径。

拓扑排序

拓扑排序是一种用于有向图中的顶点排序的算法,它使得图中的每一条有向边都从前面的顶点指向后面的顶点。拓扑排序主要应用于有向无环图(DAG)或 AOV 网,这种图没有环路,因此可以对其进行拓扑排序。

AOV 网和 AOE 网

  • AOV 网(Activity on Vertex Network):AOV 网是一种以顶点表示活动或任务的网络模型。每个顶点代表一个任务或活动,而边表示任务之间的依赖关系。在 AOV 网中,任务或活动通常表示为顶点,而依赖关系(任务的先后顺序)表示为有向边。
  • AOE 网(Activity on Edge Network):AOE 网是一种以边表示活动或任务的网络模型。每条边代表一个任务或活动,而顶点通常表示事件,表示任务的开始或完成时间。在 AOE 网中,任务的持续时间通常不包含在边上,而是与边相连的事件顶点之间的时间差。

算法流程

  1. 选择一个没有前驱顶点(入度为 0)的顶点作为起始顶点,将其加入拓扑排序的结果序列中。
  2. 从图中移除该顶点以及与之相关的边(即将与之相邻的顶点的入度减 1)。
  3. 重复步骤 1 和步骤 2,直到所有的顶点都被加入到拓扑排序的结果序列中,或者发现图中存在环路。
  4. 如果所有的顶点都被成功加入结果序列,那么这个序列就是拓扑排序的结果。
1
2
4
3
5
2
4
3
5
4
3
5
3
5
5
Output 1
Output 2
Output 4
Output 3
Node Number
1
2
3
4
5
Inital In Degree
0
1
2
2
2
Round1
0
2
1
2
Round2
1
0
2
Round3
0
1
Round4
0
Round5

拓扑排序的结果序列可能不唯一,因为有多个入度为 0 的顶点可以作为起始顶点。如果图中存在环路,那么无法进行拓扑排序,因为无法找到入度为 0 的顶点作为起始顶点。

拓扑排序的时间复杂度通常为 $O(V + E)$ ,其中 $V$ 是顶点的数量, $E$ 是边的数量。这个算法非常适合于解决任务调度和依赖关系问题,因为它可以确定任务的执行顺序或依赖关系的合理性。

关键路径

前提概念
  • 源点:在 $AOE$ 网中仅有一个入度为 $0$ 的顶点,称为开始顶点,表示整个工程的开始。
  • 汇点:在 $AOE$ 网中仅有一个出度为 $0$ 的顶点,称为结束结点,表示整个工程的结束。
  • 关键路径的长度:完成整个工程的最短时间,也是 $AOE$ 网中从源点到汇点的最长路径。
  • 关键活动:关键路径上的活动。
  • $ve(k)$:事件 $v_k$ 的最早可以发生时间。
  • $vl(k)$:事件 $v_k$ 的最晚可以发生时间。
算法步骤

概要总结:找到所有最早发生时间和最晚发生时间相同的结点,连接这些结点即可得到关键路径。

具体步骤

  1. 从源点出发,令 $ve(start) = 0$ ,按照拓扑排序求其他顶点的最早发生时间 $ve$ 。
    • 对于源点:$ve(start) = 0$。
    • 对于后续结点:$ve(k) = \text{Max} \{ ve(j) + weight(v_j, v_k) \}$,$v_k$ 为 $v_j$ 的任意后继,$weight(v_j, v_k)$ 表示 $<v_j, v_k>$ 的权值。
  2. 从汇点出发,令 $vl(end) = ve(end)$,按逆拓扑有序求其余顶点的最迟发生时间 $vl$。
    • 对于汇点:$vl(end) = 0$。
    • 对于前继结点:$vl(k) = \text{Min} \{ vl(j) - weight(v_k, v_j) \}$,$v_k$ 为 $v_j$ 的任意前驱。
  3. 连接所有 $ve(i) = vl(i)$ 的结点 $i$,得到关键路径。下图中例子中所有 $ve$ 和 $vl$ 相同的结点为$v_1, v_3, v_4, v_6$,其对应的关键路径为 $v_1 \rightarrow v_3 \rightarrow v_4 \rightarrow v_6$ 即 $a_2, a_5, a_7$。
V1
V2
V3
V4
V6
V5
a1 = 3
a2 = 3
a3 = 2
a5 = 4
a4 = 3
a6 = 3
a7 = 2
a8 = 1
v1
v2
v3
v4
v5
v6
ve(i)
0
3
3
7
6
9
vl(i)
0
5
3
7
8
9

关键路径的特点:

  • 关键路径上的所有活动都是关键活动,它是决定整个工程的关键因素,因此可通过加快关键活动来缩短整个工程的工期。
  • AOE 网中的关键路径并不唯一,对于有几条关键路径的网,只加快一条关键路径,需要同时加快多个关键路径。

用图表达树

当表达式中存在共享的子表达式时,二叉树可能不是存储这些表达式的最有效方式,因为它会重复存储共享的子表达式。在这种情况下,可以使用有向无环图(Directed Acyclic Graph, DAG)来表示表达式,这样可以避免重复,并且更有效地表示表达式。

比如对于表达式 (x + y) * ((x + y) / x),用二叉树和DAG的表示如下图:

*
+
x
y
+
x
y
/
x
*
+
x
y
/
Binary Tree
Representation
DAG
Representation

视频讲解