xxx 有向无环图(DAG)的传递闭包问题
题目描述
给定一个有向无环图(DAG),其顶点数为 n (标记为 0 到 n-1),边数为 m。我们想要计算出这个图的传递闭包。
传递闭包是一个布尔矩阵 T,大小为 n x n。如果矩阵 T[i][j] 的值为真(通常表示为 1 或 true),则意味着在图中,从顶点 i 出发,可以通过沿着有向边行走若干步(可以是 0 步或更多步)最终到达顶点 j。特别地,对于任意顶点 i,T[i][i] 总是为真(通过 0 步到达自己)。
简单来说,传递闭包清晰地描述了图中所有顶点之间的“可达性”关系。
解题过程
计算传递闭包有多种方法,我们主要讲解两种:一种是基于深度优先搜索(DFS) 的朴素方法,另一种是基于动态规划/矩阵乘法思想的 Floyd-Warshall 算法 的变形。由于图是 DAG,没有环,这给我们的计算带来了一些便利,可以减少重复计算。
方法一:基于深度优先搜索(DFS)的递归方法
这个方法的核心思想是:对于每个顶点 i,我们从 i 开始进行一次 DFS 或 BFS,所有在搜索过程中访问到的顶点 j,都可以从 i 到达,即 T[i][j] = true。
步骤详解:
-
初始化:
- 我们用一个邻接表
adj来表示输入的 DAG。adj[u]存储所有从顶点 u 直接指向的顶点 v 的列表。 - 创建一个 n x n 的布尔矩阵
reachable,初始值全部设为false。这个矩阵就是我们要计算的传递闭包 T。
- 我们用一个邻接表
-
为每个顶点启动DFS:
- 我们依次以每个顶点
src(source) 作为起点。 - 进行一次从
src出发的 DFS 遍历。由于是 DAG,这次 DFS 不需要担心遇到环导致无限递归。
- 我们依次以每个顶点
-
DFS遍历函数(递归):
- 假设当前进入的顶点是
u。 - 将
reachable[src][u]设置为true。因为从起点src可以到达当前顶点u。 - 然后,对于
u的每一个邻居v(即adj[u]中的每个顶点):- 如果
reachable[src][v]已经是true,说明之前通过其他路径已经标记过 v 的可达性,可以跳过以避免重复工作。 - 否则,递归调用 DFS 函数,以
v作为新的当前顶点继续探索。
- 如果
- 假设当前进入的顶点是
-
复杂度分析:
- 为每个顶点做一次 DFS,每次 DFS 可能会访问几乎整个图。
- 最坏情况下(比如接近完全图形态的DAG),每次DFS是 O(n+m),做 n 次就是 O(n*(n+m))。在稠密图中 m 约为 O(n^2),所以复杂度为 O(n^3)。
- 空间复杂度主要是存储邻接表的 O(n+m) 和存储传递闭包矩阵的 O(n^2)。
方法二:基于动态规划的 Floyd-Warshall 风格算法
这种方法直接对顶点进行动态规划,其状态转移思想非常经典:如果存在一个中间顶点 k,使得 i 能到达 k,并且 k 能到达 j,那么 i 就能到达 j。
步骤详解:
-
初始化可达性矩阵:
- 创建一个 n x n 的布尔矩阵
reachable。 - 首先,根据图的直接边进行初始化。对于每条边 (u, v),将
reachable[u][v]设为true。 - 同时,对于每个顶点 i,将
reachable[i][i]设为true(自己可达自己)。
- 创建一个 n x n 的布尔矩阵
-
动态规划(三层循环):
- 关键思想是,我们按顺序考虑所有顶点作为“中间跳板”或“中继点”。
- 外层循环遍历所有可能的中间顶点
k(从 0 到 n-1)。 - 内层用两层循环遍历所有的起点
i和终点j(都从 0 到 n-1)。 - 状态转移:检查如果从
i可以到达k,并且从k可以到达j,那么我们就可以推断出从i可以到达j。 - 用逻辑或运算表示:
reachable[i][j] = reachable[i][j] || (reachable[i][k] && reachable[k][j]) - 注意:这里循环的顺序很重要。我们必须将
k放在最外层。可以这样理解:当我们在处理中间顶点 k 时,我们是在问“在允许路径经过顶点 0, 1, ..., k 的情况下,i 是否能到达 j?”。通过逐步增加允许经过的顶点集合,我们最终考虑了所有可能的路径。
-
结果:
- 当三层循环结束后,
reachable矩阵就存储了完整的传递闭包信息。
- 当三层循环结束后,
-
复杂度与优势:
- 时间复杂度固定为 O(n^3),因为有三层 n 的循环。
- 空间复杂度为 O(n^2)。
- 这种方法实现简单,不依赖于图的稀疏性。对于 DAG 来说,其正确性同样是成立的,并且代码非常规整。
总结与对比
- 基于DFS的方法 在稀疏图上可能更高效,因为它只探索实际存在的边。但如果要为每个顶点都做一次DFS,在最坏情况下复杂度也是 O(n^3)。
- 基于动态规划(Floyd风格)的方法 实现简单,逻辑清晰,但复杂度固定为 O(n^3),并且即使图很稀疏,也需要进行 n^3 次布尔运算。不过,由于其常数小,并且易于理解和实现,在实际应用和小规模问题中很常见。
- 优化提示:对于 DAG,我们可以先进行一次拓扑排序。得到一个顶点线性序列后,按照拓扑逆序(从后往前)处理顶点。对于当前顶点 u,它的可达集合是所有其直接后继顶点 v 的可达集合的并集,再加上 u 自身。这种方法也能在 O(n*m) 的平均时间内完成,在特定结构下比 O(n^3) 更快。
通常,我们会根据图的大小、稀疏程度以及实现的简易性来选择具体方法。Floyd-Warshall风格的方法是学习传递闭包最基础和最重要的算法。