【问题标题】:Haskell - Calculating the shortest path using treesHaskell - 使用树计算最短路径
【发布时间】:2015-01-01 05:24:25
【问题描述】:

我正在尝试在 Haskell 中编写代码,在棋盘游戏中从 A 点到 F 点,本质上是一个矩阵,遵循最短路径。

这是董事会:

AAAA
ACCB
ADEF
*
0 0 N

机器人进入字母A,在底部(它是*),并且必须到达F,在板的底部是坐标,x = 0,y = 0,并且指向北方。 F坐标为(3,0)

诀窍是,它不能跳过一个以上的字母,它可以从 A 到 B,B 到 C 等,它可以遍历类型的字母(A 到 A,B 到 B,等)

它只能前进和转弯(左,右)所以让我去F的路径是

前进,前进,右,前进,前进,前进,右,跳跃,右,跳跃,前进,左,跳跃,左,前进,前进

一旦达到 F,就完成了。

我想尝试这种方法,使用树

                  A
                 / \
                A   D
               / \ 
              /   \
             A     C
            / \   / \
           /   \ D   C
          A     
         / \  
        /   \ 
       A
      /
     /
    A
   / \
  B   A
 / \  
C   F 

之后我只需要验证正确的路径和最短的路径吗?

问题是,我没有太多使用树木的经验。

您能指出获得最佳路径的任何其他方法吗?

非常感谢。

【问题讨论】:

  • 解决此问题的树将有 2 或 3 个分支 - 如果唯一合法的移动是 LeftRight,则为 2,如果还有合法移动,则为 3 - ForwardJump。找到最短路径(如果转动LeftRight 算作移动)然后在树的最小深度找到F。如果转动 LeftRight 不算作移动,则树将有 0-3 或 0-4 个分支,具体取决于哪些方向具有合法移动以及您是否修剪(禁止)向后移动。
  • 虽然你最终会遍历一棵状态树,但你不需要在内存中实际构建这样一棵树:你只关心找到最短路径,所以你要做的就是从初始游戏状态开始,一步一步地传播游戏,同时尝试所有可能的组合。

标签: haskell graph path-finding


【解决方案1】:

我们将通过三部分搜索树来解决这个问题。首先,我们将构建一个Tree 代表通过问题的路径,每个状态都有分支。我们想找到到达具有特定条件的状态的最短路径,因此我们将编写一个breadth first search 来搜索任何Tree。对于您提供的示例问题,这还不够快,因此我们将使用 transposition table 改进广度优先搜索,它会跟踪我们已经探索过的状态以避免再次探索它们。

构建一棵树

我们假设您的棋盘以Array from Data.Array 表示

import Data.Array

type Board = Array (Int, Int) Char

board :: Board
board = listArray ((1,1),(3,4)) ("AAAA" ++ "ACCB" ++ "ADEF")

Data.Array 没有提供默认的简单方法来确保我们使用! 查找值的索引实际上在Array 的范围内。为方便起见,我们将提供一个安全版本,如果值在 ArrayNothing 中,则返回 Just v,否则返回。

import Data.Maybe

(!?) :: Ix i => Array i a -> i -> Maybe a
a !? i = if inRange (bounds a) i then Just (a ! i) else Nothing

拼图的State 可以用机器人的position 和机器人面对的direction 的组合来表示。

data State = State {position :: (Int, Int), direction  :: (Int, Int)}
    deriving (Eq, Ord, Show)

direction 是一个单位向量,可以添加到position 以获得新的position。我们可以旋转方向向量leftrightmoveTowards

right :: Num a => (a, a) -> (a, a)
right (down, across) = (across, -down)

left ::  Num a => (a, a) -> (a, a)
left (down, across) = (-across, down)

moveTowards :: (Num a, Num b) => (a, b) -> (a, b) -> (a, b)
moveTowards (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)

要探索棋盘,我们需要能够从一个州确定哪些棋步是合法的。为此,命名移动会很有用,因此我们将创建一个数据类型来表示可能的移动。

import Prelude hiding (Right, Left)

data Move = Left | Right | Forward | Jump
    deriving (Show)

要确定棋盘上哪些动作是合法的,我们需要知道我们正在使用哪个Board 以及机器人的State。这建议使用 moves :: Board -> State -> Move 类型,但我们将在每次移动后计算新状态,以确定移动是否合法,因此为了方便起见,我们还将返回新状态。

moves :: Board -> State -> [(Move, State)]
moves board (State pos dir) =   
    (if inRange (bounds board) pos then [(Right,   State pos    (right dir)), (Left, State pos (left dir))] else []) ++
    (if next == Just here          then [(Forward, State nextPos dir)] else []) ++
    (if next == Just (succ here)   then [(Jump,    State nextPos dir)] else [])
    where
        here = fromMaybe 'A' (board !? pos)
        nextPos = moveTowards dir pos
        next = board !? nextPos

如果我们在董事会,我们可以转LeftRight;我们在板上的限制保证moves 返回的所有States 在板上都有positions。如果在nextPosnext 位置持有的值与Just here 匹配,我们可以转到Forward(如果我们不在董事会,我们假设here'A')。如果nextJust 的继任者here,我们可以Jump 对其进行处理。如果next 不在板上,则为Nothing,并且不能匹配Just hereJust (succ here)

到目前为止,我们只是提供了问题的描述,还没有涉及到用树来回答问题。我们将使用Data.Tree 中定义的玫瑰树Tree

data Tree a = Node {
        rootLabel :: a,         -- ^ label value
        subForest :: Forest a   -- ^ zero or more child trees
    }

type Forest a = [Tree a]

Tree a 的每个节点都包含一个值 a 和一个分支列表,每个分支都是一个 Tree a

我们将通过moves 函数以直接的方式构建Trees 的列表。我们将使moves 的每个结果成为NoderootLabel,并使分支成为我们explore 新状态时得到的Trees 列表。

import Data.Tree

explore :: Board -> State -> [Tree (Move, State)]
explore board = map go . moves board
    where
        go (label, state) = Node (label, state) (explore board state)

此时,我们的树是无限的;没有什么能阻止机器人在原地不停地旋转。我们不能画一个,但如果我们可以limit这棵树,我们可以画出几步。

limit :: Int -> Tree a -> Tree a
limit n (Node a ts)
    | n <= 0    = Node a []
    | otherwise = Node a (map (limit (n-1)) ts)

当我们从左下角开始面向State (4, 1) (-1, 0) 的棋盘时,我们将只显示树的前几级。

(putStrLn .
 drawForest .
 map (fmap (\(m, s) -> show (m, board ! position s)) . limit 2) .
 explore board $ State (4, 1) (-1, 0))

(Forward,'A')
|
+- (Right,'A')
|  |
|  +- (Right,'A')
|  |
|  `- (Left,'A')
|
+- (Left,'A')
|  |
|  +- (Right,'A')
|  |
|  `- (Left,'A')
|
`- (Forward,'A')
   |
   +- (Right,'A')
   |
   +- (Left,'A')
   |
   `- (Forward,'A')

广度优先搜索

广度优先搜索先探索一个级别(跨越正在搜索的“广度”)的所有可能性,然后再下降到下一个级别(进入正在搜索的“深度”)。广度优先搜索找到到达目标的最短路径。对于我们的树,这意味着在探索内层中的任何内容之前先探索一层的所有内容。我们将通过创建一个节点队列来探索将我们在下一层中发现的节点添加到队列的末尾来实现这一点。队列将始终保存当前层的节点,然后是下一层的节点。它永远不会保存上一层的任何节点,因为在我们移动到下一层之前我们不会发现这些节点。

为了实现它,我们需要一个高效的队列,所以我们将使用sequence from Data.Sequence/

import Data.Sequence (viewl, ViewL (..), (><))
import qualified Data.Sequence as Seq

我们从要探索的节点的空队列Seq.empty 和进入Trees 的空路径[] 开始。我们用&gt;&lt;(序列的串联)和go将初始可能性添加到queue的末尾。我们看queue 的开头。如果什么都没有,EmptyL,我们没有找到通往目标的路径并返回Nothing。如果那里有东西,并且它与目标 p 匹配,我们返回我们向后累积的路径。如果队列中的第一件事与目标不匹配,我们将其添加为路径的最新部分,并将其所有分支添加到 queued 的其余部分。

breadthFirstSearch :: (a -> Bool) -> [Tree a] -> Maybe [a]
breadthFirstSearch p = combine Seq.empty []
    where
        combine queue ancestors branches =
            go (queue >< (Seq.fromList . map ((,) ancestors) $ branches))
        go queue =
            case viewl queue of
                EmptyL -> Nothing
                (ancestors, Node a bs) :< queued ->
                    if p a
                    then Just . reverse $ a:ancestors
                    else combine queued (a:ancestors) bs

这让我们可以为Boards 编写我们的第一个solve。这里方便的是moves返回的所有位置都在板上。

solve :: Char -> Board -> State -> Maybe [Move]
solve goal board = fmap (map fst) . breadthFirstSearch ((== goal) . (board !) . position . snd) . explore board

如果我们为我们的电路板运行它,它永远不会完成!好吧,最终它会,但我的餐巾纸计算表明它需要大约 4000 万步。通往迷宫尽头的路径有 16 步长,机器人经常会出现 3 个选项,说明每一步要做什么。

> solve 'F' board (State (4, 1) (-1, 0))

我们可以解决更小的难题,例如

AB
AC
*

我们可以用它来代表这个谜题的棋盘

smallBoard :: Board
smallBoard = listArray ((1,1),(2,2)) ("AB" ++ "AC")

我们 solve 它在寻找 'C' 从行开始 31 寻找编号较低的行。

> solve 'C' smallBoard (State (3, 1) (-1, 0))
Just [Forward,Forward,Right,Jump,Right,Jump]

转置表

当然,这个问题肯定比探索 4000 万条可能的路径更容易解决。大多数这些路径包括在原地旋转或随机地来回蜿蜒。退化路径都共享一个属性,它们不断访问它们已经访问过的状态。在breadthFirstSeach 代码中,这些路径不断将相同的节点添加到队列中。只需记住我们已经看到的节点,我们就可以摆脱所有这些额外的工作。

我们会记住我们已经用Set from Data.Set 看到的一组节点。

import qualified Data.Set as Set

breadthFirstSearch 的签名中,我们将添加一个从节点标签到该节点分支表示的函数。只要节点外的所有分支都相同,则表示应该相等。为了快速将O(log n) 时间的表示与Set 进行比较,我们要求表示具有Ord 实例,而不仅仅是相等。 Ord 实例允许Set 检查binary search 的成员资格。

breadthFirstSearchUnseen:: Ord r => (a -> r) -> (a -> Bool) -> [Tree a] -> Maybe [a]

除了跟踪queuebreadthFirstSearchUnseen 还跟踪从Set.empty 开始的seen 的表示集。每次我们使用combine 将分支添加到queue 时,我们也会将表示添加到seen。我们只添加 unseen 分支,其表示不在我们已经 seen 的分支集中。

breadthFirstSearchUnseen repr p = combine Set.empty Seq.empty []
    where
        combine seen queued ancestors unseen =
            go
                (seen  `Set.union` (Set.fromList . map (repr . rootLabel) $ unseen))
                (queued ><         (Seq.fromList . map ((,) ancestors   ) $ unseen))
        go seen queue =
            case viewl queue of
                EmptyL -> Nothing
                (ancestors, Node a bs) :< queued ->
                    if p a
                    then Just . reverse $ ancestors'
                    else combine seen queued ancestors' unseen
                    where
                        ancestors' = a:ancestors
                        unseen = filter (flip Set.notMember seen . repr . rootLabel) bs

现在我们可以改进我们的solve 函数以使用breadthFirstSearchUnseen。一个节点的所有分支都由State 确定——到达该状态的Move 标签是无关紧要的——所以我们只使用(Move, State) 元组的snd 部分作为节点的表示。

solve :: Char -> Board -> State -> Maybe [Move]
solve goal board = fmap (map fst) . breadthFirstSearchUnseen snd ((== goal) . (board !) . position . snd) . explore board

我们现在可以非常快地solve原始拼图。

> solve 'F' board (State (4, 1) (-1, 0))
Just [Forward,Forward,Forward,Right,Forward,Forward,Forward,Right,Jump,Right,Jump,Forward,Left,Jump,Left,Jump,Jump]

【讨论】:

  • Cirdec,我感激不尽!我会仔细研究代码,如果我有任何疑问,我会指望你帮助我。说真的,我感激不尽。
  • 还有一点,我认为在导入 Data.Tree 时,应该是:import qualified Data.Tree as Tree 否则会产生歧义。
猜你喜欢
  • 2018-06-23
  • 2011-09-03
  • 1970-01-01
  • 2016-06-02
  • 1970-01-01
  • 2011-07-12
  • 2013-01-08
  • 1970-01-01
相关资源
最近更新 更多