图算法中的最小生成树与拓扑排序
1. 最小生成树相关内容
在图论里,最小生成树是一个极为关键的概念。想象有一个迷宫,每个单元格要么是空间,要么是墙壁。我们可以把迷宫表示成一个图,其中每个空间单元格对应图的一个顶点,相邻的空间单元格之间存在一条边。这样,图中的路径就对应着迷宫中可以行走的路径。
为了生成一个迷宫,我们可以在一个初始的图中构建一个随机的生成树。这个初始图代表一个没有墙壁的迷宫,所有边的权重都为 1。我们可以使用 Prim 算法或者 Kruskal 算法,不过在实际应用中,Prim 算法更为常用。
Prim 算法生成迷宫的步骤如下:
1. 从图中随机选择一个单元格,并将其转换为空间。
2. 初始时,边界包含所有与该空间相邻的墙壁。
3. 每一步,从边界中随机选择一堵墙,并将其转换为空间。
4. 更新边界,移除那些与多个空间单元格相邻的墙壁,并添加所有仅与新添加的空间相邻的墙壁。
5. 当边界为空时,算法停止,此时就生成了一个迷宫。
以下是相关的练习题:
| 题号 | 题目描述 |
| ---- | ---- |
| 7.6.17 | 假设一个计算机程序的数组中有 15 个对象,位置从 0 到 14。使用特定技术,展示程序如何执行一系列操作,如 Union(2, 8)、Union(14, 5) 等,并进行 Find 操作。 |
| 7.6.18 | 使用 Kruskal 算法和 Prim 算法,为具有特定邻接结构的图找到最小生成树(如果可能)。 |
| 7.6.19 | 利用之前的定理证明推论 7.6.8。 |
| 7.6.20 | 用反证法证明定理 7.6.9。证明应从“取任何具有恰好 n 个顶点和恰好 n - 1 条边的连通无向图 G。假设(为了推出矛盾)G 包含一个环”开始。 |
| 7.6.21 | 证明每个具有恰好 n 个顶点的无环无向图最多有 n - 1 条边。 |
| 7.6.22 | 设 G 是一个具有恰好 n 个顶点的连通无向图。证明如果 G 有超过 n - 1 条边,那么 G 有不止一个生成树。 |
| 7.6.23 | 设 G 是一个连通的、无向的、带权图。假设 G 的任意两条边的权重都不相同。证明 G 有且仅有一个最小生成树。 |
| 7.6.24 | 假设我们从一组单元素集合开始,每个集合用只有一个节点的树表示。我们对这些集合执行一系列并操作,每次操作时遵循规则:让节点数较少的树的根指向节点数较多的树的根。证明对于以这种方式生成的每棵树 T,树 T 的高度 hT 和节点数 nT 满足不等式 2hT ≤ nT。 |
2. 有向图的拓扑排序
在实际生活中,我们常常会遇到一系列需要完成的任务,并且有些任务必须在其他任务开始之前完成。同时,每次只能进行一个任务,一旦开始就必须完成才能开始下一个任务。这时,我们就需要找到一种非重复的任务列表,使得对于任意两个任务 x 和 y,如果 x 必须在 y 开始之前完成,那么 x 在列表中排在 y 之前。
我们可以将任务表示为有向图的顶点,从顶点 x 到顶点 y 的边表示任务 x 必须在任务 y 开始之前完成。这样,问题就转化为一个图论问题——拓扑排序问题。
拓扑排序问题的定义如下:给定一个有向图 G,若可能的话,找到其所有顶点的一个非重复列表,使得对于图 G 中的任意一对顶点 x 和 y,如果边 (x, y) 在图中,那么 x 在列表中排在 y 之前。任何满足这些性质的顶点列表都称为顶点的拓扑排序,找到这样的列表的过程称为将顶点排序为拓扑顺序。
例如,对于一些小图,我们可以通过观察直接找到拓扑排序。但对于一般的图,我们需要一个算法来解决这个问题。
拓扑排序存在的必要条件是图是无环的。这是因为如果图中存在环,那么无论选择环中的哪个顶点,环中的所有顶点(包括它自己)都必须在拓扑排序中排在它之后,这显然是不可能的。
下面是一些相关的引理和定理:
-
引理 7.7.2
:如果有向图中的每个顶点至少有一条指向其他顶点的边,那么该有向图至少包含一个环。
-
证明
:设 G 是一个有向图,对于 G 中的每个顶点 x,都存在一条形式为 (x, y) 的边。在 G 中任意选择一个顶点 v1,那么 G 中至少存在一个顶点 y,使得 (v1, y) 在图中。选择这样的顶点并将其命名为 v2。类似地,选择一个顶点 v3,使得 (v2, v3) 在图中。如果 v3 与 v1 相同,那么我们就找到了一个环。如果不同,继续选择顶点 v4,使得 (v3, v4) 在图中。如果 v4 与 v1 或 v2 相同,那么我们就找到了一个环。由于图中只有有限个顶点,最终这个过程必然会导致某个顶点的重复,从而得到一个环。
-
推论 7.7.3
:如果有向图是无环的,那么该图至少包含一个没有相邻顶点(即没有出边)的顶点。
-
证明
:这是引理 7.7.2 的逆否命题。
-
定理 7.7.4
:如果有向图是无环的,那么它可以进行拓扑排序。
-
证明
:采用对图中顶点数量的弱归纳法。设 S(n) 表示“每个具有 n 个顶点的无环有向图都可以进行拓扑排序”。当 n = 1 时,该陈述显然为真。对于归纳步骤,假设对于某个正整数 k,S(k) 为真,即“每个具有 k 个顶点的无环有向图都可以进行拓扑排序”。我们要证明 S(k + 1) 也为真,即“每个具有 k + 1 个顶点的无环有向图都可以进行拓扑排序”。取任意一个具有 k + 1 个顶点的无环有向图 G。根据推论 7.7.3,G 至少有一个没有出边的顶点,将其命名为 x。然后考虑从 G 中移除顶点 x 以及所有指向 x 的边后得到的子图 H。这个有向图 H 只有 k 个顶点,并且显然 H 不包含环,因为这样的环也会是 G 中的环。根据归纳假设,H 可以进行拓扑排序。将 H 的顶点列表记为 v1, v2, …, vk,我们可以将顶点 x 添加到列表的末尾,从而得到 G 的一个拓扑排序。这是因为对于图 G 中满足 (v, x) 的任意顶点 v,v 必须在扩展列表中排在 x 之前,并且图 G 中没有形式为 (x, y) 的边。
下面是拓扑排序算法的代码实现:
// The functions below assume a definition of a struct data type named "node"
// with two members, one of type int and the other of type node*.
// The first function is a driver for the second, which is recursive.
// The first function returns a NULL-terminated linked list of nodes containing a topological
// ordering of the vertices of the digraph, or NULL if the digraph has a cycle.
// reached and removed must be added as data members to the class Graph.
node * Graph:: topological_ordering ()
// to be used only for digraphs
{
reached = new bool[num_vertices];
removed = new bool[num_vertices];
bool cycle_found = false;
node * topolist = NULL;
int v;
// Initialize all the cells of the arrays reached[] and removed[] to false.
for (v = 0; v < num_vertices; ++v) {
reached[v] = false;
removed[v] = false;
}
for (v = 0; v < num_vertices && ! cycle_found; ++v)
{
if (! reached[v])
{
reached[v] = true;
dfs_topological_order(v, cycle_found, topolist);
// Call recursive function.
if (cycle_found)
{
// Deallocate all the nodes in topolist and set topolist to NULL.
node * temp;
while (topolist != NULL) {
temp = topolist;
topolist = topolist->next;
delete temp;
}
topolist = NULL;
}
}
}
return topolist;
}
// This recursive function is always called with cycle_found false. The
// vertex v will have been marked as reached immediately before the call, and
// removed[v] will be false.
// If this function returns with cycle_found still false, then vertex v, and possibly many other vertices, will have been marked
// as "removed", and copies of them will have been placed in the topolist.
void Graph::dfs_topological_order (int v, bool & cycle_found, node * & topolist)
{
node * p;
for (p = vertex[v].edge_list; p != NULL && ! cycle_found; p = p->next)
{
if (removed[p->neighbor])
;
// Do nothing; go to the next neighbor of v.
else if (reached[p->neighbor])
cycle_found = true;
else
{
reached[p->neighbor] = true;
dfs_topological_order (p->neighbor, cycle_found, topolist);
}
}
if (! cycle_found)
{
// Mark v as "removed" and attach a new node, containing v, to the front of topolist.
node * new_node = new node;
new_node->data = v;
new_node->next = topolist;
topolist = new_node;
removed[v] = true;
}
}
3. 拓扑排序算法示例
下面我们通过一个具体的例子来展示如何使用上述算法进行拓扑排序。假设有一个有向图,其邻接结构如图所示。
算法步骤如下:
1. 初始化所有的 reached[] 和 removed[] 数组的元素为 false,同时将 cycle_found 初始化为 false,topolist 初始化为空。
2. 标记顶点 0 为已到达,并从顶点 0 开始进行深度优先搜索。
- 顶点 0 的邻接顶点 4 未被移除且未被到达,标记 4 为已到达,并从 4 开始进行深度优先搜索。
- 顶点 4 的邻接顶点 6 未被移除且未被到达,标记 6 为已到达,并从 6 开始进行深度优先搜索。
- 顶点 6 的邻接顶点 9 未被移除且未被到达,标记 9 为已到达,并从 9 开始进行深度优先搜索。
- 顶点 9 的邻接顶点 8 未被移除且未被到达,标记 8 为已到达,并从 8 开始进行深度优先搜索。
- 顶点 8 没有出边,是一个汇点,标记 8 为已移除,并将 8 插入到 topolist 的开头。
- 回溯到顶点 9,发现 9 没有更多的邻接顶点,标记 9 为已移除,并将 9 插入到 topolist 的开头。
- 回溯到顶点 6,发现 6 没有更多的邻接顶点,标记 6 为已移除,并将 6 插入到 topolist 的开头。
- 回溯到顶点 4,发现其下一个邻接顶点 7 未被移除且未被到达,标记 7 为已到达,并从 7 开始进行深度优先搜索。
- 顶点 7 的唯一邻接顶点 8 已被移除,回溯到 4,发现 4 没有更多的邻接顶点,标记 4 为已移除,并将 4 插入到 topolist 的开头。
- 回溯到顶点 0,标记 0 为已移除,并将 0 插入到 topolist 中。
3. 回溯到驱动程序,将 v 增加到 1,标记 1 为已到达,并开始新的深度优先搜索。
- 顶点 1 的邻接顶点 4 已被移除,邻接顶点 5 未被移除且未被到达,标记 5 为已到达,并从 5 开始进行深度优先搜索。
- 顶点 5 没有出边,是一个汇点,标记 5 为已移除,并将 5 插入到 topolist 的开头。
- 回溯到顶点 1,发现 1 没有其他邻接顶点。
4. 驱动程序中的 for 循环将 v 增加到 2,标记 2 为已到达,并开始新的深度优先搜索。
- 顶点 2 的邻接顶点都已被到达,标记 2 为已移除,并将 2 插入到 topolist 的开头。
5. 驱动程序中的 for 循环将 v 增加到 3,标记 3 为已到达,并进行深度优先搜索,发现其邻接顶点 0 已被到达。
6. 所有顶点都已被移除,驱动程序返回指向 topolist 的指针,该列表为 (3, 2, 5, 1, 0, 7, 4, 6, 9, 8)。
4. 拓扑排序相关练习题
| 题号 | 题目描述 |
|---|---|
| 7.7.6 | (a) 为具有特定结构的有向图的顶点找到拓扑排序(如果可能)。(b) 为另一个有向图的顶点找到拓扑排序(如果可能)。 |
| 7.7.7 | 为具有特定邻接结构的有向图的顶点找到拓扑排序(如果可能)。 |
| 7.7.8 | 给出一个算法的伪代码,用于搜索任意给定的有向图,以确定该图是否无环。同时给出无向图的类似算法。 |
通过以上内容,我们了解了最小生成树的生成方法以及有向图的拓扑排序算法,这些算法在许多领域都有广泛的应用,如任务调度、项目管理等。希望大家通过阅读本文,对图算法有更深入的理解。
图算法中的最小生成树与拓扑排序
5. 最小生成树与拓扑排序的应用场景
最小生成树和拓扑排序在实际生活和计算机科学领域有着广泛的应用,下面为大家详细介绍:
| 算法类型 | 应用场景 | 具体说明 |
|---|---|---|
| 最小生成树 | 网络布线 | 在构建局域网、电力网络等时,需要连接多个节点,使用最小生成树算法可以找到成本最低的连接方案,减少布线成本。 |
| 图像分割 | 将图像中的像素点看作图的顶点,像素之间的相似性作为边的权重,通过最小生成树算法可以将图像分割成不同的区域。 | |
| 拓扑排序 | 任务调度 | 在项目管理中,任务之间存在先后依赖关系,使用拓扑排序可以确定任务的执行顺序,保证项目的顺利进行。 |
| 课程安排 | 在学校的课程设置中,有些课程需要先修其他课程,通过拓扑排序可以合理安排课程的开设顺序。 |
6. 最小生成树与拓扑排序的复杂度分析
了解算法的复杂度对于评估算法的性能至关重要,下面我们来分析最小生成树和拓扑排序算法的时间复杂度和空间复杂度。
- Prim 算法 :时间复杂度为 $O(V^2)$,其中 $V$ 是图的顶点数。如果使用优先队列优化,时间复杂度可以降低到 $O((V + E) \log V)$,其中 $E$ 是图的边数。空间复杂度为 $O(V)$。
- Kruskal 算法 :时间复杂度为 $O(E \log E)$,主要用于对边进行排序。空间复杂度为 $O(E)$。
- 拓扑排序算法 :时间复杂度为 $O(V + E)$,其中 $V$ 是图的顶点数,$E$ 是图的边数。空间复杂度为 $O(V)$。
7. 最小生成树与拓扑排序的优化思路
为了提高算法的性能,我们可以对最小生成树和拓扑排序算法进行优化,以下是一些常见的优化思路:
- Prim 算法优化 :使用优先队列来存储边界顶点,每次从优先队列中取出权重最小的顶点,这样可以将时间复杂度降低到 $O((V + E) \log V)$。
- Kruskal 算法优化 :使用并查集来判断边的加入是否会形成环,提高算法的效率。
- 拓扑排序算法优化 :使用入度数组来记录每个顶点的入度,每次选择入度为 0 的顶点进行处理,避免不必要的搜索。
8. 总结
通过本文的介绍,我们深入了解了最小生成树和拓扑排序的相关知识,包括它们的定义、算法实现、应用场景、复杂度分析和优化思路。
最小生成树算法可以帮助我们找到图中连接所有顶点的最小成本路径,而拓扑排序算法可以帮助我们确定有向图中顶点的先后顺序。这些算法在许多领域都有重要的应用,如网络设计、项目管理、图像处理等。
在实际应用中,我们需要根据具体的问题选择合适的算法,并根据算法的复杂度和优化思路来提高算法的性能。希望大家通过本文的学习,对图算法有更深入的理解,并能够在实际问题中灵活运用这些算法。
9. 流程图
下面是拓扑排序算法的 mermaid 格式流程图:
graph TD;
A[初始化 reached[] 和 removed[] 数组为 false,cycle_found 为 false,topolist 为空] --> B[选择未到达的顶点 v];
B --> C{是否存在未到达的顶点};
C -- 是 --> D[标记顶点 v 为已到达];
D --> E[从顶点 v 开始深度优先搜索];
E --> F{是否找到环};
F -- 是 --> G[清空 topolist 并返回 NULL];
F -- 否 --> H[将顶点 v 标记为已移除并插入到 topolist 开头];
H --> B;
C -- 否 --> I[返回 topolist];
这个流程图展示了拓扑排序算法的主要步骤,包括初始化、选择未到达的顶点、深度优先搜索、判断是否存在环以及更新 topolist 等。通过这个流程图,我们可以更直观地理解拓扑排序算法的执行过程。
超级会员免费看
506

被折叠的 条评论
为什么被折叠?



