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

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Tprybick (dyskusja | edycje)
Nie podano opisu zmian
Tprybick (dyskusja | edycje)
Nie podano opisu zmian
Linia 1: Linia 1:
Moduł 1. Wstęp: poprawność i złożoność algorytmów.} \myskip Podstawowym elementem przy rozwiązywaniu
\noindent {\Large \bf
zadanego problemu na komputerze jest dobór algorytmu i struktury danych. Najważniejszymi aspektami algorytmu są
%Rozdział wstępny:
jego ''poprawność'' i złożoność. Będziemy zasadniczo rozpatrywać tylko złożoność czasową i pamięciową.
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 danch. Najważnejszymi aspektami algorytmu są
jego ''poprawnoość'' 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
W przypadku złoźoności czasowej z reguły wyróżnimy pewną operację dominującą i czas
będziemy traktować jako liczbę wykonanych operacji dominujących. W ten sposób nasza analiza będzie
  zależna jedynie od algorytmu a nie od implementacji i sprzętu.
  W przypadku sortowania przeważnie operacją dominującą jest porównanie dwóch elementów
  (mniejsze, równe, mniejsze), a w przypadku przeglądania drzewa jedno przejście w drzewie między
  wierzchołkami. W przypaku algorytmów tekstowych operacją dominującą jest porównanie dwóch symboli.
Z reguły
będziemy przyjmować, że każda operacja arytmetyczna na małych liczbach daje się zrobić w jednym kroku.
będziemy przyjmować, że każda operacja arytmetyczna na małych liczbach daje się zrobić w jednym kroku.
Przez ''małe'' rozumiemy liczby mające <math>O(\log n)</math> bitów.
Przez ''małe'' rozumiemy liczby mające $O(\log n)$ bitów.
Z reguły określamy pewien parametr <math>n</math>, będący rozmiarem problemu wejściowego i
  Z reguły określamy pewien parametr $n$, będący rozmiarem problemu wejściowego i
określamy złożoność jako funkcję <math>T(n)</math>, której arguementem jest rozmiar problemu.
określamy złożoność jako funkcję $T(n)$, której arguementem jest rozmiar problemu.


Przeważnie rozważamy złożoność pesymistyczną -maksymalną złożoność dla danych tego samego rozmiaru
Przewaźnie rozważamy złożoność pesymistyczną -maksymalną złożoność dla danych tego samego rozmiaru
<math>n</math>.
$n$.
W praktyce ważniejszą może się okazać złożoność średnią, lub oczekiwaną, w tym przypadku <math>T(n)</math> jest średnią (oczekiwaną) wartością
W praktyce wańiejsą może się okaać złożoność średnią, lub oczekiwaną, w tym przypadku $T(n)$ jest średnią (oczekiwaną) wartością
złożoności dla wszystkich problemów rozmiaru <math>n</math>. Tego typu złożoność zależy  istotnie od tego, jaka się od tym kryje
złożoności dla wszystkich problemów rozmiaru $n$. 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
przetrzeń probablistyczna problemów. Z reguły zakładamy, że wszystkie problmey wejściowe tego
samego rozmiaru mogą się pojawić z tym samym prawodopodobieństwem. Jednakże jest to często mało
samego rozmiaru mogą się pojawić z tym samym prawodpodobieństwem. Jednakże jest to często mało
realistyczne założenie. Przestrzeń probabilistyczna danych wejściowych może być bardzo skomplikowana,
realistyczne założenie. Przestrzeń probabilistyczna danych wejćiowych może być bardzo skomplikowana,
prowadzić to może do bardzo trudnych (i wykraczających poza ten kurs) analiz.
prowadzić to może do bardzo trudnych (i wykraczających poza ten kurs) analiz.
\myskip '''Przykład.'''\
\myskip {\bf Przykład.}\
Przypuśćmy, że chcemy znaleźć pierwszą jedynkę w tablicy zerojedynkowej i nasz algorytm przegląda
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
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
sprawdzenie jednego elementu. Jeśli nie ma jedynki, to wykonamy $n$ 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
liczba, zatem złożoność pesymistyczna wynosi $T(n)=n$. Jeśli każdy cią binarny jest dany z tym samym
prawdopodobieństwem to łatwo policzyć, że złożoność średnia jest ograniczona przez stałą.
prawdopodobieństwem to łatwo policzyć, że złożoność średnia jest ograniczona przez stałą.
\myskip
\myskip
Do wyrażania złożoności stosujemy opis asymptotycznego wzrostu funkcji:
Do wyrażania złożoności stosujemy opis aymptotycznego wzrostu funkcji:


<math>f(n)\ =\ O(g(n))</math> oznacza
  $f(n)\ =\ O(g(n))$ oznacza
że <math>f(n) \le c\cdot g(n)</math> dla pewnej stałej <math>n</math>. \\
że $f(n) \le c\cdot g(n)$ dla pewnej stałej $n$. \\
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
Gdy $g(n)=n$ to mówimy, że $f(n)$ jest liniowa, oraz dla $g(n)=n^2$ mówimy, że złożoność $f(n)$ jest
kwadratowa. Jeśli <math>g(n)</math> jest wielomianem to wtedy mówimy o złożoności wielomianowej. Będziemy używać
kwadratowa. Jeśli $g(n)$ jest wielomianem to wtedy mówimy o złożoności wielomianowej. Będziemy używać
dodatkowych notacji
dodatkowych notacji


<math>f(n)=\Theta(g(n)),\ f(n)=\Omega(n)</math>. \myskip Były one wprowadzone na wykładach z matematyki dyskretnej.
$f(n)=\Theta(g(n)),\ f(n)=\Omega(n)$. \myskip Były one wprowadzone na wykładach z matematyki dyskretnej.
Dla przypomnienia:
Dla przypomnienia:
<math><math>f(n)=\Omega(g(n)</math>, gdy  dla nieskończenie wielu <math>n</math> i pewnej stąlej <math>c>0</math> zachodzi\  <math>f(n) \ge c\cdot g(n)</math>.
$$f(n)=\Theta(g(n)) \Leftrightarrow f(n)=O(g(n))\ \& \ g(n)=O(f(n))$$
<!--%--------------------------------------
$f(n)=\Omega(g(n)$, gdy  dla nieskończenie wielu $n$ i pewnej stąlej $c>0$ zachodzi\  $f(n) \ge c\cdot g(n)$.
-->\myskip '''Przykład'''. \\
%--------------------------------------
<math>\frac{1}{100}\cdot n^2- 2n = \Theta(n^2 )</math>.\
\myskip {\bf Przykład}. \\
<math>n^5+2^n = \Theta(2^n), n!=\Omega(10^n)</math>,\\
$\frac{1}{100}\cdot n^2- 2n = \Theta(n^2 )$.\
Jeśli <math>f(n)=(1+(-1)^n)\cdot n</math>, to <math>f(n)=\Omega(n),\ f(n)=O(n)</math> ale nie zachodzi <math>f(n)=\Theta(n)</math>.
$n^5+2^n = \Theta(2^n), n!=\Omega(10^n)$,\\
Jeśli $f(n)=(1+(-1)^n)\cdot n$, to $f(n)=\Omega(n),\ f(n)=O(n)$ ale nie zachodzi $f(n)=\Theta(n)$.
%*********************************************************
\paragraph{Konwencje językowe.} Jaki jest najlepszy język do opisu algorytmu ? Jest to przykład problemu
\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
nierozstrzygalnego. Niewątpliwie język ojczysty jest najlepszym językiem potocznym , a ulubiony język
Linia 48: Linia 60:
nieformalnych konstrukcji programistycznych, a w przypadku bardzo prostych algorytm będziemy się starali pisać
nieformalnych konstrukcji programistycznych, a w przypadku bardzo prostych algorytm będziemy się starali pisać
algorytm w języku C-podobnym.
algorytm w języku C-podobnym.
<!--%
%
-->\section{Poprawność algorytmu:\ niezmienniki, własność stopu}
\section{Poprawność algorytmu:\ niezmienniki, własność stopu}
Przez poprawność algorytmu rozumiemy to, że
Przez poprawność algorytmu rozumiemy to, że
daje on takie odpowiedzi jakich oczekujemy.
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ść.
<!--%
%
-->\subsection*{Pojęcie niezmiennika}
\subsection*{Pojęcie niezmiennika}
Poprawność algorytmu sprowadza się do spełniania określonych niezmienników na różnych etapachtego algorytmu.
Poprawność algorytmu sprowadza się do spełniania określonych niezmienników na różnych etapach
tego algorytmu.
Rozważmy kilka przykładów pozwalających zrozumieć znaczenie niezmiennika. \vskip 0.2cm \noindent
Rozważmy kilka przykładów pozwalających zrozumieć znaczenie niezmiennika. \vskip 0.2cm \noindent
Załóżmy, że mamy zbiór <math>S</math> składający się z <math>n</math> przedmiotów, gdzie <math>n</math> jest nieparzyste,
Załóżmy, że mamy zbiór $S$ składający się z $n$ przedmiotów, gdzie $n$ jest nieparzyste,
niektóre z przedmiotów są czarne a niektóre białe.
niektóre z przedmiotów są czarne a niektóre białe.


'''Algorytm''' while  <math>|S|>1</math>
{\bf Algorytm}
 
while  $|S|>1$
 
\hspace*{0.5cm} {pobierz dwa przedmioty z $S$;
 
\hspace*{0.5cm}  {\bf if} przedmioty są różnego koloru to wstaw z powrotem czarny


  \hspace*{0.5cm} {pobierz dwa przedmioty z <math>S</math>;
  \hspace*{0.5cm} {\bf else} usuń oba przemioty;


\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 <math>S</math>;
  return kolor ostatniego przedmiotu w $S$;
  \myskip
  \myskip
Zaóżmy, że mamy 1000000 czarnych przedmiotów i  1000000001 białych, jaki jest ostatni przedmiot ? Rozpatrzmy
Zaóżmy, że mamy 1000000 czarnych przedmiotów i  1000000001 białych, jaki jest ostatni przedmiot ? Rozpatrzmy
Linia 73: Linia 92:




'''Algorytm''' while  <math>|S|>1</math>
{\bf Algorytm}
 
while  $|S|>1$
 
\hspace*{0.5cm} {pobierz dwa przedmioty z $S$;
 
\hspace*{0.5cm}  {\bf if} co najmniej jeden jest czarny to wstaw z powrotem jeden czarny;


\hspace*{0.5cm} {pobierz dwa przedmioty z <math>S</math>;
\hspace*{0.5cm}       {\bf else} usuń oba przedmioty;


\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 <math>S</math>.
   return kolor ostatniego przedmiotu w $S$.
\myskip Załóżmy, że mamy  10000001 czarnych przedmiotów i  1000000001 białych, jaki jest ostatni przedmiot
\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
? 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.
równa zeru, 1 jeśli jest większa od zera.) Zatem ostatnim przedmiotem jest przedmiot czarny.
<!--%
%
-->\subsection*{Własność stopu}
\subsection*{Własność stopu}
\noindent Podstawowym elementem poprawności algorytmu jest to, że ma on własność stopu: dla poprawnych
\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,
danych wejściowych da odpowiedź po skończonym czasie. Na przykładzie dwóch prostych algorytmów pokażemy,
Linia 88: Linia 113:




     '''Algorytm''' Suma-Kwadratów-Cyfr;    \hspace*{0.5cm} while ($(n <>
     {\bf Algorytm} Suma-Kwadratów-Cyfr;
4)<math> and </math>(n<> 1))
 
     \hspace*{0.5cm} while ($(n <>
4)$ and $(n<> 1)$)


     \hspace*{0.8cm} n := suma cyfr liczby n;
     \hspace*{0.8cm} n := suma cyfr liczby n;
<!--%
%
-->\myskip
\myskip
 
    {\bf Algorytm} 6174;


    '''Algorytm''' 6174;     \hspace*{0.5cm} wejściem jest czterocyfrowa liczba naturalna niepodzielna przez 1111
     \hspace*{0.5cm} wejściem jest czterocyfrowa liczba naturalna niepodzielna przez 1111


       \hspace*{0.5cm} pierwszymi cyframi mogą być zera
       \hspace*{0.5cm} pierwszymi cyframi mogą być zera




     \hspace*{0.5cm} '''while''' $(n<>
     \hspace*{0.5cm} {\bf while} $(n<>
6174)$
6174)$


     \hspace*{0.9cm} n1 := największa liczba czterocyfrowa której cyfry są permutacją cyfr liczby <math>n</math>;
     \hspace*{0.9cm} n1 := największa liczba czterocyfrowa której cyfry są permutacją cyfr liczby $n$;


     \hspace*{0.9cm} n2 := najmniejsza liczba czterocyfrowa której  cyfry są permutacją cyfr liczby <math>n</math>;
     \hspace*{0.9cm} n2 := najmniejsza liczba czterocyfrowa której  cyfry są permutacją cyfr liczby $n$;


     \hspace*{0.9cm} n := n1 - n2;\
     \hspace*{0.9cm} n := n1 - n2;\
     \myskip
     \myskip


     '''Algorytm''' Tajemniczy;  %  \hspace*{0.5cm} n jest dodatnią liczbą naturalną;
     {\bf Algorytm} Tajemniczy;
<!--%
 
-->    \hspace*{0.5cm} while $(n<>
  %  \hspace*{0.5cm} n jest dodatnią liczbą naturalną;
%
    \hspace*{0.5cm} while $(n<>
1)$
1)$


Linia 119: Linia 150:
     %----------------------------------
     %----------------------------------
       \myskip Pierwsze dwa algorytmy mają własność stopu, w pierwszym łatwo to
       \myskip Pierwsze dwa algorytmy mają własność stopu, w pierwszym łatwo to
sprawdzić gdyż dla <math>n>100</math> następna wartość jest istotnie mniejsza. Podobnie jest w drugim algorytmie.
sprawdzić gdyż dla $n>100$ następna wartość jest istotnie mniejsza. Podobnie jest w drugim algorytmie.
Algorytm Tajemniczy jest dosyć zagadkowy, nie wiadomo, czy dla dowolnego naturalnego dodatniego <math>n</math> ma on
Algorytm Tajemniczy jest dosyć zagadkowy, nie wiadomo, czy dla dowolnego naturalnego dodatniego $n$ ma on
własność stopu.
własność stopu.


\section{Złożoność algorytmów:\ analiza sześciu prostych algorytmów}
\section{Złożoność algorymó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ą.
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
Dokładniejsze uzasadnienia i analizy odsyłamy do ćwiczeń.  Naiwne wersje tych algorytmów działają w czasie
kwadratowym.
kwadratowym.
<!--%--------------------------------------------------------
%--------------------------------------------------------
-->\paragraph{Przywódca ciągu.}\ Przywódcą ciągu jest element, który występuje w ciągu więcej
\paragraph{Przywódca ciągu.}\ Prywó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ą
razy niź połowa długości tego ciągu. Naszym problemem jest policzenie przywódcu ciągu danego tablicą
<math>A[1..n]</math>. Dla uproszczenia przyjmijmy, że w tym ciągu jest przywódca. łatwo można zmodyfikować algorytm
$A[1..n]$. Dla uprosazczenia przyjmijmy, że w tym ciągu jest przywódca. Łatwo można zmodyfikować algorytm
by sprawdzał istnienie przywódcy. \myskip
by sprawdzał istnienie przywódcy. \myskip
     \hspace*{.5cm}
     \hspace*{.5cm}
     '''Algorytm''' Liczenie-Przywódcy;
     {\bf Algorytm} Liczenie-Przywódcy;
     \begin{verbatim}
     \begin{verbatim}
     j:=0; ile:=1;
     j:=0; ile:=1;
Linia 142: Linia 173:
\end{verbatim}
\end{verbatim}
\myskip
\myskip
  Przyspieszenie wynika z następującej własności problemu:
  Przyspieszenie wynika z następującejwłasności problemu:
\ jeśli mamy dwa różne elementy w tablicy to możemy je usunąć i przywódca pozostanie taki sam. \myskip
\ jeśli mamy dwa różne elementy w tablicy to możemy je usunąć i przywódca pozostanie taki sam. \myskip
<!--%
%
--><!--%
%
-->\paragraph'''Szukanie sumy.'''\ Mamy dane dwie posortowane rosnąco tablice <math>A,B</math> i liczbę x,
\paragraph{\bf Szukanie sumy.}\ Mamy dane dwie posortowane rosnąco tablice $A,B$ i liczbę x,
pytamy  czy są  <math>a \in A,\ b \in B</math> takie, że\ <math>x=a+b</math>.
pytamy  czy są  $a \in A,\ b \in B$ takie, że\ $x=a+b$.
<!--%
%
-->\myskip
\myskip
         \hspace*{1.5cm} '''Algorytm''' Szukanie-Sumy;
         \hspace*{1.5cm} {\bf Algorytm} Szukanie-Sumy;
\begin{verbatim}
\begin{verbatim}
         i := 1; j := n;
         i := 1; j := n;
Linia 158: Linia 189:
         return false;
         return false;
\end{verbatim}
\end{verbatim}
\myskip Przyspieszenie jest możliwe dzięki odpowiedniej kolejności sprawdzania <math>i,j</math> i pominięciu zbędnych
\myskip Przyspieszenie jest możliwe dzięki odpowiedniej kolejności sprawdzania $i,j$ i pominięciu zbędnych
sprawdzeń.
sprawdzeń.
<!--%
%
-->\paragraph{Maksymalny segment.}\  Dla tablicy <math>A[1..n]</math> liczymy
\paragraph{Maksymalny segment.}\  Dla tablicy $A[1..n]$ 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>.
maksymalną wartość z zera i ze wszystkich liczb $\sum_{k=i}^j\ A[k]$, gdzie $1\le i\le j\le n$.
  \myskip
  \myskip
         \hspace*{1.5cm} '''Algorytm''' Maksymalny-Segment;
         \hspace*{1.5cm} {\bf Algorytm} Maksymalny-Segment;
         \begin{verbatim}
         \begin{verbatim}
         wynik := 0; sufiks := 0;
         wynik := 0; sufiks := 0;
Linia 173: Linia 204:
\myskip Przyspieszenie do algorytmu liniowego następuje dzięki wprowadzeniu dodatkowej zmiennej sufiks. Po
\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
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>.
$[1..i]$ oraz sufiks jest maksymalną sumą segmentu który jest sufiksem przedziału $[1..i]$.
<!--%
%
-->\paragraph'''Najbliższy mniejszy sąsiad.'''\
\paragraph{\bf Najbliższy mniejszy sąsiad.}\
<!--%
%
-->Dla każdego <math>i > 1</math> zdefiniujmy najbliższego mniejszego są siada <math>i</math> jako: \ {Lewy[i]\ =\ \max \{j<i\ :\
Dla każdego $i > 1$ zdefiniujmy najbliższego mniejszego są siada $i$ jako: \ {$Lewy[i]\ =\ \max \{j<i\ :\
A[j]<A[i]$.}
A[j]<A[i]$.}


  \noindent Dla uproszczenia zakładamy, że  <math>A[i]> 0</math> dla <math>i>0</math> oraz <math>A[0]=0</math>.  \myskip
  \noindent Dla uproszczenia zakładamy, że  $A[i]> 0$ dla $i>0$ oraz $A[0]=0$.  \myskip
         \hspace*{1.5cm} '''Algorytm''' Najbliższy-Mniejszy-Sąsiad;
         \hspace*{1.5cm} {\bf Algorytm} Najbliższy-Mniejszy-Sąsiad;
         \begin{verbatim}
         \begin{verbatim}
         for i := 1 to n do
         for i := 1 to n do
Linia 189: Linia 220:
\end{verbatim}
\end{verbatim}
\myskip Przyspieszenie następuje dzięki temu, że nie ma potrzeby sprawdzania tablicy dla indeksów istotnie
\myskip Przyspieszenie następuje dzięki temu, że nie ma potrzeby sprawdzania tablicy dla indeksów istotnie
pomiędzy <math>i-1</math> i <math>Lewy[i-1]</math> itd. Niech <math>k_i</math> będzie liczbą tych <math>j</math> dla których <math>A[j]>=A[i]</math>. Wystarczy
pomiędzy $i-1$ i $Lewy[i-1]$ itd. Niech $k_i$ będzie liczbą tych $j$ dla których $A[j]>=A[i]$. Wystarczy
pokazać, że suma wszystkich <math>k_i</math> jest liniowa. Może się zdarzyć, że niektóre <math>k_i</math> mają wartość
pokazać, że suma wszystkich $k_i$ jest liniowa. Może się zdarzyć, że niektóre $k_i$ 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>,
liniową. Zauważmy jednak, że dany indeks $j$ pojawia się co najwyżej raz w sytuacji gdy $A[j] >= A[i]$,
potem będzie ''przeskoczony''.
potem będzie ''przeskoczony''.
<!--%****************
%****************
--><!--%
%
-->\paragraph'''Najdłuższy malejący podciąg.'''\
\paragraph{\bf 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
  Niech $A[1], A[2],\ldots A[n]$ będzie ciągiem dodatnich liczb. Następujący algorytm oblicza
długość najdłuższego malejącego podciągu. \myskip
długość najdłuższgo malejącego podciągu. \myskip
         \hspace{1.5cm}
         \hspace{1.5cm}
         '''Algorytm''' Najdłuższy-Malejący;
         {\bf Algorytm} Najdłuższy-Malejący;
         \begin{verbatim}
         \begin{verbatim}
         wynik := 1;
         wynik := 1;
Linia 207: Linia 238:
             wynik := max(k, wynik);
             wynik := max(k, wynik);
\end{verbatim}
\end{verbatim}
<!--%
%
-->\paragraph{Liczba inwersji.}\ Mamy dane dwie posortowane rosnąco tablice <math>A[1..n],B[1..n]</math>, mamy policzyć liczbę par
\paragraph{Proste Pakowanie.}\ Załóżmy, że mamy n pudełek, każde o rozmiarze R, oraz n przedmiotów o
<math>i,j</math> takich, że <math>A[i]>B[j]</math>
rozmiarach $R \ge r[1]\ge r[2]\ldots \ge r[n]$. Mamy włożyć przedmioty do pudełek, co najwyżej dwa do
jednego pudełka.  Pozostawimay jako ćwiczenie analizę następującego algorytmu, który oblicza minimalną
liczbę pudełek.
  \myskip
  \myskip
         \hspace*{1.5cm} '''Algorytm''' Liczba-Inwersji;
         \hspace*{1.5cm} {\bf Algorytm} Proste-Pakowanie;
         \begin{verbatim}
         \begin{verbatim}
         wynik := 0; j := 0;
         wynik := n;
         for i := 1 to n do
         for i := 1 to n do
             while (j<n and A[i]>B[j+1]) j := j+1;
             if (i < wynik and r[i]+r[wynik] <= R)
            wynik := wynik + j;
                        wynik := wynik-1;
\end{verbatim}
\end{verbatim}
\myskip \noindent Naiwne wersje powyższych sześciu algorytmów  działają w czasie kwadratowym. W każdym z
\myskip \noindent Naiwne wersje powyzszych 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.
tych algorytmów bardzo proste spostrzeżenia prowadą do algorytmów liniowych.
<!--%------------------------------------------------------------------------
%------------------------------------------------------------------------
-->\section{Koszt zamortyzowany}
\section{Koszt zamortyzowany}
Jeśli mamy ciąg operacji <math>op_1,op_2,\ldots, op_n</math> to koszt zamortyzowany jednej z nich jest sumarycznym kosztem
Jeśli mamy ciąg operacji $op_1,op_2,\ldots, op_n$ 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
wykonania wsszystkich operacji podzielonym przez liczbę operacji, inaczej mówiąc jest to, dla danego ciągu
operacji, średni koszt jednej z nich. Zauważmy, że nie mówmy tu nic o prawdopodobieństwie, model jest
operacji, średni koszt jednej z nich. Zauważmy, że nie mówmy tu nic o prawdopodobieństwie, model jest
deterministyczny.
deterministyczny.
Linia 229: Linia 262:


\noindent Na przykład w algorytmie Najbliższy-Mniejszy-Sąsiad rozważmy ciąg operacji \vskip 0.4cm
\noindent Na przykład w algorytmie Najbliższy-Mniejszy-Sąsiad rozważmy ciąg operacji \vskip 0.4cm
\centerline{<math>op_i</math>: while <math>( A[j] >= A[i])\ j = Lewy[j]</math>} \vskip 0.1cm \noindent  Koszt pojedynczej operacji
\centerline{$op_i$: while $( A[j] >= A[i])\ j = Lewy[j]$} \vskip 0.1cm \noindent  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
może być liniowy, również sumaryczny koszt ciągu tych operacji $op_1,op_2,\ldots, op_n$ jest liniowy. Zatem
pesymistyczny koszt jednej operacji jest tutaj liniowy, natomiast zamortyzowany koszt jednej operacji jest
pesymistyczny koszt jdnej operacji jest tutaj liniowy, natomiast zamortyzowany koszt jednej operacji jest
ograniczony przez stałą. W tym przypadku wiemy, że każde sprawdzenie <math>A[j]>=A[i])</math> z wynikiem negatywnym
ograniczony przez stałą. W tym przypadku wiemy, że każde sprawdzenie $A[j]>=A[i])$ z wynikiem negatywnym
odbywa się tylko raz dla danej wartości <math>j</math>. Możemy powiedzieć, że księgujemy koszt operacji elementom <math>j</math>
odbywa się tylko raz dla danej wartości $j$. Możemy powiedzieć, że księgujemy koszt operacji elementom $j$
o tej własności. Nieformalna metoda księgowania kosztów polega na rozdzielaniu (księgowaniu) kosztu, a
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
następnie szacowania sumarycznej złożoności poprzez sumowanie wszystkich zaksęgowanych kosztów. Operacje
pożyczają, w pewnym sensie, fundusze na pokrycie kosztów z różnych źródeł. Metoda ta będzie wykorzystana
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
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 <math>2^n-1</math>,
sposób zamortyzowany jest analiza generacji reprezentacji binarnych kolejnych liczb naturlanych od 0 do $2^n-1$,
dodając jedynkę. W jednym kroku zastępujemy najdłuższy ciąg jedynek od końca zerami, następnie wstawiamy
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
jedną jedynkę. Ponieważ w sumie wstawiliśmy $2^n-1$ jedynek w ciągu $2^n-1$ operacji, to zamortyzowana
liczba operacji zamiany zera na jedynkę wynosi 1. W tym przykładzie możemy powiedzieć, że analizowaliśmy
liczba operacji zamiany zera na jedynke 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
koszt tzw. metdoda ''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
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
koszt jest proporcjonalny do liczby przedmiotów włożonych. W przypadku generowania liczb binarnych do magazynu
Linia 248: Linia 281:
\subsection*{Potencjał - Fundusz Ubezpieczeń Kosztów Algorytmicznych}
\subsection*{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ą
Metodę magazynu można uogólnić na tzw. metodę potencjału. Niech $\Phi_i$ będzie pewną liczbą naturalną
(włączając zero) odpowiadającą potencjałowi po wykonaniu <math>i</math> operacji. Zakładamy, że potencjał jest
(włączając zero) odpowiadającą potencjałowi po wykonaniu $i$ operacji. Zakładamy, że potencjał jest
początkowo zero, nigdy nie jest ujemny, a każda operacja <math>op_i</math> ma koszt proporcjonalny do
początkowo zero, nigdy nie jest ujemny, a każda operacja $op_i$ ma koszt proporcjonalny do
<math>c_i+|\Phi_i-\Phi_{i-1}|</math>. Wtedy całkowity koszt jest tego samego rzędu co <math> \sum c_i</math>. W naszych poprzednich
$c_i+|\Phi_i-\Phi_{i-1}|$. Wtedy całkowity koszt jest tego samego rzędu co $ \sum c_i$. W naszych poprzednich
przykładach rozmiar magazynu jest w tym sensie potencjałem.
przykładach rozmiar magazynu jest w tym sensie potencjałem.


Linia 259: Linia 292:
w skrócie). Koszt zamortyzowany jednej operacji jest składką, którą ta operacja wpłaca do funduszu. Dzięki
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
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
pobrać dużą kwotę, kórą płacą za koszt wykonania. Operacje $op_i$ 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ą
wykonani. Poszczególne operacje płacą drobne składki $c_i$, 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,
bioróac pieniądze z FUKA. Czasmi koszt operacji jest duży ale do tego czasu wpłacono tyle drobnych składek,
że możemy ten koszt pokryć. Istotne jest jedynie żeby FUKA nie zbankrutował i kapitał nie zszedł poniżej
ż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
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.
się do całkowitego kosztu algorytmu, który się dodajemy do sumy składek.
<!--%------------------------------
%------------------------------
--> \myskip Rozważmy  przykłady
\myskip Rozważmy  przykłady
ilustrujące wykorzystanie potencjału.
ilustrujące wykorzystanie potencjału.
<!--%-------------------------------------------------------
%-------------------------------------------------------
-->\paragraph'''Tablica dynamiczna.'''\ Przypuśćmy, że mamy dynamiczną
\paragraph{\bf Tablica dynamiczna.}\ Przypuśćmy, że mamy dynamiczną
tablicę. W każdym momencie wiemy ile elementów w tablicy jest aktywnych, elementy nieaktywne zaznaczamy. W
tablicę. W każdym momencie wiemy ile elementów w tablicy jest akywnych, elementy niekatywne zaznaczamy. W
każdej operacji, jeśli liczba elementów nieaktywnych jest mniejsza od 1/4 wielkości tablicy to tworzymy
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
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
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
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.
aktywnego elementu do nowej tablicy. Jeśli mamy $n$ operacji to całkowity koszt kopiowania jest liniowy.
Wystarczy w każdej operacji dać składkę 4 jednostek do funduszu ubezpieczeń (potencjału).  Wtedy koszt
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.
jednej dużej operacji przepisywania  zamortyzuje się zmianą potencjału.
<!--%-------------------------------------------------------
%-------------------------------------------------------
-->\paragraph'''Zastąpienie kolejki dwoma stosami.'''\
\paragraph{\bf Zastąpienie kolejki dwoma stosami.}\
Jedną kolejkę Q można zastąpić dwoma stosami <math>S1,\ S2</math>. Jeśli pierwszy element stosu lub kolejki w
Jedną kolejkę Q można zastąpić dwoma stosami $S1,\ S2$. Jeśli pierwszy element stosu lub kolejki w
reprezentacji poziomej jest w ciągu na pierwszej pozycji, oraz <math>Q = (e_1,e_2,..,e_k)</math> to dla pewnego <math>j</math> mamy:
reprezentacj poziomej jest w ciągu na pierwszej pozycji (tzn. pobieramy $e_1$, stawimay za $e_n$), oraz $Q =
(e_1,e_2,..,e_k)$ to dla pewnego $j$ mamy:


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


\noindent Operacja wstawiania do A odpowiada wstawieniu elementu do <math>S1</math>, operacja pobrania z Q odpowiada
\noindent Operacja wstawiania do A odpowiada wstawieniu elementu do $S1$, 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.
pobraniu elementu z S2, z tym, że jeśli $S2$ jest pusty to przepisujemy najpierw wszystkie elementy z S1 do S2.
Niech operacją dominującą będzie jedna operacja stosowa (wstawienie lub pobranie pojedynczego elementu ze
Niech operacją dominującą będzie jedna operacja stosowa (wstawienie lub pobranie pojedyńczego elementu ze
stosu). Wtedy ciąg <math>n</math> operacji kolejkowych, sterujących od pustej kolejki, ma koszt liniowy w tej
stosu). Wtedu ciąg $n$ operacji kolejkowych, starują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
imlementacji. Wystarczy, że każda operacja da do FUKA składkę 3 jednostek. Dowód tego pozostawiamy jako
ćwiczenie.
ćwiczenie. \vskip 0.1cm
Roważmy podobny problem, z tym że nasza kolejka jest dwustronna, możemy wkładać i
pobierać element z każdego z dwóch końców kolejki. Wtedy możemy taką kolejkę zastąpić trzema stosami
tak, że teraz również każda operacja kolejkowa będzie mieć zamortyzowany koszt stały. Elementy kolejki
trzymamy w dwóch stosach S1, S2 tak jak poprzednio. Niezmiennikem jest to, że oba stosy są niepuste lub mają
w sumie co najwyżej jeden element. Zapewniamy zachodzenie niemiennika wykorzystując trzeci stos. W momencie gdy
jeden ze stosów ma więcej niż jeden element a drugi jest pusty, korzystając z trzeciego stosu, doprowadzamy
do reprezentacji aktualnej kolejki przez stosy S1, S2, tak aby miały one tę samą liczbę elementów (z
dokładnośią do 1). Pozostawiamy jako ćwiczenie dowód (metodą potencjału) tego, że zamortyzowany koszt
jest stały.

Wersja z 11:19, 17 lip 2006

\noindent {\Large \bf %Rozdział wstępny: 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 danch. Najważnejszymi aspektami algorytmu są jego poprawnoość i złożoność.

Będziemy zasadniczo rozpatrywać tylko złożoność czasową i
pamięciową.


W przypadku złoźoności czasowej z reguły wyróżnimy pewną operację dominującą i czas
będziemy traktować jako liczbę wykonanych operacji dominujących. W ten sposób nasza analiza będzie
 zależna jedynie od algorytmu a nie od implementacji i sprzętu.
 W przypadku sortowania przeważnie operacją dominującą jest porównanie dwóch elementów
 (mniejsze, równe, mniejsze), a w przypadku przeglądania drzewa jedno przejście w drzewie między
 wierzchołkami. W przypaku algorytmów tekstowych operacją dominującą jest porównanie dwóch symboli.
Z reguły

będziemy przyjmować, że każda operacja arytmetyczna na małych liczbach daje się zrobić w jednym kroku. Przez małe rozumiemy liczby mające $O(\log n)$ bitów.

 Z reguły określamy pewien parametr $n$, będący rozmiarem problemu wejściowego i
określamy złożoność jako funkcję $T(n)$, której arguementem jest rozmiar problemu.
Przewaźnie rozważamy złożoność pesymistyczną -maksymalną złożoność dla danych tego samego rozmiaru
$n$.
W praktyce wańiejsą może się okaać złożoność średnią, lub oczekiwaną, w tym przypadku $T(n)$ jest średnią (oczekiwaną) wartością
złożoności dla wszystkich problemów rozmiaru $n$. Tego typu złożoność zależy  istotnie od tego, jaka się od tym kryje
przetrzeń probablistyczna problemów. Z reguły zakładamy, że wszystkie problmey wejściowe tego
samego rozmiaru mogą się pojawić z tym samym prawodpodobieństwem. Jednakże jest to często mało
realistyczne założenie. Przestrzeń probabilistyczna danych wejćiowych może być bardzo skomplikowana,
prowadzić to może do bardzo trudnych (i wykraczających poza ten kurs) analiz.
\myskip {\bf 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 $n$ sprawdzeń, jest to maksymalna
liczba, zatem złożoność pesymistyczna wynosi $T(n)=n$. Jeśli każdy cią 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 aymptotycznego wzrostu funkcji:
 $f(n)\ =\ O(g(n))$  oznacza

że $f(n) \le c\cdot g(n)$ dla pewnej stałej $n$. \\ Gdy $g(n)=n$ to mówimy, że $f(n)$ jest liniowa, oraz dla $g(n)=n^2$ mówimy, że złożoność $f(n)$ jest kwadratowa. Jeśli $g(n)$ jest wielomianem to wtedy mówimy o złożoności wielomianowej. Będziemy używać dodatkowych notacji

$f(n)=\Theta(g(n)),\ f(n)=\Omega(n)$. \myskip Były one wprowadzone na wykładach z matematyki dyskretnej.

Dla przypomnienia:

$$f(n)=\Theta(g(n)) \Leftrightarrow f(n)=O(g(n))\ \& \ g(n)=O(f(n))$$ $f(n)=\Omega(g(n)$, gdy dla nieskończenie wielu $n$ i pewnej stąlej $c>0$ zachodzi\ $f(n) \ge c\cdot g(n)$. %-------------------------------------- \myskip {\bf Przykład}. \\

$\frac{1}{100}\cdot n^2- 2n = \Theta(n^2 )$.\
$n^5+2^n = \Theta(2^n), n!=\Omega(10^n)$,\\
Jeśli $f(n)=(1+(-1)^n)\cdot n$, to $f(n)=\Omega(n),\ f(n)=O(n)$ ale nie zachodzi $f(n)=\Theta(n)$.

%********************************************************* \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 $S$ składający się z $n$ przedmiotów, gdzie $n$ jest nieparzyste, niektóre z przedmiotów są czarne a niektóre białe.

{\bf Algorytm}

while $|S|>1$

\hspace*{0.5cm} {pobierz dwa przedmioty z $S$;
\hspace*{0.5cm}  {\bf if} przedmioty są różnego koloru to wstaw z powrotem czarny
\hspace*{0.5cm}  {\bf else} usuń oba przemioty;
return kolor ostatniego przedmiotu w $S$;
\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:


{\bf Algorytm}

while $|S|>1$

\hspace*{0.5cm} {pobierz dwa przedmioty z $S$;

\hspace*{0.5cm} {\bf if} co najmniej jeden jest czarny to wstaw z powrotem jeden czarny;

\hspace*{0.5cm} {\bf else} usuń oba przedmioty;

 return kolor ostatniego przedmiotu w $S$.

\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ą.


   {\bf Algorytm} Suma-Kwadratów-Cyfr;
   \hspace*{0.5cm} while ($(n <>

4)$ and $(n<> 1)$)

    \hspace*{0.8cm} n := suma cyfr liczby n;

% \myskip

   {\bf 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} {\bf while} $(n<>

6174)$

    \hspace*{0.9cm} n1 := największa liczba czterocyfrowa której cyfry są permutacją cyfr liczby $n$;
    \hspace*{0.9cm} n2 := najmniejsza liczba czterocyfrowa której  cyfry są permutacją cyfr liczby $n$;
    \hspace*{0.9cm} n := n1 - n2;\
    \myskip
   {\bf 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 $n>100$ następna wartość jest istotnie mniejsza. Podobnie jest w drugim algorytmie. Algorytm Tajemniczy jest dosyć zagadkowy, nie wiadomo, czy dla dowolnego naturalnego dodatniego $n$ ma on własność stopu.

\section{Złożoność algorymó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.}\ Prywódcą ciągu jest element, który występuje w ciągu więcej razy niź połowa długości tego ciągu. Naszym problemem jest policzenie przywódcu ciągu danego tablicą $A[1..n]$. Dla uprosazczenia przyjmijmy, że w tym ciągu jest przywódca. Łatwo można zmodyfikować algorytm by sprawdzał istnienie przywódcy. \myskip

   \hspace*{.5cm}
   {\bf 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ącejwłasności problemu:

\ jeśli mamy dwa różne elementy w tablicy to możemy je usunąć i przywódca pozostanie taki sam. \myskip % % \paragraph{\bf Szukanie sumy.}\ Mamy dane dwie posortowane rosnąco tablice $A,B$ i liczbę x, pytamy czy są $a \in A,\ b \in B$ takie, że\ $x=a+b$. % \myskip

       \hspace*{1.5cm} {\bf 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,j$ i pominięciu zbędnych sprawdzeń. % \paragraph{Maksymalny segment.}\ Dla tablicy $A[1..n]$ liczymy maksymalną wartość z zera i ze wszystkich liczb $\sum_{k=i}^j\ A[k]$, gdzie $1\le i\le j\le n$.

\myskip
       \hspace*{1.5cm} {\bf 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 $[1..i]$ oraz sufiks jest maksymalną sumą segmentu który jest sufiksem przedziału $[1..i]$. % \paragraph{\bf Najbliższy mniejszy sąsiad.}\ % Dla każdego $i > 1$ zdefiniujmy najbliższego mniejszego są siada $i$ jako: \ {$Lewy[i]\ =\ \max \{j<i\ :\ A[j]<A[i]$.}

\noindent Dla uproszczenia zakładamy, że  $A[i]> 0$ dla $i>0$ oraz $A[0]=0$.  \myskip
        \hspace*{1.5cm} {\bf 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-1$ i $Lewy[i-1]$ itd. Niech $k_i$ będzie liczbą tych $j$ dla których $A[j]>=A[i]$. Wystarczy pokazać, że suma wszystkich $k_i$ jest liniowa. Może się zdarzyć, że niektóre $k_i$ mają wartość liniową. Zauważmy jednak, że dany indeks $j$ pojawia się co najwyżej raz w sytuacji gdy $A[j] >= A[i]$, potem będzie przeskoczony. %**************** % \paragraph{\bf Najdłuższy malejący podciąg.}\

Niech $A[1], A[2],\ldots A[n]$ będzie ciągiem dodatnich liczb. Następujący algorytm oblicza

długość najdłuższgo malejącego podciągu. \myskip

        \hspace{1.5cm}
        {\bf 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{Proste Pakowanie.}\ Załóżmy, że mamy n pudełek, każde o rozmiarze R, oraz n przedmiotów o rozmiarach $R \ge r[1]\ge r[2]\ldots \ge r[n]$. Mamy włożyć przedmioty do pudełek, co najwyżej dwa do jednego pudełka. Pozostawimay jako ćwiczenie analizę następującego algorytmu, który oblicza minimalną liczbę pudełek.

\myskip
       \hspace*{1.5cm} {\bf Algorytm} Proste-Pakowanie;
       \begin{verbatim}
        wynik := n;
        for i := 1 to n do
           if (i < wynik and r[i]+r[wynik] <= R)
                        wynik := wynik-1;

\end{verbatim} \myskip \noindent Naiwne wersje powyzszych sześciu algorytmów działają w czasie kwadratowym. W każdym z tych algorytmów bardzo proste spostrzeżenia prowadą do algorytmów liniowych. %------------------------------------------------------------------------ \section{Koszt zamortyzowany} Jeśli mamy ciąg operacji $op_1,op_2,\ldots, op_n$ to koszt zamortyzowany jednej z nich jest sumarycznym kosztem wykonania wsszystkich operacji podzielonym przez liczbę operacji, inaczej mówiąc jest to, dla danego ciągu operacji, średni koszt jednej z nich. Zauważmy, że nie mówmy tu nic o prawdopodobieństwie, model jest deterministyczny.


\noindent Na przykład w algorytmie Najbliższy-Mniejszy-Sąsiad rozważmy ciąg operacji \vskip 0.4cm \centerline{$op_i$: while $( A[j] >= A[i])\ j = Lewy[j]$} \vskip 0.1cm \noindent Koszt pojedyńczej operacji może być liniowy, również sumaryczny koszt ciągu tych operacji $op_1,op_2,\ldots, op_n$ jest liniowy. Zatem pesymistyczny koszt jdnej operacji jest tutaj liniowy, natomiast zamortyzowany koszt jednej operacji jest ograniczony przez stałą. W tym przypadku wiemy, że każde sprawdzenie $A[j]>=A[i])$ z wynikiem negatywnym odbywa się tylko raz dla danej wartości $j$. Możemy powiedzieć, że księgujemy koszt operacji elementom $j$ o tej własności. Nieformalna metoda księgowania kosztów polega na rozdzielaniu (księgowaniu) kosztu, a następnie szacowania sumarycznej złożoności poprzez sumowanie wszystkich zaksę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 naturlanych od 0 do $2^n-1$, 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 $2^n-1$ jedynek w ciągu $2^n-1$ operacji, to zamortyzowana liczba operacji zamiany zera na jedynke wynosi 1. W tym przykładzie możemy powiedzieć, że analizowaliśmy koszt tzw. metdoda 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 $\Phi_i$ będzie pewną liczbą naturalną (włączając zero) odpowiadającą potencjałowi po wykonaniu $i$ operacji. Zakładamy, że potencjał jest początkowo zero, nigdy nie jest ujemny, a każda operacja $op_i$ ma koszt proporcjonalny do $c_i+|\Phi_i-\Phi_{i-1}|$. Wtedy całkowity koszt jest tego samego rzędu co $ \sum c_i$. 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ę, kórą płacą za koszt wykonania. Operacje $op_i$ ubezpieczają się od kosztów ich wykonani. Poszczególne operacje płacą drobne składki $c_i$, a swój koszt za każdym razem opłacają bioróac pieniądze z FUKA. Czasmi koszt operacji jest duży ale do tego czasu wpłacono tyle drobnych składek, że możemy ten koszt pokryć. Istotne jest jedynie żeby 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. %------------------------------------------------------- \paragraph{\bf Tablica dynamiczna.}\ Przypuśćmy, że mamy dynamiczną tablicę. W każdym momencie wiemy ile elementów w tablicy jest akywnych, elementy niekatywne 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 $n$ 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. %------------------------------------------------------- \paragraph{\bf Zastąpienie kolejki dwoma stosami.}\ Jedną kolejkę Q można zastąpić dwoma stosami $S1,\ S2$. Jeśli pierwszy element stosu lub kolejki w reprezentacj poziomej jest w ciągu na pierwszej pozycji (tzn. pobieramy $e_1$, stawimay za $e_n$), oraz $Q = (e_1,e_2,..,e_k)$ to dla pewnego $j$ mamy:

$S1 = (e_n,e_{n-1},...,e_j),\ S2 = (e_{1},e_{2}, ...,e_{j-1})$.

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

Roważmy podobny problem, z tym że nasza kolejka jest dwustronna, możemy wkładać i

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