17

Busqué en la web diferentes soluciones para el problema n-queens en Haskell pero no pude encontrar ninguna que pudiera verificar posiciones inseguras en O (1) vez, como la que mantenga una matriz para las/diagonales y una para las \ diagonales.N-queens en Haskell sin lista transversal

La mayoría de las soluciones que encontré han comprobado cada nueva reina en comparación con todas las anteriores. Algo como esto: 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) 

¿Cuál sería la mejor manera de implementar un tal "O (1) enfoque" en Haskell? No estoy buscando nada "súper optimizado". Solo una forma de producir "¿esta diagonal ya está en uso?" matriz de una manera funcional.

ACTUALIZACIÓN:

Gracias por todas las respuestas, amigos! La razón por la que originalmente hice la pregunta es porque quería resolver un problema de retroceso más difícil. Sabía cómo resolverlo en un lenguaje imperativo, pero no podía pensar fácilmente en una estructura de datos puramente funcional para hacer el trabajo. Pensé que el problema de las reinas sería un buen modelo (siendo el problema de retroceso de :)) para el problema general de la estructura de datos, pero no es mi problema verdadero.

En realidad, quiero encontrar una estructura de datos que permita O (1) acceso aleatorio y contenga valores que estén en estado "inicial" (línea/diagonal libre, en el caso n-queens) o en un "final" "estado (línea ocupada/diagonal), con transiciones (libre a ocupado) siendo O (1). Esto se puede implementar utilizando matrices mutables en un lenguaje imperativo pero creo que la restricción de actualizar valores solo permite una estructura de datos puramente funcional (a diferencia de Quicksort, por ejemplo, realmente quiere matrices mutables).

Calculo que la solución de algo es tan bueno como se puede conseguir utilizando matrices inmutables en Haskell y la función "principal" se parece a lo que yo quería que fuera:

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

El principal problema parece estar encontrando una mejor estructura de datos, ya que Haskell Arrays tiene O (n) actualización. Otras sugerencias agradables están a la altura de la mítica O (1) Santo Grial:

  • DiffArrays se acercan pero se pierde en la vuelta hacia atrás. Que en realidad obtener súper :(lento.
  • STUArrays conflicto con el enfoque de retroceso bastante funcional por lo que se descartan.
  • Mapas y conjuntos tienen solamente O (log n) la actualización.

no soy muy seguro de que hay una solución global, pero parece prometedora

ACTUALIZACIÓN:.

La estructura de datos más prometedora que encontré en Trailer Arrays. Básicamente un Haskell DiffArray, pero muta cuando retrocedes.

+0

¿Se han implementado matrices de remolques en alguna parte para Haskell? – is7s

Respuesta

4

En general, es probable que se quede estancado pagando el impuesto de complejidad O(log n) para una implementación funcional no destructiva o tendrá que ceder y usar un (IO|ST|STM)UArray.

Estrictos lenguajes puros pueden tener que pagar un impuesto O(log n) sobre un lenguaje impuro que puede escribir en las referencias mediante la implementación de referencias a través de una estructura similar a un mapa; Los lenguajes perezosos a veces pueden eludir este impuesto, aunque no hay ninguna prueba de si el poder adicional que ofrece la pereza es suficiente para esquivar siempre este impuesto, incluso si se sospecha fuertemente que la pereza no es lo suficientemente poderosa.

En este caso, es difícil ver un mecanismo por el cual se pueda explotar la pereza para evitar el impuesto de referencia. Y, después de todo, es por eso que tenemos la mónada ST en primer lugar. ;)

Dicho esto, puede investigar si se puede usar algún tipo de cremallera diagonal en la placa para explotar la localidad de las actualizaciones: explotar la localidad en una cremallera es una forma común de tratar de descartar un término logarítmico.

6

Probablemente la forma más directa sería usar un UArray (Int, Int) Bool para grabar bits seguros/inseguros. Aunque copiar esto es O (n), para valores pequeños de N este es el método más rápido disponible.

Para valores grandes de N, hay tres opciones principales:

  • Data.DiffArray elimina copia sobrecarga todo el tiempo que nunca utiliza los valores antiguos de nuevo después de su modificación. Es decir, si siempre descarta el valor anterior de la matriz después de mutarlo, la modificación es O (1). Sin embargo, si accede al valor anterior de la matriz más tarde (incluso para una sola lectura), entonces se paga por completo la O (N).
  • Data.Map y Data.Set permiten O (lg n) modificaciones y búsquedas. Esto cambia la complejidad algorítmica, pero a menudo es lo suficientemente rápido.
  • Data.Array.ST STUArray s (Int, Int) Bool le dará matrices imperativas, lo que le permite implementar el algoritmo de la manera clásica (no funcional).
+0

Pensé que las actualizaciones de la matriz eran O (n) y arruinarían la complejidad del algoritmo. ¿Me equivoco? – hugomg

+0

Actualizado - tienes razón, pero para N pequeña es aún más barato :) – bdonlan

3

El problema potencial básico con este enfoque es que las matrices de las diagonales deben modificarse cada vez que se coloca una reina. La pequeña mejora del tiempo de búsqueda constante para las diagonales puede no necesariamente valer la pena el trabajo adicional de crear constantemente nuevas matrices modificadas.

Pero la mejor manera de conocer la verdadera respuesta es probarlo, así que jugaron un poco y se le ocurrió la siguiente:

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 

Esto funciona y es para n = 14 aproximadamente el 25% más rápido que la versión que mencionaste La aceleración principal proviene del uso de las matrices sin caja bdonian recomendadas. Con el Data.Array normal tiene aproximadamente el mismo tiempo de ejecución que la versión en la pregunta.

También podría valer la pena probar los otros tipos de matriz de la biblioteca estándar para ver si el uso de ellos puede mejorar aún más el rendimiento.

3

Me estoy volviendo escéptico sobre the claim que el funcional puro es generalmente O (log n). Ver también la respuesta de Edward Kmett que hace esa afirmación.Aunque eso puede aplicarse al acceso de matriz mutable aleatoria en el sentido teórico, pero el acceso de matriz mutable aleatoria probablemente no es lo que la mayoría de los algoritmos requieren, cuando se estudia adecuadamente para una estructura repetible, es decir, no aleatoria. Creo que Edward Kmett se refiere a esto cuando escribe, "explota la localidad de las actualizaciones".

Estoy pensando que O (1) es teóricamente posible en una versión funcional pura del algoritmo n-queens, agregando un método de deshacer para DiffArray, que solicita una revisión de las diferencias para eliminar duplicados y evitar reproducirlos.

Si tengo razón en mi comprensión de la forma en que funciona el algoritmo n-queens de retroceso, entonces la desaceleración causada por DiffArray se debe a que se conservan las diferencias innecesarias.

En resumen, un "DiffArray" (no necesariamente Haskell's) tiene (o podría tener) un método de elemento conjunto que devuelve una nueva copia de la matriz y almacena un registro de diferencia con la copia original, incluyendo un puntero al nueva copia modificada. Cuando la copia original necesita acceder a un elemento, esta lista de diferencias debe reproducirse a la inversa para deshacer los cambios en una copia de la copia actual. Tenga en cuenta que incluso existe la sobrecarga de que esta lista de un solo enlace debe irse hasta el final, antes de poder volver a reproducirse.

Imagine en su lugar que se almacenaron como una lista de doble enlace, y hubo una operación de deshacer de la siguiente manera.

Desde un nivel conceptual abstracto, lo que hace el algoritmo n-queens de retroceso es operar recursivamente en algunas matrices de booleanos, moviendo la posición de la reina progresivamente hacia adelante en esas matrices en cada nivel recursivo. Ver this animation.

Al resolver esto solo en mi cabeza, visualizo que el motivo por el que DiffArray es tan lento es porque cuando la reina se mueve de una posición a otra, entonces la bandera booleana para la posición original vuelve a falsa y el la nueva posición se establece en verdadero, y estas diferencias se registran, sin embargo, son innecesarias porque cuando se reproducen al revés, la matriz termina con los mismos valores que tenía antes de que comenzara la reproducción. Por lo tanto, en lugar de utilizar una operación set para volver a false, lo que se necesita es una llamada al método deshacer, opcionalmente con un parámetro de entrada que indique a DiffArray qué valor "deshacer para" buscar en la lista de diferencias de enlaces dobles antes mencionada. Si ese valor de "deshacer a" se encuentra en un registro de diferencia en la lista de doble enlace, no hay cambios intermedios conflictivos en ese mismo elemento de matriz encontrado al retroceder en la búsqueda de lista, y el valor actual es igual a "deshacer desde" valor en ese registro de diferencia, luego se puede eliminar el registro y esa vieja copia se puede volver a apuntar al siguiente registro en la lista de doble enlace.

Lo que esto logra es eliminar la copia innecesaria de toda la matriz en el retroceso. Todavía hay una sobrecarga adicional en comparación con la versión imperativa del algoritmo, para agregar y deshacer los registros de adición de diferencia, pero esto puede estar más cerca del tiempo constante, es decir, O (1).

Si entiendo correctamente el algoritmo de n-queen, el retroceso de la operación de deshacer es solo uno, por lo que no hay andar. Por lo tanto, ni siquiera es necesario almacenar la diferencia del elemento establecido al mover la posición reina, ya que se deshará antes de acceder a la copia anterior. Solo necesitamos una forma de expresar este tipo de manera segura, lo cual es bastante fácil de hacer, pero lo dejaré como un ejercicio para el lector, ya que esta publicación ya es demasiado extensa.


UPDATE: no he escrito el código para todo el algoritmo, pero en mi cabeza las n-reinas puede ser implementado con en cada fila iterado, un pliegue en la siguiente matriz de diagonales, donde cada elemento es la tupla de triplete de: (el índice de la fila está ocupado o Ninguno, la matriz de índices de filas se cruzan con la diagonal izquierda-derecha, la matriz de índices de filas se cruzan con la diagonal derecha-izquierda).Las filas se pueden iterar con recursión o un pliegue de una matriz de índices de filas (el doblez realiza la recursión).

Aquí se muestran las interfaces para la estructura de datos que imagino. La siguiente sintaxis es Copute, pero creo que está lo suficientemente cerca de Scala, que puedes entender lo que se pretende.

Tenga en cuenta que cualquier implementación de DiffArray será irrazonablemente lenta si es multiproceso, pero el algoritmo de retroceso de n-queens no requiere que DiffArray sea multiproceso. Gracias a Edward Kmett por señalar eso en los comentarios para esta respuesta.

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. 

ACTUALIZACIÓN: Estoy trabajando en la Scala implementation, que tiene una mejorada interface en comparación con lo que había sugerido anteriormente. También he explicado cómo una optimización para pliegues se acerca a la misma sobrecarga constante que una matriz mutable.

+0

Ha pasado un tiempo desde la última vez que consideré este problema, pero no creo que sea tan simple ... – hugomg

+0

Estoy trabajando en una implementación , y volveré a informar aquí más tarde. –

+0

El algoritmo de retroceso n-queen es una función recursiva que toma 3 parámetros, una matriz para cada dirección de diagonales (\ y /) y un recuento de filas. Se itera sobre las columnas en esa fila, moviendo la posición de la reina en esa fila en las matrices, y recursándose en cada posición de columna con cur_row + 1. Me parece que el movimiento de la posición de la reina en las matrices es imposible de doblar como he descrito en mi respuesta. Parece demasiado fácil, ¿no? Entonces alguien me diga por qué, o lo averiguaré cuando escriba una implementación en código. –

1

Tengo una solución. Sin embargo, la constante puede ser grande, así que realmente no espero ganar nada.

Aquí es mi estructura de datos:

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

Se permite que todas las operaciones necesarias para llevar a cabo en O (1).

El código se puede encontrar aquí: http://hpaste.org/50707

La velocidad es mala - es más lenta que la solución de referencia publicado en la pregunta en la mayoría de los insumos. Los comparé entre sí en las entradas [1,3 .. 15] y obtuve los siguientes ratios de tiempo ((tiempo de solución de referencia/mi tiempo de solución) en%):

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

Observe la desaceleración casi lineal de la solución de referencia en relación con la mía, mostrando una diferencia en la complejidad asintótica.

Mi solución es probablemente horrible en términos de rigor y cosas por el estilo, y debe alimentar a un buen compilador optimizador (como Don Stewart por ejemplo) para obtener mejores resultados.

De todos modos, creo que en este problema O (1) y O (log (n)) son indistinguibles porque log (8) es solo 3 y las constantes como esta son objeto de micro-optimizaciones en lugar de algoritmo.