Algorytmy i struktury danych/Wstęp: poprawność i złożoność algorytmu: Różnice pomiędzy wersjami
Nie podano opisu zmian |
Nie podano opisu zmian |
||
Linia 1: | Linia 1: | ||
Moduł 1. | Moduł 1. Wstęp: poprawność i złożoność algorytmów.} \myskip Podstawowym elementem przy rozwiązywaniu | ||
zadanego problemu na komputerze jest dobór algorytmu i struktury danych. Najważniejszymi aspektami algorytmu są | zadanego problemu na komputerze jest dobór algorytmu i struktury danych. Najważniejszymi aspektami algorytmu są | ||
jego ''poprawność'' i złożoność. | jego ''poprawność'' i złożoność. Będziemy zasadniczo rozpatrywać tylko złożoność czasową i pamięciową. | ||
W przypadku złożoności czasowej z reguły wyróżnimy pewną operację dominującą i czas | |||
będziemy traktować jako liczbę wykonanych operacji dominujących. W ten sposób nasza analiza będzie | 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. | zależna jedynie od algorytmu a nie od implementacji i sprzętu. |
Wersja z 15:31, 14 lip 2006
Moduł 1. Wstęp: poprawność i złożoność algorytmów.} \myskip Podstawowym elementem przy rozwiązywaniu zadanego problemu na komputerze jest dobór algorytmu i struktury danych. Najważniejszymi aspektami algorytmu są jego poprawność i złożoność. Będziemy zasadniczo rozpatrywać tylko złożoność czasową i pamięciową. W przypadku złożoności czasowej z reguły wyróżnimy pewną operację dominującą i czas
będziemy traktować jako liczbę wykonanych operacji dominujących. W ten sposób nasza analiza będzie zależna jedynie od algorytmu a nie od implementacji i sprzętu. W przypadku sortowania przeważnie operacją dominującą jest porównanie dwóch elementów (mniejsze, równe, mniejsze), a w przypadku przeglądania drzewa jedno przejście w drzewie między wierzchołkami. W przypadku algorytmów tekstowych operacją dominującą jest porównanie dwóch symboli. Z reguły
będziemy przyjmować, że każda operacja arytmetyczna na małych liczbach daje się zrobić w jednym kroku. Przez małe rozumiemy liczby mające 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 arguementem 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ść średnią, lub oczekiwaną, 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ę od tym kryje przestrzeń probablistyczna problemów. Z reguły zakładamy, że wszystkie problemy wejściowe tego samego rozmiaru mogą się pojawić z tym samym prawodopodobień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. \myskip 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łą.
\myskip
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
. \myskip Były one wprowadzone na wykładach z matematyki dyskretnej.
Dla przypomnienia:
, gdy dla nieskończenie wielu i pewnej stąlej zachodzi\ . \myskip Przykład. \\
.\ ,\\ Jeśli , to ale nie zachodzi .
\paragraph{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. \section{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ść.
\subsection*{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. \vskip 0.2cm \noindent Załóżmy, że mamy zbiór składający się z przedmiotów, gdzie jest nieparzyste, niektóre z przedmiotów są czarne a niektóre białe.
Algorytm while
\hspace*{0.5cm} {pobierz dwa przedmioty z ;
\hspace*{0.5cm} if przedmioty są różnego koloru to wstaw z powrotem czarny \hspace*{0.5cm} else usuń oba przedmioty; return kolor ostatniego przedmiotu w ; \myskip
Zaóżmy, że mamy 1000000 czarnych przedmiotów i 1000000001 białych, jaki jest ostatni przedmiot ? Rozpatrzmy niezmiennik: parzystość liczby czarnych przedmiotów.
Ponieważ na początku mamy parzystą liczbę czarnych przedmiotów, zatem na wyjściu mamy kolor biały \myskip Rozpatrzmy modyfikację tego algorytmu:
Algorytm while
\hspace*{0.5cm} {pobierz dwa przedmioty z ;
\hspace*{0.5cm} if co najmniej jeden jest czarny to wstaw z powrotem jeden czarny; \hspace*{0.5cm} else usuń oba przedmioty; return kolor ostatniego przedmiotu w . \myskip Załóżmy, że mamy 10000001 czarnych przedmiotów i 1000000001 białych, jaki jest ostatni przedmiot ? Tym razem rozważmy niezmiennik: znak liczby czarnych 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 czarny. \subsection*{Własność stopu} \noindent 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; \hspace*{0.5cm} while ($(n <>
4)(n<> 1))
\hspace*{0.8cm} n := suma cyfr liczby n;
\myskip
Algorytm 6174; \hspace*{0.5cm} wejściem jest czterocyfrowa liczba naturalna niepodzielna przez 1111
\hspace*{0.5cm} pierwszymi cyframi mogą być zera
\hspace*{0.5cm} while $(n<>
6174)$
\hspace*{0.9cm} n1 := największa liczba czterocyfrowa której cyfry są permutacją cyfr liczby ;
\hspace*{0.9cm} n2 := najmniejsza liczba czterocyfrowa której cyfry są permutacją cyfr liczby ;
\hspace*{0.9cm} n := n1 - n2;\ \myskip
Algorytm Tajemniczy; % \hspace*{0.5cm} n jest dodatnią liczbą naturalną; \hspace*{0.5cm} while $(n<>
1)$
\hspace*{1cm} if n parzyste n := n div 2; else n := 3*n+1; % %---------------------------------- \myskip Pierwsze dwa algorytmy mają własność stopu, w pierwszym łatwo to
sprawdzić gdyż dla następna wartość jest istotnie mniejsza. Podobnie jest w drugim algorytmie. Algorytm Tajemniczy jest dosyć zagadkowy, nie wiadomo, czy dla dowolnego naturalnego dodatniego ma on własność stopu.
\section{Złożoność algorytmów:\ analiza sześciu prostych algorytmów} Na przykładzie sześciu prostych algorytmów pokażemy, jak się analizuje i osiąga złożoność liniową. Dokładniejsze uzasadnienia i analizy odsyłamy do ćwiczeń. Naiwne wersje tych algorytmów działają w czasie kwadratowym. \paragraph{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 zmodyfikować algorytm by sprawdzał istnienie przywódcy. \myskip
\hspace*{.5cm} Algorytm Liczenie-Przywódcy; \begin{verbatim} j:=0; ile:=1; for i:=1 to n do if (ile:=0) {ile:=ile+1; j:=i}; else if (A[i]=A[j]) ile:=ile+1; else ile:=ile-1; return A[j];
\end{verbatim} \myskip
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. \myskip \paragraphSzukanie sumy.\ Mamy dane dwie posortowane rosnąco tablice i liczbę x, pytamy czy są takie, że\ . \myskip
\hspace*{1.5cm} Algorytm Szukanie-Sumy;
\begin{verbatim}
i := 1; j := n; while (i <= n and j > 0) if (A[i]+B[j]=x) return true; else if (A[i]+B[j]<x) i:=i+1; else j:=j-1; return false;
\end{verbatim} \myskip Przyspieszenie jest możliwe dzięki odpowiedniej kolejności sprawdzania i pominięciu zbędnych sprawdzeń. \paragraph{Maksymalny segment.}\ Dla tablicy liczymy maksymalną wartość z zera i ze wszystkich liczb , gdzie .
\myskip \hspace*{1.5cm} Algorytm Maksymalny-Segment; \begin{verbatim} wynik := 0; sufiks := 0; for i := 1 to n do sufiks := max(A[i]+sufiks,0); wynik := max(wynik,sufiks);
\end{verbatim} \myskip Przyspieszenie do algorytmu liniowego następuje dzięki wprowadzeniu dodatkowej zmiennej sufiks. Po każdym zakończeniu pętli for zachodzi: \ wynik jest maksymalną sumą przedziału zawierającego się w oraz sufiks jest maksymalną sumą segmentu który jest sufiksem przedziału . \paragraphNajbliższy mniejszy sąsiad.\ Dla każdego zdefiniujmy najbliższego mniejszego są siada jako: \ {Lewy[i]\ =\ \max \{j<i\ :\ A[j]<A[i]$.}
\noindent Dla uproszczenia zakładamy, że dla oraz . \myskip \hspace*{1.5cm} Algorytm Najbliższy-Mniejszy-Sąsiad; \begin{verbatim} for i := 1 to n do j := i-1; while ( A[j] >= A[i]) j := Lewy[j]; Lewy[i] := j;
\end{verbatim} \myskip Przyspieszenie następuje dzięki temu, że nie ma potrzeby sprawdzania tablicy dla indeksów istotnie pomiędzy i itd. 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. \paragraphNajdł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. \myskip
\hspace{1.5cm} Algorytm Najdłuższy-Malejący; \begin{verbatim} wynik := 1; for i := 1 to n do x := A[i]; A[i]:=0; k := min { j : x > A[j]}; A[k] := x ; wynik := max(k, wynik);
\end{verbatim} \paragraph{Liczba inwersji.}\ Mamy dane dwie posortowane rosnąco tablice , mamy policzyć liczbę par takich, że
\myskip \hspace*{1.5cm} Algorytm Liczba-Inwersji; \begin{verbatim} wynik := 0; j := 0; for i := 1 to n do while (j<n and A[i]>B[j+1]) j := j+1; wynik := wynik + j;
\end{verbatim} \myskip \noindent 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. \section{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ówmy tu nic o prawdopodobieństwie, model jest deterministyczny.
\noindent Na przykład w algorytmie Najbliższy-Mniejszy-Sąsiad rozważmy ciąg operacji \vskip 0.4cm
\centerline{: while } \vskip 0.1cm \noindent 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 szacowania 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. \myskip Typowym przykładem liczenia kosztu w
sposób zamortyzowany jest analiza generacji 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. W tym przykładzie możemy powiedzieć, że analizowaliśmy
koszt tzw. metoda 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.
\subsection*{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. Zakładamy, że potencjał jest początkowo zero, nigdy nie jest ujemny, a każda operacja ma koszt proporcjonalny do . 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 (FUKA, w skrócie). Koszt zamortyzowany jednej operacji jest składką, którą ta operacja wpłaca do funduszu. Dzięki temu, że wiele operacji pobiera z funduszu znacznie mniej niż wpłaca niektóre operacje mogą jednorazowo pobrać dużą kwotę, którą płacą za koszt wykonania. Operacje ubezpieczają się od kosztów ich wykonani. Poszczególne operacje płacą drobne składki , a swój koszt za każdym razem opłacają biorąc pieniądze z FUKA. 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 FUKA nie zbankrutował i kapitał nie zszedł poniżej zera. Możliwa jest również sytuacja gdy FUKA startuje z kapitałem początkowym. Wtedy kapitał ten wlicza się do całkowitego kosztu algorytmu, który się dodajemy do sumy składek.
\myskip Rozważmy przykłady
ilustrujące wykorzystanie potencjału. \paragraphTablica 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 1/4 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 ubezpieczeń (potencjału). Wtedy koszt jednej dużej operacji przepisywania zamortyzuje się zmianą potencjału. \paragraphZastą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, oraz to dla pewnego mamy:
.
\noindent 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, sterujących od pustej kolejki, ma koszt liniowy w tej implementacji. Wystarczy, że każda operacja da do FUKA składkę 3 jednostek. Dowód tego pozostawiamy jako ćwiczenie.