Algorytmy i struktury daych Wykład 5 Algorytmy i ich aaliza (ciąg dalszy) Jausz Szwabiński Pla wykładu: Studium przypadku aaliza aagramów Model matematyczy Klasyfikacja algorytmów Studium przypadku aaliza aagramów Aagram ozacza wyraz, wyrażeie lub całe zdaie powstałe przez przestawieie liter bądź sylab iego wyrazu lub zdaia, wykorzystujące wszystkie litery (głoski bądź sylaby) materiału wyjściowego: kebab babek Gregory House Huge ego, sorry "Quid est veritas?" (Co to jest prawda?, Piłat) Jezus) "Vir est qui adest" (Człowiek, który stoi przed tobą, Testowaie, czy łańcuchy zaków są aagramami, to przykład zagadieia, które moża rozwiązać algorytmami o różym tempie asymptotyczego wzrostu, a jedocześie a tyle prostego, aby moża było o tym opowiedzieć w ramach kursu ze wstępu do programowaia. Rozwiązaie 1: "odhaczaie" liter Jedym z możliwych rozwiązań aszego problemu jest sprawdzeie, czy litera z jedego łańcucha zajduje się w drugim. Jeżeli tak, "odhaczamy" ją i powtarzamy czyość dla pozostałych liter. Jeżeli po zakończeiu tej operacji wszystkie litery w drugim łańcuchu zostaą "odhaczoe", łańcuchy muszą być aagramami. Odhaczaie możemy zrealizować poprzez zastąpieie odalezioej litery wartością specjalą Noe.
I [1]: def aagramsolutio1(s1,s2): alist = list(s2) #zamień drugi łańcuch a listę pos1 = 0 stillok = True while pos1 < le(s1) ad stillok: pos2 = 0 foud = False while pos2 < le(alist) ad ot foud: if s1[pos1] == alist[pos2]: #sprawdzamy literę s1[pos1] foud = True else: pos2 = pos2 + 1 if foud: alist[pos2] = Noe else: stillok = False #przerwij, jeśli litery ie ma pos1 = pos1 + 1 #pozycja astępej litery w łańcuchu s1 retur stillok prit(aagramsolutio1('abcd','dcba')) True s1 s2 Dla każdego zaku z łańcucha musimy wykoać iterację po maksymalie elemetach listy. Każda pozycja w liście s2 musi zostać odwiedzoa raz w celu odhaczeia. Dlatego całkowita liczba wizyt elemetów listy s2 jest sumą liczb aturalych od 1 do : ( + 1) 1 1 i = = + 2 2 2 2 i=1 1 Dla dużych wyraz będzie domiował ad. Dlatego algorytm jest klasy. 2 2 1 2 O( 2 ) Rozwiązaie 2: sortowaie i porówywaie s1 s2 Zauważmy, że jeżeli i są aagramami, muszą składać się z tych samych liter, występujących taką samą liczbę razy. Jeżeli więc posortujemy każdy z łańcuchów alfabetyczie od 'a' do 'z', powiiśmy otrzymać dwa takie same łańcuchy. Do posortowaia wykorzystamy metodę sort:
I [2]: def aagramsolutio2(s1,s2): alist1 = list(s1) #koiecze, żeby skorzystać z sort alist2 = list(s2) alist1.sort() alist2.sort() pos = 0 matches = True while pos < le(s1) ad matches: if alist1[pos]==alist2[pos]: pos = pos + 1 else: matches = False retur matches prit(aagramsolutio2('abcde','edcba')) True Na pierwszy rzut oka może się wydawać, że algorytm jest klasy, poieważ wykoujemy tylko jedą iterację po elemetach łańcuchów. Jedak wywołaie metody sort rówież "kosztuje", ajczęściej 2 lub O( log ) O(). Dlatego czas wykoaia będzie zdomioway przez operację sortowaia. O( ) Rozwiązaie 3: algorytm siłowy (ag. brute force) Metoda siłowa rozwiązaia jakiegoś zadaia polega a wyczerpaiu wszystkich możliwości. Dla aagramów ozacza to wygeerowaie listy wszystkich możliwych łańcuchów ze zaków łańcucha s1 i sprawdzeie, czy s2 zajduje się a tej liście. Nie jest to jedak zalecae podejście, przyajmiej w tym przypadku. Zauważmy miaowicie, że dla ciągu zaków o długości mamy wyborów pierwszego zaku, możliwości dla zaku a drugiej pozycji, ( 2) a trzeciej pozycji itd. Musimy zatem wygeerować! łańcuchów zaków. Dla ustaleia uwagi przyjmijmy, że s1 s1 ( 1) składa się z 20 zaków. Ozacza to koieczość wygeerowaia I [3]: import math math.factorial(20) Out[3]: 2432902008176640000 łańcuchów zaków, a astępie odszukaie wśród ich ciągu. Widzieliśmy już wcześiej, ile czasu zajmuje algorytm klasy, dlatego ie jest to polecae podejście do zagadieia aagramów. O(!) s2 Rozwiązaie 4: zliczaj i porówuj s1 s2 Jeżeli i są aagramami, będą miały tę samą liczbę wystąpień litery 'a', tę samą litery 'b' itd. Dlatego możemy zliczyć liczbę wystąpień poszczególych zaków w łańcuchach i porówać te liczby ze sobą:
I [4]: def aagramsolutio4(s1,s2): c1 = [0]*26 #dla ułatwieia ograiczamy się do języka agielskiego c2 = [0]*26 for i i rage(le(s1)): pos = ord(s1[i])-ord('a') #pozycja zaku w alfabecie c1[pos] = c1[pos] + 1 for i i rage(le(s2)): pos = ord(s2[i])-ord('a') c2[pos] = c2[pos] + 1 j = 0 stillok = True while j<26 ad stillok: if c1[j]==c2[j]: j = j + 1 else: stillok = False retur stillok prit(aagramsolutio4('apple','pleap')) True Rówież w przypadku tej metody mamy do czyieia z iteracjami, jedak teraz żada z ich ie jest zagieżdżoa. Dodając kroki koiecze do wykoaia w tych iteracjach do siebie otrzymamy Jest to zatem algorytm klasy O() T() = 2 + 26, czyli ajszybszy ze wszystkich prezetowaych. Zauważmy jedak, że c1 c2 lepszą wydajość uzyskaliśmy kosztem większego obciążeia pamięci (dwie dodatkowe listy i ). To sytuacja bardzo często spotykaa w praktyce. Dlatego programując, ie raz staiemy przed koieczością wyboru, który z zasobów (czas procesora czy pamięć) ależy poświęcić.
Model matematyczy aaliza doświadczala algorytmu pozwala przewidzieć jego wydajość bez zrozumieia jego działaia model matematyczy czasu pracy algorytmu pomaga zrozumieć to działaie kocepcja modelu matematyczego została opracowaa i spopularyzowaa przez Doalda Kutha pod koiec lat 60 XX wieku Sztuka programowaia (ag. The Art of Computer Programmig, w skrócie TAOCP) http://cs.staford.edu/~uo/ (http://cs.staford.edu/~uo/) Złożoość obliczeiowa algorytmu koszt realizacji algorytmu, czyli ilość zasobów komputera iezbędych do wykoaia algorytmu złożoość czasowa pomiar rzeczywistego czasu zegarowego jest mało użyteczy ze względu a silą zależość od sposobu realizacji algorytmu, użytego kompilatora oraz maszyy, a której algorytm wykoujemy liczba operacji podstawowych w zależości od rozmiaru wejścia operacje podstawowe: podstawieie, porówaie lub prosta operacja arytmetycza złożoość pamięciowa miara ilości wykorzystaej pamięci możliwe jest obliczaie rozmiaru potrzebej pamięci fizyczej wyrażoej w bitach lub bajtach jako ilość często przyjmuje się użytą pamięć maszyy abstrakcyjej Dygresja maszyy abstrakcyje istiejące komputery różią się między sobą istotymi parametrami (p. liczba i rozmiar rejestrów, udostępiae operacje matematycze) komputery podlegają ciągłym ulepszeiom algorytmy często aalizuje się, wykorzystując abstrakcyje modele obliczeń, p.: maszya RAM maszya Turiga Maszya RAM
części składowe: jedostka kotrola (program) jedostka arytmetycza (procesor) pamięć jedostka wejścia jedostka wyjścia jedostka kotrola zawiera program oraz jego rejestr (rejestr wskazuje a istrukcję do wykoaia) jedostka arytmetycza wykouje podstawowe operacje arytmetycze pamięć składa się z poumerowaych komórek, z których każda może przechować dowolą liczbę całkowitą liczba komórek jest ieograiczoa ie ma ograiczeń a rozmiar liczby ideks komórki to jej adres komórka o adresie 0 to tzw. rejestr roboczy jedostka wejścia składa się z taśmy i głowicy taśma jest podzieloa a komórki taśma jest ieskończoa każda komórka przechowuje jedą liczbę całkowitą w daej chwili czasu głowica odczytuje tylko jedą komórkę po odczytaiu głowica przesuwa się o jedą komórkę w prawo jedostka wyjścia składa się z taśmy i głowicy taśma jest podzieloa a komórki taśma jest ieskończoa do każdej komórki może być zapisaa jeda liczba całkowita po zapisie głowica przesuwa się o jedą komórkę w prawo kofiguracja maszyy to odwzorowaie, które przypisuje liczbę aturalą do każdej komórki wejściowej, wyjściowej i pamięci oraz do rejestru programu obliczeia to sekwecja kofiguracji, z których pierwsza jest kofiguracją początkową, a każda astępa została wygeerowaa zgodie z programem dopuszale istrukcje: istrukcje przesuięcia: LOAD i STORE (kopiowaie do i z rejestru roboczego) istrukcje arytmetycze: ADD, SUBTRACT, MULTIPLY, DIVIDE
istrukcje we/wy: READ, WRITE istrukcje skoku: JUMP, JZERO, JGTZ istrukcje stopu: HALT, ACCEPT, REJECT operad to albo liczba j albo zawartość j tej komórki pamięci program to dowola sekwecja powyższych istrukcji wykoywaa a operadach Maszya Turiga https://pl.wikipedia.org/wiki/maszya_turiga (https://pl.wikipedia.org/wiki/maszya_turiga) Całkowity czas pracy algorytmu całkowity czas pracy algorytmu (lub programu) to suma wartości koszt operacji częstotliwość występowaia po wszystkich operacjach występujących w algorytmie Koszt wykoaia pojedyczej operacji pierwsze komputery dostarczae były z istrukcją zawierającą dokłady czas wykoaia każdej operacji obecie moża taki koszt oszacować eksperymetalie wykoujemy p. bilio dodawań i wyliczamy średią z czasu wykoaia pojedyczej operacji w praktyce zakłada się, że podstawowe operacje (dodawaie, odejmowaie, możeie, dzieleie, przypisaie, porówaie, deklaracja zmieej itp) wykoywae a stadardowych typach daych zajmują pewie stały czas wykoaia Częstotliwość występowaia operacji Niech: I [5]: import radom lista = [radom.radit(-10,10) for r i rage(10)] I [6]: lista Out[6]: [10, 5, 9, -4, -9, 1, 3, 10, 1, 7] Rozważmy program: I [7]: cout = 0 for i i lista: if i==0: cout = cout + 1
I [8]: cout Out[8]: 0 Częstotliwość wykoywaia poszczególych operacji w tym programie jest astępująca: Operacja przypisaie porówaie dostęp do listy ikremetacja i ikremetacja cout Liczba wystąpień 1 Uproszczeie liczeia częstości występowaia liczeie wszystkich operacji mających wpływ a czas pracy algorytmu może być żmude "It is coveiet to have a measure of the amout of work ivolved i a computig process, eve though it be a very crude oe. We may cout up the umber of times that various elemetary operatios are applied i the whole process ad the give them various weights. We might, for istace, cout the umber of additios, subtractios, multiplicatios, divisios, recordig of umbers, ad extractios of figures from tables. I the case of computig with matrices most of the work cosists of multiplicatios ad writig dow umbers, ad we shall therefore oly attempt to cout the umber of multiplicatios ad recordigs." (A. Turig, Roudig off errors i matrix processes, 1947) wybór operacji domiującej p. przypisaie, porówaie, działaie arytmetycze, dostęp do tablicy/listy ajczęściej wybiera się tę, która ajwięcej kosztuje i ajczęściej występuje I [9]: lista Out[9]: [10, 5, 9, -4, -9, 1, 3, 10, 1, 7] I [10]: cout = 0 for i i rage(le(lista)): for j i rage(i+1,le(lista)): if lista[i]+lista[j]==0: cout = cout + 1
I [11]: cout Out[11]: 1 Operacja domiująca Liczba wystąpień dostęp do listy Nasuwa się pytaie, dlaczego liczba dostępów do listy w tym przykładzie wyosi. Aby to wyjaśić, przyjrzyjmy się liczbom dostępów do listy dla poszczególych wartości iteratorów i oraz j: Sumując liczbę operacji, otrzymamy: ( 1) ( 1) Wartości i Wartości j Liczba dostępów do listy 0 1, 2, 3, 4,, 1 2( 1) 1 2, 3, 4,, 1 2( 2) 2 3, 4,, 1 2( 3) 2 1 2 1 Noe Noe 2( 1 + 2 + 3 + + 1) = 2( i) = 2 2 ( + 1) 2 = ( 1) 2 i=1 otacja przybliżoa otacja dużego O otacja dużego Ω (https://pl.wikipedia.org/wiki/asymptotycze_tempo_wzrostu (https://pl.wikipedia.org/wiki/asymptotycze_tempo_wzrostu)) otacja Θ
Klasyfikacja algorytmów Rodzaje złożoości obliczeiowej złożoość pesymistycza (ag. worst case) maksymala ilość zasobu potrzeba do wykoaia algorytmu dla dowolego wejścia złożoość oczekiwaa (ag. average case) oczekiwaa ilość zasobu potrzeba do wykoaia algorytmu zależy istotie od założeń a temat rozważaej przestrzei probabilistyczej daych wejściowych może wymagać trudych aaliz matematyczych Porówywaie algorytmów W przykładzie z aagramami pierwszy algorytm rozwiązywał zaday problem w czasie 1 T 1 1 () = +, 2 2 2 atomiast czwarty algorytm w czasie T 4 () = 2 + 26 Chcemy odpowiedzieć a pytaie, który z ich jest lepszy. I [14]: %matplotlib ilie I [15]: import matplotlib.pyplot as plt import umpy as p I [16]: = p.arage(1,20) I [17]: def T1(x): retur 0.5*x*(1+x) def T4(x): retur 2*x+26
I [18]: plt.plot(,t1(),label="$t_1()$") plt.plot(,t4(),label="$t_4()$") plt.title("aagramy - porówaie algorytmów") plt.xlabel("") plt.ylabel("czas wykoaia") plt.leged(loc=2) Out[18]: <matplotlib.leged.leged at 0x7fc1b1bc5278> dla bardzo krótkich słów algorytm T 1 okazuje się szybszy! fukcja T 4 zachowuje się jedak dużo lepiej, gdy rośie poieważ iteresuje as główie czas asymptotyczy, wybieramy T 4 : T 4() = O() = O( ) T 1 2
Rzędy złożoości obliczeiowej W zależości od asymptotyczego tempa wzrostu, algorytmy dzieli się a klasy złożoości obliczeiowej. Najczęściej wyróżia się: O() 1 log log 2 c c! Fukcja stała (ie zależy od rozmiaru wejścia) logarytmicza liiowa liiowo logarytmicza (quasi liiowa) kwadratowa wielomiaowa wykładicza silie wykładicza
Złożoość logarytmicza typowa dla algorytmów, w których problem przedstawioy dla daych o rozmiarze da się sprowadzić do problemu z daymi o rozmiarze /2 algorytmy o złożoości logarytmiczej ależą do klasy P problemów, tz. problemów łatwych, które potrafimy rozwiązać w czasie co ajwyżej wielomiaowym Przykład: wyszukiwaie biare Day jest posortoway zbiór daych (p. w liście) A oraz pewie elemet item. Chcemy odpowiedzieć a pytaie, czy item zajduje się w A. Algorytm: 1. Sprawdź środkowy elemet tablicy. Jeśli jest rówy item, zakończ przeszukiwaie. 2. Jeśli środkowy elemet jest większy iż item, kotyuuj wyszukiwaie w lewej części listy. 3. W przeciwym razie, szukaj elemetu item w prawej części listy
I [19]: def biary_search(a, x, lo=0, hi=noe): if hi is Noe: hi = le(a) while lo < hi: mid = (lo+hi)//2 midval = a[mid] if midval == x: retur mid elif midval < x: lo = mid + 1 else: hi = mid retur -1 I [20]: A = [1,2,7,8,9,12,20] I [21]: biary_search(a,9) Out[21]: 4
Dla daej listy wejściowej algorytm wymaga jedego sprawdzeia elemetu środkowego, a astępie przeszukuje biarie jedą z jej połówek. Zatem czas wykoaia da się opisać rówaiem Załóżmy, że (czyli ). Wówczas: T( 2 m 2 ) m 2 Zatem = 2 m m = log 2 T() T( ) + 1 2 T(1) = 1 T ( ) + 1 2 T ( m 4 ) + 1 + 1 2 T ( m 8 ) + 1 + 1 + 1 2 T ( m 16 ) + 1 + 1 + 1 + 1 T ( 2 m 2 m T() 1 + log 2, ) + m = T(1) + m czyli wyszukiwaie biare jest przykładem algorytmu o złożoości logarytmiczej. Złożoość liiowa typowa dla algorytmów, w których dla każdego elemetu daych wejściowych wymagaa jest pewa stała liczba operacji algorytmy tego typu rówież ależą do klasy P problemów łatwych. I [22]: def liear_search(a,x): for i i a: if i == x: retur a.idex(i) retur -1 I [23]: A Out[23]: [1, 2, 7, 8, 9, 12, 20] I [24]: liear_search(a,9) Out[24]: 4 O() Mamy tutaj maksymalie iteracji i porówań. Jest to zatem algorytm klasy.
Złożoość liiowo logarytmicza typowa dla algorytmów, w których problem postawioy dla daych o rozmiarze daje się sprowadzić w liiowej liczbie operacji do dwóch problemów o rozmiarach rówież ależy dla klasy P problemów łatwych przykłady: sortowaie przez scalaie, ogólie algorytmy typu dziel i rządź /2 Złożoość kwadratowa typowa dla algorytmów, w których dla każdej pary elemetów wejściowych trzeba wykoać stałą liczbę operacji podstawowych klasa P przykłady: aagramy metodą odhaczaia 2SUM metodą brute force Złożoość wielomiaowa typowa dla algorytmów, w których dla każdej krotki elemetów wejściowych wykoywaa jest stała liczba operacji podstawowych klasa P przykłady: 3SUM metodą brute force Złożoość wykładicza typowa dla algorytmów, w których dla każdego podzbioru daych wejściowych ależy wykoać stałą liczbę działań klasa NP przykłady wieża z Haoi (o tym a astępym wykładzie) Złożoość silie wykładicza stała liczba działań wykoywaa dla każdej permutacji daych wejściowych klasa NP przykład: problem komiwojażera rozwiązyway metodą siłową O() O( 2 ) 10 20 30 40 50 60 0,00001s 0,00002s 0,00003s 0,00004s 0,00005s 0,00006s 0,0001s 0,0004s 0,0009s 0,0016s 0,0025s 0,0036 O( 3 ) O( 2 ) O( 3 ) 10 6 10 13 0,001s 0,008s 0,027s 0,064s 0,125s 0,216s 0,001s 1,048s 17,9mi 12,7di 35,7lat 366w 0,059s 58mi 6,5lat 3855w 227* w 1,3* w O(!) 10 16 10 32 10 47 10 66 3,6s 771w 8,4* w 2,6* w 9,6* w 2,6* w (założeie: dla = 1 każdy algorytm wykouje się 10 6 s)