WIKIwyklad02: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Linia 77: Linia 77:
 
Przedstawmy program w notacji, którą formalnie wprowadzimy nieco później, obliczający  największy wspólny dzielnik zgodnie z podanym algorytmem.  
 
Przedstawmy program w notacji, którą formalnie wprowadzimy nieco później, obliczający  największy wspólny dzielnik zgodnie z podanym algorytmem.  
 
'''Euklides 1'''
 
'''Euklides 1'''
\begin{verbatim}
+
Read(a,b);      //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0  
Read(a,b);      //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0  
+
'''while''' b > 0 '''do'''
while b > 0 do  
+
begin
begin
+
  '''if''' a< b '''then''' zamień(a,b); //po wykonaniu tej instrukcji zawsze a>=b
if a< b then zamień(a,b); //po wykonaniu tej instrukcji zawsze a>=b
+
  a:=a-b  // tutaj być może raz wykonamy niepotrzebnie odejmowanie zera  
a:=a-b  // tutaj być może raz wykonamy niepotrzebnie odejmowanie zera  
+
end;
end;
+
Write(a) // wypisujemy wynik, którym jest wartość a
Write(a) // wypisujemy wynik, którym jest wartość a
+
 
\end{verbatim}
 
 
Zastanówmy się, jak długo będziemy wykonywali nasz algorytm. Załóżmy, że dane, na których pracujemy nie przekraczają <math>10^{30}</math>. Tak duże liczby jako argumenty nie są jakimś dziwactwem: w rzeczywistości szukanie  największego wspólnego dzielnika jest częścią szyfrowania RSA – powszechnie stosowanego protokołu kryptograficznego i wykonuje zazwyczaj się na znacznie większych liczbach – ponadstucyfrowych.  
 
Zastanówmy się, jak długo będziemy wykonywali nasz algorytm. Załóżmy, że dane, na których pracujemy nie przekraczają <math>10^{30}</math>. Tak duże liczby jako argumenty nie są jakimś dziwactwem: w rzeczywistości szukanie  największego wspólnego dzielnika jest częścią szyfrowania RSA – powszechnie stosowanego protokołu kryptograficznego i wykonuje zazwyczaj się na znacznie większych liczbach – ponadstucyfrowych.  
 
Dobierzmy możliwie złośliwe dane. Oczywiście aby algorytm działał jak najdłużej, należy odejmować jak najmniejsze wartości w każdym kroku. Weźmy zatem <math>a=10^{30}, b=1</math>. Widać, że dla tych danych wykona się <math>10^{30}</math> obrotów pętli: będziemy żmudnie odejmowali jedyneczkę od sporej dość liczby. Jak długo to może potrwać?
 
Dobierzmy możliwie złośliwe dane. Oczywiście aby algorytm działał jak najdłużej, należy odejmować jak najmniejsze wartości w każdym kroku. Weźmy zatem <math>a=10^{30}, b=1</math>. Widać, że dla tych danych wykona się <math>10^{30}</math> obrotów pętli: będziemy żmudnie odejmowali jedyneczkę od sporej dość liczby. Jak długo to może potrwać?
Linia 93: Linia 92:
 
Zauważmy (a Euklides to wiedział długo przed nami), że tak naprawdę odejmowanie wykonujemy tylko po to, żeby wyznaczyć resztę z dzielenia <math>a</math> przez <math>b</math>. W końcu dopóki <math>a</math> jest większe od <math>b</math> odejmujemy od a całkowite wielokrotności <math>b</math>, a zamieniamy rolami <math>a</math> z <math>b</math> gdy tylko <math>a</math> spadnie poniżej <math>b</math>. W momencie, gdy <math>a</math> z <math>b</math> zamieniają się rolami, <math>a</math> ma wartość właśnie reszty z dzielenia <math>a</math> przez <math>b</math>. Można by zatem pominąć całe to odejmowanie, jeśli udałoby się nam od razu znaleźć tę resztę z dzielenia. A to nie jest takie trudne – można choćby stosować poznane w szkole podstawowej słupkowe dzielenie, które nad kreską daje wynik dzielenia, a na dole – resztę. Zmodyfikujmy zatem algorytm Euklidesa:  
 
Zauważmy (a Euklides to wiedział długo przed nami), że tak naprawdę odejmowanie wykonujemy tylko po to, żeby wyznaczyć resztę z dzielenia <math>a</math> przez <math>b</math>. W końcu dopóki <math>a</math> jest większe od <math>b</math> odejmujemy od a całkowite wielokrotności <math>b</math>, a zamieniamy rolami <math>a</math> z <math>b</math> gdy tylko <math>a</math> spadnie poniżej <math>b</math>. W momencie, gdy <math>a</math> z <math>b</math> zamieniają się rolami, <math>a</math> ma wartość właśnie reszty z dzielenia <math>a</math> przez <math>b</math>. Można by zatem pominąć całe to odejmowanie, jeśli udałoby się nam od razu znaleźć tę resztę z dzielenia. A to nie jest takie trudne – można choćby stosować poznane w szkole podstawowej słupkowe dzielenie, które nad kreską daje wynik dzielenia, a na dole – resztę. Zmodyfikujmy zatem algorytm Euklidesa:  
 
'''Euklides 2'''
 
'''Euklides 2'''
\begin{verbatim}
+
Read(a,b);      //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0
Read(a,b);      //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0  
+
'''while''' b > 0 '''do'''
while b > 0 do  
+
begin
begin
+
  '''if''' a< b '''then''' zamień(a,b); //po wykonaniu tej instrukcji zawsze a>=b
a:=a mod b;                     
+
  a:=a-b  // tutaj być może raz wykonamy niepotrzebnie odejmowanie zera
zamień (a,b)  // reszta jest zawsze mniejsza od dzielnika, więc w ciemno możemy zamienić a z b
+
end;
end;
+
Write(a) // wypisujemy wynik, którym jest wartość a
Write(a) //Wypisujemy wynik
+
 
\end{verbatim}
+
Read(a,b);      //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0  
 +
'''while''' b > 0 '''do'''
 +
begin
 +
  a:=a '''mod''' b;                     
 +
  zamień (a,b)  // reszta jest zawsze mniejsza od dzielnika, więc w ciemno możemy zamienić a z b
 +
end;
 +
Write(a) //Wypisujemy wynik
 +
 
 
Podobnie jak poprzednio, możemy bez trudu pokazać, że program się zawsze zakończy.  
 
Podobnie jak poprzednio, możemy bez trudu pokazać, że program się zawsze zakończy.  
 
Jak długo się tym razem będzie wykonywała nasza pętla programu? Jeśli dobranie najbardziej złośliwych danych w poprzednim przypadku było proste, to teraz wcale nie jest oczywiste dla jakich danych spośród liczb 30-cyfrowych program będzie działał najdłużej. Załóżmy, że tak jak wcześniej interesuje nas liczba obrotów pętli. Pomijamy na razie koszt związany z operacją dzielenia z resztą, zakładając że jest on stały.  
 
Jak długo się tym razem będzie wykonywała nasza pętla programu? Jeśli dobranie najbardziej złośliwych danych w poprzednim przypadku było proste, to teraz wcale nie jest oczywiste dla jakich danych spośród liczb 30-cyfrowych program będzie działał najdłużej. Załóżmy, że tak jak wcześniej interesuje nas liczba obrotów pętli. Pomijamy na razie koszt związany z operacją dzielenia z resztą, zakładając że jest on stały.  
Linia 324: Linia 330:
 
</math>Oczywiście, podobnie jak poprzednio, dbamy zawsze o to, żeby pierwszy argument nie był mniejszy od drugiego i w razie czego zamieniamy je miejscami.  
 
</math>Oczywiście, podobnie jak poprzednio, dbamy zawsze o to, żeby pierwszy argument nie był mniejszy od drugiego i w razie czego zamieniamy je miejscami.  
 
'''Euklides 3'''
 
'''Euklides 3'''
\begin{verbatim}
+
Read(a,b);      //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0
Read(a,b);      //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0  
+
'''while''' b > 0 '''do'''
wynik:=1;
+
begin
while b > 0 do  
+
  '''if''' a< b '''then''' zamień(a,b); //po wykonaniu tej instrukcji zawsze a>=b
begin
+
  a:=a-b  // tutaj być może raz wykonamy niepotrzebnie odejmowanie zera
if a< b then zamień(a,b); //po wykonaniu tej instrukcji zawsze a>=b
+
end;
if parzyste(a) and parzyste(b) then wynik:=wynik*2 else // w przeciwnym razie
+
Write(a) // wypisujemy wynik, którym jest wartość a
if parzyste(a) and not parzyste(b) then a:=a div 2 else  
+
 
if not parzyste(a) and parzyste(b) then b:=b div 2 else
+
Read(a,b);      //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0  
a:=a-b
+
'''while''' b > 0 '''do'''
end;
+
begin
wynik:=wynik*a;
+
  a:=a '''mod''' b;                   
Write(a)
+
  zamień (a,b)  // reszta jest zawsze mniejsza od dzielnika, więc w ciemno możemy zamienić a z b
\end{verbatim}
+
end;
 +
Write(a) //Wypisujemy wynik
 +
 
 +
Read(a,b);      //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0
 +
wynik:=1;
 +
'''while''' b > 0 '''do'''
 +
begin
 +
  '''if''' a< b '''then''' zamień(a,b); //po wykonaniu tej instrukcji zawsze a>=b
 +
  '''if''' parzyste(a) '''and''' parzyste(b) '''then''' wynik:=wynik*2 '''else''' // w przeciwnym razie
 +
  '''if''' parzyste(a) '''and''' '''not''' parzyste(b) '''then''' a:=a '''div''' 2 '''else'''
 +
  '''if''' '''not''' parzyste(a) '''and''' parzyste(b) '''then''' b:=b '''div''' 2 else
 +
  a:=a-b
 +
end;
 +
wynik:=wynik*a;
 +
Write(a)
 +
 
 
W kodzie tym posługujemy się predykatem {\tt parzyste(x)}, który przyjmuje wartość true (prawda), jeśli x jest parzyste i false (fałsz) jeśli jest nieparzyste. Operacja div daje nam wynik dzielenia całkowitego (z ucięciem reszty).  
 
W kodzie tym posługujemy się predykatem {\tt parzyste(x)}, który przyjmuje wartość true (prawda), jeśli x jest parzyste i false (fałsz) jeśli jest nieparzyste. Operacja div daje nam wynik dzielenia całkowitego (z ucięciem reszty).  
 
Przyjrzyjmy się naszemu algorytmowi pod kątem liczby obrotów pętli. Zauważmy, że w każdym obrocie we wszystkich przypadkach, poza sytuacją obu argumentów nieparzystych, co najmniej jeden z argumentów jest połowiony. Natomiast w przypadku obu argumentów nieparzystych jeden z argumentów stanie się parzysty w wyniku odejmowania i w następnym obrocie pętli zostanie podzielony przez 2. Zatem co najmniej raz na dwa obroty pętli co najmniej jeden z argumentów jest dzielony przez 2. Ale dzieleń przez 2 można wykonać tylko logarytmicznie dużo.  
 
Przyjrzyjmy się naszemu algorytmowi pod kątem liczby obrotów pętli. Zauważmy, że w każdym obrocie we wszystkich przypadkach, poza sytuacją obu argumentów nieparzystych, co najmniej jeden z argumentów jest połowiony. Natomiast w przypadku obu argumentów nieparzystych jeden z argumentów stanie się parzysty w wyniku odejmowania i w następnym obrocie pętli zostanie podzielony przez 2. Zatem co najmniej raz na dwa obroty pętli co najmniej jeden z argumentów jest dzielony przez 2. Ale dzieleń przez 2 można wykonać tylko logarytmicznie dużo.  

Wersja z 09:41, 24 lip 2006

Wykład 1. Wprowadzenie do algorytmów. Najważniejszym pojęciem w informatyce jest algorytm. Nazwa ta ma swoje korzenie w średniowieczu i wzięła się ze zniekształconego nazwiska wielkiego uczonego arabskiego #Al Chuwarizmiego, żyjącego na przełomie VIII i IX wieku i pochodzącego z Chorezmu. Al Chuwarizmi działając w Domu Nauk kalifa Al Mamuna w Bagdadzie opublikował ważne dzieła matematyczne, wśród nich Hisab al dżabr w'al muqabala – traktat o rozwiązywaniu równań, z którego wzięła nazwę algebra – jeden z głównych działów matematyki. W traktacie tym, poza wprowadzeniem systemu zapisu pozycyjnego, zaczerpniętego od Hindusów, a zwanego arabskim, podał między innymi metody rozwiązywania równań kwadratowych. Metody te odwoływały się do pojęć geometrycznych; wtedy utożsamiano liczby i działania z miarami obiektów geometrycznych. W szczególności liczby rzeczywiste, to były długości odcinków, dodawanie to było sklejanie odcinków, mnożenie odpowiadało braniu pola prostokąta o danych bokach, a pierwiastkowanie wyznaczaniu boku kwadratu o zadanym polu. Arabowie, podobnie jak starożytni Grecy, nie znali pojęcia liczby ujemnej, stąd gdy dziś patrzymy na metody Al Chuwarizmiego, poza oczywistym pięknem, wydają się nam one nieco dziwaczne, ale cóż – wtedy inaczej po prostu nie dawało się. W pierwszym kroku rozwiązania Al Chuwarizmi polecał podzielić wszystkie współczynniki trójmianu kwadratowego przez współczynnik przy , aby uwolnić się od jednego z nich; w końcu pierwiastki przez takie podzielenie nie zmieniają się, a metoda staje się prostsza. Al Chuwarizmi rozważał zatem równania postaci dla różnych klas b i c: dodatnich, ujemnych i równych zero. Miał zatem do czynienia z 9 różnymi przypadkami i dla każdego z nich podał metodę wyznaczania pierwiastka takiego równania. Przyjrzyjmy się jednemu z nich: dane są zaczerpnięte bezpośrednio z omawianej księgi. Rozwiązujemy równanie Parser nie mógł rozpoznać (błąd składni): {\displaystyle Jest to zatem równanie z klasy równań, w których współczynnik przy x jest dodatni, a wyraz wolny ujemny (dla wygody przenieśliśmy go na prawą stronę, więc po tamtej stronie jawi się nam jako c dodatnie). Oto co proponował Al Chuwarizmi. \input{rys.AlChuwarizmi.gif} Rysujemy kwadrat o boku x. Po jednej jego stronie doklejamy prostokąt o bokach x oraz <math>\frac{10}{2}=5} , po drugiej, pod kątem prostym identyczny prostokąt. Uzupełniamy powstały kąt prosty wyznaczony przez 2 odcinki długości 5 do kwadratu i otrzymujemy duży kwadrat, którego pole wynosi . Bok tego dużego kwadratu ma więc długość , a że powstał on przez sklejenie odcinka o długości x oraz odcinka o długości 5, więc . Drugi pierwiastek w tym przypadku nas nie interesuje: jest ujemny, a więc go ,,nie ma – liczby ujemne nie istniały wówczas w świadomości współczesnych. Zauważmy, że konstrukcję tę, a zatem i stosowne obliczenia, można powtórzyć dla każdej pary (b,c) w której b>0, zaś c<0. Wyrażając jednym wzorem efekt obliczeń uzyskalibyśmy wyrażenie x=\sqrt{c+(\frac{b}{2})^2}-\frac{b}{2} </math>Obliczenie wartości mogłoby się odbywać według następującego schematu:

  1. podziel przez i zapamiętaj wynik na zmiennej pomocniczej
  2. podnieś do kwadratu
  3. dodaj do otrzymanej liczby wartość
  4. wyciągnij z otrzymanej sumy pierwiastek kwadratowy
  5. odejmij od tego, co uzyskałeś uprzednio zapamiętaną wartość
  6. otrzymana liczba jest szukanym (nieujemnym) pierwiastkiem równania .

Zróbmy parę obserwacji. Po pierwsze powyższa procedura jest wykonywalna dla każdych danych b,c>0. Zauważmy, że jedyna operacja, która mogłaby sprawić kłopoty z wykonaniem, to odejmowanie w punkcie 5 naszej procedury. Jednak można łatwo sprawdzić, że daje ono zawsze wynik dodatni: w końcu dla mamy . Po drugie, zastosowaliśmy tu pewne niewielkie, ale ważne uproszczenie algorytmu: użyliśmy w jego opisie pomocniczej zmiennej , dzięki której nie musieliśmy dwukrotnie dzielić wartości przez . Po trzecie, zapisując wzór #RownanieKwadratowe skorzystaliśmy z tradycji notacyjnej, w myśl której w wyrażeniach arytmetycznych zawsze wiadomo w jakiej kolejności wykonuje się działania. W szczególności w wyrażeniu podpierwiastkowym najpierw dzieliliśmy  przez , potem podnosiliśmy wynik do kwadratu, a na końcu dodawaliśmy do niego . To, co w podanym przez Al Chuwarizmiego przepisie wymagało uściślenia kolejności, można wyrazić wzorem w zwartej formie, umówiwszy się zawczasu co do interpretacji podanej notacji. Niezwykle ważne jest, aby używana notacja była jednoznaczna i abyśmy nie popełniali błędu przy interpretacji wyrażeń, w szczególności żebyśmy byli zgodni co do kolejności wykonywanych działań. Wrócimy do tego zagadnienia w wykładzie o gramatykach. \begin{Pytania}: \item Jak będzie działała podana metoda, dla c=0? \itemZaprojektuj geometryczną metodę w stylu Al Chuwarizmiego dla równania . Czy metoda ta da poprawne wyniki dla wszystkich równań postaci przy dodatnich b,c? \end{Pytania} Co to są zatem algorytmy? Ogólnie określamy tym mianem wszelkie przepisy postępowania, które doprowadzają do uzyskania pożądanego efektu – rozwiązania zadania. W potocznej mowie mówimy czasem o algorytmach postępowania niewiele mających wspólnego z komputerami, jednak dla informatyków algorytmy wiążą się nierozerwalnie z programowaniem. Prawdziwe problemy pojawiają się, gdy chcemy algorytm zakodować w taki sposób, żeby był dobrze wykonany przez maszynę. Nie możemy sobie pozwolić na odwoływanie się do doświadczenia, machanie rękami, domysły. Komputer niczego się nie domyśla, ba! nie rozumie języka naturalnego i potrzebna będzie nam precyzyjna notacja do komunikacji z nim. Istotą programowania jest bowiem wyrażanie algorytmów w sposób ścisły, podlegający rygorom skończonej liczby reguł, których znaczenie w jednoznaczny sposób jesteśmy określić. Al Chuwarizmi nie był oczywiście pierwszym człowiekiem, który stosował podejście algorytmiczne do rozwiązywania problemów. W rzeczywistości każdy z nas stosuje algorytmy w rozmaitych sytuacjach życiowych. Człowiek pierwotny miał algorytm polowania na mamuty, czy rozpalania ognia. Dzisiaj często wykonujemy pewne algorytmy, nie zdając sobie sprawy Warto przytoczyć parę przykładów algorytmów z życia codziennego 1.Przepisy kucharskie. Typowy przepis zawiera deklaracje obiektów (składników pichcenia) ich początkowe wartości (miary) oraz opis działań doprowadzający do przyrządzenia potrawy. #PrzepisKucharski 2.Zapis nutowy muzyki. Za pomocą szczególnego systemu notacyjnego określa się wysokości i względne czasy trwania nut i pauz między nimi. Można również i tu określić dane: są to zazwyczaj określenia instrumentów, które w partyturze występują, oraz dane początkowe, takie jak metrum czy dynamika poszczególnych części. Zauważmy, że poza standardowymi znakami na pięciolinii, kompozytorzy często stosują dodatkowe określenia takie jak crescendo, poco allegretto, piano, con fuoco itp, pozwalające wykonawcy lepiej wyczuć ich intencje. #Nuty Szczególnie atrakcyjnie wyglądają niektóre nuty kompozytorów współczesnych, którzy odchodzą czasem od tradycyjnego zapisu nutowego i starają się niekiedy wymyślić i opisać własny system notacyjny, wyrażający ich intencje. 3.Instrukcje montażu. Często do zestawów mebli, czy klocków lego, dołączona jest kartka z instrukcją montażu zapisaną za pomocą sekwencji rycin obrazujących kolejne fazy powstawania składanego obiektu. Użytkownik porównując zmiany na poszczególnych obrazkach ma się domyślić, jakie czynności, w jakiej kolejności i za pomocą jakich części ma wykonywać. Zauważmy, że i tu występuje charakterystyczny dla algorytmów opis danych: najczęściej zestaw części składowych jest wyrysowany obok historyjki obrazkowej z zaznaczeniem liczby poszczególnych elementów. #InstrukcjaMontażu 4.Opisy dojazdu. Wyjaśniając, jak dotrzeć do danego miejsca (mappy.com) wiele serwisów udostępnia opis drogi z zaznaczeniem kluczowych punktów i decyzji. #Mappy 5.Opis układów choreograficznych, scenopisy przedstawień. Tutaj też stosuje się specyficzną symbolikę i skróty notacyjne. Takich przykładów można mnożyć tysiące. Właściwie niemal wszystko, co robimy, podlega jakiemuś algorytmowi działania – przy czym warto podkreślić, że ludzie nie muszą mieć algorytmów objaśnianych dokładnie: wiele mogą wywnioskować z kontekstu, doświadczenia, po prostu domyślając się, o co może chodzić. Kucharce nie trzeba wyjaśniać, co to znaczy zeszklić cebulkę na ciemnozłoty kolor, a monterowi, co to znaczy zaizolować przewody. Z komputerami jest jednak inaczej. Są to wyjątkowo głupie urządzenia i jeśli dokładnie nie wytłumaczymy, co mają zrobić, stają się bezradne. Między bajki należy włożyć rozmaite sytuacje znane z literatury fantastyczno-naukowej, w których komputery są równorzędnymi partnerami intelektualnymi dla ludzi. Sztuczna inteligencja, nawet jeśli istnieje, bazuje na ściśle określonych algorytmach działania i nie ma tam miejsca na intuicję, domyślenie się czegokolwiek, czy nagłe olśnienie, które są doskonale znane istotom myślącym. Ludzie często nie zdają sobie sprawy, jak wiele w algorytmach, którymi się posługują, zależy od nieuświadamianego kontekstu, jak dużo muszą dopowiadać do rzekomo precyzyjnych procedur działania. Komputery bezlitośnie wyłapują luki w specyfikacji procedur i nie ma mowy, żeby domyśliły się, że wykonują jakąś bezsensowną akcję, typu wydrukowanie żądania zapłacenia 0 zł. Jeśli wyraźnie nie zapiszemy w algorytmie, że takich żądań nie należy generować, to komputer ślepo wykona nasze polecenie, choćby było ono w oczywisty sposób bezsensowne. W dalszych wykładach zobaczymy przykłady, w których algorytmy z pozoru wyglądające na poprawne i kompletne, będą miały luki powodujące błędne działanie. Ćwiczenie: znajdź w Internecie przykłady bezsensownego zachowania się komputerów. Spróbuj domyśleć się, jakiego rodzaju błąd programisty był przyczyną blamażu. Spróbujmy przymierzyć się do problemu znanego jeszcze ze starożytności. Przy dodawaniu sprowadza się mianowniki do najmniejszej wspólnej wielokrotności. Wyznaczenie jej najczęściej polega na tym, że oblicza się największy wspólny dzielnik, dzieli się jedną z liczb przez niego i wynik mnoży przez drugą. Jak jednak znaleźć największy wspólny dzielnik? \begin{ramka} Operacje dzielenia z resztą. Dla danych liczb całkowitych x,y, z zastrzeżeniem, że określamy dwie operacje: wynik i resztę z dzielenia x przez y następująco: \\ ,\\ gdzie liczby i spełniają następujące zależności: \\ oraz .\\ \end{ramka} Zatem na przykład\\ Parser nie mógł rozpoznać (błąd składni): {\displaystyle 11 \div 3 = 3, \\ 11 \bmod 3 = 2, \\ -7 \div 2 = -4\\ -7 \bmod 2 =1\\ 7 \div -2 = -4\\ 7 \bmod -2 = 1\\} \Pytanie: czy zawsze oraz ? \Podpowiedź: Sprawdź hipotezę dla oraz \Odpowiedź: Nie. Na przykład , bo , a . \begin{Problem}[NWD(a,b)]: %ramka Dane są dwie liczby całkowite nieujemne takie, że . Wyznacz największą liczbę naturalną taką, że = 0 i . Liczbę tę nazywamy największym wspólnym dzielnikiem i oraz oznaczamy przez . \end{Problem} Zauważmy, że zawsze . Załóżmy więc od tej pory, że . Wiadomo, że nie istnieje ogólny wzór na największy wspólny dzielnik. Jak więc go można wyznaczyć? Metoda, którą poznamy, to jeden z najstarszych spisanych algorytmów zwany algorytmem Euklidesa #ObrazekEuklides, od imienia jednego z największych matematyków w całej historii, który go opublikował w swoim wiekopomnym dziele „Elementy”. #Elementy Euklides zauważył, że po pierwsze gdy mniejsza z liczb jest równa zero, to największy wspólny dzielnik jest równy drugiej z nich, a gdy obie są dodatnie, to jest równy największemu wspólnemu dzielnikowi ich różnicy oraz mniejszej z nich. ref{MatematykaDyskretna...} Zapisując to zdanie za pomocą wzoru otrzymujemy Parser nie mógł rozpoznać (nieznana funkcja „\begin{array}”): {\displaystyle (a,b) =\left\{ \begin{array}{ll} a & \mbox{jeśli <math>b=0} } \\ (a-b,b) & \mbox{jeśli } \end{array} \right. </math>Taki wzór, jak powyżej, nazywamy {\em rekurencyjnym}. Istotą definiowania rekurencyjnego jest odwoływanie się w definicji jakiegoś pojęcia do niego samego, zazwyczaj zastosowanego dla prostszych danych. Tak jak tutaj: żeby zdefiniować największy wspólny dzielnik, wykorzystujemy w definicji też pojęcie największego wspólnego dzielnika, tylko dla nieco mniejszych argumentów. Na dobrą sprawę taka definicja wygląda nieco podejrzanie, ale w rzeczywistości jest całkowicie poprawna. W końcu za jej pomocą jesteśmy w stanie obliczyć największy wspólny dzielnik dla dowolnej pary argumentów. Spróbujmy: . W ciągu tym odejmowaliśmy od pierwszego argumentu drugi, a gdy wynik okazywał się od niego mniejszy, zamienialiśmy argumenty miejscami. Czy możemy być pewni, że zawsze w ten sposób postępując otrzymamy w końcu jeden z argumentów równy zero? \Ćwiczenie: Udowodnij, że zawsze w skończonej liczbie kroków zejdziemy z jednym argumentem do zera. \Podpowiedź 1: Rozważ, jak się zachowuje suma argumentów w badanym ciągu. \Rozwiązanie: Oba argumenty po każdym kroku są nieujemne, więc nieujemna jest też ich suma. Suma argumentów po każdym kroku maleje, a nie może ciąg liczb naturalnych ściśle maleć w nieskończoność – jego długość jest ograniczona przez początkową wartość tej sumy: w końcu jej wartość w każdym kroku pomniejsza się co najmniej o jedynkę. Zatem ciąg naszych operacji jest skończony. A jedyny powód przerwania obliczeń rekurencyjnych może być tylko taki, że mniejszy z argumentów równa się zero.

Zauważmy, że podaliśmy {\em przepis} otrzymywania największego wspólnego dzielnika. Jest to właśnie {\em algorytm Euklidesa}. Za jego pomocą możemy znaleźć największy wspólny dzielnik dla dowolnej pary argumentów. Przedstawmy program w notacji, którą formalnie wprowadzimy nieco później, obliczający największy wspólny dzielnik zgodnie z podanym algorytmem. Euklides 1

Read(a,b);       //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0 
while b > 0 do 
begin
  if a< b then zamień(a,b); //po wykonaniu tej instrukcji zawsze a>=b
  a:=a-b  // tutaj być może raz wykonamy niepotrzebnie odejmowanie zera 
end;
Write(a) // wypisujemy wynik, którym jest wartość a

Zastanówmy się, jak długo będziemy wykonywali nasz algorytm. Załóżmy, że dane, na których pracujemy nie przekraczają . Tak duże liczby jako argumenty nie są jakimś dziwactwem: w rzeczywistości szukanie największego wspólnego dzielnika jest częścią szyfrowania RSA – powszechnie stosowanego protokołu kryptograficznego i wykonuje zazwyczaj się na znacznie większych liczbach – ponadstucyfrowych. Dobierzmy możliwie złośliwe dane. Oczywiście aby algorytm działał jak najdłużej, należy odejmować jak najmniejsze wartości w każdym kroku. Weźmy zatem . Widać, że dla tych danych wykona się obrotów pętli: będziemy żmudnie odejmowali jedyneczkę od sporej dość liczby. Jak długo to może potrwać? Przyjmijmy, że mamy do czynienia z bardzo szybkim komputerem, który na jeden obrót pętli potrzebuje jedną dziesięciomiliardową sekundy . Zatem w ciągu doby mamy obrotów pętli. Dób w ciągu roku jest 365, czyli razem mamy obrotów pętli na rok. A od Wielkiego Wybuchu minęło niespełna 14 miliardów lat, łącznie niespełna obrotów pętli. Przyjąwszy, że ktoś włączył nasz komputer na początku Wszechświata i on do dziś z tą zawrotną prędkością wykonuje nasz program dla tych właśnie danych, widzimy, że do dziś wykonałby niespełna jedną dwudziestą wszystkich obliczeń. Jeszcze dwadzieścia parę takich Wszechświatów i program zakończyłby swoje działanie. Co należy w takiej sytuacji zrobić? Niektórzy myślą, że trzeba kupić szybszy komputer. Żarty na bok. Ale istnieją użytkownicy, którzy gdy im program działa zbyt wolno, myślą przede wszystkim o sprzęcie. W wielu przypadkach jednak główne rezerwy tkwią w samym algorytmie. Dlatego też w trakcie tego kursu będziemy wielką wagę przykładali do jakości algorytmów i szukali jak najlepszych rozwiązań. Zauważmy (a Euklides to wiedział długo przed nami), że tak naprawdę odejmowanie wykonujemy tylko po to, żeby wyznaczyć resztę z dzielenia przez . W końcu dopóki jest większe od odejmujemy od a całkowite wielokrotności , a zamieniamy rolami z gdy tylko spadnie poniżej . W momencie, gdy z zamieniają się rolami, ma wartość właśnie reszty z dzielenia przez . Można by zatem pominąć całe to odejmowanie, jeśli udałoby się nam od razu znaleźć tę resztę z dzielenia. A to nie jest takie trudne – można choćby stosować poznane w szkole podstawowej słupkowe dzielenie, które nad kreską daje wynik dzielenia, a na dole – resztę. Zmodyfikujmy zatem algorytm Euklidesa: Euklides 2

Read(a,b);       //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0 
while b > 0 do 
begin
  if a< b then zamień(a,b); //po wykonaniu tej instrukcji zawsze a>=b
  a:=a-b  // tutaj być może raz wykonamy niepotrzebnie odejmowanie zera 
end;
Write(a) // wypisujemy wynik, którym jest wartość a
Read(a,b);       //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0 
while b > 0 do 
begin
  a:=a mod b;                     
  zamień (a,b)  // reszta jest zawsze mniejsza od dzielnika, więc w ciemno możemy zamienić a z b
end;
Write(a) //Wypisujemy wynik

Podobnie jak poprzednio, możemy bez trudu pokazać, że program się zawsze zakończy. Jak długo się tym razem będzie wykonywała nasza pętla programu? Jeśli dobranie najbardziej złośliwych danych w poprzednim przypadku było proste, to teraz wcale nie jest oczywiste dla jakich danych spośród liczb 30-cyfrowych program będzie działał najdłużej. Załóżmy, że tak jak wcześniej interesuje nas liczba obrotów pętli. Pomijamy na razie koszt związany z operacją dzielenia z resztą, zakładając że jest on stały. Ćwiczenie: Znajdź dwie liczby dwucyfrowe, dla których algorytm Euklides 2 wykona się najwięcej razy. \iffalse <showhide

Podpowiedź 1:

\fi \Podpowiedź1: Maksymalna liczba obrotów pętli, to 9. \Podpowiedź2: Nie bądź rozrzutny: jeśli tę samą liczbę obrotów pętli możesz uzyskać wychodząc od mniejszych argumentów, to to zrób. \Odpowiedź: . Wykonując kolejne dzielenia z resztą otrzymujemy ciąg . Za każdym razem wynik dzielenia był równy jeden, a reszta była po prostu różnicą argumentów. Aby rozwiązać ten problem doboru najbardziej złośliwych danych, należy spojrzeć się na problem od drugiej strony: jak za pomocą najmniejszych liczb uzyskać z góry zadaną liczbę obrotów pętli? Widać, że aby pętla wykonała się raz, wystarczą dane . Ale te dane nie mogą być wynikiem wcześniejszego obrotu pętli: pamiętajmy, że pierwszy z argumentów jest zawsze dzielnikiem z poprzedniego obrotu pętli, a skoro jest nim jedynka, to reszta nie mogła być równa jeden. Zatem przy więcej niż jednym obrocie pętli najmniejsze dane dla ostatniego obrotu pętli, to (po czym dostajemy od razu parę kończącą pętlę). Jakie były zatem dane przedostatnie? Widać, że dzieliliśmy przez 2 i dostaliśmy resztę 1. Zatem w poprzednim kroku mogliśmy mieć pary (3,2) lub (5,2) lub (7,2),... Z nich para (3,2) jest najoszczędniejsza – są to liczby, które dają najmniejszą sumę, a liczba obrotów naszej pętli jest równa 2. . Jak wyglądała zatem para dwa kroki wstecz? Dzielnik musiał być równy 3, a reszta równa 2, więc w grę wchodzą pary (5,3), (8,3), (11,3),... Z nich znów najoszczędniejsza jest para (5,3). Dalej cofając się i rozumując w analogiczny sposób dostaniemy pary (8,5) i kolejno (13,8), (21,13),... . Widać zatem, że zawsze najoszczędniej będzie tak dobierać kolejną parę, aby wynik dzielenia był równy 1, czyli innymi słowy, jeśli w kolejnym obrocie pętli argumenty są równe , to w poprzednim powinny być równe . Wtedy bowiem jest resztą z dzielenia przez , natomiast iloraz równy jest . Zbadajmy zatem, jakie najmniejsze argumenty dają zadaną liczbę obrotów pętli.

Liczba obrotów a b
0 1 0
1 1 1
2 3 2
3 5 3
4 8 5
5 13 8
6 21 13

Liczby, które występują w tabeli, są znane pod nazwą {\em liczb Fibonacciego}. Mają one w informatyce duże znaczenie i warto znać podstawowe fakty o nich. Definiuje się je następująco: \begin{ramka}[Liczby Fibonacciego] \\ \\ dla \end{ramka} Każda kolejna liczba Fibonacciego jest sumą dwóch swoich poprzedniczek. Parę początkowych wyrazów tego ciągu warto znać na pamięć.

Liczba obrotów a b
0 1 0
1 1 1
2 3 2
3 5 3
4 8 5
5 13 8
6 21 13
$n$ $F_n$
0 0
1 1
2 1
3 2
4 3
5 5
6 8
7 13
8 21

Widzimy więc, że poczynając od drugiego wiersza tabeli () najbardziej złośliwych danych dla algorytmu Euklides2 mamy zawsze . Aby wymusić obrotów pętli musimy wziąć zatem co najmniej -gą i -wszą liczbę Fibonacciego. Liczby Fibonacciego rosną szybko. Konkretnie znany jest ogólny wzór na -tą liczbę Fibonacciego przypisywany Binetowi \pic{Binet}, a znany jeszcze na pewno Eulerowi \pic{Euler}100 lat przed Binetem. Dowód tego wzoru można znaleźć w [[#MatematykaDyskretna-xxx}. Wprowadzając oznaczenia , otrzymujemy wzór w nieco bardziej czytelnej postaci . Biorąc pod uwagę to, że , zaś i to, że w związku z tym składnik Parser nie mógł rozpoznać (błąd składni): {\displaystyle \hat{\varphi]]^n} w naszym wzorze bardzo szybko dąży do zera, możemy łatwo pokazać, że n-ta liczba Fibonacciego jest równa gdzie przez oznaczamy zaokrąglenie , czyli liczbę całkowitą najbliższą danej liczby rzeczywistej . Zatem n-ta liczba Fibonacciego jest prawie dokładnie równa n-tej potędze podzielonej przez . Zauważmy jeszcze, że skoro tak, to po zlogarytmowaniu obustronnie wzoru [[#KrotkiFibo} przy podstawie otrzymujemy wzór . Zatem indeks zadanej liczby Fibonacciego wynosi Parser nie mógł rozpoznać (błąd składni): {\displaystyle n=\log_{\varphi}F_n + log_{\varphi}\sqrt{5]]} . Zapamiętajmy sobie niezwykle ważny wniosek: \begin{ramka} Liczby Fibonacciego rosną wykładniczo szybko. Ich wzrost jest prawie identyczny, jak funkcji wykładniczej . \end{ramka} Wprowadźmy oznaczenie na największą liczbę Fibonacciego nieprzekraczającą , a przez jej indeks. Ponieważ dla dowolnych argumentów liczba obrotów pętli algorytmu Euklides2 nie przekracza liczby obrotów pętli dla argumentów najbardziej złośliwych, czyli oraz poprzedniej liczby Fibonacciego , a liczba obrotów pętli dla tych argumentów jest o mniejsza, niż indeks większej z nich, więc otrzymujemy szacowanie na liczbę obrotów pętli dla dowolnych argumentów i : Parser nie mógł rozpoznać (błąd składni): {\displaystyle M(a,b) \le M(FIB(a),FIB(FIB(a)) = [n]FIB(a)-2, \mbox{\rm skąd} \\ M(a,b)+2 \le <math>\log_{\varphi}F_n + log_{\varphi}\sqrt{5}} , \\ \mbox{\rm a biorąc pod uwagę, że} \mbox{\rm otrzymujemy} \\ M(a,b)\le \log_{\varphi}a </math>bowiem . Zapamiętajmy jeszcze jedną ważną prawidłowość. \begin{ramka} Indeksy liczb Fibonacciego rosną logarytmicznie wolno w stosunku do wartości tych liczb. \end{ramka} Zatem funkcja [n]FIB(x) rośnie logarytmicznie ze względu na x. Wracając do naszych danych trzydziestocyfrowych: możemy oszacować liczbę obrotów pętli przez . Zatem wykonamy nie więcej niż 150 obrotów pętli, co oczywiście będzie w zasięgu nawet bardzo wolnego komputera. Pamiętajmy, że przy dużych liczbach możemy zapomnieć o wbudowanych w języki programowania procedurach arytmetycznych. O arytmetykę musimy zadbać sami. Własną arytmetyką dużych liczb zajmiemy się później. Zauważmy pewną niedogodność. W algorytmie Euklides2 jeden krok pętli jest nieco trudniejszy. W poprzednim algorytmie Euklides1 mieliśmy tylko porównywanie liczb i ich odejmowanie. Tutaj musimy zaprogramować dzielenie z resztą. Jest to nie tylko trudniejsze, ale i ogólnie wolniejsze od odejmowania. Jeśli zdecydujemy się na algorytm szkolny dzielenia słupkowego, to trzeba będzie wykonać całą serię obliczeń realizujących kolejne kroki wyznaczania cyfr ilorazu. Każdy z tych kroków wymaga wyznaczenia stosownej cyfry wyniku, przemnożenia jej wartości przez dzielnik, a następnie odjęcia od fragmentu dzielnej tego wyniku. Robimy to z grubsza tyle razy, ile cyfr ma iloraz. Jeżeli za miarę wielkości liczby przyjmiemy długość jej reprezentacji w systemie pozycyjnym (czyli liczbę cyfr), to o ile porównywanie oraz odejmowanie można zrobić w czasie proporcjonalnym do długości tej reprezentacji, to dzielenie może wymagać kwadratowego czasu. Niech długość dzielnej wynosi . Zauważmy, że jeśli dzielnik jest o połowę krótszy od dzielnej, (czyli mając długość jest mniej więcej równy pierwiastkowi kwadratowemu z dzielnej), to iloraz będzie miał długość podobną jak dzielnik, czyli i tyle razy będzie się musiała wykonać zasadnicza pętla algorytmu dzielącego, bo tyle cyfr trzeba wyznaczyć. Z kolei wyznaczenie każdej cyfry ilorazu wymaga odjęcia jakiejś niewielkiej wielokrotności dzielnika, a więc liczby również z grusza -cyfrowej. A odejmowanie jest proporcjonalnie kosztowne do długości argumentów. Łącznie zatem cyfr ilorazu razy jednocyfrowych kroków przy odejmowaniu daje nam łącznie , więc kwadratowo wiele w stosunku do . Liczba obrotów głównej pętli algorytmu jest też proporcjonalna do , bo w dowolnym systemie pozycyjnym liczba cyfr jest proporcjonalna do logarytmu z danej liczby przy podstawie będącej bazą systemu, czyli również proporcjonalna do logarytmu przy podstawie , bo logarytmy o różnych podstawach różnią się od siebie tylko o czynnik stały. Jeśli skupimy się na operacjach na pojedynczych cyfrach, to łączna liczba wszystkich operacji nie przekroczy zatem . W rzeczywistości możemy się pokusić o przypuszczenie, że będzie to nawet mniej. Zauważmy bowiem, że złośliwe dane dla głównej pętli, to kolejne liczby Fibonacciego, a te długością różnią się co najwyżej o 1, natomiast złośliwe dane dla algorytmu dzielenia z resztą, to dane różniące się długością dwukrotnie; liczby Fibonacciego można podzielić w czasie liniowym, a nie kwadratowym. i to liczby Fibonacciego będą się pojawiały cały czas, w każdym kroku algorytmu. Jeżeli zatem zaczniemy od pary kolejnych liczb Fibonacciego, to -krotnie wykonamy dzielenie kosztujące , co nam da . Jeśli natomiast będziemy się starali wywindować koszt dzielenia, to długości kolejnych par powinny być równe . Ale wtedy łączny koszt dzieleń będzie równy . Zatem obie skrajności: złośliwe dane dla zewnętrznej i dla wewnętrznej pętli dają koszt kwadratowy. Ale czy nie można wypośrodkować złośliwości tak, aby uzyskać jednak złożoność sześcienną? \Problem: Czy można tak dobrać dane, żeby wymusić sześcienną złożoność algorytmu Euklides2? \Odpowiedź: NIE. Złożoność algorytmu Euklides2 jest jednak kwadratowa. Powstaje pytanie, czy nie dałoby się znaleźć takiego algorytmu znajdowania największego wspólnego dzielnika tak, aby zachowując złożoność kwadratową korzystać z łatwiejszych operacji niż dzielenia z resztą. Rozwiązanie jest zaskakująco proste, jeśli zauważymy parę dość oczywistych faktów. Będziemy rozważać parzystość argumentów i redukować problem ze względu na tę właśnie własność. Oznaczmy zbiór liczb parzystych przez . Kluczem do algorytmu jest spostrzeżenie, że jeśli jedna liczba jest parzysta, a druga nie, to najmniejszy wspólny dzielnik nie zmieni się, jeśli parzysty argument podzielimy przez 2. Euklides3 Parser nie mógł rozpoznać (nieznana funkcja „\begin{array}”): {\displaystyle (a,b) =\left\{ \begin{array}{ll} a & \mbox{jeśli <math>b=0} } \\ 2(a,b)& \mbox{jeśli }\\ (\frac{a}{2},b) &\mbox{jeśli }\\ (a,\frac{b}{2}) & \mbox{jeśli }\\ (a-b,b) & \mbox{jeśli }\\ \end{array} \right. </math>Oczywiście, podobnie jak poprzednio, dbamy zawsze o to, żeby pierwszy argument nie był mniejszy od drugiego i w razie czego zamieniamy je miejscami. Euklides 3

Read(a,b);       //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0 
while b > 0 do 
begin
  if a< b then zamień(a,b); //po wykonaniu tej instrukcji zawsze a>=b
  a:=a-b  // tutaj być może raz wykonamy niepotrzebnie odejmowanie zera 
end;
Write(a) // wypisujemy wynik, którym jest wartość a
Read(a,b);       //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0 
while b > 0 do 
begin
  a:=a mod b;                     
  zamień (a,b)  // reszta jest zawsze mniejsza od dzielnika, więc w ciemno możemy zamienić a z b
end;
Write(a) //Wypisujemy wynik
Read(a,b);       //Wczytujemy a i b, zakładając że użytkownik wie, że a>=b, a+b>0 
wynik:=1;
while b > 0 do 
begin
  if a< b then zamień(a,b); //po wykonaniu tej instrukcji zawsze a>=b
  if parzyste(a) and parzyste(b) then wynik:=wynik*2 else // w przeciwnym razie
  if parzyste(a) and not parzyste(b) then a:=a div 2 else 
  if not parzyste(a) and parzyste(b) then b:=b div 2 else
  a:=a-b
end;
wynik:=wynik*a;
Write(a)

W kodzie tym posługujemy się predykatem {\tt parzyste(x)}, który przyjmuje wartość true (prawda), jeśli x jest parzyste i false (fałsz) jeśli jest nieparzyste. Operacja div daje nam wynik dzielenia całkowitego (z ucięciem reszty). Przyjrzyjmy się naszemu algorytmowi pod kątem liczby obrotów pętli. Zauważmy, że w każdym obrocie we wszystkich przypadkach, poza sytuacją obu argumentów nieparzystych, co najmniej jeden z argumentów jest połowiony. Natomiast w przypadku obu argumentów nieparzystych jeden z argumentów stanie się parzysty w wyniku odejmowania i w następnym obrocie pętli zostanie podzielony przez 2. Zatem co najmniej raz na dwa obroty pętli co najmniej jeden z argumentów jest dzielony przez 2. Ale dzieleń przez 2 można wykonać tylko logarytmicznie dużo. \begin{ramka} W dziedzinie całkowitoliczbowej możemy się spojrzeć na logarytm jak na liczbę możliwych dzieleń przez 2, zanim dojdziemy do jedynki. Liczba ta jest równa podłodze z logarytmu rzeczywistego przy podstawie 2. \end{ramka} Zatem łączna liczba obrotów pętli nie przekracza . Co się dzieje w każdym obrocie pętli? Każda z operacji ma złożoność liniową. Sprawdzenie parzystości liczby wymaga zajrzenia do ostatniej cyfry. Sprawdzenie, czy liczba jest równa zero wymaga przejrzenia w najgorszym razie wszystkich jej cyfr jednokrotnie. Dzielenie przez 2 i mnożenie przez dwa, podobnie jak odejmowanie, mają złożoność liniową (czyli proporcjonalną do logarytmu z wartości a i b). Zatem złożoność algorytmu na poziomie operacji na cyfrach jest kwadratowa, czyli w tym przypadku proporcjonalna do kwadratu z logarytmu .

DZIEDZINA ALGORYTMICZNA Ważnym pojęciem przy określaniu algorytmu jest pojęcie dziedziny algorytmicznej. Algorytmy wykonują pewne operacje na argumentach i wyrażenie własności algorytmu, a w tym określenie jego złożoności dokonywane jest za pomocą tych operacji. Dziedziną algorytmiczną nazwiemy zatem system relacyjny , gdzie nazywany jest nośnikiem, a zbiory , odpowiednio zbiorem operacji i relacji określonych w , których można używać w algorytmie. Zauważmy, że od tego, jakimi operacjami i relacjami dysponujemy zależą nasze możliwości opisywania algorytmów. Zawsze musimy wiedzieć, z jakich operacji można korzystać, zanim zabierzemy się za programowanie. Czasami takie operacje przyjmują postać bibliotek gotowych procedur i funkcji – cegiełek, z których składamy nasze algorytmy. W przypadku naszych trzech algorytmów Euklidesa te trzy dziedziny, to: 1.Euklides1: Parser nie mógł rozpoznać (nieznana funkcja „\Nat”): {\displaystyle \langle \Nat, -,\le, =_0 \rangle} 2.Euklides2: Parser nie mógł rozpoznać (nieznana funkcja „\Nat”): {\displaystyle \langle \Nat, \bmod =_0 \rangle} 3.Euklides3: Parser nie mógł rozpoznać (nieznana funkcja „\Nat”): {\displaystyle \langle \Nat, -,\div_2, *_2,\le, \in_P,=_0 \rangle} , gdzie , to zwykłe odejmowanie, Parser nie mógł rozpoznać (błąd składni): {\displaystyle \bmod} operacja znajdowania reszty, - jednoargumentowa operacja dzielenia przez 2, jednoargumentowa operacja mnożenia przez 2 (zauważmy, ze przez nic innego nie musimy dzielić ani mnożyć), relacja dwuargumentowa niemniejszości, jednoargumentowa relacja parzystości, zaś relacja jednoargumentowa bycia zerem. Uświadomienie sobie, w jakiej dziedzinie algorytmicznej operujemy jest ważne między innymi z punktu widzenia porównywania algorytmów. Łatwo jest bowiem ,,skrzywdzić jakiś algorytm nie zauważając, że działa on w uboższej dziedzinie niż rzekomo lepszy, a przyjrzenie się kosztowi operacji podstawowych daje lepszy wgląd w istotę złożoności. Jeszcze parę słów na temat złożoności algorytmów. Jak mogliśmy to zauważyć, brak analizy złożoności może doprowadzić do porażki – algorytm, nawet poprawny, może stać się praktycznie bezużyteczny, jeśli będzie miał zbyt dużą złożoność. Dokładniej o złożoności będzie jeszcze na dalszych wykładach z bardziej zaawansowanej algorytmiki. Podkreślmy parę podstawowych spraw. 1.Złożoność określamy obliczając liczbę operacji dominujących, czyli takich, które najczęściej będą wykonywane 2.Złożoność wyznacza się zazwyczaj z dokładnością do rzędu wielkości, a więc zaniedbując czynnik stały. Czasami czynnik ten bierze się pod uwagę, ale dopiero wtedy, gdy porównujemy algorytmy o podobnym rzędzie złożoności. Najczęściej do określenia złożoności używa się notacji , która pozwala uwolnić się od czynnika stałego i skupia się właśnie na rzędzie wielkości. #MatematykaDyskretna.xxx \hide[Definicja notacji ]{Mówimy, że funkcja Parser nie mógł rozpoznać (nieznana funkcja „\Nat”): {\displaystyle f:\Nat \rightarrow R} jest , jeśli istnieją stała oraz liczba Parser nie mógł rozpoznać (nieznana funkcja „\Nat”): {\displaystyle m\in \Nat} takie, że dla każdego zachodzi .} 3.Złożoność zależy od rozmiaru danych. Przez rozmiar danych najczęściej rozumiemy liczbę bitów (czy bajtów) potrzebnych do zakodowania danych; znowu chodzi o rząd wielkości – ukrywamy w jednostkach. Na przykład jeśli mówimy o sortowaniu liczb, to ważne jest, ile ich jest, a nie to, z jaką dokładnością je podajemy – zwiększenie takiej dokładności w końcu rozmiar danych powiększy o stały czynnik. Stąd na przykład 1.gdy sortujemy n obiektów, rozmiarem danych jest n; 2.gdy rozważamy graf, rozmiarem danych jest suma liczby krawędzi i wierzchołków (czasem rozważamy osobno liczbę krawędzi i wierzchołków); 3.gdy rozważamy algorytmy liczbowe – tak jak w naszych przykładach obliczania największego wspólnego dzielnika – rozmiarem danych jest długość zapisu cyfrowego liczby, bo tak złożone jest podanie jej wartości 4.Czasami do wykonania algorytmu potrzeba dodatkowej pamięci na pomocnicze struktury. Wtedy również zastanawiamy się, ile tej pamięci potrzeba i wynik nazywamy {\em złożonością pamięciową} 5.Dla różnych danych algorytm może różnie się wykonywać. W praktyce rozważa się dwa rodzaje danych: pesymistyczne (czyli najbardziej złośliwe) i losowe, czyli typowe. Mówimy wtedy odpowiednio o {\em złożoności pesymistycznej} i {\em złożoności średniej}. Dobrzy informatycy wyrabiają sobie nawyk myślenia o złożoności przy programowaniu zawsze.

Poniżej rysunek ilustrujący metodę Al Chwarizmiego podpis pod rysunkiem: Geometryczne rozwiązanie równania x2+10x=39

W małym kwadracie powinno być x2