Avvio automatico di una struttura ad albero dito


16

Dopo aver lavorato con 2-3 alberi di dita per un bel po ', sono rimasto colpito dalla loro velocità nella maggior parte delle operazioni. Tuttavia, l'unico problema che ho riscontrato è il grande overhead associato alla creazione iniziale di un albero finger di grandi dimensioni. Poiché la costruzione è definita come una sequenza di operazioni di concatenazione, si finisce per costruire un gran numero di strutture dell'albero delle dita che non sono necessarie.

A causa della natura complessa degli alberi a 2-3 dita non vedo alcun metodo intuitivo per avviarli e tutte le mie ricerche sono state vuote. Quindi la domanda è: come puoi fare per avviare un albero a 2-3 dita con un sovraccarico minimo?

Per essere espliciti: data una sequenza di lunghezza nota n genera la rappresentazione ad albero dito di S con operazioni minime.SnS

Il modo ingenuo di realizzare sono chiamate successive all'operazione contro (in letteratura l' operatore ' '). Tuttavia, ciò creerà n strutture distinte di finger tree che rappresentano tutte le sezioni di S per [nS .[1..i]



@Dave In realtà ho implementato il loro documento e non affrontano la creazione efficiente.
jbondeson,

C'ero arrivato pure io.
Dave Clarke,

Potresti essere un po 'più specifico su cosa intendi per "build" in questo caso? Questo è uno svolgersi?
jbapple

@jbapple - Ho modificato per essere più esplicito, scusate la confusione.
jbondeson

Risposte:


16

GHC di Data.Sequence's replicatefunzione costruisce un fingertree a O(lgn) tempo e nello spazio, ma questo è abilitato di conoscere gli elementi che vanno a destra colonna vertebrale dell'albero dito dal get-go. Questa biblioteca è stata scritta dagli autori dell'articolo originale su alberi di 2-3 dita.

Se si desidera costruire un finger tree mediante ripetute concatenazioni, è possibile ridurre l'utilizzo dello spazio transitorio durante la costruzione modificando la rappresentazione delle spine. Le spine dorsali sugli alberi a 2-3 dita sono abilmente memorizzate come liste sincronizzate singolarmente collegate. Se, invece, si memorizzano le spine come deques, potrebbe essere possibile risparmiare spazio durante la concatenazione di alberi. L'idea è che concatenare due alberi della stessa altezza richiede O(1) spazio riutilizzando le spine degli alberi. Quando si concatenano alberi a 2-3 dita come descritto in origine, le spine che sono interne al nuovo albero non possono più essere utilizzate così come sono.

Le "Rappresentazioni puramente funzionali delle liste ordinate catenabili" di Kaplan e Tarjan descrivono una struttura ad albero delle dita più complicata. Questo documento (nella sezione 4) discute anche di una costruzione simile al suggerimento di deque che ho fatto sopra. Credo che la struttura che descrivono possa concatenare due alberi di uguale altezza in O(1) tempo e spazio. Per costruire alberi da dito, questo spazio ti fa risparmiare abbastanza?

NB: Il loro uso della parola "bootstrap" significa qualcosa di un po 'diverso dal tuo uso sopra. Significano l'archiviazione di parte di una struttura dati utilizzando una versione più semplice della stessa struttura.


Un'idea molto interessante. Dovrò esaminare questo e vedere quali sarebbero i compromessi per la struttura generale dei dati.
jbondeson

Intendevo che ci fossero due idee in questa risposta: (1) L'idea replicata (2) Concatenata più veloce per alberi di dimensioni quasi uguali. Penso che l'idea di replica possa costruire alberi di dita in pochissimo spazio extra se l'input è un array.
jbapple

Sì, ho visto entrambi. Mi dispiace di non aver commentato entrambi. Sto esaminando prima il codice di replica, anche se sto sicuramente estendendo la mia conoscenza di Haskell fino in fondo. A prima vista sembra che potrebbe risolvere la maggior parte dei problemi che sto riscontrando, a condizione che tu abbia un accesso casuale veloce. Il concat veloce potrebbe essere una soluzione un po 'più generica in caso di accesso casuale.
jbondeson,

10

Riffing sull'ottima risposta di jbapple per quanto riguarda replicate, ma usando replicateA(che replicateè costruito su) invece, ho trovato quanto segue:

--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(in una versione leggermente più efficiente) è già definito e utilizzato internamente in Data.Sequenceper la costruzione di alberi delle dita che sono i risultati di sorta.

In generale, l'intuizione per replicateAè semplice. replicateAè costruito sopra la funzione applicativeTree . applicativeTreeprende un pezzo di un albero di dimensioni me produce un albero ben bilanciato contenente ncopie di questo. I casi per un nmassimo di 8 (un singoloDeep dito) sono codificati. Qualunque cosa al di sopra di questo, e si invoca ricorsivamente. L'elemento "applicativo" è semplicemente che intercala la costruzione dell'albero con effetti di threading, come, nel caso del codice precedente, stato.

Il go funzione, che viene replicata, è semplicemente un'azione che ottiene lo stato corrente, fa apparire un elemento dall'alto e sostituisce il resto. Ad ogni invocazione, si sposta quindi più in basso nell'elenco fornito come input.

Alcune note più concrete

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

In alcuni semplici test, questo ha prodotto un interessante compromesso delle prestazioni. La funzione principale sopra era quasi 1/3 inferiore con myFromList rispetto a fromList. D'altra parte, ha myFromListutilizzato un heap costante di 2 MB, mentre lo standard ha fromListutilizzato fino a 926 MB. Quel 926 MB deriva dalla necessità di conservare l'intero elenco in memoria contemporaneamente. Nel frattempo, la soluzione con myFromListè in grado di consumare la struttura in modo pigro in streaming. Il problema con la velocità deriva dal fatto che myFromListdeve eseguire all'incirca il doppio del numero di allocazioni (come risultato della costruzione / distruzione della coppia della monade dello stato) difromList . Possiamo eliminare tali allocazioni passando a una monade di stato trasformata in CPS, ma ciò si traduce in una maggiore quantità di memoria in un dato momento, poiché la perdita di pigrizia richiede di attraversare l'elenco in modo non streaming.

D'altra parte, se invece di forzare l'intera sequenza con uno spettacolo, mi sposto semplicemente sull'estrazione della testa o dell'ultimo elemento, myFromListpresenta immediatamente una vittoria più grande - l'estrazione dell'elemento testa è quasi istantanea e l'estrazione dell'ultimo elemento è 0,8s . Nel frattempo, con lo standard fromList, l'estrazione della testa o dell'ultimo elemento costa ~ 2,3 secondi.

Questo è tutti i dettagli ed è una conseguenza di purezza e pigrizia. In una situazione con mutazione e accesso casuale, immagino che la replicatesoluzione sia strettamente migliore.

Tuttavia, solleva la questione se esiste un modo per riscrivere in applicativeTreemodo myFromListstrettamente più efficiente. Il problema è, penso, che le azioni applicative siano eseguite in un ordine diverso rispetto a quello che l'albero viene attraversato naturalmente, ma non ho completamente lavorato su come funziona, o se c'è un modo per risolverlo.


4
(1) Interessante. Questo sembra il modo corretto di fare questo compito. Sono sorpreso di sentire che questo è più lento di fromListquando l'intera sequenza è forzata. (2) Forse questa risposta è troppo pesante per il codice e dipende dalla lingua per cstheory.stackexchange.com. Sarebbe bello se puoi aggiungere una spiegazione su come replicateAfunziona in modo indipendente dalla lingua.
Tsuyoshi Ito,

9

Mentre finisci con un gran numero di strutture intermedie a fingertree, condividono la stragrande maggioranza della loro struttura tra loro. Alla fine si alloca al massimo il doppio della memoria rispetto al caso idealizzato e il resto viene liberato con la prima raccolta. Gli asintotici sono uguali a quelli che riescono a ottenere, dato che alla fine hai bisogno di un dito pieno pieno di n valori.

Puoi creare il fingertree usando Data.FingerTree.replicatee usando FingerTree.fmapWithPosper cercare i tuoi valori in un array che svolge il ruolo della tua sequenza finita, o usando traverseWithPosper staccarli da un elenco o altro contenitore di dimensioni note.

O(logn)O(n)O(logn)

O(logn)replicateAmapAccumL

TL; DR Se dovessi farlo, probabilmente userei:

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

e per indicizzare in un array di dimensioni fisse provvederei solo (arr !)per fsopra.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.