Haskell中的N-queens没有列表遍历

时间:2009-08-10 13:51:24

标签: algorithm data-structures haskell functional-programming backtracking

我在网上搜索了Haskell中n-queens问题的不同解决方案,但找不到任何可以在O(1)时间内检查不安全位置的解决方案,就像那个为/对角线保留数组的那个一个用于\ diagonals。

我找到的大多数解决方案只检查了所有以前的新女王。像这样的东西: http://www.reddit.com/r/programming/comments/62j4m/nqueens_in_haskell/

nqueens :: Int -> [[(Int,Int)]]
nqueens n = foldr qu [[]] [1..n]
    where qu k qss = [ ((j,k):qs) | qs <- qss, j <- [1..n], all (safe (j,k)) qs ]
      safe (j,k) (l,m) = j /= l && k /= m && abs (j-l) /= abs (k-m)

在Haskell中实现这种“O(1)方法”的最佳方法是什么? 我不是在寻找任何“超级优化”的东西。只是某种方式来产生“这个对角线已经被使用了吗?”数组以功能的方式。

更新

感谢所有答案,伙计们!我最初问这个问题的原因是因为我想解决一个更难回溯的问题。我知道如何用命令式语言解决它,但不能轻易想到一个纯粹的功能数据结构来完成这项工作。我认为,对于整体数据结构问题,皇后问题将是一个很好的模型( 回溯问题:)),但它不是我的真正的问题

我实际上想找到一个允许O(1)随机访问的数据结构,并保存处于“初始”状态(自由线/对角线,在n-queens情况下)或“最终”状态的值状态(占用线/对角线),转换(自由占用)为O(1)。 这可以使用命令式语言中的可变数组来实现,但我觉得更新值的限制​​只允许一个很好的纯功能数据结构(而不​​是Quicksort,例如,真的想要可变数组)。

我认为这个解决方案与在Haskell中使用不可变数组一样好,而“main”函数看起来就像我想要的那样:

-- try all positions for a queen in row n-1
place :: BoardState -> Int -> [[(Int, Int)]]
place _ 0 = [[]]
place b n = concatMap place_ (freefields b (n-1))
   where place_ p = map (p:) (place (occupy b p) (n-1))

主要问题似乎是找到一个更好的数据结构,因为Haskell Arrays有O(n)更新。 其他不错的建议没有神话般的O(1)圣杯:

  • DiffArrays接近但在回溯中陷入困境。他们实际上得到超级慢:(。
  • STUArrays与功能强大的回溯方法冲突,因此被丢弃。
  • 地图和集只有O(log n)更新。

我不确定总体上有解决方案,但似乎很有希望。

更新

我发现Trailer Arrays最有前途的数据结构。基本上是一个Haskell DiffArray,但是当你回溯时它会发生变异。

5 个答案:

答案 0 :(得分:6)

可能最直接的方法是使用UArray (Int, Int) Bool来记录安全/不安全位。虽然复制它是O(n 2 ),但对于较小的N值,这是最快的方法。

对于较大的N值,有三个主要选项:

  • Data.DiffArray只要您在修改后再次使用旧值,就会删除复制开销。也就是说,如果在变异后总是丢弃数组的旧值,则修改为O(1)。但是,如果您稍后访问该数组的旧值(即使只读取),则完全支付O(N 2 )。
  • Data.MapData.Set允许O(lg n)修改和查找。这会改变算法的复杂性,但通常足够快。
  • Data.Array.ST的STUArray s (Int, Int) Bool将为您提供命令式数组,允许您以经典(非功能)方式实现该算法。

答案 1 :(得分:4)

一般情况下,您可能会因为功能性非破坏性实施而无法支付O(log n)复杂税,否则您将不得不放弃使用(IO|ST|STM)UArray

严格的纯语言可能必须对不纯的语言支付O(log n)税,该语言可以通过类似地图的结构实现引用来写入引用;懒惰的语言有时可以避免这种税,虽然没有任何证据证明懒惰提供的额外权力是否足以总是避免这种税 - 即使强烈怀疑懒惰不够强大。

在这种情况下,很难看到一种可以利用懒惰来避免参考税的机制。而且,毕竟这就是为什么我们首先拥有ST monad。 ;)

也就是说,您可以调查是否可以使用某种板对角拉链来利用更新的位置 - 在拉链中利用局部性是尝试删除对数项的常用方法。

答案 2 :(得分:3)

这种方法的基本潜在问题是每次放置女王时都需要修改对角线的数组。对角线的常量查找时间的小改进可能不一定值得不断创建新的修改数组的额外工作。

但知道真正答案的最好方法是尝试一下,所以我玩了一下并想出了以下内容:

import Data.Array.IArray (array, (//), (!))
import Data.Array.Unboxed (UArray)
import Data.Set (Set, fromList, toList, delete)

-- contains sets of unoccupied columns and lookup arrays for both diagonals
data BoardState = BoardState (Set Int) (UArray Int Bool) (UArray Int Bool)

-- an empty board
board :: Int -> BoardState
board n
   = BoardState (fromList [0..n-1]) (truearr 0 (2*(n-1))) (truearr (1-n) (n-1))
   where truearr a b = array (a,b) [(i,True) | i <- [a..b]]

-- modify board state if queen gets placed
occupy :: BoardState -> (Int, Int) -> BoardState
occupy (BoardState c s d) (a,b)
   = BoardState (delete b c) (tofalse s (a+b)) (tofalse d (a-b))
   where tofalse arr i = arr // [(i, False)]

-- get free fields in a row
freefields :: BoardState -> Int -> [(Int, Int)]
freefields (BoardState c s d) a = filter freediag candidates
   where candidates = [(a,b) | b <- toList c]
         freediag (a,b) = (s ! (a+b)) && (d ! (a-b))

-- try all positions for a queen in row n-1
place :: BoardState -> Int -> [[(Int, Int)]]
place _ 0 = [[]]
place b n = concatMap place_ (freefields b (n-1))
   where place_ p = map (p:) (place (occupy b p) (n-1))

-- all possibilities to place n queens on a n*n board
queens :: Int -> [[(Int, Int)]]
queens n = place (board n) n

这适用于n = 14,比你提到的版本大约快25%。主要的加速来自使用推荐的未装箱阵列 bdonian 。对于普通Data.Array,它与问题中的版本具有大致相同的运行时。

尝试标准库中的其他数组类型以查看是否使用它们可以进一步提高性能也是值得的。

答案 3 :(得分:3)

我对the claim怀疑纯粹的功能通常是O(log n)。另请参阅Edward Kmett的答案。虽然这可能适用于理论意义上的随机可变阵列访问,但是随机变量阵列访问可能不是大多数算法所需要的,当正确地研究可重复结构时,即不是随机的。我认为Edward Kmett在撰写“利用更新的地方性”时会提到这一点。

我认为O(1)在n-queens算法的纯函数版本中理论上是可行的,方法是为DiffArray添加一个undo方法,该方法请求回顾差异以删除重复项并避免重放它们。 / p>

如果我理解回溯n-queens算法的运行方式是正确的,那么DiffArray引起的减速是因为保留了不必要的差异。

在摘要中,“DiffArray”(不一定是Haskell)具有(或可能有)set元素方法,该方法返回数组的新副本并存储与原始副本的差异记录,包括指向新副本的指针改变了副本。当原始副本需要访问元素时,必须反向重放此差异列表以撤消当前副本副本上的更改。请注意,在重放之前,这个单链表必须走到最后才有开销。

想象一下,这些存储为双链表,并且有一个撤消操作如下。

从一个抽象的概念层面来看,回溯n-queens算法所做的是递归地对某些布尔数组进行操作,在每个递归级别的那些数组中逐步向前移动后置位置。请参阅this animation

只在我的头脑中解决这个问题,我想象出DiffArray这么慢的原因,是因为当女王从一个位置移动到另一个位置时,原始位置的布尔标志被设置回假而新的position被设置为true,并且记录了这些差异,但它们是不必要的,因为当反向重放时,数组最终会得到与重放开始之前相同的值。因此,不是使用set操作来设置回false,而是需要一个undo方法调用,可选地使用输入参数告诉DiffArray在上述双链接差异列表中要搜索的“撤消到”值。如果在双链表中的差异记录中找到“撤消到”值,则在列表搜索中返回时找到的同一数组元素上没有冲突的中间更改,并且当前值等于“撤消”在该差异记录中的值,然后可以删除该记录,并且可以将旧副本重新指向双链表中的下一个记录。

这样做是为了在回溯时删除整个数组的不必要的复制。与算法的命令式版本相比,还存在一些额外的开销,用于添加和撤消差异记录的添加,但这可能更接近于恒定时间,即O(1)。

如果我正确理解n-queen算法,撤销操作的回溯只有一个,所以没有步行。因此,甚至不需要在移动皇后位置时存储设定元素的差异,因为在访问旧副本之前它将被撤消。我们只需要一种方法来安全地表达这种类型,这很容易做到,但我会把它作为练习留给读者,因为这篇文章已经太长了。


更新:我没有编写整个算法的代码,但在我的脑海中,n-queens可以在每个迭代行实现,在以下对角线数组上折叠,其中每个元素是三元组元组of:(它占据的行的索引或无,与左右对角线相交的行索引的数组,与左右对角线相交的行索引的数组)。可以使用递归或行索引数组的折叠来迭代行(折叠执行递归)。

下面是我设想的数据结构的接口。下面的语法是Copute,但我认为它与Scala足够接近,你可以理解它的用途。

请注意,如果DiffArray是多线程的,则任何实现都会非常慢,但是n-queens回溯算法不需要DiffArray是多线程的。感谢Edward Kmett在这个答案的评论中指出了这一点。

interface Array[T]
{
   setElement  : Int -> T -> Array[T]     // Return copy with changed element.
   setElement  : Int -> Maybe[T] -> Array[T]
   array       : () -> Maybe[DiffArray[T]]// Return copy with the DiffArray interface, or None if first called setElement() before array().
}
// An immutable array, typically constructed with Array().
//
// If first called setElement() before array(), setElement doesn't store differences,
// array will return None, and thus setElement is as fast as a mutable imperative array.
//
// Else setElement stores differences, thus setElement is O(1) but with a constant extra overhead.
// And if setElement has been called, getElement incurs an up to O(n) sequential time complexity,
// because a copy must be made and the differences must be applied to the copy.
// The algorithm is described here:
//    http://stackoverflow.com/questions/1255018/n-queens-in-haskell-without-list-traversal/7194832#7194832
// Similar to Haskell's implementation:
//    http://www.haskell.org/haskellwiki/Arrays#DiffArray_.28module_Data.Array.Diff.29
//    http://www.haskell.org/pipermail/glasgow-haskell-users/2003-November/005939.html
//
// If a multithreaded implementation is used, it can be extremely slow,
// because there is a race condition on every method, which requires internal critical sections.

interface DiffArray[T] inherits Array[T]
{
   unset       : () -> Array[T]        // Return copy with the previous setElement() undone, and its difference removed.
   getElement  : Int -> Maybe[T]       // Return the the element, or None if element is not set.
}
// An immutable array, typically constructed with Array( ... ) or Array().array.

更新:我正在处理Scala implementation,与我上面提到的相比,interface有所改进。我还解释了折叠的优化如何接近与可变数组相同的常量开销。

答案 4 :(得分:1)

我有一个解决方案。但是,常数可能很大,所以我真的不希望击败任何东西。

这是我的数据结构:

-- | Zipper over a list of integers
type Zipper = (Bool,  -- does the zipper point to an item?
               [Int], -- previous items
                      -- (positive numbers representing
                      --   negative offsets relative to the previous list item)
               [Int]  -- next items (positive relative offsets)
               )

type State =
  (Zipper, -- Free columns zipper
   Zipper, -- Free diagonal1 zipper
   Zipper  -- Free diagonal2 zipper
   )

它允许在O(1)中执行所有必需的操作。

可以在此处找到代码:http://hpaste.org/50707

速度很差 - 它比大多数输入中问题中公布的参考解决方案慢。我已经在输入[1,3 .. 15]上对彼此进行了基准测试,得到了以下时间比率((参考解决方案时间/我的解决方案时间),以%表示):

[24.66%,19.89%,23.74%,41.22%,42.54%,66.19%,84.13%,106.30%]

注意参考解相对于我的几乎线性减慢,显示渐近复杂度的差异。

我的解决方案在严格性和类似的事情方面可能很糟糕,并且必须提供给一些非常好的优化编译器(例如Don Stewart)以获得更好的结果。

无论如何,我认为在这个问题中O(1)和O(log(n))无论如何都是无法区分的,因为log(8)只有3而且这样的常量是微优化而不是算法的主题。