Bootstrapping una estructura de árbol de dedo

16

Después de trabajar con 2-3 árboles de dedos durante bastante tiempo, me ha impresionado su velocidad en la mayoría de las operaciones. Sin embargo, el único problema con el que me he encontrado es la gran sobrecarga asociada con la creación inicial de un gran árbol de dedos. Debido a que la construcción se define como una secuencia de operaciones de concatenación, terminas construyendo una gran cantidad de estructuras de árbol de dedos que no son necesarias.

Debido a la naturaleza compleja de los árboles de 2-3 dedos, no veo ningún método intuitivo para arrancarlos, y todas mis búsquedas han quedado vacías. Entonces, la pregunta es, ¿cómo podría usted arrancar un árbol de 2-3 dedos con una sobrecarga mínima?

Para ser explícito: dada una secuencia de longitud conocida genere la representación del árbol de dedos de con operaciones mínimas.SnS

La manera ingenua de lograr es llamadas sucesivas a la operación contras (en la literatura el operador ' '). Sin embargo, esto creará estructuras de árbol de dedos distintas que representan todos los cortes de para .n S [ 1 .. i ]nS[1..i]

jbondeson
fuente
1
¿Los árboles Finger: una simple estructura de datos de propósito general de Hinze y Paterson proporcionan respuestas?
Dave Clarke
@Dave Realmente implementé fuera de su papel, y no abordan la creación eficiente.
jbondeson
Me imaginé tanto.
Dave Clarke
¿Podría ser un poco más específico sobre lo que quiere decir con "compilación" en este caso? ¿Es esto un despliegue?
jbapple
@jbapple: edité para ser más explícito, perdón por la confusión.
jbondeson

Respuestas:

16

GHC Data.Sequence's replicatefunción construye una fingertree en el tiempo y el espacio, pero esto es posible gracias conocer los elementos que intervienen en la columna derecha del árbol dedo desde el primer momento. Esta biblioteca fue escrita por los autores del artículo original sobre árboles de 2-3 dedos.O(lgnorte)

Si desea construir un árbol de dedos mediante concatenación repetida, puede reducir el uso de espacio transitorio mientras construye cambiando la representación de las espinas. Las espinas de los árboles de 2-3 dedos se almacenan inteligentemente como listas sincronizadas de enlaces individuales. Si, en cambio, almacena las espinas como calcas, puede ser posible ahorrar espacio al concatenar árboles. La idea es que concatenar dos árboles de la misma altura ocupa espacio reutilizando las espinas de los árboles. Al concatenar 2-3 árboles de dedos como se describió originalmente, las espinas internas del nuevo árbol ya no se pueden usar como están.O(1)

Las "Representaciones puramente funcionales de listas ordenadas catenables" de Kaplan y Tarjan describen una estructura de árbol de dedos más complicada. Este documento (en la sección 4) también analiza una construcción similar a la sugerencia de deque que hice anteriormente. Creo que la estructura que describen puede concatenar dos árboles de igual altura en tiempo y espacio. Para construir árboles de dedos, ¿es suficiente para usted ahorrar espacio?O(1)

NB: Su uso de la palabra "bootstrapping" significa algo un poco diferente a su uso anterior. Significan almacenar parte de una estructura de datos utilizando una versión más simple de la misma estructura.

jbapple
fuente
Una idea muy interesante. Tendré que analizar esto y ver cuáles serían las compensaciones para la estructura general de datos.
jbondeson
Quería que hubiera dos ideas en esta respuesta: (1) La idea replicada (2) Concatenación más rápida para árboles de tamaño casi igual. Creo que la idea replicada puede construir árboles de dedos en muy poco espacio adicional si la entrada es una matriz.
jbapple
Sí, los vi a ambos. Lo siento, no hice comentarios sobre los dos. Primero estoy buscando el código replicado, aunque definitivamente estoy ampliando mi conocimiento de Haskell hasta el límite. A primera vista, parece que podría resolver la mayoría de los problemas que tengo, siempre que tenga un acceso aleatorio rápido. El concat rápido podría ser una solución un poco más genérica en el caso de que no haya acceso aleatorio.
jbondeson
10

Refiriéndose a la excelente respuesta de jbapple con respecto a replicate, pero usando replicateA(que replicatese basa en) en su lugar, se me ocurrió lo siguiente:

--Unlike fromList, one needs the length explicitly. 
myFromList :: Int -> [b] -> Seq b
myFromList l xs = flip evalState xs $ Seq.replicateA l go
    where go = do
           (y:ys) <- get
            put ys
            return y

myFromList(en una versión ligeramente más eficiente) ya está definido y utilizado internamente en Data.Sequencepara la construcción de árboles de los dedos que son resultados de las clases.

En general, la intuición para replicateAes simple. replicateAestá construido encima de la función applytiveTree . applicativeTreetoma un pedazo de un árbol de un tamaño my produce un árbol bien equilibrado que contiene ncopias de este. Las cajas para nhasta 8 (un solo Deepdedo) están codificadas. Cualquier cosa por encima de esto, y se invoca recursivamente. El elemento "aplicativo" es simplemente que intercala la construcción del árbol con efectos de enhebrado, como, en el caso del código anterior, el estado.

La gofunción, que se replica, es simplemente una acción que obtiene el estado actual, saca un elemento de la parte superior y reemplaza el resto. En cada invocación, por lo tanto, baja más abajo en la lista proporcionada como entrada.

Algunas notas más concretas

main = print (length (show (Seq.fromList [1..10000000::Int])))

En algunas pruebas simples, esto produjo una compensación de rendimiento interesante. La función principal anterior se ejecutó casi 1/3 más bajo con myFromList que con fromList. Por otro lado, myFromListusó un montón constante de 2 MB, mientras que el estándar fromListusó hasta 926 MB. Ese 926 MB surge de la necesidad de mantener toda la lista en la memoria a la vez. Mientras tanto, la solución myFromListes capaz de consumir la estructura de forma perezosa. El problema con la velocidad resulta del hecho de que myFromListdebe realizar aproximadamente el doble de asignaciones (como resultado de la construcción / destrucción del par de la mónada estatal) quefromList. Podemos eliminar esas asignaciones moviéndonos a una mónada de estado transformada por CPS, pero eso da como resultado retener mucha más memoria en un momento dado, porque la pérdida de la pereza requiere atravesar la lista de manera no continua.

Por otro lado, si en lugar de forzar la secuencia completa con un espectáculo, me muevo a solo extraer la cabeza o el último elemento, myFromListinmediatamente presenta una ganancia mayor: extraer el elemento de la cabeza es casi instantáneo y extraer el último elemento es 0.8s . Mientras tanto, con el estándar fromList, extraer la cabeza o el último elemento cuesta ~ 2.3 segundos.

Todo esto son detalles, y es consecuencia de la pureza y la pereza. En una situación con mutación y acceso aleatorio, me imagino que la replicatesolución es estrictamente mejor.

Sin embargo, plantea la cuestión de si hay una manera de reescribir applicativeTreetal que myFromListsea ​​estrictamente más eficiente. Creo que el problema es que las acciones aplicativas se ejecutan en un orden diferente al que se atraviesa naturalmente el árbol, pero no he trabajado completamente sobre cómo funciona esto, o si hay una manera de resolverlo.

sclv
fuente
44
(1) Interesante. Esta parece ser la forma correcta de hacer esta tarea. Me sorprende escuchar que esto es más lento que fromListcuando se fuerza toda la secuencia. (2) Tal vez esta respuesta sea demasiado pesada y dependiente del lenguaje para cstheory.stackexchange.com. Sería genial si pudiera agregar una explicación de cómo replicateAfunciona de manera independiente del idioma.
Tsuyoshi Ito
9

Si bien terminas con una gran cantidad de estructuras intermedias de árbol de dedos, comparten la gran mayoría de su estructura entre sí. Al final, asigna como máximo el doble de memoria que en el caso idealizado, y el resto se libera con la primera colección. Los asintóticos de esto son los mismos que pueden obtener, ya que al final necesita un dedo lleno de n valores.

Puede construir el árbol de dedos usando Data.FingerTree.replicatey usando FingerTree.fmapWithPospara buscar sus valores en una matriz que desempeña el papel de su secuencia finita, o usando traverseWithPospara despegarlos de una lista u otro contenedor de tamaño conocido.

O(Iniciar sesiónnorte)O(norte)O(Iniciar sesiónnorte)

O(Iniciar sesiónnorte)replicateAmapAccumL

TL; DR Si tuviera que hacer esto, probablemente usaría:

rep :: (Int -> a) -> Int -> Seq a 
rep f n = mapWithIndex (const . f) $ replicate n () 

e indexar en una matriz de tamaño fijo que simplemente proporcionaría (arr !)para farriba.

Edward KMETT
fuente