Algorytmy i struktury danych/Wstęp: poprawność i złożoność algorytmu: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Rytter (dyskusja | edycje)
Rytter (dyskusja | edycje)
Nie podano opisu zmian
Linia 5: Linia 5:




Podstawowym elementem przy rozwiązywaniu zadanego problemu na komputerze jest dobór algorytmu i struktury danch. Najważnejszymi aspektami algorytmu są jego "poprawność" i złożoność. Będziemy zasadniczo rozpatrywać tylko złożoność czasową i pamięciową.
Podstawowym elementem przy rozwiązywaniu zadanego problemu na komputerze jest dobór  
algorytmu i struktury danych. Najważniejszymi aspektami algorytmu są jego "poprawność" i  
złożoność.  
Będziemy zasadniczo rozpatrywać tylko złożoność czasową i pamięciową.


W przypadku złoźoności czasowej z reguły wyróżnimy pewną operację dominującą i czas będziemy traktować jako liczbę wykonanych operacji dominujących. W ten sposób nasza analiza będzie zależna jedynie od algorytmu a nie od implementacji i sprzętu. W przypadku sortowania przeważnie operacją dominującą jest porównanie dwóch elementów (mniejsze, równe, mniejsze), a w przypadku przeglądania drzewa jedno przejście w drzewie między  wierzchołkami. W przypaku algorytmów tekstowych operacją dominującą jest porównanie dwóch symboli. Z reguły będziemy przyjmować, że każda operacja arytmetyczna na małych liczbach daje się zrobić w jednym kroku. Przez ''małe'' rozumiemy liczby mające <math>O(\log n)</math> bitów.  Z reguły określamy pewien parametr <math>n</math>, będący rozmiarem problemu wejściowego i określamy złożoność jako funkcję <math>T(n)</math>, której arguementem jest rozmiar problemu.
W przypadku złożoności czasowej z reguły wyróżnimy pewną operację dominującą i czas będziemy  
traktować jako liczbę wykonanych operacji dominujących.  
W ten sposób nasza analiza będzie zależna jedynie od algorytmu a nie od implementacji i sprzętu. W  
przypadku sortowania przeważnie operacją dominującą jest porównanie dwóch elementów (mniejsze,  
równe, mniejsze), a w przypadku przeglądania drzewa jedno przejście w drzewie między   
wierzchołkami. W przypadku algorytmów tekstowych operacją dominującą jest porównanie dwóch  
symboli. Z reguły będziemy przyjmować, że każda operacja arytmetyczna na małych liczbach daje się  
zrobić w jednym kroku. Przez małe rozumiemy liczby mające <math>O(\log n)</math> bitów.  Z  
reguły określamy pewien parametr <math>n</math>, będący rozmiarem problemu wejściowego i  
określamy złożoność jako funkcję <math>T(n)</math>, której argumentem jest rozmiar problemu.


Przeważnie rozważamy złożoność pesymistyczną - maksymalną złożoność dla danych tego samego rozmiaru <math>n</math>. W praktyce ważniejsą może się okazać złożoność średnią, lub oczekiwaną, w tym przypadku <math>T(n)</math> jest średnią (oczekiwaną) wartością złożoności dla wszystkich problemów rozmiaru <math>n</math>. Tego typu złożoność zależy  istotnie od tego, jaka się od tym kryje przetsrzeń probablistyczna problemów. Z reguły zakładamy, że wszystkie problemy wejściowe tego samego rozmiaru mogą się pojawić z tym samym prawodpodobieństwem. Jednakże jest to często mało realistyczne założenie. Przestrzeń probabilistyczna danych wejściowych może być bardzo skomplikowana, prowadzić to może do bardzo trudnych (i wykraczających poza ten kurs) analiz.
Przeważnie rozważamy złożoność pesymistyczną - maksymalną złożoność dla danych tego samego  
rozmiaru <math>n</math>.  
W praktyce ważniejszą może się okazać złożoność średnią, lub oczekiwaną, w tym przypadku  
<math>T(n)</math> jest średnią (oczekiwaną) wartością złożoności dla wszystkich problemów  
rozmiaru <math>n</math>. Tego typu złożoność zależy  istotnie od tego, jaka się pod tym kryje  
przestrzeń probabilistyczna problemów wejsściowych. Z reguły zakładamy, że wszystkie problemy  
wejściowe tego samego rozmiaru mogą się pojawić z tym samym prawdopodobieństwem. Jednakże  
jest to często mało realistyczne założenie. Przestrzeń probabilistyczna danych wejściowych może być  
bardzo skomplikowana, prowadzić to może do bardzo trudnych (i wykraczających poza ten kurs)  
analiz.


{{
{{
przyklad|||Przypuśćmy, że chcemy znaleźć pierwszą jedynkę w tablicy zerojedynkowej i nasz algorytm przegląda tablicę od strony lewej sprawdzając kolejne elementy. Niech operacją dominującą będzie sprawdzenie jednego elementu. Jeśli nie ma jedynki, to wykonamy <math>n</math> sprawdzeń, jest to maksymalna liczba, zatem złożoność pesymistyczna wynosi <math>T(n)=n</math>. Jeśli każdy ciąg binarny jest dany z tym samym prawdopodobieństwem to łatwo policzyć, że złożoność średnia jest ograniczona przez stałą.
Przykład|||Przypuśćmy, że chcemy znaleźć pierwszą jedynkę w tablicy zerojedynkowej i nasz  
algorytm przegląda tablicę od strony lewej sprawdzając kolejne elementy.
Niech operacją dominującą będzie sprawdzenie jednego elementu. Jeśli nie ma jedynki, to wykonamy  
<math>n</math> sprawdzeń, jest to maksymalna liczba, zatem złożoność pesymistyczna wynosi  
<math>T(n)=n</math>. Jeśli każdy ciąg binarny jest dany z tym samym prawdopodobieństwem to  
łatwo policzyć, że złożoność średnia jest ograniczona przez stałą.
}}
}}


Do wyrażania złożoności stosujemy opis aymptotycznego wzrostu funkcji:
Do wyrażania złożoności stosujemy opis asymptotycznego wzrostu funkcji:
<math>f(n)\ =\ O(g(n))</math>  oznacza że <math>f(n) \le c\cdot g(n)</math> dla pewnej stałej <math>n</math>.  
<math>f(n)\ =\ O(g(n))</math>  oznacza że <math>f(n) \le c\cdot g(n)</math> dla pewnej stałej  
Gdy <math>g(n)=n</math> to mówimy, że <math>f(n)</math> jest liniowa, oraz dla <math>g(n)=n^2</math> mówimy, że złożoność <math>f(n)</math> jest
<math>n</math>.  
kwadratowa. Jeśli <math>g(n)</math> jest wielomianem to wtedy mówimy o złożoności wielomianowej.  
Gdy <math>g(n)=n</math> to mówimy, że <math>f(n)</math> jest liniowa, oraz dla  
<math>g(n)=n^2</math> mówimy, że złożoność <math>f(n)</math> jest
kwadratowa. Jeśli <math>g(n)</math> jest wielomianem to wtedy mówimy o złożoności  
wielomianowej.  




Linia 30: Linia 59:




<math>f(n)=\Theta(g(n)) \Leftrightarrow f(n)=O(g(n)) </math> & <math>  g(n)=O(f(n))\,</math> <br>
<math>f(n)=\Theta(g(n)) \Leftrightarrow f(n)=O(g(n)) </math> & <math>  g(n)=O(f(n))\,</math>  
<br>


<math>f(n)=\Omega(g(n)) \,</math>, gdy  dla nieskończenie wielu <math>n</math> i pewnej stałej <math>c>0</math> zachodzi  <math>f(n) \ge c\cdot g(n)</math>
<math>f(n)=\Omega(g(n)) \,</math>, gdy  dla nieskończenie wielu <math>n</math> i pewnej stałej  
<math>c>0</math> zachodzi  <math>f(n) \ge c\cdot g(n)</math>


{{przyklad|||
{{przyklad|||
<math>\frac{1}{100}\cdot n^2- 2n = \Theta(n^2 )\cdot n^5+2^n = \Theta(2^n), n!=\Omega(10^n)</math>,<br>
<math>\frac{1}{100}\cdot n^2- 2n = \Theta(n^2 )\cdot n^5+2^n = \Theta(2^n),  
Jeśli <math>f(n)=(1+(-1)^n)\cdot n</math>, to <math>f(n)=\Omega(n),\ f(n)=O(n)</math> ale nie zachodzi <math>f(n)=\Theta(n)\,</math>.}}
n!=\Omega(10^n)</math>,<br>
Jeśli <math>f(n)=(1+(-1)^n)\cdot n</math>, to <math>f(n)=\Omega(n),\ f(n)=O(n)</math> ale nie  
zachodzi <math>f(n)=\Theta(n)\,</math>.}}








'''Konwencje językowe.''' Jaki jest najlepszy język do opisu algorytmu ? Jest to przykład problemu nierozstrzygalnego. Niewątpliwie język ojczysty jest najlepszym językiem potocznym , a ulubiony język programowania jest  najlepszym językiem do implementacji algorytmu. Język, którym będziemy opisywaćalgorytmy jest gdzieś pomiędzy tymi językami, język potoczny nie wystarcza a konkretny język programowania może spowodować to, że "prosty" algorytm się zrobi nieczytelny.  Będziemy używać, o ile się da, nieformalnych konstrukcji programistycznych, a w przypadku bardzo prostych algorytm będziemy się starali pisać algorytm w języku C-podobnym.
'''Konwencje językowe.''' Jaki jest najlepszy język do opisu algorytmu ? Jest to przykład problemu  
nierozstrzygalnego. Niewątpliwie język ojczysty jest najlepszym  
językiem potocznym , a ulubiony język programowania jest  najlepszym językiem do implementacji  
algorytmu. Język, którym będziemy opisywać algorytmy jest gdzieś pomiędzy tymi językami, język  
potoczny nie wystarcza a konkretny język programowania może spowodować to, że "prosty" algorytm  
się zrobi nieczytelny.  Będziemy używać, o ile się da, nieformalnych konstrukcji programistycznych, a  
w przypadku bardzo prostych algorytm będziemy się starali pisać algorytm w języku C-podobnym.


== Poprawność algorytmu: niezmienniki, własność stopu ==
== Poprawność algorytmu: niezmienniki, własność stopu ==
Przez poprawność algorytmu rozumiemy to, że daje on takie odpowiedzi jakich oczekujemy. Oczywiście algorytm musi być poprawny aby miało sens rozpatrywanie jego złożoność.
Przez poprawność algorytmu rozumiemy to, że daje on takie odpowiedzi jakich oczekujemy.  
Oczywiście algorytm musi być poprawny aby miało sens rozpatrywanie jego złożoność.


=== Pojęcie niezmiennika ===
=== Pojęcie niezmiennika ===
Poprawność algorytmu sprowadza się do spełniania określonych niezmienników na różnych etapach tego algorytmu.
Poprawność algorytmu sprowadza się do spełniania określonych niezmienników na różnych etapach  
tego algorytmu.
Rozważmy kilka przykładów pozwalających zrozumieć znaczenie niezmiennika.  
Rozważmy kilka przykładów pozwalających zrozumieć znaczenie niezmiennika.  


Załóżmy, że mamy zbiór <math>S</math> składający się z <math>n</math> przedmiotów, niektóre z przedmiotów są czarne a niektóre białe. Zakładamy, że liczba czarnych przedmiotów jest nieparzysta.
Załóżmy, że mamy zbiór <math>S</math> składający się z <math>n</math> przedmiotów, niektóre z  
przedmiotów są czarne a niektóre białe. Zakładamy, że liczba czarnych przedmiotów jest nieparzysta.
 


{{algorytm|||
{{algorytm|||
Linia 56: Linia 99:
  2    pobierz dwa przedmioty z S;
  2    pobierz dwa przedmioty z S;
  3    '''if''' przedmioty są różnego koloru to wstaw z powrotem czarny
  3    '''if''' przedmioty są różnego koloru to wstaw z powrotem czarny
  4   '''else''' usuń oba przemioty;
  4 '''return''' kolor ostatniego przedmiotu w S;
5 '''return''' kolor ostatniego przedmiotu w S;
  }}
  }}


Załóżmy, że mamy 10000001 czarnych przedmiotów i  1000000001 białych, jaki jest ostatni przedmiot ? Rozpatrzmy niezmiennik: parzystość liczby czarnych przedmiotów.
Załóżmy, że mamy 10000001 czarnych przedmiotów i  1000000001 białych, jaki jest ostatni  
przedmiot ? Rozpatrzmy niezmiennik: parzystość liczby czarnych przedmiotów.


Ponieważ na początku mamy nieparzystą liczbę czarnych przedmiotów, zatem na wyjściu mamy kolor czarny.
Ponieważ na początku mamy nieparzystą liczbę czarnych przedmiotów, zatem na wyjściu mamy kolor  
czarny.




Linia 70: Linia 114:
  1 '''while'''  <math>|S|> 1</math>
  1 '''while'''  <math>|S|> 1</math>
  2    pobierz dwa przedmioty z S;
  2    pobierz dwa przedmioty z S;
  3    '''if''' co najmniej jeden jest czarny to wstaw z powrotem jeden biały;
  3    '''if''' co najmniej jeden jest biały to wstaw z powrotem jeden biały;
  4   '''else''' usuń oba przedmioty;
  4 '''return''' kolor ostatniego przedmiotu w S.
5 '''return''' kolor ostatniego przedmiotu w S.
}}
}}


Załóżmy, że mamy  10000001 czarnych przedmiotów i 1000000001 białych, jaki jest ostatni przedmiot? Tym razem rozważmy niezmiennik: znak liczby białych przedmiotów. (Znak liczby jest równy 0 jeśli jest ona równa zeru, 1 jeśli jest większa od zera.) Zatem ostatnim przedmiotem jest przedmiot biały.
Załóżmy, że mamy  10000001 czarnych przedmiotów i 1000000001 białych, jaki jest ostatni  
przedmiot? Tym razem rozważmy niezmiennik: znak liczby białych przedmiotów.  
(Znak liczby jest równy 0 jeśli jest ona równa zeru, 1 jeśli jest większa od zera.) Zatem ostatnim  
przedmiotem jest przedmiot biały.


=== Własność stopu===  
=== Własność stopu===  
Podstawowym elementem poprawności algorytmu jest to, że ma on własność stopu: dla poprawnych
Podstawowym elementem poprawności algorytmu jest to, że ma on własność stopu: dla poprawnych
danych wejściowych da odpowiedź po skończonym czasie. Na przykładzie dwóch prostych algorytmów pokażemy,
danych wejściowych da odpowiedź po skończonym czasie. Na przykładzie dwóch prostych  
algorytmów pokażemy,
że sprawdzanie własności stopu może nie być czynnością trywialną.
że sprawdzanie własności stopu może nie być czynnością trywialną.


Linia 97: Linia 144:
}}
}}
{{algorytm|X|algorytm_X|
{{algorytm|X|algorytm_X|
X
  '''while''' (n<>1)
  '''while''' (n<>1)
     '''if''' n parzyste <math>n :=</math> n div 2; '''else''' <math>n := </math>3*n+1;
     '''if''' n parzyste <math>n :=</math> n div 2; '''else''' <math>n := </math>3*n+1;
      
      
}}
}}
Pierwsze dwa algorytmy mają własność stopu, w pierwszym łatwo to sprawdzić gdyż dla <math>n>100</math> następna wartość jest istotnie mniejsza.  
Pierwsze dwa algorytmy mają własność stopu, w pierwszym łatwo to sprawdzić gdyż dla  
<math>n>100</math> następna wartość jest istotnie mniejsza.  


Jak najkrotszy koncepcyjny dowod wlasnosci stopu drugiego algorytmu pozostawiamy jako cwiczenie (nie chodzi nam tu o brutalny dowod polegajacy na sprawdzeniu wszystkich 9990 przypadkow przez komputer).  
Pozostawiamy jako ćwiczenie  jak najkrótszy koncepcyjnie dowód własności stopu obu algorytmów
(nie chodzi nam tu o brutalny dowód polegający na sprawdzeniu wszystkich przypadków przez  
komputer).  


Algorytm X jest dosyć zagadkowy, nie wiadomo, czy dla dowolnego naturalnego dodatniego <math>n</math> ma on własność stopu.
Algorytm X jest dosyć zagadkowy, nie wiadomo, czy dla dowolnego naturalnego dodatniego  
<math>n</math> ma on własność stopu.


== Złożoność algorytmów: analiza siedmiu prostych algorytmów ==
== Złożoność algorytmów: analiza siedmiu prostych algorytmów ==


Na przykładzie siedmiu prostych algorytmów pokażemy, jak się analizuje i osiąga złożoność liniową. Po dokładniejsze uzasadnienia i analizy odsyłamy do ćwiczeń.  Naiwne wersje tych algorytmów działają w czasie kwadratowym (Wybraliśmy ''"siedem"'' losowo, na przykład jest ''siedem'' dni w tygodniu no i jeszcze był taki film ''Siedmiu wspaniałych'')
Na przykładzie siedmiu prostych algorytmów pokażemy, jak się analizuje i osiąga złożoność liniową.  
Po dokładniejsze uzasadnienia i analizy odsyłamy do ćwiczeń.  Naiwne wersje tych algorytmów  
działają w czasie kwadratowym (Wybraliśmy ''"siedem"'' losowo, na przykład jest ''siedem'' dni w  
tygodniu ,był taki film ''Siedmiu wspaniałych'' itp.)


==== Przywódca ciągu ====  
==== Przywódca ciągu ====  
Przywódcą ciągu jest element, który występuje w ciągu więcej razy niż połowa długości tego ciągu. Naszym problemem jest policzenie przywódcu ciągu danego tablicą <math>A[1..n]</math>. Dla uproszczenia przyjmijmy, że w tym ciągu jest przywódca. Łatwo można modyfikować algorytm by sprawdzał istnienie przywódcy.
Przywódcą ciągu jest element, który występuje w ciągu więcej razy niż połowa długości tego ciągu.  
Naszym problemem jest policzenie przywódcy ciągu danego tablicą <math>A[1..n]</math>. Dla  
uproszczenia przyjmijmy, że w tym ciągu jest przywódca. Łatwo można modyfikować algorytm by  
sprawdzał istnienie przywódcy.


{{algorytm|Liczenie-Przywódcy|algorytm_liczenie_przywodcy|
{{algorytm|Liczenie-Przywódcy|algorytm_liczenie_przywodcy|
   Liczenie Przywódcy
   <math> ile := 0 </math>;
    ile <math>:=</math> 0;
     '''for''' i <math>:=</math> 1 to n do
     '''for''' i <math>:=</math> 1 to n do
       '''if''' <math>(ile = 0)  \{ile := ile+1; j := i\}</math>;
       '''if''' <math>(ile = 0)  \{ile := ile+1; j := i\}</math>;
       '''else if''' <math>( A[i]=A[j]) </math> ile <math>:=</math> ile+1; '''else''' ile <math>:=</math> ile-1;<br>
       '''else if''' <math>( A[i]=A[j]) </math> ile <math>:=</math> ile+1; '''else''' ile <math>:=</math>  
ile-1;<br>
     '''return''' <math>A[j]</math>;
     '''return''' <math>A[j]</math>;
}}
}}
Przyspieszenie wynika z następującej własności problemu: jeśli mamy dwa różne elementy w tablicy to możemy je usunąć i przywódca pozostanie taki sam.
Przyspieszenie wynika z następującej własności problemu: jeśli mamy dwa różne elementy w tablicy  
to możemy je usunąć i przywódca pozostanie taki sam.
 
Algorytm można zmodyfikować tak aby w czasie liniowym liczył on słabego przywódcę: element,
który występuje w tablicy więcej niż n/5 razy.
W tym przypadku potrzebne są cztery liczniki  odpowiadające czterem kandydatom na słabego
przywódcę.
Algorytm liczy  element który jest kandydatem na słabego przywódcę (jeśli istnieje taki przywódca to
na pewno jest nim wyliczony element).  


Algorytm mozna zmodyfikowac tak aby w czasie liniowym liczyl on slabego przywodce: element, ktory wystepuje w tablicy wiecej niz na przykłas n/5 razy. W tym przypadku potrzebne sa cztery liczniki  odpowiadajace czterem kandydatom na slabego przywodce.
Algorytm liczy  element który jest kandydatem na słabego przywódce (jeśli istnieje taki przywódca to na pewno jest nim wyliczony element). Kolorem żółtym na końcu zaznacza się licznik słabego przywódcy, jego nazwa jest w niebieskim kwadraciku.


<center>
<center>
<flash>file=przywodca_slaby.swf|width=520|height=270</flash></center>
<flash>file=przywodca_slaby.swf|width=520|height=270</flash></center>


Jesli istnieje slaby przywodca i mamy pięć roznych elementów to mozna je usunac bez zmiany wyniku. Pozostawiamy napisanie odpowiedniego algorytmu jako cwiczenie.
W animacji kolorem żółtym na końcu zaznacza się licznik słabego przywódcy, jego nazwa jest w
niebieskim kwadraciku.


Problem mozna rozwiazac inaczej sortując tablicę, wtedy mamy zlozonosc O(n log n). Podamy potem również rozwiązanie metodą "dziel i zwyciężaj".
Jeśli istnieje słaby przywódca i mamy pięć różnych elementów to można je usunąć bez zmiany
wyniku. Pozostawiamy napisanie odpowiedniego algorytmu jako ćwiczenie.


====Szukanie sumy==== Mamy dane dwie posortowane rosnąco tablice <math>A,B</math> i liczbę x,pytamy  czy są  <math>a \in A,\ b \in B</math> takie, że <math>x=a+b</math>.
Problem można rozwiązać inaczej sortując tablicę, wtedy mamy złożoność O(n log n). Podamy potem
również rozwiązanie metodą "dziel i zwyciężaj".
 
====Szukanie sumy==== Mamy dane dwie posortowane rosnąco tablice <math>A,B</math> i liczbę  
x,pytamy  czy są  <math>a \in A,\ b \in B</math> takie, że <math>x=a+b</math>.


{{algorytm|Szukanie Sumy|algorytm_szukanie_sumy|
{{algorytm|Szukanie Sumy|algorytm_szukanie_sumy|
Linia 145: Linia 213:
     '''return''' false;
     '''return''' false;
}}
}}
Przyspieszenie jest możliwe dzięki odpowiedniej kolejności sprawdzania <math>i,j</math> i pominięciu zbędnych sprawdzeń.
Przyspieszenie jest możliwe dzięki odpowiedniej kolejności sprawdzania <math>i,j</math> i  
pominięciu zbędnych sprawdzeń.


==== Maksymalny segment ====  Dla tablicy <math>A[1..n] \,</math> liczymy maksymalną wartość z zera i ze wszystkich liczb <math>\sum_{k=i}^j\ A[k]</math>, gdzie <math>1\le i\le j\le n</math>.
==== Maksymalny segment ====  Dla tablicy <math>A[1..n] \,</math> liczymy maksymalną  
wartość z zera i  
ze wszystkich liczb <math>\sum_{k=i}^j\ A[k]</math>, gdzie <math>1\le i\le j\le n</math>.


  '''Algorytm''' Maksymalny-Segment;<br>
  '''Algorytm''' Maksymalny-Segment;<br>
Linia 153: Linia 224:
     '''for''' i := 1 to n do
     '''for''' i := 1 to n do
       sufiks := max(A[i]+sufiks,0);
       sufiks := max(A[i]+sufiks,0);
       wynik := max(wynik,sufiks);
       wynik := max(wynik, sufiks);


Przyspieszenie do algorytmu liniowego następuje dzięki wprowadzeniu dodatkowej zmiennej sufiks. Po każdym zakończeniu  pętli "for" zachodzi: wynik jest maksymalną sumą przedziału zawierającego się w <math>[1..i]</math> oraz sufiks jest maksymalną sumą segmentu który jest sufiksem przedziału <math>[1..i]</math>.
Przyspieszenie w stosunku do algorytmu kwadratowego następuje dzięki wprowadzeniu dodatkowej  
zmiennej sufiks.  
Po każdym zakończeniu  pętli "for" zachodzi: wynik jest maksymalną sumą przedziału zawierającego  
się w <math>[1..i]</math>  
oraz sufiks jest maksymalną sumą segmentu który jest sufiksem przedziału <math>[1..i]</math>.




==== Najbliższy mniejszy sąsiad z lewej strony ====
==== Najbliższy mniejszy sąsiad z lewej strony ====
Dla każdego <math>i > 1</math> zdefiniujmy najbliższego mniejszego sąsiada <math>i</math> jako:  <math>Lewy[i] =max \{j<i : A[j]<A[i] \}</math>
Dla każdego <math>i > 1</math> zdefiniujmy najbliższego mniejszego sąsiada <math>i</math> jako:   
Dla uproszczenia zakładamy, że  <math>A[i]> 0</math> dla <math>i>0</math> oraz <math>A[0]=0</math>.
<math>Lewy[i] =max \{j<i : A[j]<A[i] \}</math>
Dla uproszczenia zakładamy, że  <math>A[i]> 0</math> dla <math>i>0</math> oraz  
<math>A[0]=0</math>.


{{algorytm|Najbliższy-Mniejszy-Sąsiad|algorytm_najblizszy_mniejszy_sasiad|
{{algorytm|Najbliższy-Mniejszy-Sąsiad|algorytm_najblizszy_mniejszy_sasiad|
Linia 170: Linia 247:
}}
}}


Przyspieszenie następuje dzięki temu, że nie ma potrzeby sprawdzania tablicy dla indeksów istotnie pomiędzy <math>i-1</math> i  <math>Lewy[i-1]</math> itd. Niech <math>k_i</math> będzie liczbą tych <math>j</math> dla których <math>A[j]>=A[i]</math>. Wystarczy
Przyspieszenie następuje dzięki temu, że nie ma potrzeby sprawdzania tablicy dla indeksów istotnie  
pokazać, że suma wszystkich <math>k_i</math> jest liniowa. Może się zdarzyć, że niektóre <math>k_i</math> mają wartość
wewnątrz przedziału  <math>[[Lewy[i-1]...(i-1)]</math> . Niech <math>k_i</math> będzie liczbą  
liniową. Zauważmy jednak, że dany indeks <math>j</math> pojawia się co najwyżej raz w sytuacji gdy <math>A[j] >= A[i]</math>,
tych <math>j</math> dla których <math>A[j]>=A[i]</math>. Wystarczy
pokazać, że suma wszystkich <math>k_i</math> jest liniowa. Może się zdarzyć, że niektóre  
<math>k_i</math> mają wartość
liniową. Zauważmy jednak, że dany indeks <math>j</math> pojawia się co najwyżej raz w sytuacji  
gdy <math>A[j] >= A[i]</math>,
potem będzie "przeskoczony".
potem będzie "przeskoczony".


==== Najdłuższy malejący podciąg ====
==== Najdłuższy malejący podciąg ====
Niech <math>A[1], A[2],\ldots A[n]</math> będzie ciągiem dodatnich liczb. Następujący algorytm oblicza długość najdłuższgo  malejącego podciągu.   
Niech <math>A[1], A[2],\ldots A[n]</math> będzie ciągiem dodatnich liczb. Następujący algorytm  
oblicza długość najdłuższgo  malejącego podciągu.   
{{algorytm|Najdłuższy-Malejący|algorytm_najdluzszy_malejacy|
{{algorytm|Najdłuższy-Malejący|algorytm_najdluzszy_malejacy|
  Najdłuższy-Malejący
  Najdłuższy-Malejący
Linia 188: Linia 270:




Algorytm może, po dodatkowym niewielkim wysiłku fizycznym procesora, podać najdłuższy maljący podciąg, albo też rozkład na minimalną liczbę podciągów niemalejących.
Algorytm może, po niewielkim dodatkowym wysiłku fizycznym procesora, podać najdłuższy maljący  
Nie jest jasne jak policzyć leksykograficznie minimalny i leksykograficznie maksymalny podciąg malejący o długości k, gzie
podciąg, albo też rozkład na minimalną liczbę podciągów niemalejących.
k jest wynikiem powyzszego algorytmu. Jak również mo¶emy się zastanowić nad liczbą wszystkick takich ciągów długości k.
Nie jest jasne jak policzyć leksykograficznie minimalny i leksykograficznie maksymalny podciąg  
malejący o długości k, gdzie
k jest wynikiem powyższego algorytmu. Jak również możemy się zastanowić nad efektywnym
algorytmem znajdowania liczby wszystkich takich ciągów długości k.


==== Proste Pakowanie====
==== Proste Pakowanie====
Załóżmy, że mamy n pudełek, każde o rozmiarze R, oraz n przedmiotów o rozmiarach <math>R \ge  r[1]\ge r[2]\ldots \ge r[n]</math>. Mamy włożyć przedmioty do pudełek, co najwyżej dwa do jednego pudełka.  Pozostawiamy jako ćwiczenie analizę następującego algorytmu, który oblicza minimalną
Załóżmy, że mamy n pudełek, każde o rozmiarze R, oraz n przedmiotów o rozmiarach <math>R \ge   
r[1]\ge r[2]\ldots \ge r[n]</math>. Mamy włożyć przedmioty do pudełek, co najwyżej dwa do jednego  
pudełka.  Pozostawiamy jako ćwiczenie analizę następującego algorytmu, który oblicza minimalną
liczbę pudełek.
liczbę pudełek.
   
   
{{algorytm|Proste-Pakowanie|algorytm_proste_pakowanie|
{{algorytm|Proste-Pakowanie|algorytm_proste_pakowanie|
Proste Pakowanie
  <math>wynik := n;</math>
  <math>wynik := n;</math>
  '''for''' <math>i := 1</math> to n do
  '''for''' <math>i := 1</math> to n do
Linia 206: Linia 292:
<center><flash>file=Klocki.swf|width=450|height=250</flash></center>
<center><flash>file=Klocki.swf|width=450|height=250</flash></center>


Naiwne wersje powyzszych sześciu algorytmów  działają w czasie kwadratowym. W każdym z tych algorytmów bardzo proste spostrzeżenia prowadą do algorytmów liniowych.
Naiwne wersje powyższych sześciu algorytmów  działają w czasie kwadratowym. W każdym z tych  
Podamy jeszcze jeden bardzo krótki ciekawy algorytm (ale bez żadnego zastosowania praktycznego).
algorytmów bardzo proste spostrzeżenia prowadzą do algorytmów liniowych.
Podamy jeszcze jeden bardzo krótki ciekawy algorytm (chociaż bez żadnego widocznego
zastosowania praktycznego).


==== Permutacje wagowe====
==== Permutacje wagowe====
Przypuśćmy, że mamy wagę
Przypuśćmy, że mamy wagę
szalkową, początkowo obie szalki sa puste, oraz mamy odważniki o numerach  1,2,..n. Waga i-tego odważnika wynosi <math> a_i</math>.
szalkową, początkowo obie szalki sa puste, oraz mamy odważniki o numerach  1,2,..n. Waga i-tego  
Dla danej permutacji <math>\Pi</math> numerów odważników będziemy je wkładać na wagę zgodnie z permutacją. Kładziemy kolejno odważniki w kolejności <math> \Pi </math> na lewą lub prawa szalkę, raz położony odważnik nie zmieia już nigdy swego położenia na szalce (wybór szalki jest dosyć
odważnika wynosi <math> a_i</math>.
niedeterminisyczny). Otrzymujemy ciąg wyników ważenia: +1 gdy lewa szalka  przeważa, wpp. -1. Ciąg ten oznaczamy przez Input.
Dla danej permutacji <math>\Pi</math> numerów odważników będziemy je wkładać na wagę  
Mówimy że  permutacja <math> \Pi </math> jest zgodna z ciągiem wyników ważeń danych tablicą Input. Zajmiemy się problemem: dany jest na wejściu ciąg Input wyników ważeń i mamy znależć
zgodnie z permutacją. Kładziemy kolejno odważniki  
jakąkolwiek permutację <math> \Pi </math> zgodną ciągiem Input. Takich permutacji może być wiele, zauważmy, że  
w kolejności <math> \Pi </math> na lewą lub prawa szalkę, raz położony odważnik nie zmieia już  
liczba permutcji wynosi n!, a liczba ciągów wyników ważeń wynosi <math> 2^n</math>, co jest liczbą znacznie mniejszą. <br>
nigdy swego położenia na szalce (wybór szalki jest dosyć
niedeterministyczny). Otrzymujemy ciąg wyników ważenia: +1 gdy lewa szalka  przeważa, wpp. -1.  
Ciąg ten oznaczamy przez Input.
Mówimy że  permutacja <math> \Pi </math> jest zgodna z ciągiem wyników ważeń danych tablicą  
Input. Zajmiemy się problemem: dany  
jest na wejściu ciąg Input wyników ważeń i mamy znalezć
jakąkolwiek permutację <math> \Pi </math> zgodną z ciągiem Input. Takich permutacji może być  
wiele. Zauważmy, że  
liczba permutacji wynosi n!, a liczba ciągów wyników ważeń wynosi <math> 2^n</math>, co jest  
liczbą znacznie mniejszą. <br>
Następujący algorytm znajduje pewną permutację zgodną.
Następujący algorytm znajduje pewną permutację zgodną.
Zakładamy że  
Zakładamy że  
  <center> <math> a_1<a_2<a_3<\ldots a_n</math></center>
  <center> <math> a_1<a_2<a_3<\ldots a_n</math></center>


Linia 234: Linia 332:




Ciąg wejściowy jest zrealizowany przez następujący ciąg wyborów wkładania kolejnego odważnika na wagę:
Ciąg wejściowy jest zrealizowany przez następujący ciąg wyborów wkładania kolejnego odważnika  
na wagę:


     L  P  L  P  P  L  L  P  P
     L  P  L  P  P  L  L  P  P


gdzie L oznacza połóż na lewą szalkę, P na prawą.
gdzie L oznacza połóż na lewą szalkę, P na prawą.
 
Korzystając (tylko częściowo) z dialeku C++ można zapisać algorytm krócej
Korzystaj"ac (tylko częściowo) z dialeku C++ można zapisać algorytm krócej




Linia 251: Linia 349:


Wynik algorytmu pozostawia pewien ''niedosyt'', generujemy dobry wynik ale
Wynik algorytmu pozostawia pewien ''niedosyt'', generujemy dobry wynik ale
w pewnym sensie jakikolwiek dobry. Nie jest jasne, jak policzyć efektywnie liczbę wszystkich permutacji zgodnych z danym ciągiem wyników, albo
w pewnym sensie jakikolwiek dobry. Nie jest jasne, jak policzyć efektywnie liczbę wszystkich  
permutacji zgodnych z danym ciągiem wyników, albo
znależć jakąś szczególną permutację, np. leksykograficznie pierwszą lub ostatnią.
znależć jakąś szczególną permutację, np. leksykograficznie pierwszą lub ostatnią.
Co będzie jeśli tablica Input zawiera również zera (wagi szalek są równe). Wtedy nie każdy ciąg Input jest realizowalny, jak to efektywnie sprawdzać.
Co będzie jeśli tablica Input zawiera również zera (wagi szalek są równe). Wtedy nie każdy ciąg Input  
jest realizowalny.  Jak to można efektywnie sprawdzać?


== Koszt zamortyzowany ==
== Koszt zamortyzowany ==
Jeśli mamy ciąg operacji <math>op_1,op_2,\ldots, op_n</math> to koszt zamortyzowany jednej z nich jest sumarycznym kosztem
Jeśli mamy ciąg operacji <math>op_1,op_2,\ldots, op_n</math> to koszt zamortyzowany jednej z  
wykonania wsszystkich operacji podzielonym przez liczbę operacji, inaczej mówiąc jest to, dla danego ciągu operacji, średni koszt jednej z nich. Zauważmy, że nie mówmy tu nic o prawdopodobieństwie, model jest deterministyczny. Na przykład w algorytmie Najbliższy-Mniejszy-Sąsiad rozważmy ciąg operacji  
nich jest sumarycznym kosztem
wykonania wszystkich operacji podzielonym przez liczbę operacji, inaczej mówiąc jest to, dla danego  
ciągu operacji, średni koszt jednej z nich. Zauważmy,  
że nie mówmy tu nic o prawdopodobieństwie, model jest deterministyczny. Na przykład w algorytmie  
Najbliższy-Mniejszy-Sąsiad rozważmy ciąg operacji  


<math>op_i</math>: while <math>( A[j] >= A[i])\ j = Lewy[j]</math>
<math>op_i</math>: while <math>( A[j] >= A[i])\ j = Lewy[j]</math>


Koszt pojedyńczej operacji może być liniowy, również sumaryczny koszt ciągu tych operacji <math>op_1,op_2,\ldots, op_n</math> jest liniowy. Zatem
Koszt pojedyńczej operacji może być liniowy, również sumaryczny koszt ciągu tych operacji  
pesymistyczny koszt jdnej operacji jest tutaj liniowy, natomiast zamortyzowany koszt jednej operacji jest  ograniczony przez stałą. W tym przypadku wiemy, że każde sprawdzenie <math>A[j]>=A[i])</math> z wynikiem negatywnym
<math>op_1,op_2,\ldots, op_n</math> jest liniowy. Zatem
odbywa się tylko raz dla danej wartości <math>j</math>. Możemy powiedzieć, że księgujemy koszt operacji elementom <math>j</math>
pesymistyczny koszt jednej operacji jest tutaj liniowy, natomiast zamortyzowany koszt jednej operacji  
o tej własności. Nieformalna metoda księgowania kosztów polega na rozdzielaniu (księgowaniu) kosztu, a
jest  ograniczony przez stałą. W tym przypadku  
następnie szacowania sumarycznej złożoności poprzez sumowanie wszystkich zaksęgowanych kosztów. Operacje
wiemy, że każde sprawdzenie <math>A[j]>=A[i])</math> z wynikiem negatywnym
pożyczają, w pewnym sensie, fundusze na pokrycie kosztów z różnych źródeł. Metoda ta będzie wykorzystana
odbywa się tylko raz dla danej wartości <math>j</math>. Możemy powiedzieć, że księgujemy koszt  
operacji elementom <math>j</math>
 
o tej własności. Nieformalna metoda księgowania kosztów polega na rozdzielaniu (księgowaniu)  
kosztu, a
następnie szacowaniu sumarycznej złożoności poprzez sumowanie wszystkich zaksięgowanych
kosztów. Operacje
pożyczają, w pewnym sensie, fundusze na pokrycie kosztów z różnych źródeł. Metoda ta będzie  
wykorzystana
do analizy algorytmu dla interesującego problemu Find-Union.  
do analizy algorytmu dla interesującego problemu Find-Union.  


Typowym przykładem liczenia kosztu w sposób zamortyzowany jest analiza generacji reprezentacji binarnych kolejnych liczb naturalnych od 0 do <math>2^n-1</math>, dodając jedynkę. W jednym kroku zastępujemy najdłuższy ciąg jedynek od końca zerami, następnie wstawiamy
Typowym przykładem liczenia kosztu w sposób zamortyzowany jest analiza generacji kolejnych
jedną jedynkę. Ponieważ w sumie wstawiliśmy <math>2^n-1</math> jedynek w ciągu <math>2^n-1</math> operacji, to zamortyzowana
reprezentacji binarnych kolejnych liczb naturalnych od 0 do <math>2^n-1</math>,  
liczba operacji zamiany zera na jedynke wynosi 1. W tym przykładzie możemy powiedzieć, że analizowaliśmy
dodając jedynkę. W jednym kroku zastępujemy najdłuższy ciąg jedynek od końca zerami, następnie  
koszt tzw. ''metodą magazynu''. W każdej operacji koszt jest proporcjonalny do liczby przedmiotów włożonych
wstawiamy
do magazynu lub do liczby przedmiotów wyjętych z magazynu. Magazyn początkowo jest pusty. Wtedy całkowity
jedną jedynkę. Ponieważ w sumie wstawiliśmy <math>2^n-1</math> jedynek w ciągu <math>2^n-
koszt jest proporcjonalny do liczby przedmiotów włożonych. W przypadku generowania liczb binarnych do magazynu
1</math> operacji, to zamortyzowana
liczba operacji zamiany zera na jedynkę wynosi 1.  
 
''Zasada magazynu.''
W ostatnim  przykładzie możemy powiedzieć, że analizowaliśmy
koszt tzw. ''metodą magazynu''. W każdej operacji koszt jest proporcjonalny do liczby przedmiotów  
włożonych
do magazynu lub do liczby przedmiotów wyjętych z magazynu. Magazyn początkowo jest pusty.  
Wtedy całkowity
koszt jest proporcjonalny do liczby przedmiotów włożonych. W przypadku generowania liczb  
binarnych do magazynu
wkładamy nowe jedynki, a wyjmujemy te jedynki, które zamieniamy na zera.
wkładamy nowe jedynki, a wyjmujemy te jedynki, które zamieniamy na zera.


=== Potencjał - Fundusz Ubezpieczeń Kosztów Algorytmicznych===  
=== Potencjał - Fundusz Ubezpieczeń Kosztów Algorytmicznych===  


Metodę magazynu można uogólnić na tzw. metodę potencjału. Niech <math>\Phi_i</math> będzie pewną liczbą naturalną (włączając zero) odpowiadającą potencjałowi po wykonaniu <math>i</math> operacji. Zakładamy, że potencjał jest początkowo zero, nigdy nie jest ujemny, a każda operacja <math>op_i</math> ma koszt proporcjonalny do <math>c_i+|\Phi_i-\Phi_{i-1}|</math>. Wtedy całkowity koszt jest tego samego rzędu co <math> \sum c_i</math>. W naszych poprzednich przykładach rozmiar magazynu jest w tym sensie potencjałem.
Metodę magazynu można uogólnić na tzw. metodę potencjału. Niech <math>\Phi_i</math> będzie  
pewną liczbą naturalną (włączając zero) odpowiadającą potencjałowi  
po wykonaniu <math>i</math> operacji. Zakładamy, że potencjał jest początkowo zero, nigdy nie jest  
ujemny, a każda operacja <math>op_i</math> ma  
koszt proporcjonalny do <math>|\Phi_i-\Phi_{i-1}|</math>. Wtedy całkowity koszt jest tego samego  
rzędu co <math> \sum c_i</math>.  
W naszych poprzednich przykładach rozmiar magazynu jest w tym sensie potencjałem.
 
Można powiedzieć obrazowo że potencjał jest kapitałem Funduszu Ubezpieczeń Kosztów
Algorytmicznych. Koszt zamortyzowany jednej operacji jest składką,
którą ta operacja wpłaca do funduszu.
 
Operacja najpierw wpłaca swoją składkę a następnie pobiera z funduszu tyle, żeby proporcjonalnie
(być może z dokładnością do stałego współczynnika) zaplacić za swój koszt wykonania.


Można powiedzieć obrazowo że potencjał jest kapitałem Funduszu Ubezpieczeń Kosztów Algorytmicznych. Koszt zamortyzowany jednej operacji jest składką, którą ta operacja wpłaca do funduszu. Dzięki temu, że wiele operacji pobiera z funduszu znacznie mniej niż wpłaca niektóre operacje mogą jednorazowo pobrać dużą kwotę, kórą płacą za koszt wykonania. Operacje <math>op_i</math> ubezpieczają się od kosztów ich
Dzięki temu, że wiele operacji pobiera z funduszu znacznie mniej niż wpłaca niektóre operacje mogą  
wykonani. Poszczególne operacje płacą drobne składki <math>c_i</math>, a swój koszt za każdym razem opłacają bioróac pieniądze z Funduszu. Czasmi koszt operacji jest duży ale do tego czasu wpłacono tyle drobnych składek,że możemy ten koszt pokryć. Istotne jest jedynie żeby Fundusz nie zbankrutował i kapitał nie zszedł poniżej zera. Możliwa jest również sytuacja gdy Fundusz startuje z kapitałem początkowym. Wtedy kapitał ten wlicza
jednorazowo pobrać dużą kwotę, którą płacą za koszt wykonania. Operacje <math>op_i</math>  
ubezpieczają się od kosztów ich
wykonani. Poszczególne operacje płacą drobne składki <math>c_i</math>, a swój koszt za każdym  
razem opłacają biorąc pieniądze z Funduszu.  
Czasami koszt operacji jest duży ale do tego czasu wpłacono tyle drobnych składek,że możemy ten  
koszt pokryć. Istotne jest jedynie  
żeby Fundusz nie zbankrutował i kapitał nie zszedł poniżej zera. Możliwa jest również sytuacja gdy  
Fundusz startuje  
z kapitałem początkowym. Wtedy kapitał ten wlicza
się do całkowitego kosztu algorytmu, który się dodajemy do sumy składek.
się do całkowitego kosztu algorytmu, który się dodajemy do sumy składek.


Rozważmy  przykłady ilustrujące wykorzystanie potencjału.
Rozważmy  przykłady ilustrujące wykorzystanie potencjału. Najistotniejsze jest określenie składek


==== Tablica dynamiczna====
==== Tablica dynamiczna====
Przypuśćmy, że mamy dynamiczną tablicę. W każdym momencie wiemy ile elementów w tablicy jest akywnych, elementy niekatywne zaznaczamy. W
Przypuśćmy, że mamy dynamiczną tablicę. W każdym momencie wiemy ile elementów w tablicy jest  
każdej operacji, jeśli liczba elementów nieaktywnych jest mniejsza od <math>\frac{1}{4}</math> wielkości tablicy to tworzymy tablicę dwa razy mniejszą i tam przepisujemy elementy aktywne, starą tablicę zwalniamy. W przeciwnym wypadku jeśli chcemy dodać element, który spowoduje przepełnienie tablicy to całą tablicę kopiujemy do tablicy dwa razy większej. Początkowo tablica ma rozmiar 1. Zakładamy, że operacją dominującą jest kopiowanie aktywnego elementu do nowej tablicy. Jeśli mamy <math>n</math> operacji to całkowity koszt kopiowania jest liniowy. Wystarczy w każdej operacji dać składkę 4 jednostek do Funduszu (potencjału).  Wtedy koszt jednej dużej operacji przepisywania  zamortyzuje się zmianą potencjału.
aktywnych, elementy nieaktywne zaznaczamy. W
każdej operacji, jeśli liczba elementów nieaktywnych jest mniejsza od <math>\frac{1}{4}</math>  
wielkości tablicy to tworzymy tablicę  
dwa razy mniejszą i tam przepisujemy elementy aktywne, starą tablicę zwalniamy. W przeciwnym  
wypadku jeśli chcemy dodać element,  
który spowoduje przepełnienie tablicy to całą tablicę kopiujemy do tablicy dwa razy większej.  
Początkowo tablica ma rozmiar 1.  
Zakładamy, że operacją dominującą jest kopiowanie aktywnego elementu do nowej tablicy. Jeśli  
mamy <math>n</math> operacji to  
całkowity koszt kopiowania jest liniowy. Wystarczy w każdej operacji dać składkę 4 jednostek do  
Funduszu (potencjału).   
Wtedy koszt jednej dużej operacji przepisywania  zamortyzuje się zmianą potencjału.




==== Zastąpienie kolejki dwoma stosami====
==== Zastąpienie kolejki dwoma stosami====
Jedną kolejkę Q można zastąpić dwoma stosami <math>S1,\ S2</math>. Jeśli pierwszy element stosu lub kolejki w
Jedną kolejkę Q można zastąpić dwoma stosami <math>S1,\ S2</math>. Jeśli pierwszy element stosu  
reprezentacj poziomej jest w ciągu na pierwszej pozycji (tzn. pobieramy <math>e_1</math>, stawimay za <math>e_n</math>), oraz <math> Q = (e_1,e_2,..,e_k)</math> to dla pewnego <math>j </math> mamy:
lub kolejki w
reprezentacji poziomej jest w ciągu na pierwszej pozycji (tzn. pobieramy <math>e_1</math>,  
stawimay za <math>e_n</math>),  
oraz <math> Q = (e_1,e_2,..,e_k)</math> to dla pewnego <math>j </math> mamy:


<math>S1 = (e_n,e_{n-1},...,e_j),\ S2 = (e_{1},e_{2}, ...,e_{j-1})</math>.
<math>S1 = (e_n,e_{n-1},...,e_j),\ S2 = (e_{1},e_{2}, ...,e_{j-1})</math>.


Operacja wstawiania do A odpowiada wstawieniu elementu do <math>S1</math>, operacja pobrania z Q odpowiada pobraniu elementu z S2, z tym, że jeśli <math>S2</math> jest pusty to przepisujemy najpierw wszystkie elementy z S1 do S2. Niech operacją dominującą będzie jedna operacja stosowa (wstawienie lub pobranie pojedyńczego elementu ze stosu). Wtedu ciąg <math>n</math> operacji kolejkowych, starujących od pustej kolejki, ma koszt liniowy w tej imlementacji. Wystarczy, że każda operacja da do Funduszu składkę 3 jednostek. Dowód tego pozostawiamy jako ćwiczenie.  
Operacja wstawiania do A odpowiada wstawieniu elementu do <math>S1</math>, operacja pobrania  
z Q odpowiada pobraniu elementu z S2  
z tym, że jeśli <math>S2</math> jest pusty to przepisujemy najpierw wszystkie elementy z S1 do S2.  
Niech operacją dominującą  
będzie jedna operacja stosowa (wstawienie lub pobranie pojedynczego elementu ze stosu). Wtedy ciąg  
<math>n</math> operacji  
kolejkowych, startujących od pustej kolejki, ma koszt liniowy w tej implementacji. Wystarczy, że  
każda operacja wkłada do Funduszu  
składkę 3 jednostek. Dowód tego pozostawiamy jako ćwiczenie.  


==== Zastąpienie kolejki dwustronnej  trzema stosami====
==== Zastąpienie kolejki dwustronnej  trzema stosami====
Roważmy podobny problem, z tym że nasza kolejka jest dwustronna, możemy wkładać i pobierać element z każdego z dwóch końców kolejki. Wtedy możemy taką kolejkę zastąpić trzema stosami tak, że teraz również każda operacja kolejkowa będzie mieć zamortyzowany koszt stały. Elementy kolejki trzymamy w dwóch stosach S1, S2 tak jak poprzednio. Niezmiennikem jest to, że oba stosy są niepuste lub mają w sumie co najwyżej jeden element. Zapewniamy zachodzenie niemiennika wykorzystując trzeci stos. W momencie gdy jeden ze stosów ma więcej niż jeden element a drugi jest pusty, korzystając z trzeciego stosu, doprowadzamy do reprezentacji aktualnej kolejki przez stosy S1, S2, tak aby miały one tę samą liczbę elementów (z dokładnością do 1). Pozostawiamy jako ćwiczenie dowód (metodą potencjału) tego, że zamortyzowany koszt jest stały.
Roważmy podobny problem, z tym że nasza kolejka jest dwustronna, możemy wkładać i pobierać  
element z każdego z dwóch końców kolejki.
Wtedy możemy taką kolejkę zastąpić trzema stosami tak, że teraz również każda operacja kolejkowa  
będzie mieć zamortyzowany koszt stały.
Elementy kolejki trzymamy w dwóch stosach S1, S2 tak jak poprzednio. Niezmiennikiem jest to, że  
oba stosy są niepuste lub mają w sumie co  
najwyżej jeden element. Zapewniamy zachodzenie niezmiennika wykorzystując trzeci stos. W  
momencie gdy jeden ze stosów ma więcej niż jeden element a  
drugi jest pusty, korzystając z trzeciego stosu, doprowadzamy do reprezentacji aktualnej kolejki przez  
stosy S1, S2, tak aby miały one tę samą  
liczbę elementów (z dokładnością do 1). Pozostawiamy jako ćwiczenie dowód (metodą potencjału)  
tego, że zamortyzowany koszt jest stały.
[[Grafika:Example.jpg]]

Wersja z 13:03, 13 wrz 2006

Wstęp: poprawność i złożoność algorytmów



Podstawowym elementem przy rozwiązywaniu zadanego problemu na komputerze jest dobór algorytmu i struktury danych. Najważniejszymi aspektami algorytmu są jego "poprawność" i złożoność. Będziemy zasadniczo rozpatrywać tylko złożoność czasową i pamięciową.

W przypadku złożoności czasowej z reguły wyróżnimy pewną operację dominującą i czas będziemy traktować jako liczbę wykonanych operacji dominujących. W ten sposób nasza analiza będzie zależna jedynie od algorytmu a nie od implementacji i sprzętu. W przypadku sortowania przeważnie operacją dominującą jest porównanie dwóch elementów (mniejsze, równe, mniejsze), a w przypadku przeglądania drzewa jedno przejście w drzewie między wierzchołkami. W przypadku algorytmów tekstowych operacją dominującą jest porównanie dwóch symboli. Z reguły będziemy przyjmować, że każda operacja arytmetyczna na małych liczbach daje się zrobić w jednym kroku. Przez małe rozumiemy liczby mające O(logn) bitów. Z reguły określamy pewien parametr n, będący rozmiarem problemu wejściowego i określamy złożoność jako funkcję T(n), której argumentem jest rozmiar problemu.

Przeważnie rozważamy złożoność pesymistyczną - maksymalną złożoność dla danych tego samego rozmiaru n. W praktyce ważniejszą może się okazać złożoność średnią, lub oczekiwaną, w tym przypadku T(n) jest średnią (oczekiwaną) wartością złożoności dla wszystkich problemów rozmiaru n. Tego typu złożoność zależy istotnie od tego, jaka się pod tym kryje przestrzeń probabilistyczna problemów wejsściowych. Z reguły zakładamy, że wszystkie problemy wejściowe tego samego rozmiaru mogą się pojawić z tym samym prawdopodobieństwem. Jednakże jest to często mało realistyczne założenie. Przestrzeń probabilistyczna danych wejściowych może być bardzo skomplikowana, prowadzić to może do bardzo trudnych (i wykraczających poza ten kurs) analiz.

Szablon:Przykład

Do wyrażania złożoności stosujemy opis asymptotycznego wzrostu funkcji: f(n) = O(g(n)) oznacza że f(n)cg(n) dla pewnej stałej n. Gdy g(n)=n to mówimy, że f(n) jest liniowa, oraz dla g(n)=n2 mówimy, że złożoność f(n) jest kwadratowa. Jeśli g(n) jest wielomianem to wtedy mówimy o złożoności wielomianowej.


Będziemy używać dodatkowych notacji:

f(n)=Θ(g(n)), f(n)=Ω(n). Były one wprowadzone na wykładach z matematyki dyskretnej.


Dla przypomnienia:


f(n)=Θ(g(n))f(n)=O(g(n)) & g(n)=O(f(n))

f(n)=Ω(g(n)), gdy dla nieskończenie wielu n i pewnej stałej c>0 zachodzi f(n)cg(n)

Przykład

1100n22n=Θ(n2)n5+2n=Θ(2n),n!=Ω(10n),
Jeśli f(n)=(1+(1)n)n, to f(n)=Ω(n), f(n)=O(n) ale nie

zachodzi f(n)=Θ(n).



Konwencje językowe. Jaki jest najlepszy język do opisu algorytmu ? Jest to przykład problemu nierozstrzygalnego. Niewątpliwie język ojczysty jest najlepszym językiem potocznym , a ulubiony język programowania jest najlepszym językiem do implementacji algorytmu. Język, którym będziemy opisywać algorytmy jest gdzieś pomiędzy tymi językami, język potoczny nie wystarcza a konkretny język programowania może spowodować to, że "prosty" algorytm się zrobi nieczytelny. Będziemy używać, o ile się da, nieformalnych konstrukcji programistycznych, a w przypadku bardzo prostych algorytm będziemy się starali pisać algorytm w języku C-podobnym.

Poprawność algorytmu: niezmienniki, własność stopu

Przez poprawność algorytmu rozumiemy to, że daje on takie odpowiedzi jakich oczekujemy. Oczywiście algorytm musi być poprawny aby miało sens rozpatrywanie jego złożoność.

Pojęcie niezmiennika

Poprawność algorytmu sprowadza się do spełniania określonych niezmienników na różnych etapach tego algorytmu. Rozważmy kilka przykładów pozwalających zrozumieć znaczenie niezmiennika.

Załóżmy, że mamy zbiór S składający się z n przedmiotów, niektóre z przedmiotów są czarne a niektóre białe. Zakładamy, że liczba czarnych przedmiotów jest nieparzysta.


Algorytm


1 while  |S|>1
2    pobierz dwa przedmioty z S;
3    if przedmioty są różnego koloru to wstaw z powrotem czarny
4 return kolor ostatniego przedmiotu w S;

Załóżmy, że mamy 10000001 czarnych przedmiotów i 1000000001 białych, jaki jest ostatni przedmiot ? Rozpatrzmy niezmiennik: parzystość liczby czarnych przedmiotów.

Ponieważ na początku mamy nieparzystą liczbę czarnych przedmiotów, zatem na wyjściu mamy kolor czarny.


Rozpatrzmy modyfikację tego algorytmu, zakładamy że n jest niezerowe.

Algorytm


1 while  |S|>1
2    pobierz dwa przedmioty z S;
3    if co najmniej jeden jest biały to wstaw z powrotem jeden biały;
4 return kolor ostatniego przedmiotu w S.

Załóżmy, że mamy 10000001 czarnych przedmiotów i 1000000001 białych, jaki jest ostatni przedmiot? Tym razem rozważmy niezmiennik: znak liczby białych przedmiotów. (Znak liczby jest równy 0 jeśli jest ona równa zeru, 1 jeśli jest większa od zera.) Zatem ostatnim przedmiotem jest przedmiot biały.

Własność stopu

Podstawowym elementem poprawności algorytmu jest to, że ma on własność stopu: dla poprawnych danych wejściowych da odpowiedź po skończonym czasie. Na przykładzie dwóch prostych algorytmów pokażemy, że sprawdzanie własności stopu może nie być czynnością trywialną.

Algorytm Suma-Kwadratów-Cyfr


 suma_kwadratow_cyfr
 1  while  ((n <>4)  and  (n<> 1))
 2  n:= suma kwadratów cyfr liczby n;


Algorytm 6174


wejściem jest czterocyfrowa liczba naturalna niepodzielna przez 1111
pierwszymi cyframi mogą być zera
while (n<>6174)
   n1:=  największa liczba czterocyfrowa której cyfry są permutacją cyfr liczby n;
   n2:= najmniejsza liczba czterocyfrowa której  cyfry są permutacją cyfr liczby n;
   n:=n1 - n2;

Algorytm X


while (n<>1)
   if n parzyste n:= n div 2; else n:=3*n+1;
   

Pierwsze dwa algorytmy mają własność stopu, w pierwszym łatwo to sprawdzić gdyż dla n>100 następna wartość jest istotnie mniejsza.

Pozostawiamy jako ćwiczenie jak najkrótszy koncepcyjnie dowód własności stopu obu algorytmów (nie chodzi nam tu o brutalny dowód polegający na sprawdzeniu wszystkich przypadków przez komputer).

Algorytm X jest dosyć zagadkowy, nie wiadomo, czy dla dowolnego naturalnego dodatniego n ma on własność stopu.

Złożoność algorytmów: analiza siedmiu prostych algorytmów

Na przykładzie siedmiu prostych algorytmów pokażemy, jak się analizuje i osiąga złożoność liniową. Po dokładniejsze uzasadnienia i analizy odsyłamy do ćwiczeń. Naiwne wersje tych algorytmów działają w czasie kwadratowym (Wybraliśmy "siedem" losowo, na przykład jest siedem dni w tygodniu ,był taki film Siedmiu wspaniałych itp.)

Przywódca ciągu

Przywódcą ciągu jest element, który występuje w ciągu więcej razy niż połowa długości tego ciągu. Naszym problemem jest policzenie przywódcy ciągu danego tablicą A[1..n]. Dla uproszczenia przyjmijmy, że w tym ciągu jest przywódca. Łatwo można modyfikować algorytm by sprawdzał istnienie przywódcy.

Algorytm Liczenie-Przywódcy


 ile:=0;
   for i := 1 to n do
      if (ile=0){ile:=ile+1;j:=i};
      else if (A[i]=A[j]) ile := ile+1; else ile := 

ile-1;

   return A[j];

Przyspieszenie wynika z następującej własności problemu: jeśli mamy dwa różne elementy w tablicy to możemy je usunąć i przywódca pozostanie taki sam.

Algorytm można zmodyfikować tak aby w czasie liniowym liczył on słabego przywódcę: element, który występuje w tablicy więcej niż n/5 razy.

W tym przypadku potrzebne są cztery liczniki  odpowiadające czterem kandydatom na słabego 

przywódcę. Algorytm liczy element który jest kandydatem na słabego przywódcę (jeśli istnieje taki przywódca to na pewno jest nim wyliczony element).


<flash>file=przywodca_slaby.swf|width=520|height=270</flash>

W animacji kolorem żółtym na końcu zaznacza się licznik słabego przywódcy, jego nazwa jest w niebieskim kwadraciku.

Jeśli istnieje słaby przywódca i mamy pięć różnych elementów to można je usunąć bez zmiany wyniku. Pozostawiamy napisanie odpowiedniego algorytmu jako ćwiczenie.

Problem można rozwiązać inaczej sortując tablicę, wtedy mamy złożoność O(n log n). Podamy potem również rozwiązanie metodą "dziel i zwyciężaj".

====Szukanie sumy==== Mamy dane dwie posortowane rosnąco tablice A,B i liczbę x,pytamy czy są aA, bB takie, że x=a+b.

Algorytm Szukanie Sumy


 Szukanie Sumy
   i:=1;j:=n;
   while (i<=n  and j>0)
      if (A[i]+B[j]=x)return true; else
      if (A[i]+B[j]<x)i:=i+1;else j:=j1;
   return false;

Przyspieszenie jest możliwe dzięki odpowiedniej kolejności sprawdzania i,j i pominięciu zbędnych sprawdzeń.

==== Maksymalny segment ==== Dla tablicy A[1..n] liczymy maksymalną wartość z zera i ze wszystkich liczb k=ij A[k], gdzie 1ijn.

Algorytm Maksymalny-Segment;
wynik := 0; sufiks := 0; for i := 1 to n do sufiks := max(A[i]+sufiks,0); wynik := max(wynik, sufiks);

Przyspieszenie w stosunku do algorytmu kwadratowego następuje dzięki wprowadzeniu dodatkowej zmiennej sufiks. Po każdym zakończeniu pętli "for" zachodzi: wynik jest maksymalną sumą przedziału zawierającego się w [1..i] oraz sufiks jest maksymalną sumą segmentu który jest sufiksem przedziału [1..i].


Najbliższy mniejszy sąsiad z lewej strony

Dla każdego i>1 zdefiniujmy najbliższego mniejszego sąsiada i jako: Lewy[i]=max{j<i:A[j]<A[i]} Dla uproszczenia zakładamy, że A[i]>0 dla i>0 oraz A[0]=0.

Algorytm Najbliższy-Mniejszy-Sąsiad


Najbliższy Mniejszy Sąsiad
for i:=1 to n do
 j:=i1;
   while (A[j]>=A[i])j:=Lewy[j];
      Lewy[i]:=j;

Przyspieszenie następuje dzięki temu, że nie ma potrzeby sprawdzania tablicy dla indeksów istotnie wewnątrz przedziału [[Lewy[i1]...(i1)] . Niech ki będzie liczbą tych j dla których A[j]>=A[i]. Wystarczy pokazać, że suma wszystkich ki jest liniowa. Może się zdarzyć, że niektóre ki mają wartość liniową. Zauważmy jednak, że dany indeks j pojawia się co najwyżej raz w sytuacji gdy A[j]>=A[i], potem będzie "przeskoczony".

Najdłuższy malejący podciąg

Niech A[1],A[2],A[n] będzie ciągiem dodatnich liczb. Następujący algorytm oblicza długość najdłuższgo malejącego podciągu.

Algorytm Najdłuższy-Malejący


Najdłuższy-Malejący
wynik:=1;
for i:=1 to n do
   x:=A[i];A[i]:=0;
   k:=min{j:x>A[j]}; 
   A[k]:=x;
   wynik:=max(k,wynik);


Algorytm może, po niewielkim dodatkowym wysiłku fizycznym procesora, podać najdłuższy maljący podciąg, albo też rozkład na minimalną liczbę podciągów niemalejących. Nie jest jasne jak policzyć leksykograficznie minimalny i leksykograficznie maksymalny podciąg malejący o długości k, gdzie k jest wynikiem powyższego algorytmu. Jak również możemy się zastanowić nad efektywnym algorytmem znajdowania liczby wszystkich takich ciągów długości k.

Proste Pakowanie

Załóżmy, że mamy n pudełek, każde o rozmiarze R, oraz n przedmiotów o rozmiarach Rr[1]r[2]r[n]. Mamy włożyć przedmioty do pudełek, co najwyżej dwa do jednego pudełka. Pozostawiamy jako ćwiczenie analizę następującego algorytmu, który oblicza minimalną liczbę pudełek.

Algorytm Proste-Pakowanie


wynik:=n;
for i:=1 to n do
   if (i<wynik  and r[i]+r[wynik]<=R)
      wynik:=wynik1;
<flash>file=Klocki.swf|width=450|height=250</flash>

Naiwne wersje powyższych sześciu algorytmów działają w czasie kwadratowym. W każdym z tych algorytmów bardzo proste spostrzeżenia prowadzą do algorytmów liniowych. Podamy jeszcze jeden bardzo krótki ciekawy algorytm (chociaż bez żadnego widocznego zastosowania praktycznego).

Permutacje wagowe

Przypuśćmy, że mamy wagę szalkową, początkowo obie szalki sa puste, oraz mamy odważniki o numerach 1,2,..n. Waga i-tego odważnika wynosi ai. Dla danej permutacji Π numerów odważników będziemy je wkładać na wagę zgodnie z permutacją. Kładziemy kolejno odważniki w kolejności Π na lewą lub prawa szalkę, raz położony odważnik nie zmieia już nigdy swego położenia na szalce (wybór szalki jest dosyć niedeterministyczny). Otrzymujemy ciąg wyników ważenia: +1 gdy lewa szalka przeważa, wpp. -1. Ciąg ten oznaczamy przez Input. Mówimy że permutacja Π jest zgodna z ciągiem wyników ważeń danych tablicą Input. Zajmiemy się problemem: dany jest na wejściu ciąg Input wyników ważeń i mamy znalezć jakąkolwiek permutację Π zgodną z ciągiem Input. Takich permutacji może być wiele. Zauważmy, że liczba permutacji wynosi n!, a liczba ciągów wyników ważeń wynosi 2n, co jest liczbą znacznie mniejszą.
Następujący algorytm znajduje pewną permutację zgodną. Zakładamy że

a1<a2<a3<an

Algorytm Permutacja-Wagowa


   p:=1;q:=n;
   for  i:=n downto 1 do
      if (i>1) and (Input[i1]Input[i]))
        Wynik[i]:=q;q:=q1;
      else  
          Wynik[i]:=p;p:=p+1;

Jeśli Input = [+1,+1,+1,-1,-1,-1,+1,+1,-1], to Wynik = [6 5 4 7 3 2 8 1 9].


Ciąg wejściowy jest zrealizowany przez następujący ciąg wyborów wkładania kolejnego odważnika na wagę:

    L  P  L  P  P  L  L  P  P

gdzie L oznacza połóż na lewą szalkę, P na prawą. Korzystając (tylko częściowo) z dialeku C++ można zapisać algorytm krócej


Algorytm Permutacja-Wagowa1


   p:=1;q:=n;
   for  i:=n downto 1 do
      if (i>1) and (Input[i1]Input[i]))
        Wynik[i]:=q; else  Wynik[i]:=p++; 

Wynik algorytmu pozostawia pewien niedosyt, generujemy dobry wynik ale w pewnym sensie jakikolwiek dobry. Nie jest jasne, jak policzyć efektywnie liczbę wszystkich permutacji zgodnych z danym ciągiem wyników, albo znależć jakąś szczególną permutację, np. leksykograficznie pierwszą lub ostatnią. Co będzie jeśli tablica Input zawiera również zera (wagi szalek są równe). Wtedy nie każdy ciąg Input jest realizowalny. Jak to można efektywnie sprawdzać?

Koszt zamortyzowany

Jeśli mamy ciąg operacji op1,op2,,opn to koszt zamortyzowany jednej z nich jest sumarycznym kosztem wykonania wszystkich operacji podzielonym przez liczbę operacji, inaczej mówiąc jest to, dla danego ciągu operacji, średni koszt jednej z nich. Zauważmy, że nie mówmy tu nic o prawdopodobieństwie, model jest deterministyczny. Na przykład w algorytmie Najbliższy-Mniejszy-Sąsiad rozważmy ciąg operacji

opi: while (A[j]>=A[i]) j=Lewy[j]

Koszt pojedyńczej operacji może być liniowy, również sumaryczny koszt ciągu tych operacji op1,op2,,opn jest liniowy. Zatem pesymistyczny koszt jednej operacji jest tutaj liniowy, natomiast zamortyzowany koszt jednej operacji jest ograniczony przez stałą. W tym przypadku wiemy, że każde sprawdzenie A[j]>=A[i]) z wynikiem negatywnym odbywa się tylko raz dla danej wartości j. Możemy powiedzieć, że księgujemy koszt operacji elementom j

o tej własności. Nieformalna metoda księgowania kosztów polega na rozdzielaniu (księgowaniu) kosztu, a następnie szacowaniu sumarycznej złożoności poprzez sumowanie wszystkich zaksięgowanych kosztów. Operacje pożyczają, w pewnym sensie, fundusze na pokrycie kosztów z różnych źródeł. Metoda ta będzie wykorzystana do analizy algorytmu dla interesującego problemu Find-Union.

Typowym przykładem liczenia kosztu w sposób zamortyzowany jest analiza generacji kolejnych reprezentacji binarnych kolejnych liczb naturalnych od 0 do 2n1, dodając jedynkę. W jednym kroku zastępujemy najdłuższy ciąg jedynek od końca zerami, następnie wstawiamy jedną jedynkę. Ponieważ w sumie wstawiliśmy 2n1 jedynek w ciągu 2n1 operacji, to zamortyzowana liczba operacji zamiany zera na jedynkę wynosi 1.

Zasada magazynu. W ostatnim przykładzie możemy powiedzieć, że analizowaliśmy koszt tzw. metodą magazynu. W każdej operacji koszt jest proporcjonalny do liczby przedmiotów włożonych do magazynu lub do liczby przedmiotów wyjętych z magazynu. Magazyn początkowo jest pusty. Wtedy całkowity koszt jest proporcjonalny do liczby przedmiotów włożonych. W przypadku generowania liczb binarnych do magazynu wkładamy nowe jedynki, a wyjmujemy te jedynki, które zamieniamy na zera.

Potencjał - Fundusz Ubezpieczeń Kosztów Algorytmicznych

Metodę magazynu można uogólnić na tzw. metodę potencjału. Niech Φi będzie pewną liczbą naturalną (włączając zero) odpowiadającą potencjałowi po wykonaniu i operacji. Zakładamy, że potencjał jest początkowo zero, nigdy nie jest ujemny, a każda operacja opi ma koszt proporcjonalny do |ΦiΦi1|. Wtedy całkowity koszt jest tego samego rzędu co ci. W naszych poprzednich przykładach rozmiar magazynu jest w tym sensie potencjałem.

Można powiedzieć obrazowo że potencjał jest kapitałem Funduszu Ubezpieczeń Kosztów Algorytmicznych. Koszt zamortyzowany jednej operacji jest składką, którą ta operacja wpłaca do funduszu.

Operacja najpierw wpłaca swoją składkę a następnie pobiera z funduszu tyle, żeby proporcjonalnie (być może z dokładnością do stałego współczynnika) zaplacić za swój koszt wykonania.

Dzięki temu, że wiele operacji pobiera z funduszu znacznie mniej niż wpłaca niektóre operacje mogą 

jednorazowo pobrać dużą kwotę, którą płacą za koszt wykonania. Operacje opi ubezpieczają się od kosztów ich wykonani. Poszczególne operacje płacą drobne składki ci, a swój koszt za każdym razem opłacają biorąc pieniądze z Funduszu. Czasami koszt operacji jest duży ale do tego czasu wpłacono tyle drobnych składek,że możemy ten koszt pokryć. Istotne jest jedynie żeby Fundusz nie zbankrutował i kapitał nie zszedł poniżej zera. Możliwa jest również sytuacja gdy Fundusz startuje z kapitałem początkowym. Wtedy kapitał ten wlicza się do całkowitego kosztu algorytmu, który się dodajemy do sumy składek.

Rozważmy przykłady ilustrujące wykorzystanie potencjału. Najistotniejsze jest określenie składek

Tablica dynamiczna

Przypuśćmy, że mamy dynamiczną tablicę. W każdym momencie wiemy ile elementów w tablicy jest aktywnych, elementy nieaktywne zaznaczamy. W każdej operacji, jeśli liczba elementów nieaktywnych jest mniejsza od 14 wielkości tablicy to tworzymy tablicę dwa razy mniejszą i tam przepisujemy elementy aktywne, starą tablicę zwalniamy. W przeciwnym wypadku jeśli chcemy dodać element, który spowoduje przepełnienie tablicy to całą tablicę kopiujemy do tablicy dwa razy większej. Początkowo tablica ma rozmiar 1. Zakładamy, że operacją dominującą jest kopiowanie aktywnego elementu do nowej tablicy. Jeśli mamy n operacji to całkowity koszt kopiowania jest liniowy. Wystarczy w każdej operacji dać składkę 4 jednostek do Funduszu (potencjału). Wtedy koszt jednej dużej operacji przepisywania zamortyzuje się zmianą potencjału.


Zastąpienie kolejki dwoma stosami

Jedną kolejkę Q można zastąpić dwoma stosami S1, S2. Jeśli pierwszy element stosu lub kolejki w reprezentacji poziomej jest w ciągu na pierwszej pozycji (tzn. pobieramy e1, stawimay za en), oraz Q=(e1,e2,..,ek) to dla pewnego j mamy:

S1=(en,en1,...,ej), S2=(e1,e2,...,ej1).

Operacja wstawiania do A odpowiada wstawieniu elementu do S1, operacja pobrania z Q odpowiada pobraniu elementu z S2 z tym, że jeśli S2 jest pusty to przepisujemy najpierw wszystkie elementy z S1 do S2. Niech operacją dominującą będzie jedna operacja stosowa (wstawienie lub pobranie pojedynczego elementu ze stosu). Wtedy ciąg n operacji kolejkowych, startujących od pustej kolejki, ma koszt liniowy w tej implementacji. Wystarczy, że każda operacja wkłada do Funduszu składkę 3 jednostek. Dowód tego pozostawiamy jako ćwiczenie.

Zastąpienie kolejki dwustronnej trzema stosami

Roważmy podobny problem, z tym że nasza kolejka jest dwustronna, możemy wkładać i pobierać element z każdego z dwóch końców kolejki.

Wtedy możemy taką kolejkę zastąpić trzema stosami tak, że teraz również każda operacja kolejkowa 

będzie mieć zamortyzowany koszt stały.

Elementy kolejki trzymamy w dwóch stosach S1, S2 tak jak poprzednio. Niezmiennikiem jest to, że 

oba stosy są niepuste lub mają w sumie co najwyżej jeden element. Zapewniamy zachodzenie niezmiennika wykorzystując trzeci stos. W momencie gdy jeden ze stosów ma więcej niż jeden element a drugi jest pusty, korzystając z trzeciego stosu, doprowadzamy do reprezentacji aktualnej kolejki przez stosy S1, S2, tak aby miały one tę samą liczbę elementów (z dokładnością do 1). Pozostawiamy jako ćwiczenie dowód (metodą potencjału) tego, że zamortyzowany koszt jest stały. Plik:Example.jpg