在Erlang中查找有向循环图中从一个顶点到达所有可能路径。

6
我想实现一个函数,它可以在有向循环图G中从源顶点V找到所有可能的顶点的路径。
现在性能不重要,我只是想理解算法。我已经阅读了深度优先搜索算法的定义,但我不完全理解该如何操作。
我没有任何完成的代码片段可以提供在这里,因为我不确定如何:
- 存储结果(以及A->B->C->我们还应该存储A->B和A->B->C); - 表示图(有向图?元组列表?); - 使用多少递归(处理每个相邻的顶点?)。
如何在Erlang中找到有向循环图中给定源顶点的所有可能路径?
更新:根据迄今为止的答案,我必须重新定义图的定义:它是一个非无环图。我知道如果我的递归函数遇到循环,它就是一个无限循环。为了避免这种情况,我可以检查当前顶点是否在结果路径列表中 - 如果是,则停止遍历并返回路径。
更新2:感谢思考启发性的评论!是的,我需要找到所有从一个源顶点到其他所有顶点的简单路径,这些路径都没有循环。
在这样的图中:
[图片]
以源顶点A为例,算法应该找到以下路径:
- A,B - A,B,C - A,B,C,D - A,D - A,D,C - A,D,C,B
以下代码可以完成此任务,但对于具有超过20个顶点的图来说不可用(我猜递归出了问题 - 占用太多内存,永远不会结束):
dfs(Graph,Source) ->
    ?DBG("Started to traverse graph~n", []),
            Neighbours = digraph:out_neighbours(Graph,Source),
    ?DBG("Entering recursion for source vertex ~w~n", [Source]),
            dfs(Neighbours,[Source],[],Graph,Source),
ok.


dfs([],Paths,Result,_Graph,Source) ->
    ?DBG("There are no more neighbours left for vertex ~w~n", [Source]),
    Result;

dfs([Neighbour|Other_neighbours],Paths,Result,Graph,Source) ->
    ?DBG("///The neighbour to check is ~w, other neighbours are: ~w~n",[Neighbour,Other_neighbours]),
    ?DBG("***Current result: ~w~n",[Result]),
    New_result = relax_neighbours(Neighbour,Paths,Result,Graph,Source),

        dfs(Other_neighbours,Paths,New_result,Graph,Source).


relax_neighbours(Neighbour,Paths,Result,Graph,Source) ->
     case lists:member(Neighbour,Paths) of 
        false ->
            ?DBG("Found an unvisited neighbour ~w, path is: ~w~n",[Neighbour,Paths]),
            Neighbours = digraph:out_neighbours(Graph,Neighbour),
            ?DBG("The neighbours of the unvisited vertex ~w are ~w, path is:
                ~w~n",[Neighbour,Neighbours,[Neighbour|Paths]]),
                dfs(Neighbours,[Neighbour|Paths],Result,Graph,Source);
            true ->
                [Paths|Result]

        end.

UPD3:

问题在于常规深度优先搜索算法会首先选取(A,B,C,D)或者(A,D,C,B)这两条路径之一,并且永远不会走第二条路径。

无论哪种情况,这都是唯一的路径——例如,当常规DFS从(A,B,C,D)返回时,它会回到A并检查D(A的第二个邻居)是否已被访问。由于常规DFS为每个顶点维护全局状态,因此D将具有“已访问”状态。

因此,我们必须引入一个递归依赖状态——如果我们从(A,B,C,D)返回到A,则应该在结果列表中将(A,B,C,D)包含,并将D标记为未访问,就像算法的开始一样。

我尝试将解决方案优化为尾递归,并且仍然无法承受算法的运行时间——对于一个只有16个顶点,每个顶点有3条边的小图,遍历需要大约4秒钟:

dfs(Graph,Source) ->
    ?DBG("Started to traverse graph~n", []),
            Neighbours = digraph:out_neighbours(Graph,Source),
    ?DBG("Entering recursion for source vertex ~w~n", [Source]),
    Result = ets:new(resulting_paths, [bag]),
Root = Source,
            dfs(Neighbours,[Source],Result,Graph,Source,[],Root).


dfs([],Paths,Result,_Graph,Source,_,_) ->
    ?DBG("There are no more neighbours left for vertex ~w, paths are ~w, result is ~w~n", [Source,Paths,Result]),
    Result;

dfs([Neighbour|Other_neighbours],Paths,Result,Graph,Source,Recursion_list,Root) ->
    ?DBG("~w *Current source is ~w~n",[Recursion_list,Source]),
    ?DBG("~w Checking neighbour _~w_ of _~w_, other neighbours are: ~w~n",[Recursion_list,Neighbour,Source,Other_neighbours]),



?    DBG("~w Ready to check for visited: ~w~n",[Recursion_list,Neighbour]),

 case lists:member(Neighbour,Paths) of 
        false ->
            ?DBG("~w Found an unvisited neighbour ~w, path is: ~w~n",[Recursion_list,Neighbour,Paths]),
New_paths = [Neighbour|Paths],
?DBG("~w Added neighbour to paths: ~w~n",[Recursion_list,New_paths]),
ets:insert(Result,{Root,Paths}),

            Neighbours = digraph:out_neighbours(Graph,Neighbour),
            ?DBG("~w The neighbours of the unvisited vertex ~w are ~w, path is: ~w, recursion:~n",[Recursion_list,Neighbour,Neighbours,[Neighbour|Paths]]),
                dfs(Neighbours,New_paths,Result,Graph,Neighbour,[[[]]|Recursion_list],Root);
            true -> 
            ?DBG("~w The neighbour ~w is: already visited, paths: ~w, backtracking to other neighbours:~n",[Recursion_list,Neighbour,Paths]),
ets:insert(Result,{Root,Paths})

end,

        dfs(Other_neighbours,Paths,Result,Graph,Source,Recursion_list,Root).

有什么方法可以在合理的时间内运行它?
3个回答

2
我不理解问题。如果我有一个图 G = (V, E) = ({A,B}, {(A,B),(B,A)}),那么从 A 到 B 有无限条路径 {[A,B], [A,B,A,B], [A,B,A,B,A,B], ...}。如何找到循环图中任意顶点的所有可能路径?
编辑:
你有没有尝试计算或猜测某些图的可能路径增长?如果你有完全连接的图,你会得到:
2 - 1 3 - 4 4 - 15 5 - 64 6 - 325 7 - 1956 8 - 13699 9 - 109600 10 - 986409 11 - 9864100 12 - 108505111 13 - 1302061344 14 - 16926797485 15 - 236975164804 16 - 3554627472075 17 - 56874039553216 18 - 966858672404689 19 - 17403456103284420 20 - 330665665962403999
你确定想要找到所有节点的所有路径吗?这意味着如果你在一秒钟内计算一百万条路径,那么在具有 20 个节点的完全连接图中计算所有节点的所有路径需要 10750 年。这是你任务的上限,所以我认为你不会想这样做。我认为你想要其他东西。

非常好的编辑!在具有N个顶点和M条边的图中,计算可能的路径数量是否可行?我想下次在开始编码之前最好先数一数 :) - skanatek

2

编辑: 好的,我现在明白了,你想要找到有向图中从一个顶点出发的所有简单路径。所以深度优先搜索加回溯是适合的,正如你已经意识到的那样。一般的想法是去到一个相邻的顶点,然后去到另一个(不是已经访问过的),并继续走,直到遇到死路为止。然后回溯到你上次停留的顶点并选择另一个相邻的顶点,以此类推。 你需要正确地处理琐碎的部分,但这不应该太难。例如,在每个步骤中,您需要根据您是否已经访问过它们来标记顶点为“已探索”或“未探索”。性能不应该成为问题,一个正确实现的算法应该只需要O(n^2)的时间。所以我不知道你做错了什么,也许你访问了太多的邻居?例如,可能你正在重新访问已经访问过的邻居,并且陷入循环之类的东西。

我没有真正阅读过你的程序,但深度优先搜索的维基页面有一个简短、简单的伪代码程序,你可以尝试复制到你的语言中。将图形存储为邻接列表可使它更容易。

编辑: 是的,对不起,你是对的,标准DFS搜索不会像现在这样工作,你需要稍微调整一下,以便不会重复访问已经访问过的顶点。所以你可以访问除了你当前路径中已经存储的顶点之外的任何顶点。 当然,这意味着我的运行时间完全错误,你的算法的复杂度将会很高。如果你的图的平均复杂度是d+1,那么大约有d*d*d*...*d = d^n条可能的路径。 所以即使每个顶点只有3个相邻顶点,当你超过20个顶点时,仍然会有相当多的路径。 实际上没有办法避免这个问题,因为如果你想让你的程序输出所有可能的路径,那么你确实必须输出所有的d^n。

我想知道你是否需要这个任务,还是只是出于兴趣而尝试编程。如果是后者,你只能满足于小型、稀疏连接的图形。


我会说这是一项特定的任务,因为出于某种兴趣而在业余时间完成 :) 什么是图的复杂度? "d^n"中的n是什么意思? 我尝试添加一个约束条件 - 计算给定一对顶点之间的所有可能路径。我的类似Pregel的实现显示了一个不错的结果 - 它在大约100毫秒内找到了20个顶点图中3个边缘每个顶点的2425条路径。然而,在30个顶点的图中,它只找到了274条路径,这显然是错误行为的结果。但我认为我应该再发一个关于这个问题的问题。 - skanatek
1
复杂度是您的程序操作所需步骤数量的估计。如果您的图中有n个顶点,每个顶点有d个邻居,则从起始顶点大约会有(d-1)^n条不同的路径(因为每次到达一个顶点时,都有d-1个新分支可供选择)。查找“算法复杂度”以了解更多信息,这对于这些类型的算法非常重要。重要的是,(d-1)^n是一个指数函数,这意味着它会非常快地变得非常大。指数算法通常是无用的,因为它们很慢。 - HexTree

1

这并不是通过改进算法得到的解决方案,但您可以通过生成多个工作线程来提高性能,可能为每个一级节点产生一个线程,然后聚合结果。这通常可以相对容易地改进天真的暴力算法。

您可以在这里看到一个示例:Some Erlang Matrix Functions,在maximise_assignment函数中(注释从今天的第191行开始)。同样,那里的基本算法相当天真和暴力,但并行化使许多形式的矩阵加速得非常好。

我过去曾使用类似的方法来查找图中汉密尔顿路径的数量。


谢谢指出这一点!我尝试通过为每个顶点生成一个进程来模拟Pregel模型。它将性能提高了4倍以上-现在计算20个顶点(每个顶点4条边)的图中所有路径只需要大约1秒钟。但是,正如Hynek-Pichi-Vychodil所指出的那样-路径数量是巨大的,我必须重新考虑整个方法。 - skanatek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接