Algorytmy i struktury danych/Wstęp: poprawność i złożoność algorytmu: Różnice pomiędzy wersjami
m |
|||
Linia 9: | Linia 9: | ||
W przypadku złożoności czasowej, z reguły wyróżnimy pewną operację dominującą, a czas będziemy | W przypadku złożoności czasowej, z reguły wyróżnimy pewną operację dominującą, a czas będziemy | ||
traktować jako liczbę wykonanych operacji dominujących. | 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 | + | W ten sposób nasza analiza będzie zależna jedynie od algorytmu, a nie od implementacji i sprzętu. W |
przypadku sortowania, operacją dominującą jest przeważnie porównanie dwóch elementów, | przypadku sortowania, operacją dominującą jest przeważnie porównanie dwóch elementów, | ||
− | a w przypadku przeglądania drzewa jedno przejście w drzewie między | + | 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 | wierzchołkami. W przypadku algorytmów tekstowych operacją dominującą jest porównanie dwóch | ||
symboli. Zazwyczaj określamy pewien parametr <math>n</math>, będący rozmiarem problemu wejściowego i | symboli. Zazwyczaj określamy pewien parametr <math>n</math>, będący rozmiarem problemu wejściowego i | ||
Linia 20: | Linia 20: | ||
Złożoność algorytmu może być rozumiana w sensie złożoności najgorszego przypadku lub złożoności średniej. Złożoność najgorszego przypadku nazywamy złożonością pesymistyczną - jest to maksymalna złożoność dla danych tego samego | Złożoność algorytmu może być rozumiana w sensie złożoności najgorszego przypadku lub złożoności średniej. Złożoność najgorszego przypadku nazywamy złożonością pesymistyczną - jest to maksymalna złożoność dla danych tego samego | ||
rozmiaru <math>n</math>. | rozmiaru <math>n</math>. | ||
− | W praktyce ważniejszą może się okazać złożoność średnia | + | W praktyce ważniejszą może się okazać złożoność średnia lub oczekiwana. W tym przypadku |
<math>T(n)</math> jest średnią (oczekiwaną) wartością złożoności dla wszystkich problemów | <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 | rozmiaru <math>n</math>. Tego typu złożoność zależy istotnie od tego, jaka się pod tym kryje | ||
Linia 63: | Linia 63: | ||
nierozstrzygalnego. Niewątpliwie język ojczysty jest najlepszym | nierozstrzygalnego. Niewątpliwie język ojczysty jest najlepszym | ||
językiem potocznym, a ulubiony język programowania jest najlepszym językiem do implementacji | 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 | + | 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ć, że "prosty" algorytm | potoczny nie wystarcza, a konkretny język programowania może spowodować, że "prosty" algorytm | ||
się zrobi nieczytelny. Będziemy używać, o ile się da, nieformalnych konstrukcji programistycznych, a | się zrobi nieczytelny. Będziemy używać, o ile się da, nieformalnych konstrukcji programistycznych, a | ||
Linia 148: | Linia 148: | ||
== Złożoność algorytmów: siedem prostych przykładów == | == Złożoność algorytmów: siedem prostych przykładów == | ||
− | Poniżej dokonamy | + | Poniżej dokonamy pobieżnej analizy złożoności obliczeniowej siedmiu algorytmów i wykażemy, że ich złożoności są lepsze od złożoności kwadratowej rozwiązań naiwnych. |
Dokładne analizy pozostawiamy jako ćwiczenia. Liczba ''siedem'' jest losowa - na przykład jest ''siedem'' dni w | Dokładne analizy pozostawiamy jako ćwiczenia. Liczba ''siedem'' jest losowa - na przykład jest ''siedem'' dni w | ||
tygodniu, był taki film pt. ''Siedmiu wspaniałych'', itp. | tygodniu, był taki film pt. ''Siedmiu wspaniałych'', itp. | ||
Linia 228: | Linia 228: | ||
}} | }} | ||
− | Przyspieszenie, w | + | Przyspieszenie, w stosunku do algorytmu naiwnego, następuje dzięki temu, że nie ma potrzeby sprawdzania tablicy dla indeksów istotnie |
wewnątrz przedziału <math>[Lewy[i-1]...(i-1)]</math>. Niech <math>k_i</math> będzie liczbą | wewnątrz przedziału <math>[Lewy[i-1]...(i-1)]</math>. Niech <math>k_i</math> będzie liczbą | ||
tych <math>j</math>, dla których <math>A[j]>=A[i]</math> (w wierszu 3). Wystarczy | tych <math>j</math>, dla których <math>A[j]>=A[i]</math> (w wierszu 3). Wystarczy | ||
Linia 254: | Linia 254: | ||
Złożoność alorytmu istotnie zależy od implementacji instrukcji: <br> | Złożoność alorytmu istotnie zależy od implementacji instrukcji: <br> | ||
<center> <math>k := min \{ j : x \ge A[j]\}</math>. </center> | <center> <math>k := min \{ j : x \ge A[j]\}</math>. </center> | ||
− | Ponieważ wszystko się odbywa w tablicy możemy minimum policzyć łatwo w czasie logarytmicznym stosując szukanie binarne w posortowanej tablicy | + | Ponieważ wszystko się odbywa w tablicy, możemy minimum policzyć łatwo w czasie logarytmicznym stosując szukanie binarne w posortowanej tablicy |
(segment tablicy <math> A[1..i] </math> jest posortowany). | (segment tablicy <math> A[1..i] </math> jest posortowany). | ||
Zatem całkowity koszt algorytm jest <math> O(n \log n) </math> przy dosyć prostej implementacji. | Zatem całkowity koszt algorytm jest <math> O(n \log n) </math> przy dosyć prostej implementacji. | ||
− | Algorytm może,po niewielkim ''dodatkowym wysiłku procesora'', podać najdłuższy malejący | + | Algorytm może, po niewielkim ''dodatkowym wysiłku procesora'', podać najdłuższy malejący |
podciąg, albo też rozkład na minimalną liczbę podciągów niemalejących. | 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 | Nie jest jasne, jak policzyć leksykograficznie minimalny i leksykograficznie maksymalny podciąg | ||
Linia 349: | Linia 349: | ||
następnie szacowaniu sumarycznej złożoności poprzez sumowanie wszystkich zaksięgowanych | następnie szacowaniu sumarycznej złożoności poprzez sumowanie wszystkich zaksięgowanych | ||
kosztów. Operacje | kosztów. Operacje | ||
− | pożyczają | + | pożyczają w pewnym sensie fundusze na pokrycie kosztów z różnych źródeł. Metoda ta będzie |
wykorzystana | wykorzystana | ||
do analizy algorytmu dla interesującego problemu Find-Union. | do analizy algorytmu dla interesującego problemu Find-Union. | ||
Linia 403: | Linia 403: | ||
− | Rozważmy przykłady ilustrujące wykorzystanie potencjału. Najistotniejsze jest określenie składek | + | Rozważmy przykłady ilustrujące wykorzystanie potencjału. Najistotniejsze jest określenie składek. |
==== Tablica dynamiczna==== | ==== Tablica dynamiczna==== |
Wersja z 09:11, 16 lis 2006
Wykład Algorytmy i struktury danych jest poświęcony przede wszystkim koncepcyjnym i strukturalnym metodom efektywnego rozwiązywania
problemów na komputerze. Podstawowym elementem przy rozwiązywaniu zadanego problemu jest dobór
algorytmu i struktury danych. Najważniejszymi aspektami algorytmu są jego poprawność i
złożoność (czasowa i pamięciowa).
W przypadku złożoności czasowej, z reguły wyróżnimy pewną operację dominującą, a 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, operacją dominującą jest przeważnie porównanie dwóch elementów, 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. Zazwyczaj określamy pewien parametr
, będący rozmiarem problemu wejściowego i określamy złożoność jako funkcję , której argumentem jest rozmiar problemu. Z reguły będziemy przyjmować, że każda operacja arytmetyczna na małych liczbach daje się wykonać w jednym kroku. Przez "małe" rozumiemy liczby mające bitów.
Złożoność algorytmu może być rozumiana w sensie złożoności najgorszego przypadku lub złożoności średniej. Złożoność najgorszego przypadku nazywamy złożonością pesymistyczną - jest to maksymalna złożoność dla danych tego samego
rozmiaru .
W praktyce ważniejszą może się okazać złożoność średnia lub oczekiwana. W tym przypadku
jest średnią (oczekiwaną) wartością złożoności dla wszystkich problemów
rozmiaru . Tego typu złożoność zależy istotnie od tego, jaka się pod tym kryje
przestrzeń probabilistyczna danych wejściowych. Z reguły zakładamy, że wszystkie dane
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.
Rozważmy następujący przykład. Przypuśćmy, że chcemy znaleźć pierwszą jedynkę w n-elementowej 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
sprawdzeń. Jest to maksymalna liczba, zatem złożoność pesymistyczna wynosi . 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 asymptotycznego wzrostu funkcji:
oznacza, że dla pewnej stałej . Gdy , to mówimy, że jest liniowa, oraz dla mówimy, że złożoność jest kwadratowa. Jeśli jest wielomianem, to wtedy mówimy o złożoności wielomianowej.
Będziemy używać dodatkowych notacji:
. Były one wprowadzone na wykładach z matematyki dyskretnej.
Przykład
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ć, że "prosty" algorytm się zrobi nieczytelny. Będziemy używać, o ile się da, nieformalnych konstrukcji programistycznych, a w przypadkach bardzo prostych będziemy się starali pisać algorytm w języku Pascalo-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ści.
Pojęcie niezmiennika
Poprawność algorytmu sprowadza się do spełniania określonych niezmienników na różnych etapach wykonywania tego algorytmu. Rozważmy kilka przykładów pozwalających zrozumieć znaczenie niezmiennika.
Załóżmy, że mamy zbiór
składający się z przedmiotów. Niektóre z przedmiotów są czarne, a niektóre białe. Zakładamy, że liczba czarnych przedmiotów jest nieparzysta.
Algorytm Biało-czarne1
1 whiledo begin 2 pobierz dwa przedmioty z S; 3 if przedmioty są różnego koloru then wstaw z powrotem czarny 4 end; 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.
Ponieważ na początku mamy nieparzystą liczbę czarnych przedmiotów, zatem wynikiem jest kolor czarny.
Rozpatrzmy modyfikację tego algorytmu, zakładamy że jest niezerowe.
Algorytm Biało-czarne2
1 whiledo begin 2 pobierz dwa przedmioty z S; 3 if co najmniej jeden jest biały then wstaw z powrotem jeden biały; 4 end; 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.
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 trzech prostych algorytmów pokażemy, że sprawdzanie własności stopu może nie być czynnością trywialną.
Algorytm Suma-Kwadratów-Cyfr
1 whileand do 2 suma kwadratów cyfr liczby ;
Algorytm 6174
//jest czterocyfrową liczbą naturalną niepodzielną przez 1111 // pierwszymi cyframi n mogą być zera 1 while (n<>6174) do begin 2 największa liczba czterocyfrowa, której cyfry są permutacją cyfr liczby ; 3 najmniejsza liczba czterocyfrowa, której cyfry są permutacją cyfr liczby ; 4 5 end
Algorytm Bez-Sensu
1 whiledo 2 if parzyste then 3 else ;
Pierwsze dwa algorytmy mają własność stopu. W pierwszym łatwo to sprawdzić, gdyż dla
następna wartość jest istotnie mniejsza.Pozostawiamy jako ćwiczenie znalezienie najkrótszego koncepcyjnie dowodu 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 Bez-Sensu jest dosyć zagadkowy. Nie wiadomo, czy dla każdej dodatniej liczby całkowitej
ma on własność stopu.Złożoność algorytmów: siedem prostych przykładów
Poniżej dokonamy pobieżnej analizy złożoności obliczeniowej siedmiu algorytmów i wykażemy, że ich złożoności są lepsze od złożoności kwadratowej rozwiązań naiwnych. Dokładne analizy pozostawiamy jako ćwiczenia. Liczba siedem jest losowa - na przykład jest siedem dni w tygodniu, był taki film pt. 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 w tablicy
. Dla uproszczenia przyjmijmy, że w tym ciągu jest przywódca. Łatwo zmodyfikować algorytm tak, by sprawdzał istnienie przywódcy.Algorytm Liczenie-Przywódcy
1;
2 for i 1 to n do 3 if then 4 5 else if then 6 else ;
7 return ;
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ł słabego przywódcę: element, który występuje w tablicy więcej niż
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). 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ść
. Podamy potem również rozwiązanie metodą "dziel i zwyciężaj".
W animacji kolorem żółtym na końcu jest zaznaczony licznik słabego przywódcy, a jego nazwa jest umieszczona w niebieskim kwadraciku.
====Szukanie sumy==== Mamy dane dwie tablice posortowane rosnąco
i liczbę , pytamy, czy istnieją takie, że .Algorytm Szukanie Sumy
1
2 while and do 3 if then return true; 4 else if then 5 else ;
6 return false;
Przyspieszenie jest możliwe dzięki odpowiedniej kolejności sprawdzania
i pominięciu zbędnych sprawdzeń.==== Maksymalny segment ==== Dla tablicy
liczymy maksymalną wartość z zera i ze wszystkich liczb , gdzie .Algorytm Maksymalny-Segment;
1 wynik := 0; sufiks := 0;
2 for i := 1 to n do 3 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
oraz sufiks jest maksymalną sumą segmentu który jest sufiksem przedziału .
Najbliższy mniejszy sąsiad z lewej strony
Dla każdego
zdefiniujmy najbliższego mniejszego sąsiada jako . Dla uproszczenia zakładamy, że dla oraz .Algorytm Najbliższy-Mniejszy-Sąsiad
1 forto n do 2 3 while do 4
Przyspieszenie, w stosunku do algorytmu naiwnego, następuje dzięki temu, że nie ma potrzeby sprawdzania tablicy dla indeksów istotnie wewnątrz przedziału
. Niech będzie liczbą tych , dla których (w wierszu 3). Wystarczy pokazać, że suma wszystkich jest liniowa ze względu na n. Może się zdarzyć, że niektóre mają wartość liniową. Zauważmy jednak, że dany indeks pojawia się co najwyżej raz w sytuacji, gdy , potem będzie "przeskoczony".Najdłuższy malejący podciąg
Niech
będzie ciągiem dodatnich liczb. Następujący algorytm oblicza długość najdłuższego malejącego podciągu (w kolejności od lewej do prawej strony).Algorytm Najdłuższy-Malejący
1
2 for to n do
3 4 5 ;
Poprawność algorytmu nie jest całkowicie oczywista, uzasadnienie można znależć w rozwiązaniu stosownego zadania (patrz Ćwiczenia). Jeśli wejściem jest [5,2,1,3,7] to w momencie gdy algorytm kończy obliczenia tablica A jest równa [7,3,1,0,0]. Zauważmy, że [7,3,1] wcale nie jest podciągiem (w kierunku od lewej do prawej strony) tablicy wejściowej. Tym niemniej wynik jest poprawny.
Złożoność alorytmu istotnie zależy od implementacji instrukcji:
Ponieważ wszystko się odbywa w tablicy, możemy minimum policzyć łatwo w czasie logarytmicznym stosując szukanie binarne w posortowanej tablicy (segment tablicy
jest posortowany). Zatem całkowity koszt algorytm jest przy dosyć prostej implementacji.Algorytm może, po niewielkim dodatkowym wysiłku procesora, podać najdłuższy maleją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
, gdzie jest wynikiem powyższego algorytmu. Możemy się też zastanowić nad efektywnym algorytmem znajdowania liczby wszystkich takich ciągów długości .Proste Pakowanie
Załóżmy, że mamy n pudełek, każde o rozmiarze R, oraz n przedmiotów o rozmiarach
. 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 do zapełnienia.Algorytm Proste-Pakowanie
1
2 for to n do 3 if and
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 lepszych niż kwadratowe. Podamy jeszcze jeden bardzo krótki ciekawy algorytm (chociaż bez żadnego widocznego zastosowania praktycznego).
Permutacje wagowe
Rozważmy wagę
szalkową, na której początkowo obie szalki są puste. Mamy do dyspozycji odważniki o numerach
Następujący algorytm znajduje pewną permutację zgodną Input.
Zakładamy, że
Algorytm Permutacja-Wagowa
12 for downto do 3 if and then 4 5 else 6 ;
Jeśli
= [+1,+1,+1,-1,-1,-1,+1,+1,-1], to = [6, 5, 4, 7, 3, 2, 8, 1, 9]. Ciąg jest zrealizowany przez następujący ciąg wyborów wkładania kolejnego odważnika:gdzie L oznacza połóż na lewą szalkę, P na prawą.
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
zawiera również zera (wagi szalek są równe)? Wtedy nie każdy ciąg jest realizowalny. Jak to można efektywnie sprawdzać?Koszt zamortyzowany
Jeśli mamy ciąg operacji
, 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ówimy tu nic o prawdopodobieństwie - model jest deterministyczny. Na przykład w algorytmie Najbliższy-Mniejszy-Sąsiad rozważmy ciąg operacji: while
Koszt pojedynczej operacji może być liniowy, również sumaryczny koszt ciągu tych operacji
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 z wynikiem negatywnym odbywa się tylko raz dla danej wartości . Możemy powiedzieć, że księgujemy koszt operacji elementom 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 reprezentacji binarnych kolejnych liczb naturalnych od 0 do
przez dodawanie jedynki. 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 jedynek w ciągu 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
będzie pewną liczbą naturalną (włączając zero) odpowiadającą potencjałowi po wykonaniu -tej operacji. Wyglądałoby to mniej tajemniczo, gdybyśmy zamiast pisali .Niech
będzie rzeczywistym kosztem wykonania i-tej operacji.Zakładamy, że potencjał jest początkowo zero, nigdy nie jest ujemny oraz że:
Wtedy całkowity koszt jest tego samego rzędu co
. 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. Jeśli wszystkie wpłaty są takie same, to koszt zamortyzowany jednej operacji jest wpłatą (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) zapłacić 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. 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ę dodaje 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
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 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
. Jeśli pierwszy element stosu lub kolejki w reprezentacji poziomej jest w ciągu na pierwszej pozycji (tzn. pobieramy , stawiamy za ), oraz , to dla pewnego mamy:.
Inaczej mówiąc, pierwszy element kolejki jest na wierzchołku drugiego stosu, a ostatni element kolejki jest na wierzchołku pierwszego stosu.
Operacja wstawiania do Q odpowiada wstawieniu elementu do
, operacja pobrania z Q odpowiada pobraniu elementu z S2 z tym, że jeśli 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 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
Rozważ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 i 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.