Come implementare una coda con tre stack?


136

Mi sono imbattuto in questa domanda in un libro sugli algoritmi ( Algorithms, 4th Edition di Robert Sedgewick e Kevin Wayne).

Coda con tre pile. Implementare una coda con tre stack in modo che ogni operazione di coda esegua un numero costante (nel caso peggiore) di operazioni di stack. Attenzione: alto grado di difficoltà.

So come fare una coda con 2 pile ma non riesco a trovare la soluzione con 3 pile. Qualche idea ?

(oh e, questo non è un compito :))


30
Immagino sia una variante della Torre di Hanoi .
Gumbo,

14
@Jason: Questa domanda non è un duplicato, poiché richiede il tempo ammortizzato O (1) mentre questo richiede il caso peggiore O (1) per ogni operazione. La soluzione a due stack di DuoSRX è già O (1) tempo ammortizzato per operazione.
Interjay

15
L'autore sicuramente non stava scherzando quando ha detto "Attenzione: alto grado di difficoltà".
BoltClock

9
@Gumbo purtroppo la complessità temporale della Torre di Hanoi non è quasi mai costante!
prusswan,

12
Nota: la domanda nel testo è stata aggiornata a questa: implementare una coda con un numero costante di stack [non "3"] in modo che ogni operazione di coda richieda un numero costante (nel caso peggiore) di operazioni di stack. Attenzione: alto grado di difficoltà. ( algs4.cs.princeton.edu/13stacks - Sezione 1.3.43). Sembra che il Prof. Sedgewick abbia concesso la sfida originale.
Mark Peters,

Risposte:


44

SOMMARIO

  • L'algoritmo O (1) è noto per 6 stack
  • L'algoritmo O (1) è noto per 3 stack, ma utilizzando una valutazione lazy che in pratica corrisponde ad avere strutture di dati interne extra, quindi non costituisce una soluzione
  • Le persone vicino a Sedgewick hanno confermato di non essere a conoscenza di una soluzione a 3 stack all'interno di tutti i vincoli della domanda originale

DETTAGLI

Ci sono due implementazioni dietro questo link: http://www.eecs.usma.edu/webs/people/okasaki/jfp95/index.html

Uno di questi è O (1) con tre stack MA utilizza un'esecuzione lazy, che in pratica crea strutture di dati intermedie extra (chiusure).

Un altro è O (1) ma utilizza stack SIX. Tuttavia, funziona senza esecuzione pigra.

AGGIORNAMENTO: L'articolo di Okasaki è qui: http://www.eecs.usma.edu/webs/people/okasaki/jfp95.ps e sembra che in realtà usi solo 2 pile per la versione O (1) che ha una valutazione pigra. Il problema è che si basa davvero su una valutazione pigra. La domanda è se può essere tradotto in un algoritmo a 3 stack senza valutazione pigra.

AGGIORNAMENTO: Un altro algoritmo correlato è descritto nel documento "Stacks versus Deques" di Holger Petersen, pubblicato nella 7a Conferenza annuale sull'informatica e la combinatoria. Puoi trovare l'articolo da Google Libri. Controllare le pagine 225-226. Ma l'algoritmo non è in realtà una simulazione in tempo reale, è una simulazione in tempo lineare di una coda doppia su tre pile.

gusbro: "Come ha detto @Leonel qualche giorno fa, ho pensato che sarebbe stato giusto verificare con il Prof. Sedgewick se conosceva una soluzione o ci fosse stato un errore. Quindi gli ho scritto. Ho appena ricevuto una risposta (anche se non da lui stesso, ma da un collega di Princeton), quindi mi piace condividere con tutti voi. Sostanzialmente ha detto che non conosceva algoritmi utilizzando tre pile E gli altri vincoli imposti (come non usare la valutazione pigra). Conosceva un algoritmo usando 6 pile come sappiamo già guardando le risposte qui. Quindi immagino che la domanda sia ancora aperta per trovare un algoritmo (o dimostrare che non è possibile trovarne uno). "


Ho appena sorvolato i documenti e i programmi nel tuo link. Ma se vedo bene non usano stack, usano le liste come tipo base. E esp. in questi elenchi sono costruiti con intestazione e resto, quindi sembra sostanzialmente simile alla mia soluzione (e penso che non sia giusto).
flolo,

1
Salve, le implementazioni sono in un linguaggio funzionale in cui gli elenchi corrispondono a stack purché i puntatori non siano condivisi e non lo siano; la versione a sei stack può essere realmente implementata usando sei stack "semplici". Il problema con la versione a due / tre pile è che utilizza strutture di dati nascoste (chiusure).
Antti Huima,

Sei sicuro che la soluzione a sei stack non condivida i puntatori? In rotate, sembra che l' frontelenco venga assegnato a entrambi oldfronte f, e questi vengono quindi modificati separatamente.
interjay

14
Il materiale di origine in algs4.cs.princeton.edu/13stacks è stato modificato: 43. Implementare una coda con un numero costante di [non "3"] stack in modo che ogni operazione di coda occupi un numero costante (nel caso peggiore) di stack operazioni. Attenzione: alto grado di difficoltà. Il titolo della sfida dice comunque "Coda con tre pile" :-).
Mark Peters,

3
@AnttiHuima Il link a sei pile è morto, sai se esiste da qualche parte?
Quentin Pradet,

12

Ok, questo è davvero difficile, e l'unica soluzione che potrei trovare, mi ricorda la soluzione di Kirks al test di Kobayashi Maru (in qualche modo imbrogliato): L'idea è che usiamo una pila di pile (e la usiamo per modellare un elenco ). Chiamo le operazioni en / dequeue e push and pop, quindi otteniamo:

queue.new() : Stack1 = Stack.new(<Stack>);  
              Stack2 = Stack1;  

enqueue(element): Stack3 = Stack.new(<TypeOf(element)>); 
                  Stack3.push(element); 
                  Stack2.push(Stack3);
                  Stack3 = Stack.new(<Stack>);
                  Stack2.push(Stack3);
                  Stack2 = Stack3;                       

dequeue(): Stack3 = Stack1.pop(); 
           Stack1 = Stack1.pop();
           dequeue() = Stack1.pop()
           Stack1 = Stack3;

isEmtpy(): Stack1.isEmpty();

(StackX = StackY non è una copia dei contenuti, solo una copia di riferimento. È solo per descriverlo facilmente. Potresti anche usare una matrice di 3 stack e accedervi tramite indice, lì cambierai semplicemente il valore della variabile indice ). Tutto è in O (1) in termini di stack-operation.

E sì, lo so che è discutibile, perché abbiamo implicito più di 3 pile, ma forse dà ad altri di voi buone idee.

EDIT: esempio di spiegazione:

 | | | |3| | | |
 | | | |_| | | |
 | | |_____| | |
 | |         | |
 | |   |2|   | |
 | |   |_|   | |
 | |_________| |
 |             |
 |     |1|     |
 |     |_|     |
 |_____________|

Ho provato qui con un po 'di arte ASCII per mostrare Stack1.

Ogni elemento è racchiuso in una pila di elementi singoli (quindi abbiamo solo una pila di pile tipesa).

Si vede per rimuovere prima pop il primo elemento (lo stack contenente qui gli elementi 1 e 2). Quindi pop l'elemento successivo e scartare lì 1. Successivamente diciamo che il primo stack poped è ora il nostro nuovo Stack1. Per dire un po 'più funzionale - queste sono liste implementate da pile di 2 elementi in cui l'elemento superiore è cdr e il primo / sotto elemento superiore è auto . Gli altri 2 stanno aiutando le pile.

Esp è complicato l'inserimento, poiché in qualche modo devi immergerti in profondità nelle pile nidificate per aggiungere un altro elemento. Ecco perché Stack2 è lì. Stack2 è sempre lo stack più interno. L'aggiunta è quindi solo di inserire un elemento e quindi di inserire un nuovo Stack2 (ed è per questo che non ci è permesso di toccare Stack2 nella nostra operazione di dequeue).


Ti andrebbe di spiegare come funziona? Forse rintracciare spingendo 'A', 'B', 'C', 'D' e poi saltar fuori 4 volte?
MAK,

1
@Iceman: No Stack2 ha ragione. Non vengono persi, poiché Stack fa sempre riferimento allo stack più interno in Stack1, quindi sono ancora impliciti in Stack1.
flolo,

3
Sono d'accordo che è barare :-). Non sono 3 pile, sono 3 riferimenti di pila. Ma una lettura piacevole.
Mark Peters,

1
È uno schema intelligente, ma se lo capisco correttamente, finirà per avere bisogno di n stack quando ci sono n elementi inseriti nella coda. La domanda richiede esattamente 3 pile.
MAK,

2
@MAK: Lo so, ecco perché ha esplicitamente dichiarato che è stato ingannato (ho anche speso reputazione in una taglia perché sono anche curioso della vera soluzione). Ma almeno si può rispondere al commento di Prusswan: il numero di pile è importante, perché la mia soluzione è davvero valida, quando puoi usare quanto vuoi.
flolo,

4

Proverò una prova per dimostrare che non può essere fatto.


Supponiamo che ci sia una coda Q che è simulata da 3 pile, A, B e C.

asserzioni

  • ASRT0: = Inoltre, supponi che Q possa simulare le operazioni {coda, dequeue} in O (1). Ciò significa che esiste una sequenza specifica di stack push / pop per ogni operazione di coda / dequeue da simulare.

  • Senza perdita di generalità, supponiamo che le operazioni in coda siano deterministiche.

Consenti agli elementi in coda in Q di essere numerati 1, 2, ..., in base al loro ordine di coda, con il primo elemento in coda in Q definito come 1, il secondo in 2 e così via.

Definire

  • Q(0) := Lo stato di Q quando ci sono 0 elementi in Q (e quindi 0 elementi in A, B e C)
  • Q(1) := Lo stato di Q (e A, B e C) dopo 1 operazione di coda attivata Q(0)
  • Q(n) := Lo stato di Q (e A, B e C) dopo n operazioni in coda attivate Q(0)

Definire

  • |Q(n)| :=il numero di elementi in Q(n)(quindi |Q(n)| = n)
  • A(n) := lo stato dello stack A quando lo stato di Q è Q(n)
  • |A(n)| := il numero di elementi in A(n)

E definizioni simili per le pile B e C.

Banalmente,

|Q(n)| = |A(n)| + |B(n)| + |C(n)|

---

|Q(n)| è ovviamente illimitato su n.

Pertanto, almeno uno dei |A(n)|, |B(n)|o |C(n)|è illimitato su n.

WLOG1, supponiamo che lo stack A sia illimitato e che gli stack B e C siano limitati.

Definisci * B_u :=un limite superiore di B * C_u :=un limite superiore di C *K := B_u + C_u + 1

WLOG2, per una n tale che |A(n)| > K, selezionare K elementi da Q(n). Supponiamo che 1 di questi elementi sia A(n + x), per tutti x >= 0, cioè che l'elemento sia sempre nello stack A, indipendentemente da quante operazioni di coda sono state eseguite.

  • X := quell'elemento

Quindi possiamo definire

  • Abv(n) :=il numero di oggetti in pila A(n)superiore a X
  • Blo(n) :=il numero di elementi nello stack A(n)inferiore a X

    | A (n) | = Abv (n) + Blo (n)

ASRT1 :=Il numero di pop richiesti per dequeue X Q(n)è almenoAbv(n)

Da ( ASRT0) e ( ASRT1), ASRT2 := Abv(n)deve essere limitato.

Se Abv(n)è illimitato, quindi se sono necessari 20 dequeues per dequeue X Q(n), richiederà almeno Abv(n)/20pop. Che è illimitato. 20 può essere qualsiasi costante.

Perciò,

ASRT3 := Blo(n) = |A(n)| - Abv(n)

deve essere illimitato.


WLOG3, possiamo selezionare gli elementi K dalla parte inferiore di A(n)e uno di questi è disponibile A(n + x)per tuttix >= 0

X(n) := quell'elemento, per ogni dato n

ASRT4 := Abv(n) >= |A(n)| - K

Ogni volta che un elemento viene messo in coda in Q(n)...

WLOG4, supponiamo che B e C siano già riempiti fino ai limiti superiori. Supponiamo che X(n)sia stato raggiunto il limite superiore per gli elementi sopra . Quindi, un nuovo elemento entra in A.

WLOG5, supponiamo che, di conseguenza, il nuovo elemento debba entrare di seguito X(n).

ASRT5 := Il numero di pop richiesti per inserire un elemento di seguito X(n) >= Abv(X(n))

Da (ASRT4), Abv(n)non ha limiti il ​​n.

Pertanto, il numero di pop richiesti per inserire un elemento di seguito X(n)è illimitato.


Ciò contraddice ASRT1, pertanto, non è possibile simulare una O(1)coda con 3 pile.


ie

Almeno 1 stack deve essere illimitato.

Per un elemento che rimane in quella pila, il numero di elementi sopra di esso deve essere limitato oppure l'operazione di dequeue per rimuovere tale elemento non sarà limitata.

Tuttavia, se il numero di elementi sopra di esso è limitato, raggiungerà un limite. Ad un certo punto, un nuovo elemento deve entrare sotto di esso.

Dato che possiamo sempre scegliere il vecchio elemento tra uno dei pochi elementi più bassi di quello stack, può esserci un numero illimitato di elementi sopra di esso (basato sulla dimensione illimitata dello stack illimitato).

Per inserire un nuovo elemento al di sotto di esso, poiché sopra di esso è presente un numero illimitato di elementi, è necessario un numero illimitato di pop per posizionare il nuovo elemento al di sotto di esso.

E quindi la contraddizione.


Ci sono 5 dichiarazioni WLOG (senza perdita di generalità). In un certo senso, possono essere compresi intuitivamente come veri (ma dato che sono 5, potrebbe richiedere del tempo). La prova formale che non si perde alcuna generalità può essere derivata, ma è estremamente lunga. Sono stati omessi.

Ammetto che tale omissione potrebbe lasciare in discussione le dichiarazioni WLOG. Con la paranoia di un programmatore per i bug, ti preghiamo di verificare le istruzioni WLOG, se lo desideri.

Anche il terzo stack è irrilevante. Ciò che conta è che esiste una serie di pile illimitate e una serie di pile illimitate. Il minimo necessario per un esempio è di 2 pile. Il numero di pile deve essere, ovviamente, finito.

Infine, se ho ragione sul fatto che non ci sono prove, allora dovrebbe esserci una prova induttiva più semplice disponibile. Probabilmente in base a ciò che accade dopo ogni coda (tenere traccia di come influenza il peggior caso di dequeue dato l'insieme di tutti gli elementi nella coda).


2
Penso che la dimostrazione funzioni per questi presupposti, ma non sono sicuro che tutte le pile debbano essere vuote affinché la coda sia vuota o che la somma delle dimensioni delle pile debba essere uguale alla dimensione della coda.
Mikeb,

3
"WLOG1, supponiamo che lo stack A sia illimitato e che gli stack B e C siano limitati." Non puoi presumere che alcune pile siano limitate, poiché ciò le renderebbe inutili (sarebbero uguali a O (1) spazio di archiviazione aggiuntivo).
Interjay

3
A volte le cose banali non sono così banali: | Q | = | A | + | B | + | C | è giusto, se si assume che per ogni voce in Q si aggiunge esattamente uno in A, B o C, ma può darsi che il loro sia un algoritmo, che aggiunge sempre un elemento due volte a due pile o anche a tutti e tre. E se funziona in questo modo, WLOG1 non regge più (es. Immagina C una copia di A (non che abbia senso, ma forse c'è un algoritmo, con un ordine diverso o altro ...)
flolo

@flolo e @mikeb: entrambi avete ragione. | Q (n) | dovrebbe essere definito come | A (n) | + | B (n) | + | C (n) |. E poi | Q (n) | > = n. Successivamente, la prova funzionerebbe con n e nota che fino a quando | Q (n) | più grande, si applica la conclusione.
Dingfeng Quek,

@interjay: puoi avere 3 pile illimitate e nessuna pila limitata. Quindi invece di "B_u + C_u + 1", la prova può semplicemente usare "1". Fondamentalmente, l'espressione rappresenta "somma del limite superiore in pile limitate + 1", quindi il numero di pile limitate non avrebbe importanza.
Dingfeng Quek,

3

Nota: questo dovrebbe essere un commento all'implementazione funzionale delle code in tempo reale (il caso peggiore in tempo costante) con elenchi collegati singolarmente. Non posso aggiungere commenti a causa della reputazione, ma sarebbe bello se qualcuno potesse cambiarlo in un commento aggiunto alla risposta di antti.huima. Poi di nuovo, è un po 'lungo per un commento.

@ antti.huima: gli elenchi collegati non sono gli stessi di uno stack.

  • s1 = (1 2 3 4) --- un elenco collegato con 4 nodi, ciascuno che punta a quello a destra e che contiene i valori 1, 2, 3 e 4

  • s2 = saltato (s1) --- s2 ora è (2 3 4)

A questo punto, s2 è equivalente a popped (s1), che si comporta come uno stack. Tuttavia, s1 è ancora disponibile per riferimento!

  • s3 = saltato (saltato (s1)) --- s3 è (3 4)

Possiamo ancora sbirciare in s1 per ottenere 1, mentre in una corretta implementazione dello stack, l'elemento 1 è passato da s1!

Cosa significa questo?

  • s1 è il riferimento all'inizio della pila
  • s2 è il riferimento al secondo elemento dello stack
  • s3 è il riferimento al terzo ...

Le ulteriori liste collegate create ora servono ciascuna come riferimento / puntatore! Un numero finito di pile non può farlo.

Da quello che vedo negli articoli / nel codice, tutti gli algoritmi fanno uso di questa proprietà degli elenchi collegati per conservare i riferimenti.

Modifica: mi riferisco solo agli algoritmi delle liste collegate 2 e 3 che fanno uso di questa proprietà delle liste collegate, come le ho lette per prime (sembravano più semplici). Ciò non significa che siano o meno applicabili, ma solo per spiegare che le liste collegate non sono necessariamente identiche. Leggerò quello con 6 quando sono libero. @Welbog: grazie per la correzione.


La pigrizia può anche simulare la funzionalità puntatore in modi simili.


L'uso dell'elenco collegato risolve un problema diverso. Questa strategia può essere utilizzata per implementare code in tempo reale in Lisp (o almeno Lisps che insistono sulla costruzione di tutto da liste collegate): fare riferimento a "Operazioni in coda in tempo reale in Lisp puro" (collegato tramite i collegamenti di antti.huima). È anche un bel modo di progettare elenchi immutabili con tempo di operazione O (1) e strutture condivise (immutabili).


1
Non posso parlare con gli altri algoritmi nella risposta di antti, ma la soluzione a sei tempi costanti ( eecs.usma.edu/webs/people/okasaki/jfp95/queue.hm.sml ) non usa questa proprietà degli elenchi , poiché l'ho implementato nuovamente in Java usando java.util.Stackoggetti. L'unico posto in cui viene utilizzata questa funzione è un'ottimizzazione che consente di "duplicare" stack immutabili in tempo costante, che gli stack Java di base non possono fare (ma che possono essere implementati in Java) poiché sono strutture mutabili.
Welbog,

Se si tratta di un'ottimizzazione che non riduce la complessità computazionale, non dovrebbe influire sulla conclusione. Sono contento di avere finalmente una soluzione, ora per verificarla: ma non mi piace leggere SML. Ti dispiace condividere il tuo codice Java? (:
Dingfeng Quek

Purtroppo non è una soluzione definitiva in quanto utilizza sei stack anziché tre. Tuttavia, potrebbe essere possibile dimostrare che sei pile sono una soluzione minima ...
Welbog,

@Welbog! Puoi condividere la tua implementazione a 6 stack? :) Sarebbe bello vederlo :)
Antti Huima

1

Puoi farlo a tempo costante ammortizzato con due pile:

------------- --------------
            | |
------------- --------------

L'aggiunta O(1)e la rimozione è O(1)se il lato da cui vuoi prendere non è vuoto e O(n)altrimenti (dividi l'altro stack in due).

Il trucco è vedere che l' O(n)operazione verrà eseguita solo ogni O(n)volta (se si divide, ad esempio a metà). Quindi, il tempo medio per un'operazione è O(1)+O(n)/O(n) = O(1).

Anche se questo può sembrare un problema, se stai usando un linguaggio imperativo con uno stack basato su array (il più veloce), avrai comunque solo un tempo costante ammortizzato.


Per quanto riguarda la domanda originale: dividere una pila in realtà richiede una pila intermedia aggiuntiva. Questo potrebbe essere il motivo per cui l'attività includeva tre pile.
Thomas Ahle,
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.