在具有多个起始节点的有向无环图 (DAG) 中查找所有路径的最快方法?
Fastest way to find all paths in a directed acyclic graph (DAG) with multiple starting nodes?
我正在尝试在具有多个起始节点的有向无环图 (DAG) 中查找所有路径。我已经有了由列表列表表示的 DAG,连同从开始到终止的每一级节点:
dag = [[4, 6, 7], [5, 6, 7], [4, 5, 6], [4, 5, 7],
[], [], [], [], [1, 3], [1, 2], [0, 2, 3]]
levels = [[8, 9, 10], [0, 1, 2, 3], [4, 5, 6, 7]]
按级别着色的示例 DAG 如下所示:
由于我有 3 个起始节点 [8, 9, 10]
,我的第一个想法是执行 3 个 DFS。下面是我的代码:
def get_all_paths(dag, sources):
def dfs(data, path, paths=None):
if paths is None:
paths = []
prev = path[-1]
if data[prev]:
for val in data[prev]:
new_path = path + [val]
paths = dfs(data, new_path, paths)
else:
paths += [path]
return paths
all_paths = []
for i in sources:
paths = dfs(data=dag, path=[i], paths=[])
all_paths += paths
return all_paths
all_paths = get_all_paths(dag, sources=levels[0])
print(all_paths)
输出:
[[8, 1, 5], [8, 1, 6], [8, 1, 7], [8, 3, 4], [8, 3, 5], [8, 3, 7],
[9, 1, 5], [9, 1, 6], [9, 1, 7], [9, 2, 4], [9, 2, 5], [9, 2, 6],
[10, 0, 4], [10, 0, 6], [10, 0, 7], [10, 2, 4], [10, 2, 5],
[10, 2, 6], [10, 3, 4], [10, 3, 5], [10, 3, 7]]
%timeit 24.6 µs ± 577 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
但是,当图很大或者起始节点很多时,我的代码会变得很慢。众所周知,在一个DAGG(V,E)
中寻找所有路径的DFS时间复杂度是O(V+E)。所以我的方法的时间复杂度是O(m(V+E)),其中m是起始节点的数量。在我的代码中,每个节点都被访问了 m 次,但我觉得仍然可以只访问每个节点一次,特别是考虑到 levels
并且我没有使用它。也许 BFS 可以,但我不确定如何编写。有什么建议吗?
编辑
这里有一些答案的基准测试
我已经根据@chepner 的评论调整了我的 BFS 代码。
def get_all_paths(dag, sources):
def dfs(data, path, paths=None):
if paths is None:
paths = []
prev = path[-1]
if data[prev]:
for val in data[prev]:
new_path = path + [val]
paths = dfs(data, new_path, paths)
else:
paths.append(path[1:])
return paths
dag.append(sources)
return dfs(data=dag, path=[len(dag)-1], paths=[])
%timeit get_all_paths(dag, sources=levels[0])
输出:
9.73 µs ± 112 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
测试@aminrd 的回答:
from collections import defaultdict
def aminrd(dag, levels):
paths = defaultdict(list)
levels_reversed = levels[::-1]
for node in levels_reversed[0]:
paths[node] = [[node]]
for lvl in levels_reversed[1:]:
for src in lvl:
for dst in dag[src-1]:
paths[src] += [[src] + p for p in paths[dst]]
result = []
for lvl_0 in levels[0]:
result += paths[lvl_0]
%timeit aminrd(dag, levels)
输出
10.7 µs ± 164 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
在单个增广图中寻找路径。如果您的起始节点集是 S
,请为 S
中的每个 s
添加一个新的起始节点 S0
和边 (S0, s)
。单个 DFS 完成后,您可以简单地从每个路径中删除 S0
,留下原始图中的路径。
这可能会减少 运行 dfs
多次创建的一些重复,但不会解决不可避免的事实,即您的 运行 时间与路径数量成正比待找到。
通过从最后一级的节点开始,您可以创建一个散列,这样您就可以先从叶子开始构建,然后逐级向上构建。我也猜测你在问题中的 dag
与你发布的数字不完全相同:
from collections import defaultdict
dag = [[5, 4, 6, 7], [5, 6, 7], [4, 5, 6], [4, 5, 7],
[], [], [], [1, 3], [1, 2], [0, 2, 3]]
levels = [[8, 9, 10], [0, 1, 2, 3], [4, 5, 6, 7]]
paths = defaultdict(list)
levels_reversed = levels[::-1]
for node in levels_reversed[0]:
paths[node] = [[node]]
for lvl in levels_reversed[1:]:
for src in lvl:
for dst in dag[src-1]:
paths[src] += [[src] + p for p in paths[dst]]
result = []
for lvl_0 in levels[0]:
result += paths[lvl_0]
结果:
[[8, 1, 5], [8, 1, 4], [8, 1, 6], [8, 1, 7],
[8, 3, 4], [8, 3, 5], [8, 3, 6], [9, 1, 5],
[9, 1, 4], [9, 1, 6], [9, 1, 7], [9, 2, 5],
[9, 2, 6], [9, 2, 7], [10, 2, 5], [10, 2, 6],
[10, 2, 7], [10, 3, 4], [10, 3, 5], [10, 3, 6]]
我正在尝试在具有多个起始节点的有向无环图 (DAG) 中查找所有路径。我已经有了由列表列表表示的 DAG,连同从开始到终止的每一级节点:
dag = [[4, 6, 7], [5, 6, 7], [4, 5, 6], [4, 5, 7],
[], [], [], [], [1, 3], [1, 2], [0, 2, 3]]
levels = [[8, 9, 10], [0, 1, 2, 3], [4, 5, 6, 7]]
按级别着色的示例 DAG 如下所示:
由于我有 3 个起始节点 [8, 9, 10]
,我的第一个想法是执行 3 个 DFS。下面是我的代码:
def get_all_paths(dag, sources):
def dfs(data, path, paths=None):
if paths is None:
paths = []
prev = path[-1]
if data[prev]:
for val in data[prev]:
new_path = path + [val]
paths = dfs(data, new_path, paths)
else:
paths += [path]
return paths
all_paths = []
for i in sources:
paths = dfs(data=dag, path=[i], paths=[])
all_paths += paths
return all_paths
all_paths = get_all_paths(dag, sources=levels[0])
print(all_paths)
输出:
[[8, 1, 5], [8, 1, 6], [8, 1, 7], [8, 3, 4], [8, 3, 5], [8, 3, 7],
[9, 1, 5], [9, 1, 6], [9, 1, 7], [9, 2, 4], [9, 2, 5], [9, 2, 6],
[10, 0, 4], [10, 0, 6], [10, 0, 7], [10, 2, 4], [10, 2, 5],
[10, 2, 6], [10, 3, 4], [10, 3, 5], [10, 3, 7]]
%timeit 24.6 µs ± 577 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
但是,当图很大或者起始节点很多时,我的代码会变得很慢。众所周知,在一个DAGG(V,E)
中寻找所有路径的DFS时间复杂度是O(V+E)。所以我的方法的时间复杂度是O(m(V+E)),其中m是起始节点的数量。在我的代码中,每个节点都被访问了 m 次,但我觉得仍然可以只访问每个节点一次,特别是考虑到 levels
并且我没有使用它。也许 BFS 可以,但我不确定如何编写。有什么建议吗?
编辑
这里有一些答案的基准测试
我已经根据@chepner 的评论调整了我的 BFS 代码。
def get_all_paths(dag, sources):
def dfs(data, path, paths=None):
if paths is None:
paths = []
prev = path[-1]
if data[prev]:
for val in data[prev]:
new_path = path + [val]
paths = dfs(data, new_path, paths)
else:
paths.append(path[1:])
return paths
dag.append(sources)
return dfs(data=dag, path=[len(dag)-1], paths=[])
%timeit get_all_paths(dag, sources=levels[0])
输出:
9.73 µs ± 112 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
测试@aminrd 的回答:
from collections import defaultdict
def aminrd(dag, levels):
paths = defaultdict(list)
levels_reversed = levels[::-1]
for node in levels_reversed[0]:
paths[node] = [[node]]
for lvl in levels_reversed[1:]:
for src in lvl:
for dst in dag[src-1]:
paths[src] += [[src] + p for p in paths[dst]]
result = []
for lvl_0 in levels[0]:
result += paths[lvl_0]
%timeit aminrd(dag, levels)
输出
10.7 µs ± 164 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
在单个增广图中寻找路径。如果您的起始节点集是 S
,请为 S
中的每个 s
添加一个新的起始节点 S0
和边 (S0, s)
。单个 DFS 完成后,您可以简单地从每个路径中删除 S0
,留下原始图中的路径。
这可能会减少 运行 dfs
多次创建的一些重复,但不会解决不可避免的事实,即您的 运行 时间与路径数量成正比待找到。
通过从最后一级的节点开始,您可以创建一个散列,这样您就可以先从叶子开始构建,然后逐级向上构建。我也猜测你在问题中的 dag
与你发布的数字不完全相同:
from collections import defaultdict
dag = [[5, 4, 6, 7], [5, 6, 7], [4, 5, 6], [4, 5, 7],
[], [], [], [1, 3], [1, 2], [0, 2, 3]]
levels = [[8, 9, 10], [0, 1, 2, 3], [4, 5, 6, 7]]
paths = defaultdict(list)
levels_reversed = levels[::-1]
for node in levels_reversed[0]:
paths[node] = [[node]]
for lvl in levels_reversed[1:]:
for src in lvl:
for dst in dag[src-1]:
paths[src] += [[src] + p for p in paths[dst]]
result = []
for lvl_0 in levels[0]:
result += paths[lvl_0]
结果:
[[8, 1, 5], [8, 1, 4], [8, 1, 6], [8, 1, 7],
[8, 3, 4], [8, 3, 5], [8, 3, 6], [9, 1, 5],
[9, 1, 4], [9, 1, 6], [9, 1, 7], [9, 2, 5],
[9, 2, 6], [9, 2, 7], [10, 2, 5], [10, 2, 6],
[10, 2, 7], [10, 3, 4], [10, 3, 5], [10, 3, 6]]