有向无环图的反图与逆向思维

  |  

摘要: DAG 中的祖先节点 -> 反图的后代节点

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


本文我们通过一个题目来看一下有向无环图的反图的性质。首先我们简要介绍一下反图的概念以及有向无环图反图的特性,然后看题目。

此外本题还有更巧妙的逆向思维的处理方法。

反图

设原图为 $D$,其反图为 $D’$,由于在反图 $D’$ 中原图 $D$ 的所有的边方向都被反转,因此如果原图中存在一条从节点 A 指向节点 B 的边,在反图中就会变成从节点 B 指向节点 A 的边。

对于有向无环图(Directed Acyclic Graph,简称DAG),其反图保持了无环的特性,也是一棵有向无环图。对于有向无环图,在原图中作为祖先节点的每个节点,在反图中将成为后代节点,反之亦然。

反图的概念在有向图中非常重要,原图表示的是一种有向关系,而其反图则表示了这种关系的逆关系,这就允许我们从不同的角度研究图的性质。在某些情况下,研究反图的性质可以简化问题的求解。

例如,如果原图表示了一个函数调用的流程,那么其反图可能表示了函数返回值的传递流程。如果原图表示了一个任务的依赖关系,那么其反图可能表示了任务完成后的传递关系。

下面我们看一个相关的题目。

题目

给你一个正整数 n ,它表示一个 有向无环图 中节点的数目,节点编号为 0 到 n - 1 (包括两者)。

给你一个二维整数数组 edges ,其中 edges[i] = [fromi, toi] 表示图中一条从 fromi 到 toi 的单向边。

请你返回一个数组 answer,其中 answer[i]是第 i 个节点的所有 祖先 ,这些祖先节点 升序 排序。

如果 u 通过一系列边,能够到达 v ,那么我们称节点 u 是节点 v 的 祖先 节点。

提示:

1
2
3
4
5
6
7
1 <= n <= 1000
0 <= edges.length <= min(2000, n * (n - 1) / 2)
edges[i].length == 2
0 <= fromi, toi <= n - 1
fromi != toi
图中不会有重边。
图是 有向 且 无环 的。

示例 1:

输入:n = 8, edgeList = [[0,3],[0,4],[1,3],[2,4],[2,7],[3,5],[3,6],[3,7],[4,6]]
输出:[[],[],[],[0,1],[0,2],[0,1,3],[0,1,2,3,4],[0,1,2,3]]
解释:
上图为输入所对应的图。

  • 节点 0 ,1 和 2 没有任何祖先。
  • 节点 3 有 2 个祖先 0 和 1 。
  • 节点 4 有 2 个祖先 0 和 2 。
  • 节点 5 有 3 个祖先 0 ,1 和 3 。
  • 节点 6 有 5 个祖先 0 ,1 ,2 ,3 和 4 。
  • 节点 7 有 4 个祖先 0 ,1 ,2 和 3 。

示例 2:

输入:n = 5, edgeList = [[0,1],[0,2],[0,3],[0,4],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
输出:[[],[0],[0,1],[0,1,2],[0,1,2,3]]
解释:
上图为输入所对应的图。

  • 节点 0 没有任何祖先。
  • 节点 1 有 1 个祖先 0 。
  • 节点 2 有 2 个祖先 0 和 1 。
  • 节点 3 有 3 个祖先 0 ,1 和 2 。
  • 节点 4 有 4 个祖先 0 ,1 ,2 和 3 。

题解

算法:图的遍历

由于有向无环图的反图也是有向无环图,同时原图中的祖先节点变成了反图中的后代节点k。因此我们要对每个节点求祖先节点,就可以转化为对反图的每个节点求后代节点。而求每个节点的后代节点,简单地通过图的遍历即可解决。

因此完整算法就是先根据边表建立反图,并记录反图中各个节点的入度。从每个入度为 0 的点出发,进行一轮 DFS 遍历,过程中维护 $result[u]$,表示 $u$ 的所有后代节点(不包含 $u$)。

对于遍历到的节点 $u$,首先递归地处理 $u$ 的各个子节点 $v$,然后将 $v$ 的所有后代节点 $result[v]$ 记入 $result[u]$,然后将子节点 $v$ 本身记入 $result[u]$,然后回溯即可。

这里需要注意我们处理的结构是有向无环图,而不是树,因此 $u$ 的不同子节点 $v$,其 $result[v]$ 可能有重复的部分,需要处理去重。因此在 DFS 时 $result[u]$ 应该用哈希表维护,最后再转换为数组,然后再排序。

代码 (C++)

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Solution {
public:
vector<vector<int>> getAncestors(int n, vector<vector<int>>& edges) {
vector<vector<int>> rg(n);
vector<int> indegrees(n);
for(const vector<int> &e: edges)
{
int u = e[0];
int v = e[1];
rg[v].push_back(u);
indegrees[u] += 1;
}

vector<unordered_set<int>> _result(n);

for(int u = 0; u < n; ++u)
{
if(indegrees[u] > 0)
continue;
dfs(u, rg, _result);
}

vector<vector<int>> result(n);

for(int i = 0; i < n; ++i)
{
result[i] = vector<int>(_result[i].begin(), _result[i].end());
sort(result[i].begin(), result[i].end());
}

return result;
}

void dfs(int u, const vector<vector<int>>& rg, vector<unordered_set<int>>& _result)
{
if(!_result[u].empty())
return;
for(int v: rg[u])
{
_result[u].insert(v);
dfs(v, rg, _result);
for(int i: _result[v])
_result[u].insert(i);
}
}
};

代码 (Python)

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
class Solution:
def getAncestors(self, n: int, edges: List[List[int]]) -> List[List[int]]:
rg = [[] for _ in range(n)]
indegrees = [0] * n
for u, v in edges:
rg[v].append(u)
indegrees[u] += 1

result = [set() for _ in range(n)]

def dfs(u: int):
if result[u]:
return
for v in rg[u]:
result[u].add(v)
dfs(v)
result[u].update(result[v])

for u in range(n):
if indegrees[u] != 0:
continue
dfs(u)

for i in range(n):
result[i] = list(result[i])
result[i].sort()

return result

算法2:逆向思维

对于节点 $u$,刚才我们考虑的是有哪些节点是 $u$ 的后代节点。如果逆向思维,就可以考虑 $u$ 可以作为哪些节点的祖先节点。

这样我们就可以按编号从小到大依次枚举每个节点 $u$,然后走一遍以 $u$ 为起点的 DFS,过程中访问到的所有节点都记 $u$ 为其祖先节点。

由于枚举 $u$ 是有序的,于是要求的次序自然就满足了,以 $u$ 为起点的 DFS 过程中每个节点最多访问一次,于是重复问题也没了。

代码 (C++)

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
30
class Solution {
public:
vector<vector<int>> getAncestors(int n, vector<vector<int>>& edges) {
vector<vector<int>> g(n);
for(const vector<int> &e: edges)
{
int u = e[0];
int v = e[1];
g[u].push_back(v);
}

vector<vector<int>> result(n);
vector<int> visited(n, -1);
for(int u = 0; u < n; ++u)
dfs(u, g, result, visited, u);

return result;
}

void dfs(int u, const vector<vector<int>>& g, vector<vector<int>>& result, vector<int>& visited, const int root)
{
if(visited[u] == root)
return;
visited[u] = root;
if(u != root)
result[u].push_back(root);
for(int v: g[u])
dfs(v, g, result, visited, root);
}
};

代码 (Python)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution:
def getAncestors(self, n: int, edges: List[List[int]]) -> List[List[int]]:
g = [[] for _ in range(n)]
for u, v in edges:
g[u].append(v)

result = [[] for _ in range(n)]
visited = [-1] * n

def dfs(u: int, root: int):
if visited[u] == root:
return
visited[u] = root
if u != root:
result[u].append(root)
for v in g[u]:
dfs(v, root)

for u in range(n):
dfs(u, u)

return result

Share