Algoritmuselmélet 1. gyakorlat megoldások Gyakorlatvezető: Engedy Balázs Ordó, omega, theta, rekurzió 01.0.08. 10:15 11:45 Bemelegítés 1. Az f(n) = O(g(n)) jelölés egyenletnek tekinthető-e? Mi fejezi ki a relációt a kifejezésben és mik között áll fenn? Megoldás. A nagyságrendi sorra tekintve nyilvánvalóan igaz pl., hogy: 1 n = O(n ), n = O(n ). Ezeket egyenletként értelmezve levonhatnánk a következtetést, miszerint: n = O(n ) = n, tehát: n = n, ami nyilvánvalóan hülyeség. (Miért is következne két, egy-egy függvény nagyságrendjéről szóló állításból, hogy azok szabad n változója épp 0 vagy 1-gyel egyenlő?) Tehát itt valami másról van szó. Az O, Θ és Ω jelöléseket sokkal inkább függvények közti relációk egy felettébb furcsa írásmódjaként kell elképzelni. A nagy ordó példájánál maradva, a relációt a teljes = O() szerkezet fejezi ki, és az f(n) és g(n) függvények (mint dolgok ) között áll fenn. Tehát az f(n) = O(g(n)) jelentése: az f(n) és g(n) függvények (ilyen sorrendben) nagy ordó relációban állnak, azaz az ordó definíciója szerint f(n) nagyságrendileg nem nagyobb g(n)-nél.. Miért kézenfekvő az algoritmusok hatékonyságának leírására az O-jelölés? (Gondoljunk a lépésszám, illetve az ordó pontos definíciójára.) Megoldás. Az O-jelölés definíciójának három kulcsfontosságú eleme van: Felső becslés Egy algoritmus lépésszámát általában worst-case értelemben vizsgáljuk, azaz azt mondjuk, hogy egy n méretű bemeneten maximálisan f(n) lépést végez. Egy általános n-méretű bemenetre tehát az f(n) lépésszám egy felső becslés, ami jól összehangban van az O definíciójával, miszerint az is felső becslés. Fontos azért hangsúlyozni, hogy míg definíció szerint f(n) egy éles felső becslés (tehát van olyan n-hosszú bemenet, ami tényleg f(n) lépést igényel), addig az ordós komplexitásmegadás bármennyire gyenge lehet (tehát egy f(n) = n maximális lépésszámú algoritmusra is mondhatjuk, hogy O(n ), csak senki nem fogja megvenni a piacon). Sokszor pont azért használunk akár best- és average-case futásidők vizsgálatakor is O(... )-jelölést, mert a pontos maximális lépésszám meghatározása túlságosan bonyolult lenne, ezért csak becsüljük. Konstans szorzó A felső becslés fennállását csak egy c konstans faktortól eltekintve követeljük meg. Ez is kedvező, hisz egy algoritmus lépésszámában rejtőző nagy konstans faktor egy gyorsabb számítógéppel orvosolható, a lépésszám maradék (a bemenet méretétől függő) része viszont nem, így igazából ez utóbbi írja jól a magának az algoritmusnak a komplexitását. Ezért is tekintünk el a konstans faktortól. 1 Tehét a feladat kérdése f(n) = n, f (n) = n, g(n) = g (n) = n helyettesítéssel. Lásd: http://hu.wikipedia.org/wiki/reláció (innen a. definíció és az értelmezés fontos) vagy http: //wiki.vmg.sulinet.hu/doku.php?id=matematika:halmazok:relacio. Aki még nem találkozott ezekkel a fogalmakkal, ezeket, kérem, olvassa el, mielőtt továbbhalad, legalább a reláció formális definíciójáig. 1
Küszöbszám A felső becslésnek csak egy adott n 0 küszöbértéknél nagyobb n méretű bemenetekre kell fennállnia. Ezáltal ténylegesen az algoritmusunk nagy méretű bemeneteken mutatott viselkedését ragadhatjuk meg (ez szokott a lényeges lenni), és nagyvonalúan eltekinthetünk attól, hogy a nagyon kis méretű bemeneteken bizonyos speciális esetek miatt össze-vissza alakul a lépésszám. Feladatok 3. Bizonyítsuk be, hogy x + 4x + 17 = O(x 3 ), de x 3 O(x + 4x + 17)! Megoldás. A definíció szerint x + 4x + 17 = O(x 3 ) bizonyításához azt kell megmutatnunk, hogy létezik c > 0 konstans és n 0 küszöbszám (ezt mi választhatjuk öncélúan!), amelyekkel igaz, hogy: x + 4x + 17 c x 3, minden x n 0 -re. Milyen módszerrel állhatunk neki egy ilyen bizonyításnak? Az első lehetőség, hogy ekvivalens átalakításokkal olyan alakra hozzuk az egyenlőtlenséget, amelyről látszik, hogy alkalmas c és n 0 -nal fennáll: 3 x + 4x + 17 c x 3 x + 4x + 17 c x 3 1 x + 4 x + 17 x c 3 az utolsó alakban c = 1 + 4 + 17 = választással jól látszik, hogy minden x n 0 = 1-re fennáll (hisz a bal oldali tagok rendre 1, 4 és 17 alatt maradnak). Készen vagyunk. Sokszor azonban mindez nem ilyen egyszerű. Ekkor egy elsőre furcsának tűnő technika segíthet: egy erősebb állítást bizonyítunk, könnyebben! Esetünkben: még a bal oldalnál is nagyobb valamire mutatjuk meg, hogy kisebb a jobb oldalnál alkalmas c és n 0 -nal! Ebből nyilván következik az eredeti állítás. Ez a gyakorlatban a következőképpen nézhet ki: a bal oldalt elkezdjük lépésenként felülről becsülgetni (majorálni), célszerűen a domináns tagokat megtartva, a többit pedig egy-egy domináns taggal felülről becsülve. Egészen addig, amíg egy olyan alakhoz nem jutunk, amelyről már egyszerűen meg tudjuk mutatni, hogy kisebb az eredeti jobb oldalnál: x + 4x + 17 x + 4x + 17x = x x 3 x 1 Itt most nagyon ügyesek voltunk: a majorálások során épített egyenlőtlenség-lánccal konkrétan nem csak egy olyan alakhoz jutottunk el, amit már az első módszerrel könnyen összehasonlíthatunk az eredeti jobb oldallal, hanem c = helyettesítéssel konkrétan magához a jobb oldalhoz jutottunk. A lánc két végét összevetve adódik a bizonyítandó állítás. Már csak n 0 értéke kérdéses: nyilván úgy kell megválasztanunk, hogy a láncban bármely két szomszédos egyenlőtlenség igaz legyen (és így helyesen vonjuk le következtetésként, hogy a lánc első eleme az utolsónál). Sokszor segítségünkre van, ha olyan majorálást végzünk, ami csak elegendően nagy x-ekre igaz. Ekkor célszerűen az alsó határt feltüntetjük a -jel alatt, és vegül n 0 -t ezen küszöbök maximumaként választjuk, jelen esetben 1-nek. 3 az abszolútértéket elhagytuk, hisz nemnegatív x-ekre úgyis minden nemnegatív
A x 3 O(x + 4x + 17) jellegű (tagadó) állításokat célszerű indirekt módon bizonyítani. Feltesszük, hogy az ordós becslés igaz, azaz létezik c és n 0 (ezeket itt nem mi választjuk, az értéküket sem ismerjük, csak azt, hogy léteznek), hogy fennáll: x 3 c x + 4x + 17, minden x n 0 -re. majd ebből ellentmondásra jutunk. Ez itt tipikusan egy olyan jellegű egyenlőtlenség (helyes) levezetését jelenti, hogy egy x-ben monoton növekvő kifejezés kisebb valami konstans kifejezésnél (amiben esetleg c szerepel) ami nyilvánvalóan nem teljesülhet minden x n 0 -re. Itt végig olyan lépéseket kell végeznünk, hogy az előzőből következzen a következő alak, tehát (pont fordítva, mint előbb) itt a bal oldalt minorálhatjuk, a jobb oldalt majorálhatjuk. A példánkban: x 3 c (x + 4x + 17) c x x 3 x c x c amivel ellentmondásra jutottunk, hiszen ez elegendően nagy x-ekre nem lesz igaz, bármi is legyen c. 4. Egy A algoritmusról azt tudjuk, hogy az n hosszú bemeneteken a lépésszáma O(n log n). Lehetséges-e, hogy: (alant az x az x bemenet hosszát jelöli) (a) van olyan x bemenet, amin a lépésszáma x 3? (b) minden x bemeneten legfeljebb 07 x lépést használ? (c) minden páros hosszú x bemeneten legalább x x lépést használ? Megoldás. Jelölje A maximális lépésszámát az n-hosszú bemeneten f(n). Ekkor a definíció szerint A-ról akkor mondhatjuk, hogy O(n log n), ha létezik c és n 0, hogy: f(n) c n log n, minden n n 0 -re. (a) Ez azt jelenti, hogy f( x ) x 3. Ettől még lehet f(n) = O(n log n), ha n 0 > x (a kis méretű bemenetek kaotikus lépésszámai nem számítanak), vagy ha c értéke elég nagy: x 3 c x log x amiből c x. Tehát a válasz igen. log x (b) Tehát f(n) 07n minden n-re. Az egyenlőtlenséget továbbírva: f(n) 07n 07n log n, ha log n 1,azaz n tehát n 0 = és c = 07 választással lehetséges, hogy az A algoritmusra (b) állítás igaz, miközben továbbra O(n log n). (c) Ez azt jelentené, hogy f(n) n n minden páros n-re. Viszont A komplexitása O(n log n), tehát f(n) már két oldalról is behatárolt: A két szélső kifjezést összevetve: n n f(n) c n log n, minden PÁROS n n 0-re. n n c n log n n c log n, ami épp azt jelentené, hogy n = O(log n), ami a nagyságrendi sorra tekintve rögtön látszik, hogy hamis. Tehát ez nem lehetséges. Ez egyébként intuitíve is láthtató: mivel A lépésszámának nagyságrendje legfeljebb n log n, nyilván nem lehetséges az, hogy minden bemeneten egy ennél nagyobb nagyságrendű, n n-nyi lépést használjon. 3
5. Az alábbi függvényeket rendezd olyan sorrendbe, hogy ha f j az f i után következik a sorban, akkor f i (n) = O(f j (n)) teljesüljön! Indokold is meg, miért jó a választott sorrend! f 1 (n) = 8n.5, f (n) = 5 n + 1000n, f 3 (n) = log n, f 4 (n) = 07n log n. Megoldás. Vegyük észre, hogy f 3 nem más, mint: f 3 (n) = log n = log n log n = ( log n) log n = n log n. Ekkor a domináns tagokat megvizsgálva sejthetjük, hogy a sorrend: f, f 4, f 1, f 3 kell hogy legyen. Ennek bizonyításához az ordó tranzitivitása miatt elég megmutatni bármely két szomszédről, hogy nagyságrendileg egymást követik. f (n) = O(f 4 (n)) bizonyítása A 4. feladat második módszerével, azaz majorálással: 5 n + 1000n 5n + 1000n 07n 07n log n, n 1 n tehát n 0 = max{1, } =, és c = 1 választással kész. f 4 (n) = O(f 3 (n)) bizonyítása A 4. feladat első módszerével, azaz ekvivalens átalakításokkal: 07n log n c 8n.5 07 log n c 8 n, ekkor a nagyságrendi sorra tekintve látható, hogy c = 07/8, n 0 = 1 választással ez minden n n 0 -ra igaz lesz. Kész vagyunk. f 1 (n) = O(f 3 (n)) bizonyítása A 4. feladat második módszerével, azaz majorálással: 8n.5 8 n log n, log n.5 tehát c = 8 és pl. n 0 = 8 (hiszen így log 8 = 3.5) választással jók vagyunk. 6. Ugyanarra a feladatra van két algoritmusunk A és B, a maximális lépésszámukat leíró függvények legyenek f A és f B. Tudjuk, hogy f A (n) = O(f B (n) log n). Következik-e ebből, hogy: (a) A minden bemenetre gyorsabb, mint B? (b) A nagy bemenetekre gyorsabb, mint B? És ha f A (n) log n = O(f B (n))? Megoldás. A feladat állítása szerint A lépésszáma nagyságrendileg felülről becsülhető egy B lépésszámánál picit (egy log n-es faktorral) nagyobb nagyságrendű függvénnyel. Tehát f A nagyságrendje akár egy log n-es faktorral nagyobb is lehet f B nagyságrendjénél, pl. az f A (n) = log n, f B (n) = 1 konkrét lépésszámok esetében a feladat feltétele teljesül. Nyilvánvaló, hogy ekkor n = 1 kivételével mindig a (konstans időben lefutó) B lesz a gyorsabb. A válasz tehát: nem, nem. Ezek után f A (n) log n = O(f B (n)) jelentése: az A lépésszámánál egy picit (egy log n- es faktorral) nagyobb nagyságrendű függvény nagyságrendileg felülről becsülhető B lépésszámával. Tehát B lépésszáma még legrosszabb esetben is egy log n-es nagyságrenddel rosszabb A-énál. Ugyanakkor persze elképzelhető, hogy A nagy konstans faktorral bír, így a kis bemenetekre lassabb, viszont ezt az hátrányt egyszercsak behozza, mivel egy log n-es faktor hiányzik belőle. Tehát: nem, igen. 4
7. Legyen f 1 (n) = n 3 log n és f (n) = 07 4 log n log n. Igaz-e, hogy f 1 = O(f ), ill. f = O(f 1 )? Megoldás. Célszerű a ekvivalens átalakításokkal közvetlenül nekiállni, és mindkét oldalnak a logaritmusát venni! 8. Egy algoritmus T (n) lépésszámára (rekurzívan) igaz, hogy: (és legyen T (1) = 1) (a) T (n) T (n 1) +. Igaz-e, hogy T (n) = O(n)? (b) T (n) T (n 1). Igaz-e, hogy T (n) = O( n )? (c) T (n) T ( ) n + 1. Igaz-e, hogy T (n) = O(log (n))? (d) T (n) T (n ) + n, illetve T (3) = és T (6) = 5. Milyen felső korlátot adhatunk a lépésszámra? Megoldás. Rekurzívan adott függvényeknél először célszerű megtippelni a nagyságrendjét. Ehhez azt nézzük meg, hogy az index mekkora növekményével mennyit nő a függvény értéke: az (a) feladatnál ha az index 1-gyel nő, a függvényérték -vel, tehát ez a meredekségű lineáris függvény rekurzív megadása, a (b)-nél rendre kétszerésre nő, tehát ez egy exponenciális függvény. A (c) részfeladatban a függvényérték 1-gyel nő mialatt az index kétszeresére változik, tehát ez (az előző inverze) egy logaritmus függvény. (a) A precíz számítás történhet teljes indukcióval, vagy a rekurzió ismételt behelyettesítésével. Utóbbi módszernél rendre beírjuk a T (i)-be a rekurziós képletet egészen addig, amíg a T számsorozat egy kezdőeleméig jutunk, megvizsgáljuk, egy-egy lépéskor milyen tényezők/- tagok keletkeztek, és megszámoljuk, hányat kellett lépnünk, hogy pl. T (1)-ig eljussunk: (b) Hasonlóan: T (n) T (n 1) + T (n ) + + T (n 3) + + +... T (1) + } + + {{... + } = 1 + (n 1) = O(n). n 1 darab T (n) T (n 1) T (n )... T (1) } {{... } = 1 n 1 = O( n ). n 1 darab (c) Itt nyilván n alsóegészrészével indexelünk: T (n) T ( n ) + 1 T (n 4 ) + 1 + 1 T (n 8 ) + 1 + 1 + 1... T (1) + } 1 + 1 + {{... + 1 } = 1 + log n = O(log n). log n darab (d) A páros és páratlan indexeket külön kell kezelnünk. Először nézzük a párosakat, majd a páratlanokat: T (k) T (k ) + k T (k 4) + k + k k(k + 1) k(k + 1) T (6) + k + (k 1) + + 5 + 4 = 5 + 3 1 = 1 T (k + 1) T (k 1) + k + 1 T (k 3) + k + 1 + k 1 T (3) 1 (k 1) + k + (k 1) + + 3 + = = 1 k(k + 1) k(k + 1) (k 1) + 1 1 +. Mindkét esetben T (n) 1 + n(n+1), így T (n) = O(n ). 5
9. Egy algoritmus maximális lépésszáma az n hosszú bemeneteken L(n). Tudjuk, hogy n > 3 esetén L(n) L(n 1) + n teljesül, és hogy L(3) = 3. Következik-e ebből, hogy az algoritmus lépésszáma O(n )? Megoldás. Világos, hogy L(1) és L() értékéről semmit nem tudunk, de ez nem is érdekes, hiszen legyen n 0 3. Megmutatjuk, ezzel viszont már létezik olyan c > 0 konstans, amellyel az O(n ) definíciója teljesül, azaz: L(n) c n n n 0 -ra. Sejtés, hogy c = 1 jó lesz. Teljes indukcióval megmutatjuk, hogy valóban. Az n = 3 esetre L(3) = 3 1 9, ez rendben van. Most tegyük fel, hogy valamilyen n-ig már beláttuk a fenti egyenlőtlenséget, megmutatjuk, hogy ebből n + 1-re is következik: L(n + 1) L(n) + n + 1 n + n + 1 n + n + 1 = (n + 1) n (n + 1). Itt az első egyenlőtlenség a rekurzív definícióból, a második az indukciós feltétel miatt következik. Ezzel készen is vagyunk. 10. Bizonyítsuk be, hogy az F (n) = 1, 1,, 3, 5,... Fibonacci-sorra teljesül, hogy F (n) = O(γ n ), ahol γ = 1+ 5. Megoldás. Ugyanúgy teljes indukcióval megy a bizonyítás, mint az előző feladatnál. 11. Hogyan tudjuk egy adott algoritmus hatékonyságát (pszeudo- vagy forráskódja alapján) az ordó-jelölés segítségével egyszerűen megbecsülni? Elemezzük a feladatsor végén található C++ kód függvényeinek komplexitását n függvényében! Megoldás. Egy kis elméleti bevezető. Tegyük fel, hogy van két szubrutinunk, az első maximális lépésszáma f 1, a másodiké f. Világos, hogy ha a két szubrutint egymás után hajtjuk végre, az így kapott összetett algoritmus maximális lépésszáma f 1 (n) + f (n) lesz. Mi van akkor azonban, ha a két szubrutin lépésszámára csak egy nagyságrendi felső becslést tudunk adni, tehát annyit tudunk, hogy az első komplexitása O(g 1 (n)), a másodiké O(g (n)). Mondhatjuk-e ekkor, hogy a kettő egymás utáni futtatásából álló összetett algoritmus komplexitása O(g 1 (n) + g (n))? Szerencsére igen, igaz az, hogy ha f 1 (n) = O(g 1 (n)) és f (n) = O(f (n)), akkor f 1 (n) + f (n) = O(g 1 (n) + g (n)). Ez a definíció segítségével elég könnyen bizonyítható. A definíció értelmében ugyanis: f 1 (n) c 1 g 1 (n), minden n n 0,1 -re. f (n) c g (n), minden n n 0, -re, a két azonos értelmű egyenlőtlenséget pedig összeadhatjuk: f 1 (n) + f 1 (n) c 1 g 1 (n) +c g (n) max{c1, c} ( g 1 (n) + g (n) ), n max{n 0,1, n 0, }-re. Amennyiben (jogosan) feltesszük, hogy mind g 1, mind g nemnegatív függvények, n 0 = max{n 0,1, n 0, }, c = max{c1, c} választással ez épp f 1 (n) + f (n) = O(g 1 (n) + g (n)) definíciója. Hasonló állítás igaz a szorzásra: f 1 (n) f (n) = O(g 1 (n) g (n)), az előző jelölésekkel. Mindez azt jelenti, hogy ahhoz, hogy egy összetett algoritmus komplexitására (ordós) nagyságrendi felső becslést tudjunk mondani, elég ismerni az összetevőinek lépésszámára is csak 6
egy nagyságrendi felső becslést. Ráadásul az O(... )-s jelölésben szereplő kifejezések természetes módon adódnak és szorzódnak össze. Definiálhatunk tehát az ordós tagok között egyfajta szimbolikus összeadás és szorzás műveletet: O(g 1 (n)) + O(g (n)) = O(g 1 (n) + g (n)) O(g 1 (n)) O(g (n)) = O(g 1 (n) sg (n)), ahol előbbi az egymás után futtatás, míg utóbbi annak komplexitását méri, ha O(g 1 (n))-szer futtatunk le egy O(g (n)) komplexitású algoritmust (például egy ciklusban). Továbbá világos, hogy f(n) = O(f(n)), tehát ha valaminek tudjuk a pontos lépésszámát, akkor ez a függvény egyben jó nagyságrendi felső becslésnek is. Hasznos még tudni, hogy ha f(n) = O(K g(n)), ahol K egy konstans, akkor f(n) = O(g(n)) is (tehát a konstansokkal a szimbolikus számítás során egyszerűsíthetünk), illetve ha f(n) = O(g(n) + h(n)) és g(n) = O(p(n)), akkor f(n) = O(p(n) + h(n)) is (tehát az ordó argumentumabeli kifejezésben szereplő tagokat náluk nagyobb nagyságrendűre cserélhetjük ez a tranzitivitás kiterjesztésének tekinthető). A fentiek rendkívül hasznosak, ugyanis a legtöbb esetben algoritmusainkat elemi komponensekből építjük fel, amelyek komplexitását jól ismerjük. A fent leírt szimbolikus számolási szabályokkal pedig természetes módon kiszámolhatjuk ezek segítségével az összetett algoritmus komplexitását. Például sum_of_first_nn komplexitása n függvényében (ahol O(1) a konstans idejű művelet): O(1) }{{} változó inicializálása + }{{} n O(1) = O(1 + n 1) = O(n) }{{} ennyiszer fut le a ciklus számolás a ciklustörzsben Ezzel a calculate_sums komplexitása n függvényében (az if-else vezérlési szerkezetnél az egyes ágak komplexitásai összeadódnak, lásd 7. feladat (b) része): n O(n) + O(1) + n O(1) = O(nn + 1 + 1 n) = O(n ). }{{}}{{} if-ág else-ág Figyeljük meg, hogy az if-ágban nagyvonalúan a lehető legnagyobb i értékkel, n-nel számoltuk a sum_of_first_nn szubrutinhívás komplexitását. Ez jelen esetben éles felső becslés (mivel 1++...+n = n (n+1) = Θ(n )), de olykor elképzelhető, hogy ilyen esetekben túlságosan gyenge lesz a végső becslésünk. Ekkor sajnos a konkrét lépésszámokkal kell bonyolultan végigdolgozni. A get_first_at_least függvény csak didaktikai célokat szolgál: figyeljük meg, hogy a treshold paraméter függvényében a ciklus 1 és n iteráció között bárhol terminálhat, tehát a (nem maximális) lépésszám ezek között bárhol lehet. Rendkívül szemléletes ilyenkor az O(n) jelölés, ami tükrözi, hogy a lépésszámra n egy felső becslés. Puska és tudni érdemes dolgok Nagyságrendek, növekvően: log(n), log (n),..., n, n, n, n 3,..., 1.1 n, n,..., n!, n n. Nagyságrendi felső becslés: f(n) = O(g(n)) ( f egyenlő nagy ordó g ), ha: c > 0 és n 0 > 0, hogy n n 0 : f(n) c g(n). Nagyságrendi alsó becslés: f(n) = Ω(g(n)) ( f egyenlő nagy omega g ), ha: c > 0 és n 0 > 0, hogy n n 0 : f(n) c g(n). 7
Azonos nagyságrendek: f(n) = Θ(g(n)) ( f egyenlő nagy theta g ), ha: f(n) = O(g(n)), f(n) = Ω(g(n)) egyszerre teljesül. Ha nem írjuk külön, a logaritmus alapja minding. De igazából nem fontos, hiszen log a (x) = Θ(log b (x)). Egy kis csalás: legtöbbször feltesszük, hogy az alapműveletek O(1) időben végezhetőek, a számok nagyságától függetlenül. Forráskód a 11. feladathoz 1 int sum_of_first_nn ( int n) { 3 int r =0; 4 for ( int i =0; i<n; ++i ) { r += i*i; } 5 return r; 6 } 7 void calculate_ sums ( int n, int T[], bool quick ) 8 { 9 if(! quick ) { 10 for ( int i =0; i<n; ++i ) { T[i] = sum_of_first_nn (i); } 11 } else { 1 T [0] = 0; 13 for ( int i =0; i<n; ++i ) { T[i] = T[i -1] + i*i; } 14 } 15 } 16 int get_ first_ at_ least ( int n, int T[], int treshold ) 17 { 18 for ( int i =0; i<n; ++i ) { if( T[i] >= treshold ) return T[i]; } 19 return -1; 0 } 8