简体   繁体   English

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

[英]N-queens in Haskell without list traversal

I searched the web for different solutions to the n-queens problem in Haskell but couldn't find any that could check for unsafe positions in O(1) time, like that one that you keep an array for the / diagonals and one for the \\ diagonals. 我在网上搜索了Haskell中n-queens问题的不同解决方案,但找不到任何可以在O(1)时间内检查不安全位置的解决方案,就像你为/对角线保留一个数组而另一个用于\\ diagonals。

Most solutions I found just checked each new queen against all the previous ones. 我找到的大多数解决方案只是检查了所有以前的新女王。 Something like this: http://www.reddit.com/r/programming/comments/62j4m/nqueens_in_haskell/ 像这样的东西: 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)

What would be the best way to implement such an "O(1) approach" in Haskell? 在Haskell中实现这种“O(1)方法”的最佳方法是什么? I am not looking for anything "super-optimized". 我不是在寻找任何“超级优化”的东西。 Just some way to produce the "is this diagonal already used?" 只是某种方式来产生“这个对角线已经被使用了吗?” array in a functional manner. 数组以功能的方式。

UPDATE: 更新:

Thanks for all the answers, folks! 感谢所有答案,伙计们! The reason I originally asked the question is because I wanted to solve a harder backtracking problem. 我最初问这个问题的原因是因为我想解决一个更难回溯的问题。 I knew how to solve it in an imperative language but could not readily think of a purely functional data structure to do the job. 我知道如何用命令式语言解决它,但不能轻易想到一个纯粹的功能数据结构来完成这项工作。 I figured that the queens problem would be a good model (being the backtracking problem :) ) for the overall data-structure problem, but it isn't my real problem though. 我盘算了一下,皇后问题将是整个数据结构问题的一个很好的模式(即回溯问题:)),但它虽然不是我真正的问题。

I actually want to find a data structure that allows O(1) random access and holds values that are either on a "initial" state (free line/diagonal, in the n-queens case) or in a "final" state (occupied line/diagonal), with transitions (free to occupied) being O(1). 我实际上想找到一个允许O(1)随机访问的数据结构,并保持处于“初始”状态(自由线/对角线,在n-queens情况下)或处于“最终”状态(被占用)的值线/对角线),转换(自由占用)为O(1)。 This can be implemented using mutable arrays in an imperative language but I feel that the restriction of updating values only allows for a nice purely functional data structure (as opposed to Quicksort, for example, that really wants mutable arrays). 这可以使用命令式语言中的可变数组来实现,但我觉得更新值的限制​​只允许一个很好的纯功能数据结构(例如,与Quicksort相反,它真的需要可变数组)。

I figure that sth's solution is as good as you can get using immutable arrays in Haskell and the "main" function looks like what I wanted it to be: 我认为,在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))

The main problem seems to be finding a better data structure though, as Haskell Arrays have O(n) updating. 主要的问题似乎是找到一个更好的数据结构,因为Haskell Arrays有O(n)更新。 Other nice suggestions fall short of the mythical O(1) holy grail: 其他不错的建议没有神话般的O(1)圣杯:

  • DiffArrays come close but mess up in the backtracking. DiffArrays接近但在回溯中陷入困境。 They actually get super slow :( . 他们实际上变得非常慢:(。
  • STUArrays conflict with the pretty functional backtracking approach so they are discarded. STUArrays与功能强大的回溯方法冲突,因此它们被丢弃。
  • Maps and Sets have only O(log n) updating. 地图和集只有O(log n)更新。

I am not really sure there is a solution overall, but it seems promising. 我不确定总体上有解决方案,但看起来很有希望。

UPDATE: 更新:

The most promising data structure I found where Trailer Arrays. 我发现Trailer Arrays最有前途的数据结构。 Basically a Haskell DiffArray but it mutates back when you backtrack. 基本上是一个Haskell DiffArray,但是当你回溯时它会发生变异。

Probably the most straightforward way would be to use a UArray (Int, Int) Bool to record safe/unsafe bits. 可能最直接的方法是使用UArray (Int, Int) Bool记录安全/不安全位。 Although copying this is O(n 2 ), for small values of N this is the fastest method available. 虽然复制它是O(n 2 ),但对于较小的N值,这是可用的最快方法。

For larger values of N, there are three major options: 对于较大的N值,有三个主要选项:

  • Data.DiffArray removes copy overhead as long as you never use the old values again after modifying them . 只要在修改后再也不使用旧值, Data.DiffArray就会删除复制开销。 That is, if you always throw away the old value of the array after mutating it, the modification is O(1). 也就是说,如果在变异后总是丢弃数组的旧值,则修改为O(1)。 If, however, you access the old value of the array later (even for only a read), the O(N 2 ) is paid then in full. 但是,如果稍后访问数组的旧值(即使只读取),则完全支付O(N 2 )。
  • Data.Map and Data.Set allow O(lg n) modifications and lookups. Data.MapData.Set允许O(lg n)修改和查找。 This changes the algorithmic complexity, but is often fast enough. 这改变了算法的复杂性,但通常足够快。
  • Data.Array.ST's STUArray s (Int, Int) Bool will give you imperative arrays, allowing you to implement the algorithm in the classic (non-functional) manner. Data.Array.ST的STUArray s (Int, Int) Bool将为您提供命令式数组,允许您以经典(非功能)方式实现该算法。

In general you are probably going to be stuck paying the O(log n) complexity tax for a functional non-destructive implementation or you'll have to relent and use an (IO|ST|STM)UArray . 一般情况下,您可能会因为功能性非破坏性实现而无法支付O(log n)复杂性税,或者您必须放弃并使用(IO|ST|STM)UArray

Strict pure languages may have to pay an O(log n) tax over an impure language that can write to references by implementing references through a map-like structure; 严格的纯语言可能必须通过不纯的语言支付O(log n)税,该语言可以通过类似地图的结构实现引用来写入引用; lazy languages can sometimes dodge this tax, although there is no proof either way whether or not the extra power offered by laziness is sufficient to always dodge this tax -- even if it is strongly suspected that laziness isn't powerful enough. 懒惰的语言有时可以避免这种税,虽然没有任何证据证明懒惰提供的额外权力是否足以总是避免这种税 - 即使强烈怀疑懒惰不够强大。

In this case it is hard to see a mechanism by which laziness could be exploited to avoid the reference tax. 在这种情况下,很难看到一种可以利用懒惰来避免参考税的机制。 And, after all that is why we have the ST monad in the first place. 而且,毕竟这就是为什么我们首先拥有ST monad。 ;) ;)

That said, you might investigate whether or not some kind of board-diagonal zipper could be used to exploit locality of updates -- exploiting locality in a zipper is a common way to try to drop a logarithmic term. 也就是说,您可以调查是否可以使用某种板对角拉链来利用更新的位置 - 在拉链中利用局部性是尝试删除对数项的常用方法。

The basic potential problem with this approach is that the arrays for the diagonals need to be modified every time a queen is placed. 这种方法的基本潜在问题是每次放置女王时都需要修改对角线的阵列。 The small improvement of constant lookup time for the diagonals might not necessarily be worth the additional work of constantly creating new modified arrays. 对角线的常量查找时间的小改进可能不一定值得不断创建新的修改数组的额外工作。

But the best way to know the real answer is to try it, so I played around a bit and came up with the following: 但知道真正答案的最好方法是尝试一下,所以我玩了一下并提出了以下内容:

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

This works and is for n=14 roughly 25% faster than the version you mentioned. 这适用于n = 14,比你提到的版本快大约25%。 The main speedup comes from using the unboxed arrays bdonian recommended. 主要的加速来自使用bdonian推荐的未装箱阵列。 With the normal Data.Array it has about the same runtime as the version in the question. 使用普通的Data.Array它与问题中的版本具有大致相同的运行时。

It might also be worth it to try the other array types from the standard library to see if using them can further improve performance. 尝试标准库中的其他数组类型以查看是否使用它们可以进一步提高性能也是值得的。

I am becoming skeptical about the claim that pure functional is generally O(log n). 我对纯功能通常为O(log n) 的主张持怀疑态度。 See also Edward Kmett's answer which makes that claim. 另请参阅Edward Kmett的答案。 Although that may apply to random mutable array access in the theoretical sense, but random mutable array access is probably not what most any algorithm requires, when it is properly studied for repeatable structure, ie not random. 虽然这可能适用于理论意义上的随机可变阵列访问,但是当对可重复结构进行适当研究时,随机可变阵列访问可能不是大多数算法所需要的,即不是随机的。 I think Edward Kmett refers to this when he writes, "exploit locality of updates". 我认为Edward Kmett在撰写“利用更新的地方性”时会提到这一点。

I am thinking O(1) is theoretically possible in a pure functional version of the n-queens algorithm, by adding an undo method for the DiffArray, which requests a look back in differences to remove duplicates and avoid replaying them. 我认为O(1)在n-queens算法的纯函数版本中理论上是可行的,通过为DiffArray添加一个undo方法,它请求回顾差异以删除重复并避免重放它们。

If I am correct in my understanding of the way the backtracking n-queens algorithm operates, then the slowdown caused by the DiffArray is because the unnecessary differences are being retained. 如果我理解回溯n-queens算法的运行方式是正确的,那么DiffArray引起的减速是因为保留了不必要的差异。

In the abstract, a "DiffArray" (not necessarily Haskell's) has (or could have) a set element method which returns a new copy of the array and stores a difference record with the original copy, including a pointer to the new changed copy. 在摘要中,“DiffArray”(不一定是Haskell)具有(或可能具有)set元素方法,该方法返回数组的新副本并将差异记录与原始副本一起存储,包括指向新更改副本的指针。 When the original copy needs to access an element, then this list of differences has to be replayed in reverse to undo the changes on a copy of the current copy. 当原始副本需要访问元素时,必须反向重放此差异列表以撤消当前副本副本上的更改。 Note there is even the overhead that this single-linked list has to be walked to the end, before it can be replayed. 请注意,在重放之前,这个单链表必须走到最后才有开销。

Imagine instead these were stored as a double-linked list, and there was an undo operation as follows. 想象一下,这些存储为双链表,并且有一个撤消操作如下。

From an abstract conceptual level, what the backtracking n-queens algorithm does is recursively operate on some arrays of booleans, moving the queen's position incrementally forward in those arrays on each recursive level. 从抽象的概念层面来看,回溯n-queens算法所做的是递归地对某些布尔数组进行操作,在每个递归级别的那些数组中逐步向前移动皇后的位置。 See this animation . 这个动画

Working this out in my head only, I visualize that the reason DiffArray is so slow, is because when the queen is moved from one position to another, then the boolean flag for the original position is set back to false and the new position is set to true, and these differences are recorded, yet they are unnecessary because when replayed in reverse, the array ends up with the same values it has before the replay began. 仅仅在脑海中解决这个问题,我想象出DiffArray这么慢的原因是因为当女王从一个位置移动到另一个位置时,原始位置的布尔标志被设置回假并且新位置被设置为了真实,并记录了这些差异,但它们是不必要的,因为当反向重放时,数组最终会得到重播开始前的相同值。 Thus instead of using a set operation to set back to false, what is needed is an undo method call, optionally with an input parameter telling DiffArray what "undo to" value to search for in the aforementioned double-linked list of differences. 因此,不是使用set操作来设置回false,而是需要一个undo方法调用,可选地使用输入参数告诉DiffArray在上述双链接差异列表中要搜索的“撤消到”值。 If that "undo to" value is found in a difference record in the double-linked list, there are no conflicting intermediate changes on that same array element found when walking back in the list search, and the current value equals the "undo from" value in that difference record, then the record can be removed and that old copy can be re-pointed to the next record in the double-linked list. 如果在双链表中的差异记录中找到“撤消到”值,则在列表搜索中返回时找到的同一数组元素上没有冲突的中间更改,并且当前值等于“撤消”在该差异记录中的值,然后可以删除该记录,并且可以将旧副本重新指向双链表中的下一个记录。

What this accomplishes is to remove the unnecessary copying of the entire array on backtracking. 这样做是为了在回溯时删除整个数组的不必要的复制。 There is still some extra overhead as compared to the imperative version of the algorithm, for adding and undoing the add of difference records, but this can be nearer to constant time, ie O(1). 与算法的命令式版本相比,仍然存在一些额外的开销,用于添加和撤消差异记录的添加,但是这可以更接近于恒定时间,即O(1)。

If I correctly understand the n-queen algorithm, the lookback for the undo operation is only one, so there is no walk. 如果我正确理解n-queen算法,撤销操作的回溯只有一个,所以没有步行。 Thus it isn't even necessary to store the difference of the set element when moving the queen position, since it will be undone before the old copy will be accessed. 因此,甚至不需要在移动皇后位置时存储设定元素的差异,因为在访问旧副本之前它将被撤消。 We just need a way to express this type safely, which is easy enough to do, but I will leave it as an exercise for the reader, as this post is too long already. 我们只需要一种方法来安全地表达这种类型,这很容易做到,但我会把它留作读者练习,因为这篇帖子已经太久了。


UPDATE: I haven't written the code for the entire algorithm, but in my head the n-queens can be implemented with at each iterated row, a fold on the following array of diagonals, where each element is the triplet tuple of: (index of row it is occupied or None, array of row indices intersecting left-right diagonal, array of row indices intersecting right-left diagonal). 更新:我没有编写整个算法的代码,但在我的脑海中,n-queens可以在每个迭代行实现,在以下对角线数组上折叠,其中每个元素是三元组元组:(它占据的行索引或无,与左右对角线相交的行索引数组,与左右对角线相交的行索引数组)。 The rows can be iterated with recursion or a fold of an array of row indices (the fold does the recursion). 可以使用递归或行索引数组的折叠来迭代行(折叠执行递归)。

Here follows the interfaces for the data structure I envision. 以下是我设想的数据结构的接口。 The syntax below is Copute, but I think it is close enough to Scala, that you can understand what is intended. 下面的语法是Copute,但我认为它与Scala足够接近,你可以理解它的意图。

Note that any implementation of DiffArray will be unreasonably slow if it is multithreaded, but the n-queens backtracking algorithm doesn't require DiffArray to be multithreaded. 请注意,如果DiffArray是多线程的,那么它的任何实现都会非常慢,但是n-queens回溯算法不需要DiffArray是多线程的。 Thanks to Edward Kmett for pointing that out in the comments for this answer. 感谢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.

UPDATE: I am working on the Scala implementation , which has an improved interface compared to what I had suggested above. 更新:我正在研究Scala实现 ,与我上面提到的相比,它具有改进的接口 I have also explained how an optimization for folds approaches the same constant overhead as a mutable array. 我还解释了折叠的优化如何接近与可变数组相同的常量开销。

I have a solution. 我有一个解决方案。 However, the constant may be large, so I don't really hope beating anything. 但是,常数可能很大,所以我真的不希望击败任何东西。

Here is my data structure: 这是我的数据结构:

-- | 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
   )

It allows all of the required operations to be performed in O(1). 它允许在O(1)中执行所有必需的操作。

The code can be found here: http://hpaste.org/50707 代码可以在这里找到: http//hpaste.org/50707

The speed is bad -- it's slower than the reference solution posted in the question on most inputs. 速度很差 - 它比大多数输入中问题中发布的参考解决方案慢。 I've benchmarked them against each other on inputs [1,3 .. 15] and got the following time ratios ((reference solution time / my solution time) in %): 我已经在输入[1,3 .. 15]上对它们进行了基准测试,得到了以下时间比率((参考解决时间/我的求解时间)%):

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

Notice almost linear slow-down of the reference solution relative to mine, showing difference in asymptotic complexity. 注意参考解相对于我的几乎线性减慢,显示渐近复杂度的差异。

My solution is probably horrible in terms of strictness and things like that, and must be fed to some very good optimizing compiler (like Don Stewart for example) to get better results. 我的解决方案在严格性和类似的事情方面可能很糟糕,并且必须提供给一些非常好的优化编译器(例如Don Stewart)以获得更好的结果。

Anyway, I think in this problem O(1) and O(log(n)) are indistinguishable anyway because log(8) is just 3 and constants like this are subject of micro-optimisations rather than of algorithm. 无论如何,我认为在这个问题中O(1)和O(log(n))无论如何都是难以区分的,因为log(8)只是3,这样的常量是微优化而不是算法的主题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM