如何使用Select monad来解决n-queens?

时间:2017-02-21 21:12:14

标签: haskell monads backtracking n-queens

我试图理解Select monad是如何工作的。显然,它是Cont的堂兄,它可以用于回溯搜索。

我有这个基于列表的解决n-queens问题的方法:

-- All the ways of extracting an element from a list.
oneOf :: [Int] -> [(Int,[Int])] 
oneOf [] = [] 
oneOf (x:xs) = (x,xs) : map (\(y,ys) -> (y,x:ys)) (oneOf xs)

-- Adding a new queen at col x, is it threathened diagonally by any of the
-- existing queens?
safeDiag :: Int -> [Int] -> Bool
safeDiag x xs = all (\(y,i) -> abs (x-y) /= i) (zip xs [1..])

nqueens :: Int -> [[Int]]
nqueens queenCount = go [] [1..queenCount]
  where
    -- cps = columsn of already positioned queens. 
    -- fps = columns that are still available
    go :: [Int] -> [Int] -> [[Int]]
    go cps [] = [cps]
    go cps fps = [ps | (p,nfps) <- oneOf fps, ps <- go (p:cps) nfps, safeDiag p cps]

我正在努力调整此解决方案以改为使用Select

似乎Select允许您抽象用于比较答案的“评估函数”。该函数传递给runSelect。我觉得我的解决方案中的safeDiag之类的东西可以作为评估函数,但是如何构建Select计算本身?

另外,单独使用Select monad是否足够,或者我是否需要在列表上使用转换器版本?

3 个答案:

答案 0 :(得分:3)

Select可被视为&#34; compact&#34;中搜索的抽象。空间,由一些谓词引导。您在评论中提到过SAT,您是否尝试将问题建模为SAT实例并将其放在基于Select的求解器中(本着this paper的精神)?您可以专门搜索以在phi内硬连线N-queens特定约束,并将SAT解算器转换为N-queens解算器。

答案 1 :(得分:3)

受jd823592的回答启发,在查看paper中的SAT示例后,我写了这段代码:

import Data.List 
import Control.Monad.Trans.Select

validBoard :: [Int] -> Bool
validBoard qs = all verify (tails qs)
  where
    verify [] = True
    verify (x : xs) = and $ zipWith (\i y -> x /= y && abs (x-y) /= i) [1..] xs

nqueens :: Int -> [Int]
nqueens boardSize = runSelect (traverse selectColumn columns) validBoard
  where
  columns = replicate boardSize [1..boardSize]
  selectColumn candidates = select $ \s -> head $ filter s candidates ++ candidates

它似乎(尽管很慢)到达有效的解决方案:

ghci> nqueens 8
[1,5,8,6,3,7,2,4]
但是,我不太了解它。特别是,sequence适用于Select的方式,将整个板上工作的函数(validBoard)转换为采用单列索引的函数,看起来非常神奇。

基于sequence的解决方案存在以下缺陷:将列后插入列不排除为后续任务选择相同列的可能性;我们最终不必要地探索注定的分支。

如果我们希望我们的列选择受到先前决策的影响,我们需要超越Applicative并使用Monad的力量:

nqueens :: Int -> [Int]
nqueens boardSize = fst $ runSelect (go ([],[1..boardSize])) (validBoard . fst)
  where
  go (cps,[]) = return (cps,[])
  go (cps,fps) = (select $ \s ->
    let candidates = map (\(z,zs) -> (z:cps,zs)) (oneOf fps)
    in  head $ filter s candidates ++ candidates) >>= go

monadic版本仍然存在这样的问题,它只检查完成的板,当原始的基于列表的解决方案在发现部分完成的板发生冲突后立即退回。我不知道如何使用Select

答案 2 :(得分:3)

我意识到这个问题已有将近4年的历史了,但已经有了一个答案,但是我想补充一些信息,以便将来遇到这个问题的任何人。具体来说,我想尝试回答2个问题:

  • 返回单个值的多个Select如何组合在一起以创建返回值序列的单个Select?
  • 当解决路径注定要失败时,是否有可能提早返回?

连锁选择

Select在transformers库中作为monad转换器实现(如图),但让我们看一下如何单独为>>=实现Select

(>>=) :: Select r a -> (a -> Select r b) -> Select r b
Select g >>= f = Select $ \k ->
  let choose x = runSelect (f x) k
  in  choose $ g (k . choose)

我们首先定义一个新的Select,它接受​​类型为k的输入a -> r(回想起Select包装类型为(a -> r) -> a的函数)。您可以将k视为一个函数,该函数针对给定的r返回类型为a的“分数”,Select函数可以使用该分数确定要返回哪个a

在新的Select内,我们定义了一个名为choose的函数。此函数将某些x传递给函数f,这是单子绑定的a -> m b部分:它将m a计算的结果转换为新的计算{{1 }}。因此m b将使用该f并返回一个新的x,然后Select将使用我们的评分函数choose运行。您可以将k视为一个函数,询问“如果我选择choose并将其传递给下游,最终结果将是什么?”

在第二行,我们返回x。函数choose $ g (k . choose)k . choose和我们最初的评分函数choose组成:它接收一个值,计算选择该值的下游结果,并返回该下游结果的分数。换句话说,我们创建了一种“透视”评分功能:与其返回给定值的分数,它不返回最终结果的分数,如果选择该值,我们将得到 。通过将“透视”评分函数传递给k(我们要绑定的原始g),我们可以选择导致最终结果的中间值对于。一旦有了该中间值,我们只需将其传递回Select并返回结果。

这就是我们在传递一个对值数组进行操作的评分函数时如何将单值Select组合在一起的方法:每个Select都对选择值的假设最终结果进行评分,而不一定是值本身。应用实例遵循相同的策略,唯一的区别是下游Select的计算方式(不是将候选值传递到choose函数中,而是将候选函数映射到第二个Select上)。

早退

那么,我们如何在提早返回时使用Select?我们需要某种方法来访问构成Select的代码范围内的评分功能。一种方法是在另一个Select中构造每个Select,就像这样:

a -> m b

这允许我们测试进行中的序列,并在递归失败时将其短路。 (我们可以通过调用sequenceSelect :: Eq a => [a] -> Select Bool [a] sequenceSelect [] = return [] sequenceSelect domain@(x:xs) = select $ \k -> if k [] then runSelect s k else [] where s = do choice <- elementSelect (x:|xs) fmap (choice:) $ sequenceSelect (filter (/= choice) domain) 来测试序列,因为评分功能包含了我们递归排列的所有前置词。)

这是整个解决方案:

k []

简而言之:我们以递归方式构造一个Select,在使用元素时将其从域中删除,如果域已用尽或走错了方向,则终止递归。

另一个附加功能是import Data.List import Data.List.NonEmpty (NonEmpty(..)) import Control.Monad.Trans.Select validBoard :: [Int] -> Bool validBoard qs = all verify (tails qs) where verify [] = True verify (x:xs) = and $ zipWith (\i y -> x /= y && abs (x - y) /= i) [1..] xs nqueens :: Int -> [Int] nqueens boardSize = runSelect (sequenceSelect [1..boardSize]) validBoard sequenceSelect :: Eq a => [a] -> Select Bool [a] sequenceSelect [] = return [] sequenceSelect domain@(x:xs) = select $ \k -> if k [] then runSelect s k else [] where s = do choice <- elementSelect (x:|xs) fmap (choice:) $ sequenceSelect (filter (/= choice) domain) elementSelect :: NonEmpty a -> Select Bool a elementSelect domain = select $ \p -> epsilon p domain -- like find, but will always return something epsilon :: (a -> Bool) -> NonEmpty a -> a epsilon _ (x:|[]) = x epsilon p (x:|y:ys) = if p x then x else epsilon p (y:|ys) 函数(基于希尔伯特的epsilon operator)。对于大小为N的域,它将最多检查N-1个项目……这听起来似乎不算是一笔大的节省,但是正如您从上述说明中所知道的,epsilon通常会在整个剩余部分中开始计算,因此最好将谓词调用保持在最低水平。

关于p的好处是它的通用性:它可用于在其中创建任何sequenceSelect

  • 我们正在不同元素的有限域内搜索
  • 我们要创建一个序列,其中每个元素恰好包含一次(即域的排列)
  • 我们要测试部分序列,如果不通过谓词则放弃它们

希望这有助于澄清问题!


P.S。这是指向Observable笔记本的链接,在该笔记本中,我用Java语言实现了Select monad以及n皇后求解器的演示:https://observablehq.com/@mattdiamond/the-select-monad