Jak zaimplementować kolejkę z trzema stosami?

136

Natknąłem się na to pytanie w książce o algorytmach ( Algorithms, 4th Edition autorstwa Roberta Sedgewicka i Kevina Wayne'a).

Kolejka z trzema stosami. Zaimplementuj kolejkę z trzema stosami, tak aby każda operacja kolejki miała stałą (w najgorszym przypadku) liczbę operacji na stosie. Ostrzeżenie: wysoki stopień trudności.

Wiem, jak ustawić kolejkę z 2 stackami, ale nie mogę znaleźć rozwiązania przy 3 stackach. Dowolny pomysł ?

(no i to nie jest praca domowa :))

DuoSRX
źródło
30
Myślę, że to wariant Tower of Hanoi .
Gumbo,
14
@Jason: To pytanie nie jest powtórzeniem, ponieważ prosi o O (1) zamortyzowany czas, podczas gdy to pyta o O (1) najgorszy przypadek dla każdej operacji. Rozwiązanie DuoSRX z dwoma stosami jest już O (1) zamortyzowanym czasem na operację.
interjay
15
Autor z pewnością nie żartował, mówiąc „Uwaga: wysoki stopień trudności”.
BoltClock
9
@Gumbo, niestety, złożoność czasowa Wieży Hanoi nie jest prawie stała!
prusswan,
12
Uwaga: Pytanie w tekście zostało zaktualizowane do tego: Zaimplementuj kolejkę ze stałą liczbą stosów [nie „3”], aby każda kolejka miała stałą (najgorszą) liczbę operacji na stosie. Ostrzeżenie: wysoki stopień trudności. ( algs4.cs.princeton.edu/13stacks - sekcja 1.3.43). Wygląda na to, że prof. Sedgewick podjął pierwotne wyzwanie.
Mark Peters

Odpowiedzi:

44

PODSUMOWANIE

  • Algorytm O (1) jest znany dla 6 stosów
  • Algorytm O (1) jest znany dla 3 stosów, ale wykorzystujący leniwą ocenę, która w praktyce odpowiada posiadaniu dodatkowych wewnętrznych struktur danych, więc nie stanowi rozwiązania
  • Ludzie w pobliżu Sedgewick potwierdzili, że nie są świadomi rozwiązania z trzema stosami w ramach wszystkich ograniczeń pierwotnego pytania

DETALE

Za tym łączem kryją się dwie implementacje: http://www.eecs.usma.edu/webs/people/okasaki/jfp95/index.html

Jednym z nich jest O (1) z trzema stosami, ALE używa leniwego wykonywania, co w praktyce tworzy dodatkowe pośrednie struktury danych (domknięcia).

Kolejnym z nich jest O (1), ale używa SZEŚCIU stosów. Jednak działa bez leniwego wykonania.

AKTUALIZACJA: Artykuł Okasakiego jest tutaj: http://www.eecs.usma.edu/webs/people/okasaki/jfp95.ps i wygląda na to, że w rzeczywistości używa on tylko 2 stosów dla wersji O (1), która ma leniwą ocenę. Problem w tym, że tak naprawdę opiera się na leniwej ocenie. Pytanie brzmi, czy można to przetłumaczyć na algorytm z 3 stosami bez leniwej oceny.

AKTUALIZACJA: Inny powiązany algorytm został opisany w artykule „Stacks versus Deques” autorstwa Holgera Petersena, opublikowanym na 7th Annual Conference on Computing and Combinatorics. Możesz znaleźć artykuł w Książkach Google. Sprawdź strony 225-226. Ale algorytm nie jest w rzeczywistości symulacją w czasie rzeczywistym, jest to symulacja czasu liniowego podwójnej kolejki na trzech stosach.

gusbro: „Jak powiedział @Leonel kilka dni temu, pomyślałem, że byłoby uczciwie sprawdzić u prof. Sedgewicka, czy znał rozwiązanie lub był jakiś błąd. Napisałem więc do niego. Właśnie otrzymałem odpowiedź (choć nie od siebie, ale od kolegi z Princeton), więc lubię się z wami wszystkimi podzielić. Powiedział po prostu, że nie znał algorytmu używającego trzech stosów ORAZ innych narzuconych ograniczeń (takich jak brak leniwej oceny). Znał algorytm wykorzystujący 6 stosów, jak już wiemy, patrząc na odpowiedzi tutaj. Myślę więc, że pytanie jest nadal otwarte, aby znaleźć algorytm (lub udowodnić, że nie można go znaleźć).

antti.huima
źródło
Właśnie przeleciałem nad dokumentami i programami w twoim linku. Ale jeśli dobrze rozumiem, nie używają stosów, używają list jako podstawowego typu. I zwł. w tych listach są zbudowane z nagłówkiem i resztą, więc zasadniczo wygląda podobnie do mojego rozwiązania (i myślę, że nie jest w porządku).
flolo,
1
Cześć, implementacje są w języku funkcjonalnym, w którym listy odpowiadają stosom, o ile wskaźniki nie są udostępniane i nie są; wersja z sześcioma stosami może być naprawdę zaimplementowana przy użyciu sześciu „zwykłych” stosów. Problem z wersją z dwoma / trzema stosami polega na tym, że używa ona ukrytych struktur danych (zamknięć).
Antti Huima,
Czy na pewno rozwiązanie z sześcioma stosami nie udostępnia wskaźników? W programie rotatewygląda na to, że frontlista jest przypisywana do obu oldfronti f, a następnie są one oddzielnie modyfikowane.
interjay
14
Materiał źródłowy w algs4.cs.princeton.edu/13stacks został zmieniony: 43. Zaimplementuj kolejkę ze stałą liczbą [nie "3"] stosów, aby każda operacja kolejki zajmowała stałą (w najgorszym przypadku) liczbę stosów operacje. Ostrzeżenie: wysoki stopień trudności. W tytule wyzwania nadal widnieje napis „Kolejka z trzema stosami” :-).
Mark Peters
3
@AnttiHuima Link z sześcioma stosami jest martwy, czy wiesz, czy gdzieś istnieje?
Quentin Pradet
12

Ok, to jest naprawdę trudne, a jedyne rozwiązanie, jakie mogłem wymyślić, pamięta mnie rozwiązanie Kirksa do testu Kobayashi Maru (w jakiś sposób oszukane): Pomysł jest taki, że używamy stosu stosów (i używamy tego do modelowania listy ). Nazywam operacje en / dequeue i push and pop, a następnie otrzymujemy:

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 to nie kopiowanie zawartości, tylko kopia odniesienia. Służy tylko do łatwego opisania. Możesz również użyć tablicy 3 stosów i uzyskać do nich dostęp poprzez indeks, tam po prostu zmieniasz wartość zmiennej indeksu ). Wszystko jest w O (1) w terminach operacji stosu.

I tak, wiem, że jest to dyskusyjne, ponieważ mamy ukryte więcej niż 3 stosy, ale może daje innym dobre pomysły.

EDYCJA: przykład objaśnienia:

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

Próbowałem tutaj z małą grafiką ASCII, aby pokazać Stack1.

Każdy element jest zawinięty w pojedynczy stos elementów (więc mamy tylko bezpieczny stos stosów).

Widzisz, aby usunąć, najpierw usuwamy pierwszy element (stos zawierający tutaj element 1 i 2). Następnie zdejmij następny element i rozpakuj tam 1. Następnie mówimy, że pierwszy wyskakujący stos jest teraz naszym nowym Stack1. Mówiąc trochę bardziej funkcjonalnie - są to listy implementowane przez stosy 2 elementów, gdzie górny element jest cdr, a pierwszy / pod górny element to samochód . Pozostałe 2 pomagają stackować.

Szczególnie trudne jest wstawianie, ponieważ musisz jakoś zanurkować głęboko w zagnieżdżone stosy, aby dodać kolejny element. Dlatego jest tam Stack2. Stos 2 jest zawsze najbardziej wewnętrznym stosem. Dodawanie jest wtedy po prostu wpychaniem elementu, a następnie umieszczaniem na wierzchu nowego Stack2 (i dlatego nie możemy dotykać Stack2 w naszej operacji usuwania z kolejki).

flolo
źródło
Czy mógłbyś wyjaśnić, jak to działa? Może wyśledzić wciskając „A”, „B”, „C”, „D”, a następnie 4 razy wyskakując?
MAK
1
@Iceman: Żaden Stack2 nie ma racji. Nie są tracone, ponieważ Stack zawsze odnosi się do najbardziej wewnętrznego stosu w Stack1, więc nadal są niejawne w Stack1.
flolo
3
Zgadzam się, że to oszustwo :-). To nie są 3 stosy, to 3 odniesienia do stosów. Ale przyjemna lektura.
Mark Peters
1
To sprytny schemat, ale jeśli dobrze go zrozumiem, będzie wymagał n stosów, gdy do kolejki zostanie wstawionych n elementów. Pytanie dotyczy dokładnie 3 stacków.
MAK
2
@MAK: Wiem, dlatego wprost stwierdził, że jest oszukany (reputację wydałem nawet na nagrodę, bo jestem też ciekawa prawdziwego rozwiązania). Ale przynajmniej można odpowiedzieć na komentarz prusswana: Liczba stosów jest ważna, ponieważ moje rozwiązanie jest rzeczywiście poprawne, kiedy możesz używać tyle, ile chcesz.
flolo
4

Spróbuję udowodnić, że nie da się tego zrobić.


Załóżmy, że istnieje kolejka Q, która jest symulowana przez 3 stosy, A, B i C.

Asercje

  • ASRT0: = Ponadto załóżmy, że Q może symulować operacje {queue, dequeue} w O (1). Oznacza to, że istnieje określona sekwencja wypychania / wyskakiwania stosu dla każdej operacji kolejkowania / usuwania z kolejki, która ma być symulowana.

  • Bez utraty ogólności załóżmy, że operacje kolejki są deterministyczne.

Niech elementy w kolejce do Q będą ponumerowane 1, 2, ... w oparciu o ich kolejność w kolejce, przy czym pierwszy element, który jest umieszczony w kolejce do Q jest zdefiniowany jako 1, drugi jako 2 i tak dalej.

Definiować

  • Q(0) := Stan Q, gdy jest 0 elementów w Q (a więc 0 elementów w A, B i C)
  • Q(1) := Stan Q (i A, B i C) po 1 operacji kolejki na Q(0)
  • Q(n) := Stan Q (i A, B i C) po n operacjach kolejki Q(0)

Definiować

  • |Q(n)| :=liczba elementów w Q(n)(a zatem |Q(n)| = n)
  • A(n) := stan stosu A, gdy stan Q jest Q(n)
  • |A(n)| := liczba elementów w A(n)

I podobne definicje dla stosów B i C.

Trywialne,

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

---

|Q(n)| jest oczywiście nieograniczony n.

Dlatego też, co najmniej jeden |A(n)|, |B(n)|lub |C(n)|jest nieograniczona od n.

WLOG1, załóżmy, że stos A jest nieograniczony, a stosy B i C są ograniczone.

Zdefiniuj * B_u :=górną granicę B * C_u :=górną granicę C *K := B_u + C_u + 1

WLOG2, dla n takiego, że |A(n)| > Kwybierz K elementów z Q(n). Załóżmy, że 1 z tych elementów jest A(n + x)dla wszystkich x >= 0, tj. Element jest zawsze na stosie A, bez względu na to, ile operacji kolejki jest wykonywanych.

  • X := ten element

Wtedy możemy zdefiniować

  • Abv(n) :=liczba przedmiotów w stosie A(n)powyżej X
  • Blo(n) :=liczba elementów w stosie A(n)poniżej X

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

ASRT1 :=Liczba popów wymagana do usunięcia X z kolejki Q(n)wynosi co najmniejAbv(n)

Od ( ASRT0) i ( ASRT1), ASRT2 := Abv(n)muszą być ograniczone.

Jeśli Abv(n)jest nieograniczony, to jeśli do usunięcia z kolejki X jest wymaganych 20 po usunięciu z kolejki Q(n), będzie to wymagało co najmniej Abv(n)/20wyskoków. Który jest nieograniczony. 20 może być dowolną stałą.

W związku z tym,

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

musi być nieograniczony.


WLOG3, możemy wybrać elementy K od dołu A(n), a jeden z nich jest A(n + x)dla wszystkichx >= 0

X(n) := ten element dla dowolnego n

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

Za każdym razem, gdy element jest umieszczany w kolejce do Q(n)...

WLOG4przypuśćmy, że B i C są już wypełnione do górnych granic. Załóżmy, że osiągnięto górną granicę dla elementów powyżej X(n). Następnie nowy element wchodzi do A.

WLOG5załóżmy, że w rezultacie nowy element musi zostać wprowadzony poniżej X(n).

ASRT5 := Liczba wyskakujących okienek wymagana do umieszczenia elementu poniżej X(n) >= Abv(X(n))

Od (ASRT4), Abv(n)jest nieograniczony na n.

Dlatego liczba wyskakujących okienek wymaganych do umieszczenia elementu poniżej X(n)jest nieograniczona.


Jest to sprzeczne z ASRT1tym, że nie można zasymulować O(1)kolejki z 3 stosami.


To znaczy

Co najmniej 1 stos musi być nieograniczony.

W przypadku elementu, który pozostaje w tym stosie, liczba elementów powyżej niego musi być ograniczona lub operacja usunięcia z kolejki w celu usunięcia tego elementu będzie nieograniczona.

Jeśli jednak liczba elementów powyżej jest ograniczona, osiągnie limit. W pewnym momencie nowy element musi wejść pod nią.

Ponieważ zawsze możemy wybrać stary element spośród jednego z kilku najmniejszych elementów tego stosu, nad nim może znajdować się nieograniczona liczba elementów (w oparciu o nieograniczony rozmiar nieograniczonego stosu).

Aby wprowadzić nowy element pod nim, ponieważ nad nim znajduje się nieograniczona liczba elementów, wymagana jest nieograniczona liczba wyskoków, aby umieścić nowy element pod nim.

I stąd sprzeczność.


Istnieje 5 stwierdzeń WLOG (bez utraty ogólności). W pewnym sensie można je intuicyjnie zrozumieć jako prawdziwe (ale biorąc pod uwagę, że mają 5 lat, może to zająć trochę czasu). Formalny dowód, że nie utracono żadnej ogólności, można uzyskać, ale jest on niezwykle długi. Zostały pominięte.

Przyznaję, że takie pominięcie może pozostawić kwestionowane wypowiedzi WLOG. Jeśli masz paranoję programisty dotyczącą błędów, zweryfikuj oświadczenia WLOG, jeśli chcesz.

Trzeci stos również nie ma znaczenia. Liczy się to, że istnieje zestaw ograniczonych stosów i zestaw nieograniczonych stosów. Minimum potrzebne na przykład to 2 stosy. Oczywiście liczba stosów musi być ograniczona.

Wreszcie, jeśli mam rację, że nie ma dowodu, powinien być dostępny łatwiejszy dowód indukcyjny. Prawdopodobnie na podstawie tego, co dzieje się po każdej kolejce (obserwuj, jak wpływa to na najgorszy przypadek usunięcia z kolejki, biorąc pod uwagę zestaw wszystkich elementów w kolejce).

Dingfeng Quek
źródło
2
Myślę, że dowód działa dla tych założeń - ale nie jestem pewien, czy wszystkie stosy muszą być puste, aby kolejka była pusta, lub że suma rozmiarów stosów musi być równa rozmiarowi kolejki.
Mikeb,
3
„WLOG1, załóżmy, że stos A jest nieograniczony, a stosy B i C są ograniczone”. Nie możesz zakładać, że niektóre stosy są ograniczone, ponieważ to uczyniłoby je bezwartościowymi (byłyby takie same jak dodatkowe miejsce O (1)).
interjay
3
Czasami błahe rzeczy nie są takie błahe: | Q | = | A | + | B | + | C | jest tylko słuszne, jeśli założysz, że dla każdego wpisu w Q dodajesz dokładnie jeden do A, B lub C, ale może być tak, że istnieje jakiś algorytm, który zawsze dodaje element dwa razy do dwóch stosów lub nawet do wszystkich trzech. A jak to działa w ten sposób, to WLOG1 już nie trzyma (np. Wyobraź sobie C kopię A (nie żeby to miało sens, ale może jest jakiś algorytm, z inną kolejnością czy cokolwiek ...)
flolo
@flolo i @mikeb: Oboje macie rację. | Q (n) | należy zdefiniować jako | A (n) | + | B (n) | + | C (n) |. A potem | Q (n) | > = n. Następnie dowód działałby z n i zauważ, że tak długo, jak | Q (n) | większy, wniosek ma zastosowanie.
Dingfeng Quek
@interjay: Możesz mieć 3 nieograniczone stosy i żadnych ograniczonych stosów. Wtedy zamiast „B_u + C_u + 1”, dowód może po prostu użyć „1”. Zasadniczo wyrażenie to reprezentuje „sumę górnej granicy w ograniczonych stosach + 1”, więc liczba ograniczonych stosów nie miałaby znaczenia.
Dingfeng Quek
3

Uwaga: ma to być komentarz do funkcjonalnej implementacji kolejek czasu rzeczywistego (w najgorszym przypadku o stałym czasie) z listami połączonymi pojedynczo. Nie mogę dodawać komentarzy ze względu na reputację, ale byłoby miło, gdyby ktoś mógł to zmienić na komentarz dołączony do odpowiedzi przez antti.huima. Z drugiej strony, komentarz jest trochę długi.

@ antti.huima: Połączone listy to nie to samo, co stos.

  • s1 = (1 2 3 4) - połączona lista z 4 węzłami, z których każdy wskazuje ten po prawej stronie i zawiera wartości 1, 2, 3 i 4

  • s2 = popped (s1) --- s2 to teraz (2 3 4)

W tym momencie s2 jest równoważne popped (s1), które zachowuje się jak stos. Jednak s1 jest nadal dostępny w celach informacyjnych!

  • s3 = popped (popped (s1)) --- s3 is (3 4)

Nadal możemy zajrzeć do s1, aby uzyskać 1, podczas gdy we właściwej implementacji stosu element 1 zniknął z s1!

Co to znaczy?

  • s1 to odniesienie do szczytu stosu
  • s2 to odniesienie do drugiego elementu stosu
  • s3 jest odniesieniem do trzeciego ...

Utworzone teraz dodatkowe połączone listy służą jako odniesienie / wskaźnik! Skończona liczba stacków nie może tego zrobić.

Z tego, co widzę w artykułach / kodzie, wszystkie algorytmy wykorzystują tę właściwość list połączonych, aby zachować odniesienia.

Edycja: odnoszę się tylko do algorytmów list połączonych 2 i 3, które wykorzystują tę właściwość list połączonych, ponieważ czytałem je najpierw (wyglądały na prostsze). Nie ma to na celu pokazania, że ​​są lub nie mają zastosowania, tylko po to, aby wyjaśnić, że połączone listy niekoniecznie są identyczne. Przeczytam ten z 6, kiedy będę wolny. @Welbog: Dzięki za korektę.


Lenistwo może również symulować funkcjonalność wskaźnika w podobny sposób.


Korzystanie z listy połączonej rozwiązuje inny problem. Strategia ta może być użyta do implementacji kolejek czasu rzeczywistego w Lisp (lub przynajmniej Lisps, który nalega na budowanie wszystkiego z list połączonych): Zobacz "Operacje kolejki w czasie rzeczywistym w Pure Lisp" (link do linków antti.huima). Jest to również dobry sposób na projektowanie niezmiennych list z czasem działania O (1) i współdzielonymi (niezmiennymi) strukturami.

Dingfeng Quek
źródło
1
Nie mogę mówić do innych algorytmów w odpowiedzi antti, ale rozwiązanie z sześcioma stosami o stałym czasie ( eecs.usma.edu/webs/people/okasaki/jfp95/queue.hm.sml ) nie używa tej właściwości list , ponieważ zaimplementowałem go ponownie w Javie przy użyciu java.util.Stackobiektów. Jedynym miejscem, w którym ta funkcja jest używana, jest optymalizacja, która pozwala na „duplikowanie” niezmiennych stosów w stałym czasie, czego nie mogą zrobić podstawowe stosy Java (ale które można zaimplementować w Javie), ponieważ są to struktury zmienne.
Welbog
Jeśli jest to optymalizacja, która nie zmniejsza złożoności obliczeniowej, nie powinna wpływać na wniosek. Cieszę się, że w końcu mam rozwiązanie, teraz je zweryfikuję: Ale nie lubię czytać SML. Chcesz podzielić się swoim kodem java? (:
Dingfeng Quek
Nie jest to ostateczne rozwiązanie, ponieważ niestety używa sześciu stosów zamiast trzech. Jednak może udałoby się udowodnić, że sześć stacków to minimalne rozwiązanie ...
Welbog,
@Welbog! Czy możesz udostępnić swoją implementację z 6 stosami? :) Fajnie byłoby to zobaczyć :)
Antti Huima
1

Możesz to zrobić w amortyzowanym, stałym czasie z dwoma stosami:

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

Dodawanie jest O(1)i usuwanie jest O(1)wtedy, gdy strona, z której chcesz wziąć, nie jest pusta iw O(n)inny sposób (podziel drugi stos na dwie części).

Sztuczka polega na tym, aby zobaczyć, że O(n)operacja będzie wykonywana tylko za każdym O(n)razem (jeśli podzielisz, np. Na pół). Stąd średni czas operacji wynosi O(1)+O(n)/O(n) = O(1).

Chociaż może się to wydawać problemem, jeśli używasz języka imperatywnego ze stosem opartym na tablicach (najszybszym), i tak będziesz mieć amortyzowany stały czas.

Thomas Ahle
źródło
Co do pierwotnego pytania: dzielenie stosu w rzeczywistości wymaga dodatkowego stosu pośredniego. Być może dlatego twoje zadanie obejmowało trzy stosy.
Thomas Ahle