C'è un mucchio stabile?


32

Esiste una struttura di dati di coda prioritaria che supporta le seguenti operazioni?

  • Inserisci (x, p) : aggiungi un nuovo record x con priorità p
  • StableExtractMin () : restituisce ed elimina il record con la priorità minima, rompendo i legami per ordine di inserzione .

Pertanto, dopo Inserisci (a, 1), Inserisci (b, 2), Inserisci (c, 1), Inserisci (d, 2), una sequenza di StableExtractMin restituisce a, quindi c, quindi b, quindi d.

Ovviamente si potrebbe usare qualsiasi struttura di dati di coda di priorità memorizzando la coppia (p,time) come priorità effettiva, ma sono interessato a strutture di dati che non memorizzano esplicitamente i tempi di inserimento (o ordine di inserimento), per analogia allo smistamento stabile.

Equivalentemente (?): Esiste una versione stabile di heapsort che non richiede Ω(n) spazio extra?


Penso che intendi "a, quindi c, quindi b, quindi d"?
Ross Snider,

Heap con elenco collegato di record + albero binario bilanciato digitato in base alla priorità che punta all'elenco collegato corrispondente non funzionerà? Cosa mi sto perdendo?
Aryabhata,

Moron: Questo sta memorizzando esplicitamente l'ordine di inserimento, che è esattamente ciò che voglio evitare. Ho chiarito la dichiarazione del problema (e risolto il refuso di Ross).
Jeffε,

Risposte:


16

Il metodo Bently-Saxe fornisce una coda di priorità stabile abbastanza naturale.

Archivia i tuoi dati in una sequenza di array ordinati . A i ha dimensione 2 i . Ogni array mantiene anche un contatore c i . Le voci dell'array A i [ c i ] , , A i [ 2 i - 1 ] contengono dati.A0,,AkAi2iciAi[ci],,Ai[2i1]

Per ogni , tutti gli elementi in A i sono stati aggiunti più di recente rispetto a quelli in A i + 1 e all'interno di ciascun elemento A i sono ordinati per valore con i legami spezzati posizionando gli elementi più vecchi davanti agli elementi più nuovi. Questo significa che possiamo unire A i e A i + 1 e conservare questo ordine. (Nel caso di legami durante l'unione, prendi l'elemento da A i + 1. )iAiAi+1AiAiAi+1Ai+1

Per inserire un valore , trova il più piccolo i tale che A i contenga 0 elementi, unisci A 0 , , A i - 1 e x , memorizzalo in A i e imposta c 0 , , c i in modo appropriato.xiAiA0,,Ai1xAic0,,ci

Per estrarre il minimo, trova l'indice più grande tale che il primo elemento in A i [ c i ] sia minimo su tutto i e incrementa c i .iAi[ci]ici

Secondo l'argomento standard, questo dà tempo ammortizzato per operazione ed è stabile a causa dell'ordinamento sopra descritto.O(logn)

Per una sequenza di inserimenti ed estrazioni, utilizza n voci di array (non tenere matrici vuote) più O ( log n ) parole di dati contabili. Non risponde alla versione della domanda di Mihai, ma mostra che il vincolo stabile non richiede molto spazio in testa. In particolare, mostra che non vi è alcun limite inferiore di Ω ( n ) sullo spazio aggiuntivo necessario.nnO(logn)Ω(n)

Aggiornamento: Rolf Fagerberg sottolinea che se possiamo memorizzare valori null (non dati), allora l'intera struttura di dati può essere impacchettata in un array di dimensioni , dove n è il numero di inserimenti finora.nn

Innanzitutto, nota che possiamo raggruppare in un array in quell'ordine (con prima A k , seguito da A k - 1 se non è vuoto, e così via). La struttura di questo è completamente codificata dalla rappresentazione binaria di n , il numero di elementi inseriti finora. Se la rappresentazione binaria di n ha 1 in posizione i , allora A i occuperà la posizione dell'array 2 i , altrimenti non occuperà posizioni dell'array.Ak,,A0AkAk1nniAi2i

Quando si inserisce, e la lunghezza del nostro array, aumentare di 1 e possiamo unire A 0 , ... , A i più il nuovo elemento utilizzando algoritmi di fusione stabili sul posto esistenti.nA0,,Ai

Ora, dove usiamo valori nulli è nel liberarci dei contatori . In A i , memorizziamo il primo valore, seguito da c i valori null, seguito dai restanti 2 valori i - c i - 1 . Durante un extract-min, possiamo ancora trovare il valore da estrarre nel tempo O ( log n ) esaminando A 0 [ 0 ] , , A k [ 0 ] . Quando troviamo questo valore in A i [ 0ciAicio2io-cio-1O(logn)A0[0],,Ak[0] impostiamo A i [ 0 ] su null e quindieseguiamo una ricerca binaria su A i per trovare il primo valore non nullo A i [ c i ] e scambiare A i [ 0 ] e A i [ c i ] .Ai[0]Ai[0]AiAi[ci]Ai[0]Ai[ci]

Il risultato finale: l'intera struttura può essere implementata con un array la cui lunghezza è incrementata con ogni inserimento e un contatore, , che conta il numero di inserimenti.n


1
Questo utilizza potenzialmente O (n) spazio extra in un dato istante dopo O (n) estrazioni, no? A questo punto potresti anche memorizzare la priorità ...
Mehrdad,

10

Non sono sicuro di quali siano i tuoi vincoli; si qualifica quanto segue? Archivia i dati in un array, che interpretiamo come un albero binario implicito (come un heap binario), ma con gli elementi di dati al livello inferiore dell'albero anziché ai suoi nodi interni. Ogni nodo interno dell'albero memorizza il più piccolo dei valori copiati dai suoi due figli; in caso di legami, copia il bambino sinistro.

Per trovare il minimo, guarda la radice dell'albero.

Per eliminare un elemento, contrassegnarlo come eliminato (eliminazione lenta) e propagare l'albero (ogni nodo sul percorso della radice che conteneva una copia dell'elemento eliminato dovrebbe essere sostituito con una copia dell'altro figlio). Mantieni un numero di elementi eliminati e, se mai diventa una frazione troppo grande di tutti gli elementi, ricostruisci la struttura preservando l'ordine degli elementi al livello inferiore: la ricostruzione richiede un tempo lineare, quindi questa parte aggiunge solo un tempo ammortizzato costante al complessità operativa.

Per inserire un elemento, aggiungilo alla successiva posizione libera nella riga inferiore dell'albero e aggiorna il percorso alla radice. Oppure, se la riga inferiore diventa piena, raddoppia la dimensione dell'albero (di nuovo con un argomento di ammortamento; nota che questa parte non è affatto diversa dalla necessità di ricostruire quando un heap binario standard supera il suo array).

Tuttavia, non è una risposta alla versione più rigorosa della domanda di Mihai, perché utilizza il doppio della memoria rispetto a una struttura di dati implicita reale, anche se ignoriamo pigramente il costo dello spazio per la gestione delle eliminazioni.


Mi piace questo. Proprio come con un normale min-heap ad albero implicito, probabilmente l'albero implicito 3-ary o 4-ary sarà più veloce a causa degli effetti della cache (anche se hai bisogno di più confronti).
Jonathan Graehl,

8

La seguente è una valida interpretazione del tuo problema:

Devi memorizzare N chiavi in ​​un array di A [1..N] senza informazioni ausiliarie in modo tale da poter supportare: * insert key * delete min, che seleziona il primo elemento inserito se ci sono più minimi

Questo sembra piuttosto difficile, dato che la maggior parte delle strutture di dati implicite gioca il trucco di codificare i bit nell'ordinamento locale di alcuni elementi. Qui se più ragazzi sono uguali, il loro ordine deve essere preservato, quindi non sono possibili tali trucchi.

Interessante.


1
Penso che questo dovrebbe essere un commento, non una risposta, in quanto non risponde alla domanda originale. (Puoi eliminarlo e aggiungerlo come commento.)
Jukka Suomela,

5
Sì, questo sito Web è un po 'ridicolo. Abbiamo reputazione, bonus, premi, ogni sorta di modi per commentare che non riesco a capire. Vorrei che sembrasse meno un gioco per bambini.
Mihai,

1
Penso che abbia bisogno di più rappresentanti per pubblicare un commento. questo è il problema.
Suresh Venkat,

@Suresh: Oh, giusto, non me lo ricordavo. Come dovremmo effettivamente gestire questo tipo di situazione (ad esempio, un nuovo utente deve chiedere chiarimenti prima di rispondere a una domanda)?
Jukka Suomela,

2
non è facile uscirne. L'ho visto spesso su MO. Mihai non avrà problemi a guadagnare rep, se è il Mihai penso che sia :)
Suresh Venkat,

4

Risposta breve: non puoi.

Risposta leggermente più lunga:

Avrai bisogno di spazio extra per memorizzare l '"età" della tua voce che ti permetterà di discriminare tra priorità identiche. E avrai bisogno di Ω ( n ) spazio per informazioni che consentano inserimenti e recuperi rapidi. Più il tuo payload (valore e priorità).Ω(n)Ω(n)

E, per ogni carico di memorizzare, sarete in grado di "nascondere" alcune informazioni l'indirizzo (ad esempio, significa Y è più vecchio di X). Ma in quelle informazioni "nascoste", nasconderai le informazioni "età", o le informazioni "recupero rapido". Non entrambi.addr(X)<addr(Y)


Risposta molto lunga con pseudo-matematica traballante inesatta:

Nota: la fine della seconda parte è imprecisa, come detto. Se un ragazzo di matematica potesse fornire una versione migliore, sarei grato.

Pensiamo alla quantità di dati coinvolti in una macchina X-bit (diciamo 32 o 64-bit), con record (valore e priorità) parole macchina larghe.P

Hai un set di potenziali record che è parzialmente ordinato: e ( a , 1 ) = ( a , 1 )(a,1)<(a,2)(a,1)=(a,1) ma non puoi confrontare e ( b , 1 ) .(a,1)(b,1)

Tuttavia, si desidera poter confrontare due valori non confrontabili dal proprio set di record, in base a quando sono stati inseriti. In modo da avere qui un altro insieme di valori: quelli che sono stati inseriti, e si vuole migliorare con un ordine parziale: se e solo se X è stato inserito prima Y .X<YXY

Nel peggiore dei casi, la memoria verrà riempita con i record del modulo (con ? Diverso per ognuno), quindi dovrai fare affidamento interamente sul tempo di inserimento per decidere quale va prima fuori.(?,1)?

  • Il tempo di inserimento (rispetto ad altri record ancora nella struttura) richiede bit di informazione (con payload P-byte e 2 X byte di memoria accessibili).Xlog2(P)2X
  • Il payload (il valore e la priorità del tuo record) richiede parole macchina di informazioni.P

Ciò significa che devi in qualche modo memorizzare extra per ogni record memorizzato. E questo è O ( n ) per n record.Xlog2(P)O(n)n

Ora, quante informazioni ci fornisce ogni "cella" di memoria?

  • bit di dati ( W è la larghezza della parola macchina).WW
  • bit di indirizzo.X

Supponiamo ora (il payload è largo almeno una parola macchina (di solito un ottetto)). Ciò significa che X - lP1 , in modo da poter adattare le informazioni sull'ordine di inserimento all'indirizzo della cella. Questo è ciò che accade in uno stack: le celle con l'indirizzo più basso sono entrate per prime nello stack (e usciranno per ultime).Xlog2(P)<X

Quindi, per memorizzare tutte le nostre informazioni, abbiamo due possibilità:

  • Memorizza l'ordine di inserimento nell'indirizzo e il payload in memoria.
  • Conserva entrambi in memoria e lascia l'indirizzo libero per qualche altro utilizzo.

Ovviamente, per evitare sprechi, useremo la prima soluzione.


Ora per le operazioni. Suppongo che desideri avere:

  • con O ( lInsert(task,priority) complessità temporale.O(logn)
  • con O (StableExtractMin() complessità temporale.O(logn)

Diamo un'occhiata a :StableExtractMin()

L'algoritmo davvero molto generale va così:

  1. Trova il record con la priorità minima e il "tempo di inserimento" minimo in .O(logn)
  2. Rimuoverlo dalla struttura in .O(logn)
  3. Restituirlo.

Ad esempio, nel caso di un heap, sarà organizzato in modo leggermente diverso, ma il lavoro è lo stesso: 1. Trova il record minimo in 2. Rimuovilo dalla struttura in O ( 1 ) 3. Correzione tutto in modo che la prossima volta # 1 e # 2 siano ancora O ( 1 ) cioè "ripari l'heap". Questo deve essere fatto in "O (log n)" 4. Restituisce l'elemento.0(1)O(1)O(1)

Tornando all'algoritmo generale, vediamo che per trovare il record nel tempo , abbiamo bisogno di un modo veloce per scegliere quello giusto tra 2 ( X - l o g 2 ( P ) ) candidati (peggio caso, la memoria è piena).O(logn)2(Xlog2(P))

Ciò significa che dobbiamo memorizzare bit di informazioni al fine di recuperare quell'elemento (ogni bit taglia in due lo spazio candidato, quindi abbiamo bisezioni O ( l o g n ) , che significa O ( l oXlog2(P)O(logn) complessità temporale).O(logn)

Questi bit di informazioni potrebbero essere memorizzati come l'indirizzo dell'elemento (nell'heap, il min è a un indirizzo fisso) oppure, ad esempio con puntatori (in un albero di ricerca binario (con puntatori), è necessario seguire in media per arrivare al minuto).O(logn)

Ora, quando si elimina quell'elemento, avremo bisogno di aumentare il record minimo successivo in modo che abbia la giusta quantità di informazioni per consentire il recupero di prossima volta, cioè, quindi ha X - l o g 2 ( P ) frammenti di informazioni che la discriminano dagli altri candidati.O(logn)Xlog2(P)

Cioè, se non ha già abbastanza informazioni, dovrai aggiungerne alcune. In un albero di ricerca binario (non bilanciato), le informazioni sono già lì: dovresti mettere un puntatore NULL da qualche parte per eliminare l'elemento e senza ulteriori operazioni, il BST è ricercabile in tempo in media.O(logn)

Dopo questo punto, è leggermente abbozzato, non sono sicuro di come formularlo. Ma ho la netta sensazione che ciascuno degli elementi rimanenti nel tuo set avrà bisogno di avere bit di informazioni che aiuteranno a trovare il prossimo min e aumentarlo con abbastanza informazioni in modo che possano essere trovate in O ( l o gXlog2(P) prossima volta.O(logn)

L'algoritmo di inserimento di solito ha solo bisogno di aggiornare parte di queste informazioni, non penso che costerà di più (dal punto di vista della memoria) per farlo funzionare velocemente.


Ciò significa che avremo bisogno di conservare più bit di informazioni per ciascun elemento. Quindi, per ogni elemento, abbiamo:Xlog2(P)

  • Il tempo di inserimento, bit.Xlog2(P)
  • Il carico utile parole della macchina P.P
  • L'informazione "ricerca veloce", Xlog2(P) bit .

Dato che utilizziamo già i contenuti della memoria per memorizzare il payload e l'indirizzo per memorizzare i tempi di inserimento, non ci resta spazio per memorizzare le informazioni di "ricerca rapida". Quindi dovremo allocare un po 'di spazio extra per ogni elemento, e quindi "sprecare" spazio extra.Ω(n)


hai davvero intenzione di dare la tua risposta in CW?
Suresh Venkat,

Sì. La mia risposta non è corretta al 100%, come indicato all'interno, e sarebbe bello se qualcuno potesse correggerla anche se non sono più in SO o altro. La conoscenza dovrebbe essere condivisa, la conoscenza dovrebbe essere mutevole. Ma forse ho frainteso l'uso di CW, in tal caso, per favore dimmelo :). EDIT: spiacenti, in effetti ho appena scoperto che non riceverò alcun rappresentante dai post di CW e che il contenuto è concesso in licenza CC-wiki in alcun modo ... Peccato :).
Suzanne Dupéron,

3

Se implementi la tua coda di priorità come un albero binario bilanciato (una scelta popolare), devi solo assicurarti che quando aggiungi un elemento all'albero, questo viene inserito a sinistra di tutti gli elementi con uguale priorità.
In questo modo, l'ordine di inserimento viene codificato nella struttura dell'albero stesso.


1
Ma questo aggiunge O (n) spazio per i puntatori, che penso sia ciò che l'interrogante vuole evitare?
Jeremy,

-1

Non penso sia possibile

caso concreto:

       x
    x    x
  x  x  1  x
1  x  

heap min con tutti x> 1

l'heapizing alla fine darà una scelta del genere

       x
    1    1
  x  x  x  x
x  x  

ora quale 1 propagare alla radice?

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.