【问题标题】:Is there any way to not use explicit recursion in this algorithm?有没有办法在这个算法中不使用显式递归?
【发布时间】:2015-01-13 02:07:09
【问题描述】:

所以我正在研究将模式与列表匹配的问题,例如: match "abba" "redbluebluered" -> Truematch "abba" "redblueblue" -> False 等。我写了一个可行的算法,我认为它是合理可以理解的,但我不确定是否有更好的方法可以在没有显式递归的情况下做到这一点。

import Data.HashMap.Strict as M
match :: (Eq a, Eq k, Hashable k) => [k] -> [a] -> HashMap k [a] -> Bool
match []     [] _ = True
match []     _  _ = False
match _      [] _ = False
match (p:ps) s  m =
  case M.lookup p m of
    Just v ->
      case stripPrefix v s of
        Just post -> match ps post m
        Nothing   -> False
    Nothing -> any f . tail . splits $ s
      where f (pre, post) = match ps post $ M.insert p pre m
            splits xs = zip (inits xs) (tails xs)

我会这样称呼它match "abba" "redbluebluered" empty。实际算法很简单。该地图包含已匹配的模式。最后是 [a -> "red", b -> "blue"]。如果下一个模式是我们以前见过的,只要尝试匹配它,如果可以的话,向下递归。否则失败并返回 false。

如果下一个模式是新的,只需尝试将新模式映射到字符串中的每个前缀并向下递归。

【问题讨论】:

    标签: haskell recursion coding-style fold


    【解决方案1】:

    我想修改你的签名并返回超过Bool。你的解决方案就变成了:

    match :: (Eq a, Ord k) => [k] -> [a] -> Maybe (M.Map k [a])
    match = m M.empty where
      m kvs (k:ks) vs@(v:_) = let splits xs = zip (inits xs) (tails xs)
                               f (pre, post) t =
                                   case m (M.insert k pre kvs) ks post of
                                     Nothing -> t
                                     x       -> x
                              in case M.lookup k kvs of
                                    Nothing -> foldr f Nothing . tail . splits $ vs
                                    Just p -> stripPrefix p vs >>= m kvs ks
      m kvs [] [] = Just kvs
      m _   _  _  = Nothing
    

    使用已知的折叠技巧,我们可以得到一个函数:

    match ks vs = foldr f end ks M.empty vs where
      end m [] = Just m
      end _ _  = Nothing
      splits xs = zip (inits xs) (tails xs)
      f k g kvs vs = let h (pre, post) = (g (M.insert k pre kvs) post <|>)
                     in case M.lookup k kvs of
                       Nothing -> foldr h Nothing $ tail $ splits vs
                       Just p  -> stripPrefix p vs >>= g kvs
    

    这里的match 是折叠所有键以生成一个函数的函数,该函数采用Map 和一个a 字符串,它返回一个Map 匹配子字符串的键。与a 的字符串完全匹配的条件由foldr - end 应用的最后一个函数跟踪。如果end 提供了一个映射和一个空字符串a,则匹配成功。

    使用函数f折叠键列表,该函数有四个参数:当前键、匹配键列表其余部分的函数g(即f折叠或end ),已经匹配的键映射,以及a 字符串的其余部分。如果密钥已经在地图中找到,那么只需去掉前缀并将地图和其余部分提供给g。否则,尝试为不同的拆分组合提供修改后的地图和 as 的其余部分。只要gh 中产生Nothing,就会懒惰地尝试这些组合。

    【讨论】:

      【解决方案2】:

      这与解析问题非常相似,所以让我们从解析器 monad 中获得提示:

      • match 应该返回解析的所有可能延续的列表
      • 如果匹配失败,它应该返回空列表
      • 当前的分配集将是必须进行计算的状态

      要了解我们的发展方向,让我们假设我们有这个神奇的单子。尝试将“abba”与字符串匹配将如下所示:

      matchAbba = do
        var 'a'
        var 'b'
        var 'b'
        var 'a'
        return ()  -- or whatever you want to return
      
      test = runMatch matchAbba "redbluebluered"
      

      原来这个 monad 是 List monad 之上的 State monad。 List monad 提供回溯,State monad 携带当前的分配和输入。

      代码如下:

      import Data.List
      import Control.Monad
      import Control.Monad.State
      import Control.Monad.Trans
      import Data.Maybe
      import qualified Data.Map as M
      import Data.Monoid
      
      type Assigns = M.Map Char String
      
      splits xs = tail $ zip (inits xs) (tails xs)
      
      var p = do
        (assigns,input) <- get
        guard $ (not . null) input
        case M.lookup p assigns of
          Nothing -> do (a,b) <- lift $ splits input
                        let assigns' = M.insert p a assigns
                        put (assigns', b)
                        return a
          Just t  -> do guard $ isPrefixOf t input
                        let inp' = drop (length t) input
                        put (assigns, inp')
                        return t
      
      matchAbba :: StateT (Assigns, String) [] Assigns
      matchAbba = do
        var 'a'
        var 'b'
        var 'b'
        var 'a'
        (assigns,_) <- get
        return assigns
      
      test1 = evalStateT matchAbba (M.empty, "xyyx") 
      test2 = evalStateT matchAbba (M.empty, "xyy") 
      test3 = evalStateT matchAbba (M.empty, "redbluebluered") 
      
      matches :: String -> String -> [Assigns]
      matches pattern input = evalStateT monad (M.empty,input)
        where monad :: StateT (Assigns, String) [] Assigns
              monad = do sequence $ map var pattern
                         (assigns,_) <- get
                         return assigns
      

      试试,例如:

      matches "ab" "xyz"
      -- [fromList [('a',"x"),('b',"y")],fromList [('a',"x"),('b',"yz")],fromList [('a',"xy"),('b',"z")]]
      

      要指出的另一件事是,将像“abba”这样的字符串转换为一元值do var'a'; var'b'; var 'b'; var 'a' 的代码很简单:

      sequence $ map var "abba"
      

      更新:正如@Sassa NF 指出的那样,要匹配您要定义的输入结尾:

      matchEnd :: StateT (Assigns,String) [] ()
      matchEnd = do
        (assigns,input) <- get
        guard $ null input
      

      然后将其插入到 monad 中:

              monad = do sequence $ map var pattern
                         matchEnd
                         (assigns,_) <- get
                         return assigns
      

      【讨论】:

      • 和一个常见的解析器问题一样,这里需要检查完整解析的输入。修改最后两行:(assigns, r) &lt;- get; guard $ r == []; return assigns
      • sequence . map fmapM f
      【解决方案3】:

      这是另一种解决方案,我认为更具可读性,并且与其他解决方案一样低效:

      import Data.Either
      import Data.List
      import Data.Maybe
      import Data.Functor
      
      splits xs = zip (inits xs) (tails xs)
      
      subst :: Char -> String -> Either Char String -> Either Char String
      subst p xs (Left q) | p == q = Right xs
      subst p xs       q           = q
      
      match' :: [Either Char String] -> String -> Bool
      match'            []  [] = True
      match' (Left  p : ps) xs = or [ match' (map (subst p ixs) ps) txs
                                    | (ixs, txs) <- tail $ splits xs]
      match' (Right s : ps) xs = fromMaybe False $ match' ps <$> stripPrefix s xs
      match'            _   _  = False
      
      match = match' . map Left
      
      main = mapM_ (print . uncurry match)
          [ ("abba"    , "redbluebluered"                    ) -- True
          , ("abba"    , "redblueblue"                       ) -- False
          , ("abb"     , "redblueblue"                       ) -- True
          , ("aab"     , "redblueblue"                       ) -- False
          , ("cbccadbd", "greenredgreengreenwhiteblueredblue") -- True
          ]
      

      这个想法很简单:不用Map,而是将模式和匹配的子字符串存储在一个列表中。因此,当我们遇到一个模式 (Left p) 时,我们将所有出现的该模式替换为一个子字符串,并递归地调用 match' 并使用该子字符串进行条带化,并对每个属于 inits 的子字符串重复此操作处理过的字符串。如果我们遇到已经匹配的子字符串(Right s),那么我们只是尝试剥离这个子字符串,并在连续尝试时递归调用match',否则返回False

      【讨论】:

        猜你喜欢
        • 2019-09-10
        • 2019-07-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-04-15
        • 1970-01-01
        相关资源
        最近更新 更多