Haskell 深度优先图遍历具有无限循环

Haskell Depth-First Graph Traversal has Infinite Loop

提问人:lrainey6-eng 提问时间:11/10/2023 最后编辑:lrainey6-eng 更新时间:11/18/2023 访问量:149

问:

我最近花了一些时间尝试在 Haskell 中制作深度优先遍历算法。但是,对于我的函数,我希望该函数返回一个“访问”列表,该列表按顺序访问了每个节点。

演示:使用图形,输出应该是因为我开始并在 上结束。[('A', "BC"), ('B', "ACD"), ('C', "AB"), ('D', "B")]"ABCBD"AD

这是我的代码:

import Data.List (elemIndex)

trav :: [(Char, String)] -> Char -> Char -> String -> String
trav [] _ _ _ = []
trav graph node endNode visited
    | not (elem node visited) = trav graph node endNode (visited ++ [node])  -- repeat but after adding this node to visited list.
    | node == endNode = visited
    | not (null unvisitedNodes) = trav graph (head unvisitedNodes) endNode visited
    | not (null visited) = if (length prevIndex) > 1 then (trav graph (visited !! (getIndex (head prevIndex) visited)) endNode (visited ++ [(visited !! (getIndex (head prevIndex) visited))])) else (trav graph (head (tail (reverse visited))) endNode (visited ++ [(head (tail (reverse visited)))]))
    | otherwise = visited
    where
        unvisitedNodes = [x | x <- getAttachedVertexes graph node, not (elem x visited)]
        prevIndex = [x | x <- (reverse visited), x < (head (reverse visited))]

getAttachedVertexes :: [(Char, String)] -> Char -> [Char]
getAttachedVertexes graph node = case lookup node graph of
    Just value -> value
    Nothing -> ""

getIndex :: Eq a => a -> [a] -> Int
getIndex item list = case elemIndex item list of
    Just index -> index
    Nothing -> error "help"

这段代码实际上工作正常,并输出了我在上面写的理想输出,当输入以下内容时:trav [('A', "BC"), ('B', "ACD"), ('C', "AB"), ('D', "B")] 'A' 'D' ""

但是,当我输入以下输入时,此代码会启动无限递归循环:trav [('A', "CDE"), ('B', "D"), ('C', "AD"), ('D', "ABC"), ('E', "A")] 'A' 'E' ""

显然没有错误消息,因为它是一个无限循环。


我的思考过程:

我刚刚用更新的 Haskell 代码编辑了这个问题,这是以下思考的结果:

我首先认为问题的一部分是,在回溯时,回溯到的节点被添加到列表中,这意味着当我执行时,返回的项目实际上是当前正在遍历的同一节点。visitedhead (tail (reverse visited))

我将这段代码“翻译”成 Python 3,这是我最有经验的语言,并以 Haskell 格式保存它,即一个包含一系列语句的函数,所有语句中都有一行。ifreturn

接下来,我尝试修复上述错误。它为我上面说的第一个输入返回正确的输出 (),但是,当我尝试上面的第二个输入 () 时,它再次启动无限循环(好吧,至少在堆栈溢出之前)。{"A":["B", "C"], "B":["A", "C", "D"], "C":["A", "B"], "D":["B"]}{"A":['C', 'D', 'E'], "B":['D'], "C":['A','D'], "D":['A','B','C'], "E":['A']}

下面说了 Python 代码:

def trav(graph, node, endNode, visited):
    if node not in visited:
        return trav(graph, node, endNode, (visited + [node]))
    if node == endNode:
        return visited
    unvisitedNodes = [x for x in getAttachedNodes(graph, node) if x not in visited]
    if len(unvisitedNodes) > 0:
        return trav(graph, unvisitedNodes[0], endNode, visited)
    if len(visited) > 0:
        prevIndex = [x for x in reversed(visited) if x < visited[-1]]
        if len(prevIndex) > 1:
            return trav(graph, visited[visited.index(prevIndex[0])], endNode, (visited + [visited[visited.index(prevIndex[0])]]))
        else:
            return trav(graph, visited[-2], endNode, (visited + [visited[-2]]))
    return visited


if __name__ == '__main__':
    graph1 = {"A":["B", "C"], "B":["A", "C", "D"], "C":["A", "B"], "D":["B"]}
    node = input("Node: ")
    endNode = input("EndNode: ")
    graph2 = {"A":['C', 'D', 'E'], "B":['D'], "C":['A','D'], "D":['A','B','C'], "E":['A']}
    visited = []
    print(trav(graph2, node, endNode, visited))

从那以后,我重新编写了 Haskell 代码,现在它在所有测试情况下的运行方式与上述 Python 代码完全相同。显然,这只对那些有能力使用 Python 的人有所帮助。我写它是为了希望能够理解这个问题。我的下一次编辑将是调试此 Python 代码的结果。

对于它的混乱和低效,我深表歉意,但我想尽可能地反映上面的 Haskell 代码。

无论如何,我包含这个是为了希望使我的算法更清晰。

总之,问题不在于我所解释的,我认为它就在上面。

再次感谢。

Haskell 函数编程 无限循环 深度优先搜索

评论

1赞 willeM_ Van Onsem 11/10/2023
为什么在输出中重复两次?B
1赞 willeM_ Van Onsem 11/10/2023
@irainey6-eng:但是那不是很容易进入一个无限循环吗:如果映射到和到和,它可以在和之间开始乒乓球,并且永远不会到达。abbacabc
2赞 Daniel Wagner 11/10/2023
这是(第二个示例)循环的最小输入吗?尝试使输入尽可能小,可以更容易地发现问题 - 因为它们隐藏的地方更少。有什么理由?你怎么知道你总是需要跳回一个节点?有没有必须跳回两个节点的情况?为什么(不是)?head (tail (reverse visited))
2赞 lrainey6-eng 11/11/2023
对于查看此问题的每个人来说,我可能已经发现了问题的一部分 - 我认为(借助上面@DanielWagner的评论)每当它回溯一个节点时,它仍然会将该节点添加到访问列表中,因此,如果连续中的第二个节点也没有未访问的相邻节点,它将再次“回溯”到倒数第二项, 但这一次,倒数第二项实际上是当前正在检查的节点。我会做一些游戏,如果我想出如何解决这个问题,我会把它作为答案发布。
1赞 n. m. could be an AI 11/13/2023
无论如何,这样的输出是没有意义的。该算法会告诉你它找到了一条路径,但它不会向你显示它找到的路径。相反,它会转储其内部工作数据。为什么有人会想要这样的输出?

答:

0赞 Geoffrey Warne 11/11/2023 #1

@lrainey6-eng,

首先,正如 @willeM_ Van Onsem 在对您的问题的评论中指出的那样,您的树中有循环。让我们用不同的输入来尝试一下。让我们尝试以下节点列表:[('A', “BC”), ('B', “DE”), ('C', “FG”), ('D', “HI”), ('E', “JK”), ('F', “LM”), ('G', “NO”)]。如果我们从“A”开始搜索“O”,我们应该期待“ABDHIEJKCFLMGN”的路径。正确?

           A-0
          / \
         /   \
        /     \
       /       \
      /         \
     B-1         C-8 
    / \         / \
   /   \       /   \
  D-2   E-5   F-9   G-12
 / \   / \   / \   / \
H   I J   K L   M N   O
3   4 6   7 10 11 13 14

我无法遵循您的程序,但我编写了自己的程序进行比较。我想尝试将其放入 Python 中,只是为了体验。无论如何,这是我想出的:

{-# LANGUAGE TemplateHaskell #-}
-- Necessary for running `quickCheckAll` as `runTests` (see EOF)

import Test.QuickCheck
import Test.QuickCheck.All

-- Just for clarity
type Label = Char
type Node = (Label, [Label])
type Path = [Label]

-- Lazily traverse the depthOrder path, until target is found
pathTo :: [Node] -> Label -> Path
pathTo nodes target = takeWhile (not . (==) target)
                      (depthOrder nodes)

-- Find root -- node label that is not a sub of any other nodes
-- Start with root label and `cons` with expansion of root subs
depthOrder :: [Node] -> Path
depthOrder nodes = root : concatMap (expand nodes) subs
  where
    (root, subs) = head [(l, ss) | (l,ss) <- nodes, l `notElem` allSubs]
    allSubs = concat [subs | (label,subs) <- nodes]

-- Every expansion of a sub label, recursively calls for expansion of the
-- subs that follow from that label
expand :: [Node] -> Label -> Path
expand nodes label = label : concatMap (expand nodes)
                                       (getSubs label nodes)

-- For every sub label we search the node list for a node with that label,
-- and if there is one, get the subs for that node.
getSubs :: Char -> [Node] -> [Char]
getSubs label nodes = safeHead (dropWhile (not . (==) label . fst) nodes)
  where safeHead [] = []
        safeHead ((label,subs):ns) = subs 

----------------------------------------------------------------------------

-- simple expansion of one label into a path, based on depth order with
-- left sub searched befor right sub
prop_expand0 :: Property
prop_expand0 = property (expand [('B',"DE")] 'B' == "BDE")

-- if there is no node label in the node list that matches, the label is a
-- "leaf"
prop_expand1 :: Property
prop_expand1 = property (expand [('A',"BC")] 'Z' == "Z")

-- expanding from a predetermined root
prop_expand2 :: Property
prop_expand2 = property
  (expand [('A',"BC"),('B',"DE"),('C',"FG")] 'A' == "ABDECFG")

-- getting depthOrder for a node list, requires first finding a root
prop_depthOrder0 :: Property
prop_depthOrder0 = property
  (depthOrder [('B',"DE"),('C',"FG"),('A',"BC")] == "ABDECFG")

-- once we have a DEFINITION of the depth order of the node list, a simple
-- 'take' of the order will give us the path to the target label
prop_pathTo0 :: Property
prop_pathTo0 = property
  (pathTo [('A',"BC"),('B',"DE"),('C',"FG")] 'G' == "ABDECF")
  
prop_depthOrder1 :: Property
prop_depthOrder1 = property
  (depthOrder [('A', "BC"), ('B', "DE"), ('C', "FG"), ('D', "HI"),
               ('E', "JK"), ('F', "LM"), ('G', "NO")] == "ABDHIEJKCFLMGNO")
-- which is what we predicted at the outset


-- necessary pieces for running `quickCheckAll` on all our test properties
return []
runTests = $quickCheckAll

如果我们运行:?pathTo [('t',"Hr"),('h',"ij"),('q',"sT"),('i'," tE"),('T',"r"), (' ',""),('E',"!q"),('H',"e")] 'q'

评论

0赞 n. m. could be an AI 11/11/2023
这些并不意味着是树。
0赞 Geoffrey Warne 11/11/2023
@n. m. 可能是 AI:谢谢!我不得不在DFS上查找图表。现在我明白了问题所在。有趣!
0赞 lrainey6-eng 11/11/2023
@GeoffreyWarne非常感谢您抽出时间,我真的很感激 - 唯一的问题是我正在尝试使其也与图形兼容(因此在实践中也与树兼容)。基本上,理想情况下,它可以与任何一组节点一起使用,每个节点至少有一个连接到另一个节点,并且所有节点都作为一个整体连接(即没有无法从任何其他节点访问的节点)。另一件事是,理想情况下,“访问”列表也会跟踪回溯 - 我已经用完了空间,所以将在第二条评论中解释这一点
1赞 Geoffrey Warne 11/18/2023
@lrainey6-eng:我的荣幸!我不会撒谎......我为此苦苦挣扎,但我学到了一些东西。我发布了另一个带有修改的答案,以使我的原始程序在图形和树上工作。
1赞 Geoffrey Warne 11/18/2023
@lrainey6-eng:接下来我会检查你完成的程序。现在我明白了这个问题,我想看看你是如何解决它的。
1赞 lrainey6-eng 11/13/2023 #2

我刚刚解决了这个问题。以下是更新后的 Haskell 代码:

import Data.List (elemIndex)

trav :: [(Char, String)] -> Char -> Char -> String -> String
trav [] _ _ _ = []
trav graph node endNode visited
    | not (elem node visited) = trav graph node endNode (visited ++ [node])  -- repeat but after adding this node to visited list.
    | node == endNode = visited
    | not (null unvisitedAttachedNodes) = trav graph (head unvisitedAttachedNodes) endNode visited
    | not (null visited) = if not (null attachedNodesWithUnvisited) then (trav graph (head attachedNodesWithUnvisited) endNode (visited ++ [(head attachedNodesWithUnvisited)])) else (trav graph (head (tail (reverse visited))) endNode (visited ++ [head (tail (reverse visited))]))
    | otherwise = visited
    where
        unvisitedAttachedNodes = [x | x <- getAttachedVertexes graph node, not (elem x visited)]
        prevIndex = [x | x <- (reverse visited), x < (head (reverse visited))]
        unvisitedPrevNodes = [x | x <- prevIndex, elem x [y | y <- (getAttachedVertexes graph x), not (elem y visited)]]
        nodeToGoBackTo = visited !! (getIndex (head (reverse unvisitedPrevNodes)) visited)
        attachedNodesWithUnvisited = [nb | nb <- (getAttachedVertexes graph node), (hasUnvisited graph nb visited)]

getAttachedVertexes :: [(Char, String)] -> Char -> [Char]
getAttachedVertexes graph node = case lookup node graph of
    Just value -> value
    Nothing -> ""

hasUnvisited :: [(Char, String)] -> Char -> String -> Bool
hasUnvisited graph node visited = not (null unvisited) where
    unvisited = [x | x <- (getAttachedVertexes graph node), not (elem x visited)]

count :: Eq a => a -> [a] -> Int
count item list = length (filter (== item) list)

getIndex :: Eq a => a -> [a] -> Int
getIndex item list = case elemIndex item list of
    Just index -> index
    Nothing -> error "help"

下面是更新的等效 Python 代码:

def trav(graph, node, endNode, visited):
    if node not in visited:
        return trav(graph, node, endNode, (visited + [node]))
    if node == endNode:
        return visited
    unvisitedAttachedNodes = [x for x in getAttachedNodes(graph, node) if x not in visited]
    if len(unvisitedAttachedNodes) > 0:
        return trav(graph, unvisitedAttachedNodes[0], endNode, visited)
    if len(visited) > 0:  # needs to backtrack
        attachedNodesWithUnvisited = [nb for nb in graph[node] if has_unvisited(graph, nb, visited)]
        if len(attachedNodesWithUnvisited) > 0:
            return trav(graph, attachedNodesWithUnvisited[0], endNode, visited + [attachedNodesWithUnvisited[0]])
        else:
            return trav(graph, visited[-2], endNode, visited + [visited[-2]])
    return visited

我意识到无限循环是由以下原因引起的,每当需要回溯时,程序只返回一个节点,但随后将该节点添加到访问列表中,因此(或 Python 等效项意味着程序在两个节点之间继续运行。正如您在新的 Python 翻译中清楚地看到的那样,除非有一个附加节点具有未访问的相邻节点,否则我现在将这样做。head (tail (reverse visited))visited[-2]visited[-2]

现在,这适用于我用它测试过的所有图形。

非常感谢大家的帮助!

评论

0赞 Geoffrey Warne 11/18/2023
干得好,@lrainey6 eng!很高兴你弄清楚了!我对我的最后一个答案发表了一些评论,为什么我认为索引和显式计算的问题可以通过一种更惯用的功能方法来避免。我赞扬你在 Haskell 和 Python 之间进行翻译的努力。如果我可以这么大胆,我想建议你意识到 Haskell 它建立在一个非常不同的计算范式之上。如果你不试图将命令式方法硬塞进去,你对 Haskell 的理解和便利性将得到改善。
0赞 Geoffrey Warne 11/18/2023
由于 Haskelll 是基于 lambda 演算的,所以“一切”都运行在代数中发现的扩展和替换过程上,并在我的答案中说明(我希望)。由于 Python 本身包含函数式编程的许多特性(例如列表推导式和映射),因此尝试将我使用的策略翻译成 Python 可能会很有启发性。在我看来,一个人越早忘记命令式方法,他们就会在 Haskell 上走得更远。
1赞 lrainey6-eng 11/22/2023
@GeoffreyWarne非常感谢 - 一点也不,我同意,我认为在我用任何语言制作任何程序之前,我倾向于从不规则结构的角度来思考问题的解决方案,这应该归咎于这一点,这本身就是由于我缺乏任何非边缘语言的经验造成的。我只用Haskell编程了两三个星期,所以我希望能有所改进。谢谢
0赞 Geoffrey Warne 11/18/2023 #3

enter image description here

在我之前的回答(见上文)中,我包括了一个用于对树进行深度优先搜索的程序,其中节点不会自行循环。有人向我指出,这里的问题涉及可以循环回溯的图形。在这里,我将调整以前的程序,使其也适用于一般的图形(包括树)。

将上述树木程序调整为图形通常需要进行多次修改。有点微不足道的是,如果没有明确的根节点,该函数就会返回,就像树一样。相反,一般图形使用一个名为的函数,该函数既需要起点,也需要目标标签。(不包含在下面的代码中,请参阅前面的答案开始)pathToNothingpathFromTo

主要区别在于作为程序核心的功能。以前,只有树,我们可以毫无顾虑地将相同的节点列表传递给每个递归扩展,因为永远不会出现扩展导致自身的另一个扩展和无限循环的情况。对于图形,深度搜索路径要求每个节点在展开时从节点列表中删除,因此无法再次展开或“访问”。expand

附加函数 , 删除具有给定标签的节点和子节点。此外,我们之前允许节点列表包含未明确列为终端节点(“叶子”)本身的子节点。这会导致不必要的复杂情况,因此我们必须坚持(通过不变性)将子节点作为节点包含在内,如下所示: .remove[('A',"BC"),('B',""),('C',"") … ]

remove nodes label = [(l,[s | s <-ss, s /= label])
                            | (l,ss) <- nodes, l /= label]

由于搜索路径在最左边的分支之前向下扩展,然后再向右边的分支展开,因此子节点的扩展取决于从左侧的节点返回的节点列表。这意味着我们不能再像以前使用'concatMap(扩展节点)(getSubs标签节点)“那样对所有子节点使用简单的映射。现在我们需要使用一个递归函数,将修改后的节点列表传递给每个连续的子扩展。

通过这些更改,仅树的基本程序通常适用于图形。

比较:

-- Every expansion of a sub label, recursively calls for expansion of the
-- subs that follow from that label
expand :: [Node] -> Label -> Path
expand nodes label = label : concatMap (expand nodes)
                                       (getSubs label nodes)

自:

-- Every expansion of a subnode label, recursively calls for expansion of
-- the subnodes that follow from THAT label.
-- To avoid loops and 'visiting' nodes more than once, every expansion will
-- remove the node being expanded from derivative expansions.
-- Since leftmost branches are evaluated first, the node list is resolved
-- before passing to the next branch right, and then the branch right of it.
expand :: [Node] -> [Label] -> Path
expand _ [] = ""
expand nodes (label:ls)
  | getSubs label nodes == Nothing = ""             -- no longer in list
                        ++ expand nodes ls          -- cont to other nodes
  | getSubs label nodes == Just ("") = [label]      -- a leaf
                        ++ expand (nodes `remove` [label]) ls
  | otherwise = label : leftPath ++ recOnRSubs      -- left to right
                        ++ expand                   ---- through subnodes
                           (nodes `remove` (label : leftPath ++ recOnRSubs))
                           ls
  where
    (lSub:rSubs) = fromJust (getSubs label nodes)
    leftPath = expand (nodes `remove` [label]) [lSub]
    recOnRSubs = expand (nodes `remove` (label:leftPath)) rSubs

remove :: [Node] -> [Label] -> [Node]
remove nodes visited = [(l, [s | s <- ss, s `notElem` visited])
                               | (l,ss) <- nodes, l `notElem` visited]

注意:在它说的行中,我已经明确了最左边子节点的扩展,然后再移动到右边子节点上的递归。这不是绝对必要的。人们可以在子节点上使用递归版本来包含最左边的节点,但我喜欢左边路径的显式方式。我认为,它有助于引导读者。otherwise = label : leftPath ++ recOnRSubs …

评论

0赞 Geoffrey Warne 11/18/2023
我看到我的答案正在失分。据推测,他们被否决了,因为我“没有回答问题”。如果你允许的话,我想说一下我的回答是如何通过更深入地理解函数范式来解决原始问题的。
0赞 Geoffrey Warne 11/18/2023
最初的发帖者发现问题出在告诉程序“回溯”到哪个节点时获得了错误的索引。这暗示了函数式计算方式和命令式计算方式之间的分歧。这种策略是命令式方法的特征,在这种方法中,程序必须告诉计算机去哪里以及下一步该做什么。索引的使用对 Haskell 来说是相当不惯用的。我的程序更像 Haskell,因为它根本不使用索引。
0赞 Geoffrey Warne 11/18/2023
在任何时候,我的程序都不必告诉计算机它在哪里以及在哪里继续。路径按顺序展开。当路径完全展开并遇到死胡同时,它会停止,然后计算机继续执行下一条路径。没有必要明确地告诉计算机“回溯”到哪里。正如原始发帖人自己发现的那样,他们的程序错误地计算了回溯。我的回答表明,采用更惯用的功能策略将首先避免命令式错误。
1赞 lrainey6-eng 11/22/2023
非常感谢您的分析和解决方案,我非常感谢您为此花费的时间。对于阅读本文的其他人来说,这实际上有助于解决我的问题,所以如果没有投反对票,我将不胜感激,干杯