|
|
(Nie pokazano 48 wersji utworzonych przez 5 użytkowników) |
Linia 1: |
Linia 1: |
| <font size="6">Wstęp: poprawność i złożoność algorytmów</font>
| | == Forma zajęć == |
| | Wykład (30 godzin) + laboratorium (30 godzin) |
| | == Opis == |
| | Projektowanie i analiza algorytmów. Przegląd podstawowych algorytmów i struktur danych. |
|
| |
|
| | == Sylabus == |
| | === Autorzy === |
| | * Krzysztof Diks — Uniwersytet Warszawski, Wydział Matematyki, Informatyki i Mechaniki |
| | * Adam Malinowski — Uniwersytet Warszawski, Wydział Matematyki, Informatyki i Mechaniki |
| | * Wojciech Rytter — Uniwersytet Warszawski, Wydział Matematyki, Informatyki i Mechaniki |
| | * Tomasz Waleń — Uniwersytet Warszawski, Wydział Matematyki, Informatyki i Mechaniki |
|
| |
|
| __TOC__
| | === Wymagania wstępne === |
| | * Wstęp do programowania |
| | * Metody programowania |
| | * Matematyka dyskretna |
| | * Logika i teoria mnogości |
|
| |
|
| | === Zawartość === |
| | * Podstawowe zasady analizy algorytmów: |
| | ** poprawność |
| | ** złożoność obliczeniowa algorytmu (pesymistyczna, oczekiwana) |
| | **koszt zamortyzowany: metoda potencjału |
| | * Podstawowe techniki i struktury: |
| | **metoda ''dziel i zwyciężaj'' |
| | **metoda zachłanna |
| | **pogramowanie dynamiczne |
| | **transformacyjna konstrukcja algorytmu |
| | **elementarne struktury danych: stosy, kolejki, listy |
| | * Sortowanie: |
| | ** sortowanie przez porównania (InsertionSort, QuickSort, MergeSort) |
| | ** proste kolejki priorytetowe: kopce binarne |
| | **HeapSort |
| | ** sortowanie pozycyjne |
| | ** złożoność problemu sortowania |
| | * Selekcja: |
| | ** algorytm Hoare'a |
| | ** algorytm ''magicznych piątek'' |
| | * Wyszukiwanie i proste słowniki: |
| | ** wyszukiwanie liniowe i binarne |
| | ** prosty słownik: drzewa poszukiwań binarnych |
| | ** haszowanie |
| | * Efektywne implementacje słownika: |
| | ** drzewa AVL |
| | ** drzewa typu ''splay'' |
| | ** B-drzewa |
| | * Złożone struktury danych: |
| | ** wzmocnione kolejki priorytetowe: kolejki dwumianowe, kopce Fibonacciego |
| | ** efektywne sumowanie zbiorów rozłącznych |
| | * Algorytmy grafowe: |
| | ** DFS i jego zastosowania |
| | ** problemy ścieżkowe -- Algorytm Dijkstry |
| | ** minimalne drzewo rozpinające |
| | * Wyszukiwanie wzorca w tekstach: |
| | ** prefikso-sufiksy |
| | ** algorytm Knutha-Morisa-Pratta |
| | *Tekstowe struktury danych: |
| | **tablice sufiksowe |
| | **drzewa sufiksowe |
| | * NP-zupełność: |
| | ** klasa NP |
| | ** problemy NP-trudne i NP-zupełne |
|
| |
|
| Podstawowym elementem przy rozwiązywaniu zadanego problemu na komputerze jest dobór
| | === Moduły=== |
| 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
| | * [[Algorytmy i struktury danych/Wstęp: poprawność i złożoność algorytmu|Wstęp: poprawność i złożoność algorytmu]] ([[ASD Ćwiczenia 1|Ćwiczenia]]) |
| traktować jako liczbę wykonanych operacji dominujących.
| | * [[Algorytmy i struktury danych/Wstęp: elementarne techniki algorytmiczne i struktury danych|Wstęp: elementarne techniki algorytmiczne i struktury danych]] ([[ASD Ćwiczenia 2|Ćwiczenia]]) |
| W ten sposób nasza analiza będzie zależna jedynie od algorytmu a nie od implementacji i sprzętu. W
| | * [[Algorytmy i struktury danych/Sortowanie - BubbleSort, SelectionSort, InsertionSort| Sortowanie przez porównania: BubleSort, SelectionSort i InsertionSort]] ([[ASD Ćwiczenia 3|Ćwiczenia]]) |
| przypadku sortowania przeważnie operacją dominującą jest porównanie dwóch elementów (mniejsze,
| | * [[Algorytmy i struktury danych/Sortowanie: MergeSort, HeapSort i QuickSort| Sortowanie przez porównania: MergeSort, HeapSort i QuickSort ]] ([[ASD Ćwiczenia 4|Ćwiczenia]]) |
| równe, mniejsze), a w przypadku przeglądania drzewa jedno przejście w drzewie między
| | * [[Algorytmy i struktury danych/Sortowanie: dolna granica i sortowanie pozycyjne| Sortowanie: dolna granica, sortowanie pozycyjne]] ([[ASD Ćwiczenia 5|Ćwiczenia]]) |
| wierzchołkami. W przypadku algorytmów tekstowych operacją dominującą jest porównanie dwóch
| | * [[Algorytmy_i_struktury_danych/Selekcja| Selekcja]] ([[ASD Ćwiczenia 6|Ćwiczenia]]) |
| symboli. Z reguły będziemy przyjmować, że każda operacja arytmetyczna na małych liczbach daje się
| | * [[Algorytmy_i_struktury_danych/Wyszukiwanie| Wyszukiwanie]] ([[ASD Ćwiczenia 7|Ćwiczenia]]) |
| zrobić w jednym kroku. Przez małe rozumiemy liczby mające <math>O(\log n)</math> bitów. Z
| | * [[Algorytmy_i_struktury_danych/Słowniki| Słowniki]] ([[ASD Ćwiczenia 8|Ćwiczenia]]) |
| reguły określamy pewien parametr <math>n</math>, będący rozmiarem problemu wejściowego i
| | * [[Algorytmy_i_struktury_danych/Kolejki priorytetowe| Kolejki priorytetowe]] ([[ASD Ćwiczenia 9|Ćwiczenia]]) |
| określamy złożoność jako funkcję <math>T(n)</math>, której argumentem jest rozmiar problemu.
| | * [[Algorytmy_i_struktury_danych/Find-Union| Find-Union]] ([[ASD Ćwiczenia 10|Ćwiczenia]]) |
| | * [[Algorytmy i struktury danych/Algorytmy grafowe - najlżejsze ścieżki| Algorytmy grafowe - najlżejsze ścieżki]] ([[ASD Ćwiczenia 11|Ćwiczenia]]) |
| | * [[Algorytmy i struktury danych/Przeszukiwanie grafów| Algorytmy grafowe - przeszukiwanie grafów]] ([[ASD Ćwiczenia 12|Ćwiczenia]]) |
| | * [[Algorytmy i struktury danych/Algorytmy tekstowe I| Algorytmy tekstowe I]] ([[ASD Ćwiczenia 13|Ćwiczenia]]) |
| | * [[Algorytmy_i_struktury_danych/Algorytmy_tekstowe_II| Algorytmy tekstowe II]] ([[ASD Ćwiczenia 14|Ćwiczenia]]) |
| | * [[Algorytmy i struktury danych/NP-zupełność| NP-zupełność]] ([[ASD Ćwiczenia 15|Ćwiczenia]]) |
|
| |
|
| Przeważnie rozważamy złożoność pesymistyczną - maksymalną złożoność dla danych tego samego
| | === Literatura === |
| rozmiaru <math>n</math>.
| | # ''Algorytmy i struktury danych'', L. Banachowski, K. Diks, W. Rytter, Wydawnictwa Naukowo - Techniczne, 2006. |
| W praktyce ważniejszą może się okazać złożoność średnią, lub oczekiwaną, w tym przypadku
| | # ''Wprowadzenie do algorytmów'', Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein, Wydawnictwa Naukowo - Techniczne, 2004. |
| <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.
| |
| | |
| 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
| |
| <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 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>.
| |
| 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.
| |
| | |
| | |
| Będziemy używać dodatkowych notacji:
| |
| | |
| <math>f(n)=\Theta(g(n)),\ f(n)=\Omega(n)</math>.
| |
| Były one wprowadzone na wykładach z matematyki dyskretnej.
| |
| | |
| | |
| Dla przypomnienia:
| |
| | |
| | |
| <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>
| |
| | |
| {{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>
| |
| 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.
| |
| | |
| == 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 <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|||
| |
| 1 '''while''' <math>|S|> 1</math>
| |
| 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''' <math>|S|> 1</math>
| |
| 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|suma_kwadratow_cyfr
| |
| 1 '''while''' ((n <>4) and (n<> 1))
| |
| 2 <math> n := </math> suma kwadratów cyfr liczby <math> n </math>;
| |
| }}
| |
| | |
| | |
| {{algorytm|6174|algorytm_6174|
| |
| wejściem jest czterocyfrowa liczba naturalna niepodzielna przez 1111
| |
| pierwszymi cyframi mogą być zera
| |
| '''while''' (n<>6174)
| |
| <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>n := </math>n1 - n2;
| |
| }}
| |
| {{algorytm|X|algorytm_X|
| |
| '''while''' (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.
| |
| | |
| 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.
| |
| | |
| == 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ą <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|
| |
| <math> ile := 0 </math>;
| |
| '''for''' i <math>:=</math> 1 to n do
| |
| '''if''' <math>(ile = 0) \{ile := ile+1; j := i\}</math>;
| |
| '''else if''' <math>( A[i]=A[j]) </math> <math> ile:=ile+1; </math> '''else'''
| |
| <math> ile :=ile-1</math>;<br>
| |
| '''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.
| |
| | |
| 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).
| |
| | |
| | |
| <center>
| |
| <flash>file=przywodca_slaby.swf|width=520|height=270</flash></center>
| |
| | |
| 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 <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|
| |
| Szukanie Sumy
| |
| <math>i := 1; j := n;</math>
| |
| '''while''' <math>(i <= n</math> and <math> j > 0)</math>
| |
| '''if''' <math>(A[i]+B[j]=x) </math>'''return''' true; else
| |
| '''if''' <math>(A[i]+B[j]<x) i:=i+1; </math>'''else''' <math>j:=j-1</math>;
| |
| '''return''' false;
| |
| }}
| |
| 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>.
| |
| | |
| '''Algorytm''' Maksymalny-Segment;<br>
| |
| 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 <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 ====
| |
| 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 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|
| |
| Najbliższy Mniejszy Sąsiad
| |
| '''for''' <math>i := 1</math> to n do
| |
| <math> j := i-1;</math>
| |
| '''while''' <math>( A[j] >= A[i]) j := Lewy[j];</math>
| |
| <math>Lewy[i] := j;</math>
| |
| }}
| |
| | |
| 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ą
| |
| 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".
| |
| | |
| ==== 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.
| |
| {{algorytm|Najdłuższy-Malejący|algorytm_najdluzszy_malejacy|
| |
| Najdłuższy-Malejący
| |
| <math>wynik := 1;</math>
| |
| '''for''' <math>i := 1</math> to n do
| |
| <math>x := A[i]; A[i]:=0;</math>
| |
| <math>k := min \{ j : x > A[j]\};</math>
| |
| <math>A[k] := x ;</math>
| |
| <math>wynik := max(k, wynik);</math>
| |
| }}
| |
| | |
| | |
| 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 <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.
| |
|
| |
| {{algorytm|Proste-Pakowanie|algorytm_proste_pakowanie|
| |
| <math>wynik := n;</math>
| |
| '''for''' <math>i := 1</math> to n do
| |
| '''if''' <math>(i < wynik</math> and <math> r[i]+r[wynik] <= R)</math>
| |
| <math>wynik := wynik-1;</math>
| |
| }}
| |
| | |
| <center><flash>file=Klocki.swf|width=450|height=250</flash></center>
| |
| | |
| 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 <math> a_i</math>.
| |
| 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ć
| |
| 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ą.
| |
| Zakładamy że
| |
| | |
| <center> <math> a_1<a_2<a_3<\ldots a_n</math></center>
| |
| | |
| {{algorytm|Permutacja-Wagowa|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; q:=q-1; </math>
| |
| '''else '''
| |
| <math> Wynik[i] := p; p:=p+1</math>;
| |
| }}
| |
| | |
| 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|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
| |
| 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 <math>op_1,op_2,\ldots, op_n</math> 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
| |
| | |
| <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
| |
| pesymistyczny koszt jednej 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
| |
| 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.
| |
| | |
| Typowym przykładem liczenia kosztu w sposób zamortyzowany jest analiza generacji kolejnych
| |
| 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
| |
| jedną jedynkę. Ponieważ w sumie wstawiliśmy <math>2^n-1</math> jedynek w ciągu <math>2^n-
| |
| 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.
| |
| | |
| === 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>|\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.
| |
| | |
| 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 <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.
| |
| | |
| 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 <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====
| |
| Jedną kolejkę Q można zastąpić dwoma stosami <math>S1,\ S2</math>. Jeśli pierwszy element stosu
| |
| 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>.
| |
| | |
| 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====
| |
| 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]]
| |