Compressione efficiente di alberi senza etichetta


20

Prendi in considerazione alberi binari senza etichetta e con radici. Possiamo comprimere tali alberi: quando sono puntatori ai sottoalberi e T ' con T = T ' (interpretazione = la parità strutturali), abbiamo memorizzare (WLOG) T e sostituire tutti i puntatori alle T ' con puntatori a T . Vedi la risposta di uli per un esempio.TTT=T=TTT

Fornire un algoritmo che accetta come input un albero nel senso sopra indicato e calcola il numero (minimo) di nodi che rimangono dopo la compressione. L'algoritmo dovrebbe essere eseguito nel tempo (nel modello di costo uniforme) con n il numero di nodi nell'input.O(nlogn)n

Questa è stata una domanda d'esame e non sono stato in grado di trovare una buona soluzione, né ne ho vista una.


E qual è "il costo", "il tempo", l'operazione elementare qui? Il numero di nodi visitati? Il numero di bordi attraversati? E come viene specificata la dimensione dell'input?
uli

Questa compressione dell'albero è un'istanza di hash consing . Non sono sicuro se questo porta a un metodo di conteggio generico.
Gilles 'SO- smetti di essere malvagio'

@uli ho chiarito che cos'è . Penso che il "tempo" sia abbastanza specifico, però. In contesti non simultanei, ciò equivale a contare le operazioni che in termini di Landau equivalgono a contare l'operazione elementare che si verifica più spesso. n
Raffaello

@Raphael Ovviamente posso indovinare quale dovrebbe essere l'operazione elementare prevista e probabilmente sceglierò lo stesso di tutti gli altri. Ma, e so di essere pedante qui, ogni volta che vengono dati i "limiti di tempo" è importante dichiarare ciò che viene contato. Si tratta di scambi, confronti, aggiunte, accessi alla memoria, nodi controllati, bordi attraversati, il nome? È come omettere l'unità di misura in fisica. Sono o 1010kg ? E suppongo che l'accesso alla memoria sia quasi sempre l'operazione più frequente. 10ms
uli

@uli Questi sono i dettagli che il “modello di costo uniforme” dovrebbe trasmettere. È doloroso definire con precisione quali operazioni sono elementari, ma nel 99,99% dei casi (incluso questo) non c'è ambiguità. Le classi di complessità fondamentalmente non hanno unità, non misurano il tempo necessario per eseguire un'istanza, ma il modo in cui questa volta varia man mano che l'input aumenta.
Gilles 'SO- smetti di essere malvagio' il

Risposte:


10

Sì, è possibile eseguire questa compressione nel tempo , ma non è facile :) Facciamo prima alcune osservazioni e poi presentiamo l'algoritmo. Supponiamo che l'albero inizialmente non sia compresso - questo non è davvero necessario ma semplifica l'analisi.O(nlogn)

Innanzitutto, definiamo induttivamente la "parità strutturale". Sia e T due (sotto) alberi. Se T e T ' sono entrambi gli alberi nulli (senza vertici), sono strutturalmente equivalenti. Se T e T 'non sono entrambi alberi nulli, allora sono strutturalmente equivalenti se i loro figli sinistri sono strutturalmente equivalenti e i loro figli giusti sono strutturalmente equivalenti. L '"equivalenza strutturale" è il punto fisso minimo su queste definizioni.TTTTTT

Ad esempio, due nodi foglia qualsiasi sono strutturalmente equivalenti, poiché entrambi hanno gli alberi null come entrambi i loro figli, che sono strutturalmente equivalenti.

Dato che è piuttosto fastidioso dire "i loro figli sinistri sono strutturalmente equivalenti e così sono i loro figli giusti", spesso diremo "i loro figli sono strutturalmente equivalenti" e intendono lo stesso. Nota anche che a volte diciamo "questo vertice" quando intendiamo "la sottostruttura radicata in questo vertice".

La definizione sopra ci dà immediatamente un indizio su come eseguire la compressione: se conosciamo l'equivalenza strutturale di tutti i sottotitoli con profondità al massimo , allora possiamo facilmente calcolare l'equivalenza strutturale dei sottotitoli con profondità d + 1 . Dobbiamo fare questo calcolo in modo intelligente per evitare un tempo di esecuzione O ( n 2 ) .dd+1O(n2)

L'algoritmo assegnerà identificatori a ogni vertice durante la sua esecuzione. Un identificatore è un numero nell'insieme . Gli identificatori sono unici e non cambiano mai: assumiamo quindi di impostare una variabile (globale) su 1 all'inizio dell'algoritmo e ogni volta che assegniamo un identificatore a un vertice, assegniamo il valore corrente di quella variabile al vertice e all'incremento il valore di quella variabile.{1,2,3,,n}

Trasformiamo innanzitutto l'albero di input in (al massimo ) elenchi contenenti vertici di uguale profondità, insieme a un puntatore al loro genitore. Questo è facilmente eseguibile in O ( n ) tempo.nO(n)

Per prima cosa comprimiamo tutte le foglie (possiamo trovare queste foglie nell'elenco con vertici di profondità 0) in un singolo vertice. Assegniamo a questo vertice un identificatore. La compressione di due vertici viene effettuata reindirizzando il genitore di uno dei vertici in modo che punti invece all'altro vertice.

Facciamo due osservazioni: in primo luogo, ogni vertice ha figli di profondità strettamente inferiore e, in secondo luogo, se abbiamo eseguito la compressione su tutti i vertici di profondità inferiori a (e abbiamo dato loro degli identificatori), allora due vertici di profondità d sono strutturalmente equivalenti e può essere compresso se gli identificatori dei loro figli coincidono. Quest'ultima osservazione deriva dal seguente argomento: due vertici sono strutturalmente equivalenti se i loro figli sono strutturalmente equivalenti, e dopo la compressione questo significa che i loro puntatori indicano gli stessi figli, il che a sua volta significa che gli identificatori dei loro figli sono uguali.dd

Esaminiamo tutte le liste con nodi di uguale profondità da piccola profondità a grande profondità. Per ogni livello creiamo un elenco di coppie di numeri interi, in cui ogni coppia corrisponde agli identificatori dei figli di alcuni vertici su quel livello. Abbiamo che due vertici in quel livello sono strutturalmente equivalenti se le loro coppie intere corrispondenti sono uguali. Usando l'ordinamento lessicografico, possiamo ordinarli e ottenere gli insiemi di coppie di numeri interi uguali. Comprimiamo questi set in vertici singoli come sopra e forniamo loro identificatori.

Le osservazioni di cui sopra dimostrano che questo approccio funziona e risulta nell'albero compresso. Il tempo di esecuzione totale è più il tempo necessario per ordinare gli elenchi che creiamo. Poiché il numero totale di coppie di numeri interi che creiamo è n , questo ci dà che il tempo di esecuzione totale è O ( n log n ) , come richiesto. Contare il numero di nodi rimasti al termine della procedura è banale (basta guardare quanti identificatori abbiamo distribuito).O(n)nO(nlogn)


Non ho letto la tua risposta in dettaglio, ma penso che tu abbia più o meno reinventato il consumo di hashish, con uno strano modo specifico per cercare i nodi.
Gilles 'SO- smetti di essere malvagio' il

@Alex "probabilmente i bambini di degreegrado strettamente più piccolo " dovrebbero essere depth? E nonostante gli alberi CS crescano verso il basso, trovo che "l'altezza di un albero" sia meno confusa della "profondità di un albero".
uli

Bella risposta. Sento che dovrebbe esserci un modo per aggirare l'ordinamento. Il mio secondo commento sulla risposta di @Gilles è valido anche qui.
Raffaello

@uli: sì, hai ragione, l'ho corretto (non so perché ho confuso quelle due parole). L'altezza e la profondità sono due concetti leggermente diversi, e avevo bisogno di quest'ultimo :) Ho pensato di attenermi alla convenzionale "profondità" piuttosto che confondere tutti scambiandoli.
Alex ten Brink

4

La compressione di una struttura di dati non mutabile in modo tale da non duplicare alcun sotterma strutturalmente uguale è nota come hash consing . Questa è una tecnica importante nella gestione della memoria nella programmazione funzionale. L'hash consing è una sorta di memorizzazione sistematica per le strutture dati.

Andremo a hash-contro dell'albero e contiamo i nodi dopo il consumo di hash. L'hash che consenta una struttura di dati di dimensione può sempre essere eseguito in O ( nn operazioni; il conteggio del numero di nodi alla fine è lineare nel numero di nodi.O(nlg(n))

Considero gli alberi con la seguente struttura (scritta qui nella sintassi di Haskell):

data Tree = Leaf
          | Node Tree Tree

Per ogni costruttore, dobbiamo mantenere una mappatura dai suoi possibili argomenti al risultato dell'applicazione del costruttore a questi argomenti. Le foglie sono banali. Per i nodi, manteniamo una mappa parziale finita dove T è l'insieme degli identificatori dell'albero e N è l'insieme degli identificatori dei nodi; T = N { } dove è l'unico identificatore foglia. (In termini concreti, un identificatore è un puntatore a un blocco di memoria.)nodes:T×TNTNT=N{}

Possiamo usare una struttura di dati tempo logaritmico per nodes, come un albero di ricerca binario bilanciato. Di seguito chiamerò lookup nodesl'operazione che cerca una chiave nella nodesstruttura dei dati e insert nodesl'operazione che aggiunge un valore sotto una nuova chiave e restituisce quella chiave.

Ora attraversiamo l'albero e aggiungiamo i nodi mentre procediamo. Sebbene scrivo in uno pseudocodice simile a Haskell, tratterò nodescome una variabile mutabile globale; lo aggiungeremo sempre e solo, ma gli inserimenti devono essere sottoposti a thread. La addfunzione ricorre su un albero, aggiungendo i suoi sottoalberi alla nodesmappa e restituisce l'identificatore della radice.

insert (p1,p2) =
add Leaf = $\ell$
add (Node t1 t2) =
    let p1 = add t1
    let p2 = add t2
    case lookup nodes (p1,p2) of
      Nothing -> insert nodes (p1,p2)
      Just p -> p

Il numero di insertchiamate, che è anche la dimensione finale della nodesstruttura dei dati, è il numero di nodi dopo la massima compressione. (Se necessario, aggiungine uno per l'albero vuoto).


Puoi fornire un riferimento per "L'hash che consenta una struttura di dati di dimensione può sempre essere fatto nelle operazioni O ( n l g ( n ) ) "? Tieni presente che avrai bisogno di alberi bilanciati per ottenere il tempo di esecuzione desiderato. nO(nlg(n))nodes
Raffaello

Stavo solo considerando le strutture di hashing per i numeri in modo strutturato in modo tale che il calcolo indipendente dell'hash per lo stesso albero producesse sempre lo stesso risultato. Anche la tua soluzione va bene, a condizione che disponiamo di strutture dati mutabili. Penso che possa essere ripulito un po ', però; l'interleaving di inserte adddovrebbe essere reso esplicito e una funzione che risolve effettivamente il problema dovrebbe essere data, imho.
Raffaello

1
Il consing di @Raphael Hash si basa su una struttura di mappe finite su tuple di puntatori / identificatori, è possibile implementarlo con tempo logaritmico per la ricerca e l'aggiunta (ad esempio con un albero di ricerca binario bilanciato). La mia soluzione non richiede mutabilità; Faccio nodesuna variabile mutabile per comodità, ma puoi inserirla dappertutto. Non ho intenzione di dare il codice completo, questo non è SO.
Gilles 'SO-smetti di essere malvagio' il

1
Le strutture di @Raphael Hashing , invece di assegnare loro numeri arbitrari, sono un po 'confuse. Nel modello di costo uniforme, è possibile codificare qualsiasi cosa in un intero di grandi dimensioni e eseguire operazioni a tempo costante su di esso, il che non è realistico. Nel mondo reale, puoi usare gli hash crittografici per avere un mapping uno a uno di fatto da insiemi infiniti a un intervallo finito di numeri interi, ma sono lenti. Se usi un checksum non crittografico come hash, devi pensare alle collisioni.
Gilles 'SO-smetti di essere malvagio' il

3

Ecco un'altra idea che mira a (iniettivamente) codificare la struttura degli alberi in numeri, piuttosto che etichettarli arbitrariamente. Per questo, usiamo che la fattorizzazione primaria di qualsiasi numero è unica.

Ai nostri scopi, sia denoti una posizione vuota nell'albero, e N ( l , r ) un nodo con sottostruttura sinistra l e sottostruttura destra r . N ( E , E ) sarebbe una foglia. Adesso mollaEN(l,r)lrN(E,E)

f(E)=0f(N(l,r))=2f(l)3f(r)

Usando , possiamo calcolare l'insieme di tutti i sottotitoli contenuti in un albero dal basso verso l'alto; in ogni nodo, uniamo i set di codifiche ottenute dai bambini e aggiungiamo un nuovo numero (che può essere calcolato in tempo costante dalle codifiche dei bambini).f

Quest'ultima ipotesi è un allungamento su macchine reali; in questo caso, si preferirebbe usare qualcosa di simile alla funzione di accoppiamento di Cantor anziché esponenziazione.

O(nlogn)O(nlogn)


1

Poiché le foto non sono consentite nei commenti:

inserisci qui la descrizione dell'immagine

in alto a sinistra: un albero di input

in alto a destra: anche i sottotitoli radicati nei nodi 5 e 7 sono isomorfi.

in basso a sinistra e a destra: gli alberi compressi non sono definiti in modo univoco.

7+5|T|6+|T|


Questo è davvero un esempio dell'operazione desiderata, grazie. Nota che i tuoi esempi finali sono identici se non fai distinzione tra riferimenti originali e aggiunti.
Raffaello

-1

Modifica: ho letto la domanda poiché T e T ′ erano figli dello stesso genitore. Ho preso anche la definizione di compressione come ricorsiva, nel senso che potresti comprimere due sottotitoli precedentemente compressi. Se questa non è la vera domanda, la mia risposta potrebbe non funzionare.

O(nlogn)T(n)=2T(n/2)+cn

def Comp(T):
   if T == null:
     return 0
   leftCount = Comp(T.left)
   rightCount = Comp(T.right)
   if leftCount == rightCount:
     if hasSameStructure(T.left, T.right):
       T.right = T.left
       return leftCount + 1
     else
       return leftCount + rightCount + 1    

Dov'è hasSameStructure()una funzione che confronta due sottostrutture già compresse in tempo lineare per vedere se hanno la stessa struttura esatta. Scrivere una funzione ricorsiva temporale lineare che attraversa ciascuna e controlla se una sottostruttura ha un figlio sinistro ogni volta che l'altra lo fa ecc. Non dovrebbe essere difficile.

nnr

T(n)=T(n1)+T(n2)+O(1) if nnr
2T(n/2)+O(n) otherwise

E se i sottotitoli non fossero fratelli? Cura di ((T1, T1), (T2, T1)) T1 può essere salvato due volte utilizzando un puntatore due alla terza occorrenza.
uli

TT

Le domande affermano semplicemente che due sottotitoli sono identificati come isomorfi. Non si dice nulla sul fatto che abbiano lo stesso genitore. Se un sottotree T1 appare tre volte in un albero, come nel mio esempio precedente ((T1, T1), (T1, T2)) due occorrenze possono essere compresse indicando la terza orcurenza.
uli
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.