Haskell:使用 List Monad 计算周期

Posted

技术标签:

【中文标题】Haskell:使用 List Monad 计算周期【英文标题】:Haskell: Compute Cycles with List Monad 【发布时间】:2021-08-19 19:19:58 【问题描述】:
-- 1. Graph structure: nodes and adjacency matrix (i.e. the edges) 
data Node = A | B | C | D | E | F deriving (Show,Eq,Ord)

adj :: (Node,Node) -> Bool
adj p = case p of
  (A,B) -> True
  (A,C) -> True  
  (B,C) -> True
  (B,F) -> True
  (C,D) -> True
  (D,E) -> True
  (E,B) -> True
  (E,F) -> True
  (F,A) -> True
  (_,_) -> False

type Path = [Node]

-- 2. Auxiliary functions
adjacentNodes :: Node -> [Node] -> [Node]
adjacentNodes n ns = filter (\x -> adj(n,x)) ns

allNodes :: [Node]
allNodes = [A,B,C,D,E,F]

choice :: ([a],[a]) -> [a]
choice = uncurry (++)

-- 3. To do
addtoEnd :: Path -> [Node] -> [Path]
addtoEnd p ns = undefined

hCycles :: Node -> [Path]
hCycles n = undefined

我有这个代码(它是给我们的,我不能改变它或类型)并且需要使用 list monad(和 do 表示法)定义函数 hCycleshCycles 应该为图像中图形的任何通用节点计算哈密顿循环。

问题是我不太确定如何用 list monad 来做到这一点......尽管如此,我想我有这个函数的第一个版本:

hCycles :: Node -> [Path]
hCycles n = do 
            p <- [[n]]
            nextNode <- adjacentNodes n allNodes
            if n == nextNode
            then [p]
            else addtoEnd p allNodes

if/else 仍然有一个奇怪的行为,因为 hCycles 没有被再次调用,我什至不认为它是递归的......我该如何解决这个问题?

【问题讨论】:

这是一个相当大的练习——到目前为止你尝试了什么? hCycles node 应该返回列表中第一个和最后一个元素为 node 的所有路径,路径中的所有节点但除了 node 两次? 看起来这与 monads 或 do 符号关系不大。您应该首先考虑要用于计算此类循环的算法。 (然后,该算法中的某些非确定性选择可能可以使用 list monad 来实现。) @Carsten 就是这样 @chi 事情是我不太确定从哪里开始,我知道第一个和最后一个节点是相同的,并且没有其他节点可以出现两次......也许开始建立一个列表(路径)访问的节点,每次添加一个,检查它是否不是第二次发生? 您的addtoEnd 太慢了。使用 : 前置 a 而不是附加。你不在乎这条路是哪条路。 【参考方案1】:

您好,我想是时候给您一些可以解决您问题的版本了:

hCycles :: Node -> [Path]
hCycles n = 
    filter isValidPathLength $ map (n:) $ go [] (adjacentNodes n allNodes)
    where
    isValidPathLength path =
        length path == length allNodes + 1
    -- note: go will only care about a path to n 
    -- but will take care of not visiting nodes two-times
    go _ [] = [] -- fail if there is no node left to discover
    go visited toVisit = do
        cur <- toVisit
        if cur == n then
            pure [n] -- found n
        else do
            let neighboursToVisit = filter (`notElem` visited) $ adjacentNodes cur allNodes
            pathToEnd <- go (cur:visited) neighboursToVisit
            pure $ cur:pathToEnd

我注意到您的adj 不适合您的图片,因此我将其更改为

adj :: (Node,Node) -> Bool
adj p = case p of
  (A,B) -> True
  (A,C) -> True  
  (B,C) -> True
  (B,F) -> True
  (C,D) -> True
  (D,E) -> True
  (E,B) -> True
  (E,F) -> True
  (F,A) -> True
  (_,_) -> False

(你的好像不是有向图)

你会得到:

> hCycles A
[[A,B,C,D,E,F,A],[A,C,D,E,B,F,A]]

一些注释

我不关心这里的性能(例如,有更好的数据结构来管理 visited 然后是一个列表) - 这个是蛮力的深度优先搜索 - 如果你愿意,你可以适应它BFS - 这是一个很好的练习 IMO(你可能想摆脱 do 符号的东西......嘿,你要求它)

【讨论】:

是的,我也觉得很奇怪,并与我的老师确认,adj 不正确(我也在问题中进行了编辑)。非常感谢您的解释,很清楚!【参考方案2】:

在 list monad 中的一行:

x <- f y

期望f 返回一个列表。 x 将依次使用列表中的每个值进行实例化,因此 do 子句的其余部分将使用这些值中的每一个运行。

您将看到adjacentNodes 返回一个节点列表。因此,从n 开始,您可以像这样考虑它连接的每个节点:

nextNode <- adjacentNode n allNodes

编写这个函数:

steps :: [Nodes] -> Path -> [Path]
steps _ [] = fail "Need a node to start with."
steps ns path@(n:rest) = do
   nextNode <- adjacentNode n ns
   guard $ not $ elem nextNode path   -- Stop if we've already visited this node.
   return $ nextNode : path

你可以把它想象成寻找单一路径的算法,它(感谢 list monad)神奇地找到所有可能的路径。这不是完整的答案,但它应该足以让您开始使用。

(注意:我还没有实际测试过这段代码。)

【讨论】:

我已经有那个功能addtoEnd(不知道我怎么没贴在帖子里,但我现在更正了)。仍然我不太确定如何开始,我所知道的是第一个和最后一个节点是相同的,并且没有其他节点可以出现两次......也许开始构建一个访问节点的列表(路径),并且,每次添加一个,检查一下是否不是第二次发生? @CrisTeller 好的,现在让它递归。添加起始节点作为一个额外的参数,它只是向下传递递归并使用它来检查nextNode 是否是起始节点。如果是则终止递归,否则使用扩展路径再次调用steps @PaulJohnson 这是一个学校项目,所以技术上我无法更改/添加函数参数 我将hCycles 的尝试添加到问题中 @CrisTeller 不,您需要更改 addToEnd 以包含您认为合适的任何参数。如果它还没有到达起始节点,那么只需在addToEnd 的末尾调用addToEnd。阅读en.wikibooks.org/wiki/Haskell/Recursion 了解更多线索。那么hCycles 只是对addToEnd 的调用,带有额外参数的起始值。

以上是关于Haskell:使用 List Monad 计算周期的主要内容,如果未能解决你的问题,请参考以下文章

在 Haskell 中使用 Logic Monad

Haskell学习-monad

Haskell Monad(下)

在haskell中构建一个非确定性的monad转换器

为啥 Haskell 异常只能在 IO monad 中捕获?

Monad_Haskell笔记10