Algorytmy i struktury danych/Wstęp: poprawność i złożoność algorytmu: Różnice pomiędzy wersjami
Nie podano opisu zmian |
|||
Linia 3: | Linia 3: | ||
Podstawowym elementem przy rozwiązywaniu zadanego problemu na komputerze jest dobór | Podstawowym elementem przy rozwiązywaniu zadanego problemu na komputerze jest dobór | ||
algorytmu i struktury danych. Najważniejszymi aspektami algorytmu są jego poprawność i | algorytmu i struktury danych. Najważniejszymi aspektami algorytmu są jego "poprawność" i | ||
złożoność. | złożoność. | ||
Zasadniczo rozpatrywać będziemy tylko złożoność czasową i pamięciową. | |||
W przypadku złożoności czasowej z reguły wyróżnimy pewną operację dominującą | 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 | przypadku sortowania, operacją dominującą jest przeważnie porównanie dwóch elementów (mniejsze, | ||
równe, mniejsze), a w przypadku przeglądania drzewa jedno przejście w drzewie między | równe, mniejsze <font color=red>???</font>), 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. Z reguły będziemy przyjmować, że każda operacja arytmetyczna | symboli. Z reguły będziemy przyjmować, że każda operacja arytmetyczna na małych liczbach daje się | ||
zrobić w jednym kroku. Z reguły określamy pewien parametr <math>n</math>, będący rozmiarem problemu wejściowego i | zrobić w jednym kroku. Przez małe rozumiemy liczby mające <math>O(\log n)</math> bitów. Z | ||
określamy złożoność jako funkcję <math>T(n)</math>, której argumentem jest rozmiar problemu | 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 | Przeważnie rozważamy złożoność pesymistyczną - maksymalną 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ść | 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 33: | Linia 33: | ||
Niech operacją dominującą będzie sprawdzenie jednego elementu. Jeśli nie ma jedynki, to wykonamy | 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>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 | <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łą. | łatwo policzyć, że złożoność średnia jest ograniczona przez stałą. | ||
Do wyrażania złożoności stosujemy opis asymptotycznego 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>f(n)\ =\ O(g(n))</math> oznacza, że <math>f(n) \le c\cdot g(n)</math> dla pewnej stałej | ||
<math>n</math>. | |||
Gdy <math>g(n)=n</math> to mówimy, że <math>f(n)</math> jest liniowa, | 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>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 mówimy o złożoności | kwadratowa. Jeśli <math>g(n)</math> jest wielomianem, to wtedy mówimy o złożoności | ||
wielomianowej. | wielomianowej. | ||
Linia 47: | Linia 47: | ||
Będziemy używać dodatkowych notacji: | Będziemy używać dodatkowych notacji: | ||
<math>f(n)=\Theta(g(n)),\ f(n)=\Omega | <math>f(n)=\Theta(g(n)),\ f(n)=\Omega(n)</math>. | ||
Były one wprowadzone na wykładach z matematyki dyskretnej. | Były one wprowadzone na wykładach z matematyki dyskretnej. | ||
Linia 58: | Linia 57: | ||
<br> | <br> | ||
<math>f(n)=\Omega(g(n)) \ | <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) | <math>\frac{1}{100}\cdot n^2- 2n = \Theta(n^2 )\cdot n^5+2^n = \Theta(2^n), | ||
n!=\Omega(10^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 | '''Konwencje językowe.''' Jaki jest najlepszy język do opisu algorytmu? Jest to przykład problemu | ||
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ć to, że "prosty" algorytm | 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 | się zrobi nieczytelny. Będziemy używać, o ile się da, nieformalnych konstrukcji programistycznych, a | ||
w | w przypadkach bardzo prostych 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. | 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ść. | Oczywiście algorytm musi być poprawny, aby miało sens rozpatrywanie jego złożoność. | ||
=== Pojęcie niezmiennika === | === Pojęcie niezmiennika === | ||
Linia 94: | Linia 98: | ||
Załóżmy, że mamy 10000001 czarnych przedmiotów i 1000000001 białych, jaki jest ostatni | Załóżmy, że mamy 10000001 czarnych przedmiotów i 1000000001 białych, jaki jest ostatni | ||
przedmiot ? Rozpatrzmy niezmiennik: parzystość liczby czarnych przedmiotów. | przedmiot? Rozpatrzmy niezmiennik: parzystość liczby czarnych przedmiotów. | ||
Ponieważ na początku mamy nieparzystą liczbę czarnych przedmiotów, zatem na wyjściu mamy kolor | Ponieważ na początku mamy nieparzystą liczbę czarnych przedmiotów, zatem na wyjściu mamy kolor | ||
Linia 105: | Linia 109: | ||
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 biały to wstaw z powrotem jeden biały; | 3 '''if''' co najmniej jeden jest biały, to wstaw z powrotem jeden biały; | ||
4 '''return''' kolor ostatniego przedmiotu w S. | 4 '''return''' kolor ostatniego przedmiotu w S. | ||
}} | }} | ||
Linia 111: | Linia 115: | ||
Załóżmy, że mamy 10000001 czarnych przedmiotów i 1000000001 białych, jaki jest ostatni | 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. | 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 | (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. | przedmiotem jest przedmiot biały. | ||
Linia 130: | Linia 134: | ||
pierwszymi cyframi mogą być zera | pierwszymi cyframi mogą być zera | ||
'''while''' (n<>6174) | '''while''' (n<>6174) | ||
<math>n1 :=</math> największa liczba czterocyfrowa której cyfry są permutacją cyfr liczby n; | <math>n1 :=</math> największa liczba czterocyfrowa, której cyfry są permutacją cyfr liczby n; | ||
<math>n2 :=</math> najmniejsza liczba czterocyfrowa której cyfry są permutacją cyfr liczby n; | <math>n2 :=</math> najmniejsza liczba czterocyfrowa, której cyfry są permutacją cyfr liczby n; | ||
<math>n := </math>n1 - n2; | <math>n := </math>n1 - n2; | ||
}} | }} | ||
Linia 139: | Linia 143: | ||
}} | }} | ||
Pierwsze dwa algorytmy mają własność stopu, w pierwszym łatwo to sprawdzić gdyż dla | Pierwsze dwa algorytmy mają własność stopu, w pierwszym łatwo to sprawdzić, gdyż dla | ||
<math>n>100</math> następna wartość jest istotnie mniejsza. | <math>n>100</math> następna wartość jest istotnie mniejsza. | ||
Linia 153: | Linia 157: | ||
Na przykładzie siedmiu prostych algorytmów pokażemy, jak się analizuje i osiąga złożoność liniową. | 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 | Po dokładniejsze uzasadnienia i analizy odsyłamy do ćwiczeń. Naiwne wersje tych algorytmów | ||
działają w czasie kwadratowym. | działają w czasie kwadratowym (Wybraliśmy ''"siedem"'' losowo, na przykład jest ''siedem'' dni w | ||
tygodniu ,był taki film pt. ''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. | 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 | 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 | uproszczenia przyjmijmy, że w tym ciągu jest przywódca. Łatwo można modyfikować algorytm, by | ||
sprawdzał istnienie przywódcy. | sprawdzał istnienie przywódcy. | ||
Linia 169: | Linia 174: | ||
'''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 | 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. | 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, | 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. | 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 | W tym przypadku potrzebne są cztery liczniki odpowiadające czterem kandydatom na słabego | ||
przywódcę. | przywódcę. | ||
Algorytm liczy element który jest kandydatem na słabego przywódcę (jeśli istnieje taki przywódca to | 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 | 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. | 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 | 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". | również rozwiązanie metodą "dziel i zwyciężaj". | ||
Linia 237: | Linia 242: | ||
Przyspieszenie następuje dzięki temu, że nie ma potrzeby sprawdzania tablicy dla indeksów istotnie | Przyspieszenie 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>. Wystarczy | 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 | pokazać, że suma wszystkich <math>k_i</math> jest liniowa. Może się zdarzyć, że niektóre | ||
<math>k_i</math> mają wartość | <math>k_i</math> mają wartość | ||
liniową. Zauważmy jednak, że dany indeks <math>j</math> pojawia się co najwyżej raz w sytuacji | 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>, | gdy <math>A[j] >= A[i]</math>, | ||
potem będzie "przeskoczony". | potem będzie "przeskoczony". | ||
Linia 246: | Linia 251: | ||
==== 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 | Niech <math>A[1], A[2],\ldots A[n]</math> będzie ciągiem dodatnich liczb. Następujący algorytm | ||
oblicza długość | 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 258: | Linia 263: | ||
Algorytm może, | Algorytm może, po niewielkim dodatkowym wysiłku fizycznym 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 | ||
malejący o długości k, gdzie | malejący o długości k, gdzie | ||
k jest wynikiem powyższego algorytmu. | 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. | algorytmem znajdowania liczby wszystkich takich ciągów długości k. | ||
Linia 281: | Linia 286: | ||
Naiwne wersje powyższych sześciu algorytmów działają w czasie kwadratowym. W każdym z tych | 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 | algorytmów bardzo proste spostrzeżenia prowadzą do algorytmów liniowych. | ||
Podamy jeszcze jeden bardzo krótki ciekawy algorytm (chociaż bez żadnego widocznego | Podamy jeszcze jeden bardzo krótki ciekawy algorytm (chociaż bez żadnego widocznego | ||
zastosowania praktycznego). | zastosowania praktycznego). | ||
Linia 294: | Linia 298: | ||
w kolejności <math> \Pi </math> na lewą lub prawa szalkę, raz położony odważnik nie zmieia już | 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ć | 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. | niedeterministyczny). Otrzymujemy ciąg wyników ważenia: +1, gdy lewa szalka przeważa, wpp. -1. | ||
Ciąg ten oznaczamy przez Input. | Ciąg ten oznaczamy przez Input. | ||
Mówimy że permutacja <math> \Pi </math> jest zgodna z ciągiem wyników ważeń danych tablicą | Mówimy, że permutacja <math> \Pi </math> jest zgodna z ciągiem wyników ważeń danych tablicą | ||
Input. Zajmiemy się problemem: dany | Input. Zajmiemy się problemem: dany | ||
jest na wejściu ciąg Input wyników ważeń i mamy znalezć | jest na wejściu ciąg Input wyników ważeń i mamy znalezć | ||
Linia 304: | Linia 308: | ||
liczbą znacznie mniejszą. <br> | 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 319: | Linia 323: | ||
Jeśli Input = [+1,+1,+1,-1,-1,-1,+1,+1,-1], to Wynik = [6 5 4 7 3 2 8 1 9]. | 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 Input jest zrealizowany przez następujący ciąg wyborów wkładania kolejnego odważnika: | Ciąg Input jest zrealizowany przez następujący ciąg wyborów wkładania kolejnego odważnika: | ||
<center> L P L P P L L P P </center> | <center> L P L P P L L P P </center>, | ||
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żemy zapisać algorytm krócej | |||
Wynik algorytmu pozostawia pewien niedosyt | |||
{{algorytm|Permutacja-Wagowa1|algorytm_permutacja_wagowa| | |||
<math> p:=1; q:=n;</math> | |||
'''for''' <math>i:=n </math> '''downto''' <math>1</math> '''do''' | |||
'''if''' <math>(i>1)\ \textrm{and}\ (Input[i-1] \ne Input[i])</math>) | |||
<math> Wynik[i]:= q-- </math>; '''else ''' <math> Wynik[i] := p++ </math>; | |||
}} | |||
Wynik algorytmu pozostawia pewien niedosyt. Generujemy dobry wynik, ale | |||
w pewnym sensie jakikolwiek dobry. Nie jest jasne, jak policzyć efektywnie liczbę wszystkich | w pewnym sensie jakikolwiek dobry. Nie jest jasne, jak policzyć efektywnie liczbę wszystkich | ||
permutacji zgodnych z danym ciągiem wyników, albo | permutacji zgodnych z danym ciągiem wyników, albo | ||
znaleźć jakąś szczególną permutację, np. leksykograficznie pierwszą lub ostatnią. | |||
Co będzie | 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 można | 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 | Jeśli mamy ciąg operacji <math>op_1,op_2,\ldots, op_n</math>, to koszt zamortyzowany jednej z | ||
nich jest sumarycznym kosztem | nich jest sumarycznym kosztem | ||
wykonania wszystkich operacji podzielonym przez liczbę operacji, inaczej mówiąc jest to, dla danego | 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, | ciągu operacji, średni koszt jednej z nich. Zauważmy, | ||
że nie | ż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 | Najbliższy-Mniejszy-Sąsiad rozważmy ciąg operacji | ||
Linia 385: | Linia 398: | ||
Zakładamy, że potencjał jest początkowo zero, nigdy nie jest | Zakładamy, że potencjał jest początkowo zero, nigdy nie jest | ||
ujemny | ujemny oraz że: | ||
<center> <math>\Phi_i=\Phi_{i-1}+wplata(i)-wyplata(i)</math> oraz <math> koszt(i)\le wyplata(i) </math>.</center> | <center> <math>\Phi_i=\Phi_{i-1}+wplata(i)-wyplata(i)</math> oraz <math> koszt(i)\le wyplata(i) </math>.</center> | ||
Linia 394: | Linia 407: | ||
Można powiedzieć obrazowo że potencjał jest kapitałem Funduszu Ubezpieczeń Kosztów | 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. | 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. | 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ą | 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 | jednorazowo pobrać dużą kwotę, którą płacą za koszt wykonania. Operacje | ||
ubezpieczają się od kosztów ich | ubezpieczają się od kosztów ich | ||
wykonania. Poszczególne operacje płacą drobne składki <math>wplata(i)</math>, a swój koszt za każdym | |||
razem opłacają biorąc pieniądze z Funduszu. | 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 | 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 | koszt pokryć. Istotne jest jedynie, | ||
żeby Fundusz nie zbankrutował i kapitał nie zszedł poniżej zera. Możliwa jest również sytuacja gdy | żeby Fundusz nie zbankrutował i kapitał nie zszedł poniżej zera. Możliwa jest również sytuacja, gdy | ||
Fundusz startuje | Fundusz startuje | ||
z kapitałem początkowym. Wtedy kapitał ten wlicza | z kapitałem początkowym. Wtedy kapitał ten wlicza | ||
Linia 411: | Linia 424: | ||
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==== | ||
Przypuśćmy, że mamy dynamiczną tablicę. W każdym momencie wiemy, ile elementów w tablicy jest | Przypuśćmy, że mamy dynamiczną tablicę. W każdym momencie wiemy, ile elementów w tablicy jest | ||
aktywnych | aktywnych, elementy nieaktywne zaznaczamy. W | ||
każdej operacji, jeśli liczba elementó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, | dwa razy mniejszą i tam przepisujemy elementy aktywne, starą tablicę zwalniamy. W przeciwnym | ||
który spowoduje przepełnienie tablicy to całą tablicę kopiujemy do tablicy dwa razy większej. | 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. | Początkowo tablica ma rozmiar 1. | ||
Zakładamy, że operacją dominującą jest kopiowanie aktywnego elementu do nowej tablicy. Jeśli | Zakładamy, że operacją dominującą jest kopiowanie aktywnego elementu do nowej tablicy. Jeśli | ||
mamy <math>n</math> operacji to | mamy <math>n</math> operacji, to | ||
całkowity koszt kopiowania jest liniowy. Wystarczy w każdej operacji dać składkę 4 jednostek do | całkowity koszt kopiowania jest liniowy. Wystarczy w każdej operacji dać składkę 4 jednostek do | ||
Funduszu (potencjału). | Funduszu (potencjału). | ||
Wtedy koszt jednej dużej operacji przepisywania zamortyzuje się zmianą potencjału. | Wtedy koszt jednej dużej operacji przepisywania zamortyzuje się zmianą potencjału. | ||
==== Zastąpienie kolejki dwoma stosami==== | ==== Zastąpienie kolejki dwoma stosami==== | ||
Linia 431: | Linia 446: | ||
lub kolejki w | lub kolejki w | ||
reprezentacji poziomej jest w ciągu na pierwszej pozycji (tzn. pobieramy <math>e_1</math>, | reprezentacji poziomej jest w ciągu na pierwszej pozycji (tzn. pobieramy <math>e_1</math>, | ||
stawiamy za <math>e_n</math>), | |||
oraz <math> Q = (e_1,e_2,..,e_k)</math>, to dla pewnego <math>j </math> mamy: | 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>. | ||
Inaczej mówiąc | Inaczej mówiąc pierwszy element kolejki jest na wierzchołku drugiego stosu, a | ||
ostatni | ostatni lement kolejki jest na wierzchołku pierwszego stosu. | ||
Operacja wstawiania do A odpowiada wstawieniu elementu do <math>S1</math>, operacja pobrania | Operacja wstawiania do A odpowiada wstawieniu elementu do <math>S1</math>, operacja pobrania | ||
z Q odpowiada pobraniu elementu z S2 | 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. | z tym, że jeśli <math>S2</math> jest pusty, to przepisujemy najpierw wszystkie elementy z S1 do S2. | ||
Niech operacją dominującą | Niech operacją dominującą | ||
będzie jedna operacja stosowa (wstawienie lub pobranie pojedynczego elementu ze stosu). Wtedy ciąg | będzie jedna operacja stosowa (wstawienie lub pobranie pojedynczego elementu ze stosu). Wtedy ciąg | ||
<math>n</math> operacji | <math>n</math> operacji | ||
kolejkowych | kolejkowych startujących od pustej kolejki ma koszt liniowy w tej implementacji. Wystarczy, że | ||
każda operacja wkłada do Funduszu | każda operacja wkłada do Funduszu | ||
składkę 3 jednostek. Dowód tego pozostawiamy jako ćwiczenie. | 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 | 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. | 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 | 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. | będzie mieć zamortyzowany koszt stały. | ||
Elementy kolejki trzymamy w dwóch stosach S1, S2 tak jak poprzednio. Niezmiennikiem jest to, że | 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 | oba stosy są niepuste, lub mają w sumie co | ||
najwyżej jeden element. Zapewniamy zachodzenie niezmiennika wykorzystując trzeci stos. W | 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 | 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 | drugi jest pusty, korzystając z trzeciego stosu, doprowadzamy do reprezentacji aktualnej kolejki przez | ||
stosy S1 | 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) | liczbę elementów (z dokładnością do 1). Pozostawiamy jako ćwiczenie dowód (metodą potencjału) | ||
tego, że zamortyzowany koszt jest stały. | tego, że zamortyzowany koszt jest stały. | ||
[[Grafika:Example.jpg]] |
Wersja z 16:43, 24 wrz 2006
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ść.
Zasadniczo rozpatrywać będziemy tylko złożoność czasową i pamięciową.
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 (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 bitów. Z reguły 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.
Przeważnie rozważamy złożoność pesymistyczną - maksymalną 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 problemów wejś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.
Rozważmy następujący 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 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.
Dla przypomnienia:
&
, gdy dla nieskończenie wielu i pewnej stałej zachodzi
Przykład
,
Jeśli , to , ale nie
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 przypadkach bardzo prostych 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 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
1 while 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 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 suma kwadratów cyfr liczby ;
Algorytm 6174
wejściem jest czterocyfrowa liczba naturalna niepodzielna przez 1111 pierwszymi cyframi mogą być zera while (n<>6174) największa liczba czterocyfrowa, której cyfry są permutacją cyfr liczby n; najmniejsza liczba czterocyfrowa, której cyfry są permutacją cyfr liczby n; n1 - n2;
Algorytm X
while (n<>1) if n parzyste n div 2; else 3*n+1;
Pierwsze dwa algorytmy mają własność stopu, w pierwszym łatwo to sprawdzić, gdyż dla 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 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 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 tablicą . 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
; for i 1 to n do if ; else if else ;
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ł 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). 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".
W animacji kolorem żółtym na końcu zaznacza się licznik słabego przywódcy, jego nazwa jest w niebieskim kwadraciku.
====Szukanie sumy==== Mamy dane dwie posortowane rosnąco tablice i liczbę x,pytamy czy są takie, że .
Algorytm Szukanie Sumy
Szukanie Sumy while and if return true; else if else ; 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;
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 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
Najbliższy Mniejszy Sąsiad for to n do while
Przyspieszenie 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 . Wystarczy pokazać, że suma wszystkich jest liniowa. 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ższgo malejącego podciągu.
Algorytm Najdłuższy-Malejący
Najdłuższy-Malejący for to n do
Algorytm może, po niewielkim dodatkowym wysiłku fizycznym 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 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 . 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
for to n do 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 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 .
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 , co jest
liczbą znacznie mniejszą.
Następujący algorytm znajduje pewną permutację zgodną.
Zakładamy, że
Algorytm Permutacja-Wagowa
for downto do if ) else ;
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 Input 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ą. Korzystając (tylko częściowo) z dialeku C++, możemy zapisać algorytm krócej
Algorytm Permutacja-Wagowa1
for downto do if ) ; else ;
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 , 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 pojedyńczej 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 kolejnych reprezentacji binarnych kolejnych liczb naturalnych od 0 do , 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 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 operacji. (Wyglądałoby to mniej tajemniczo, gdybyśmy zamiast pisali Bilans(i).
Niech będzie kosztem wykonania operacji i-tej.
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. Operacje ubezpieczają się od kosztów ich wykonania. Poszczególne operacje płacą drobne składki , 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ę 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 lement kolejki jest na wierzchołku pierwszego stosu.
Operacja wstawiania do A 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
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 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. Plik:Example.jpg