17

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

我发现的大多数解决方案只是将每个新皇后与之前的所有皇后进行对比。像这样的东西: 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)。这可以在命令式语言中使用可变数组来实现,但我觉得更新值的限制​​只允许一个很好的纯函数数据结构(例如,与真正需要可变数组的快速排序相反)。

我认为某事的解决方案与在 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 数组有 O(n) 更新。其他不错的建议没有达到神话般的 O(1) 圣杯:

  • DiffArrays 接近但在回溯中搞砸了。他们实际上变得超级慢:(。
  • STUArrays 与漂亮的功能回溯方法冲突,因此它们被丢弃。
  • Maps 和 Sets 只有 O(log n) 更新。

我不确定是否有整体解决方案,但似乎很有希望。

更新:

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

4

5 回答 5

6

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

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

  • 只要您在修改旧值后不再使用旧值,Data.DiffArray 就会消除复制开销。也就是说,如果你总是在改变后丢弃数组的旧值,那么修改是 O(1)。但是,如果您稍后访问数组的旧值(即使只是读取),则 O(N 2 ) 将被全额支付。
  • Data.MapData.Set允许 O(lg n) 修改和查找。这改变了算法的复杂性,但通常足够快。
  • Data.Array.STSTUArray s (Int, Int) Bool将为您提供命令式数组,允许您以经典(非功能性)方式实现算法。
于 2009-08-10T14:03:03.263 回答
5

一般来说,您可能会被困在O(log n)为功能性非破坏性实现支付复杂性税,或者您将不得不放弃并使用(IO|ST|STM)UArray.

严格的纯语言可能不得不为O(log n)不纯语言缴纳税款,该语言可以通过类似地图的结构实现引用来写入引用;懒惰的语言有时可以避开这种税收,尽管无论哪种方式都没有证据表明懒惰提供的额外力量是否足以总是避开这种税收——即使人们强烈怀疑懒惰还不够强大。

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

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

于 2009-08-12T19:43:17.480 回答
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下,它的运行时间与问题中的版本大致相同。

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

于 2009-08-10T19:03:01.627 回答
3

我对纯函数通常是 O(log n)的说法持怀疑态度。另请参阅提出该主张的 Edward Kmett 的回答。虽然这可能适用于理论上的随机可变数组访问,但是当对可重复结构(即不是随机的)进行适当研究时,随机可变数组访问可能不是大多数算法所需要的。我认为 Edward Kmett 在写“利用更新的局部性”时提到了这一点。

我认为 O(1) 在理论上可能在 n-queens 算法的纯函数版本中,通过为 DiffArray 添加一个撤消方法,该方法要求回顾差异以删除重复项并避免重放它们。

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

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

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

从抽象的概念层面来看,回溯 n-queens 算法所做的是对一些布尔数组进行递归操作,在每个递归级别上,在这些数组中递增地向前移动皇后的位置。看这个动画

仅在我的脑海中解决这个问题,我想象 DiffArray 如此慢的原因是因为当皇后从一个位置移动到另一个位置时,原始位置的布尔标志设置回 false 并设置新位置为真,这些差异被记录下来,但它们是不必要的,因为当反向重放时,数组最终会得到与重放开始之前相同的值。因此,不需要使用 set 操作将其设置回 false,而是需要调用 undo 方法,可选地使用输入参数告诉 DiffArray 在上述差异的双链表中搜索什么“撤消到”值。如果在双链表的差异记录中找到该“撤消到”值,

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

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


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

下面是我设想的数据结构的接口。下面的语法是 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 实现,与我上面建议的相比,它具有改进的界面。我还解释了折叠优化如何接近与可变数组相同的恒定开销。

于 2011-08-25T17:43:38.183 回答
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 并且像这样的常量是微优化而不是算法的主题。

于 2011-08-27T10:11:43.643 回答