Rekurzió szenasi.sandor@nik.bmf.hu PPT 2007/2008 tavasz http://nik.bmf.hu/ppt 1 Témakörök Rekurzió alapjai Rekurzív algoritmusok végrehajtása Visszalépéses keresés Programtranszformációk 2 Rekurzió alapelve Ahhoz, hogy megérthessük a rekurziót, először meg kell értenünk a rekurziót (ismeretlen szerző) Rekurzív algoritmusokat általában akkor használunk, ha az alapfeladat túl bonyolult, azonban rekurzív hívásokkal ezt vissza tudjuk vezetni egyszerűbb (rész)feladatok megoldására Ennek megfelelően egy rekurzív feladatot általában ehhez hasonlóan definiálunk: triviális megoldása általános eset egyszerűsítése Technikai értelemben rekurzív algoritmusnak azt tekintjük, ami közvetve (közvetlen rekurzió) vagy más függvények közbeiktatásával (közvetett rekurzió) meghívja önmagát 3 1
Rekurzív függvény felépítése Az előzőek alapján egy lehetséges definíció: g(x) ha t(x) f(x) = h f i(x) ha t(x) f(x) a rekurzív függvény t(x) triviális esethez jutottunk-e? g(x) triviális esetben a megoldás h(x) utólagos feldolgozás i(x) előzetes feldolgozás Feltételezzük, hogy bármelyik bemenetből a triviális megoldás biztosan, véges lépésből elérhető A függvény általában nem csak önmagát hívja, hanem ez előtt és után van lehetősége műveleteket végezni a továbbítandó paramétereken 4 Rekurzív függvény általános pszeudokódja Az előző oldalon látottaknak megfelelően: Függvény Rekurzív(x) Ha t(x) akkor Rekurzív g(x) a i(x) b Rekurzív(a) c h(b) Rekurzív c A pszeudokódban található függvényhívások értelemszerűen feladattól függően jelenthetnek tetszőleges összetett műveletet A fenti csak egy általános minta, a konkrét feladatok ettől egészen eltérő szerkezeteket is igényelhetnek (pl. több hívás, ezek eredményei között műveletek) 5 Rekurzív függvény általános pszeudokódja Rekurzív feladat megoldásának lehetőségei Rekurzív specifikáció Nemrekurzív specifikáció Rekurzív algoritmus Nemrekurzív algoritmus Rekurzív programnyelv Nemrekurzív programnyelv Számítógép Az implementált programot minden esetben Neumann elvű (tehát nem rekurzív) gépen hajtjuk végre 6 2
Témakörök Rekurzió alapjai Rekurzív algoritmusok végrehajtása Visszalépéses keresés Programtranszformációk 7 Verem Verem adatszerkezet adatok tárolására szolgáló szerkezet mindig az utoljára belehelyezett elemet (push) tudjuk belőle kiolvasni (pop) (LIFO Last In First Out) kiolvasáskor az elem egyben törlődik is BE BE 5 4 5 4 35 7 BE BE 3 7 KI 7 KI 3 BE 5 később részletesebben tárgyaljuk Processzor verem a verem gyakran használt adatszerkezet az operációs rendszer, illetve a processzor működése során eljárások hívásakor a verem tárolja el a hívó utasítást követő utasítás címét (ide kell majd visszatérni a befejezés után) szintén a veremben tárolódnak a meghívott eljárás paraméterei, lokális változói stb. ez természetesen implementációtól függ, a mi számunkra azonban ez az egyszerűsített működési elv is megfelelő 8 Eljárások hívásának (egy lehetséges) módja E1(1, 2) Eljárás E1(a, b) x lokális változó E2(a+b) Eljárás E2(d) y lokális változó C 1 2 x C 3 y verem aktuális állapota Minden függvényhíváskor eltárolódik a visszatérési cím, illetve a függvény lokális változói Visszatéréskor ezek törlődnek a veremből Ez alapján látható, hogy az általuk látott adatok szempontjából az egymás után hívott függvények valójában egymástól függetlenek Lásd változók élettartama 9 3
Rekurzív eljáráshívás Függvény Fakt(a) x = Fakt(3) Ha a = 0 Függvény akkor Fakt(a) a = 3 Függvény Fakt(a) a = 2 Ha a = 0 akkor Fakt 1 Ha a = 0 Függvény akkor Fakt(a) a = 1 a = 0 Fakt 1 Ha a = 0 akkor Fakt 1 (6) Fakt a * Fakt(a-1) (1) Fakt 1 (2) Fakt a * Fakt(a-1) (1) Fakt a * Fakt(a-1) Fakt a * Fakt(a-1) x C 3.. C 2.. C 1.. C 0.. verem aktuális állapota Látható, hogy rekurzív hívás esetén is egymástól független adatokon dolgoznak az egyes függvények Az ugyanolyan nevű lokális változók emiatt nincsenek egymásra hatással Ennek figyelembevételével kell megoldanunk az egyes függvények közötti adatcserét 10 Rekurzív algoritmus bemenete Biztosítani kell, hogy minden meghívott függvény hozzáférjen a működéséhez szükséges bemenő paraméterekhez Bemenet biztosítása külső változókon keresztül az egyes futó függvény példányok nem látják egymás lokális adatait, ezért a mindegyik számára szükséges bemeneti adatokat nem tudjuk ezek között tárolni a függvényen kívüli adatokat azonban a rekurzió minden szintjéről elérjük (globális változók, objektum adattagjai stb.) tárhely/futásidő szempontjából optimális megoldás Bemenet biztosítása paramétereken keresztül mint minden más függvénynél, a rekurzív függvényeknél is van lehetőség minden bemenő paramétert átadni a függvény hívásakor ebben az esetben értelemszerűen a következő rekurzív híváskor mindezt újra át kell adni ez jóval áttekinthetőbb, bár kevésbé hatékony megoldást nyújt 11 Rekurzív algoritmus kimenete Rekurzív hívási lánc során problémát jelenthet az eredmény visszaadás, mivel az eredeti hívó, illetve a végeredményt elérő függvény között számos függvényhívás állhat Eredmény visszaadás külső változókon keresztül a bemenethez hasonlóan itt is van arra lehetőség, hogy a végeredményig eljutó szint egy külső változóban eltárolja az eredményt, a hívó pedig majd innen kiolvassa Eredmény visszaadás függvény visszatérési értékkel hagyományos függvényekhez hasonlóan a rekurzív függvények is rendelkezhetnek visszatérési értékkel az önmagát meghívó függvénynek azonban biztosítania kell, hogy az (önmagától) visszakapott értéket továbbítsa a hívója felé Eredmény visszaadás paraméterekkel amennyiben az újrahíváskor is mindig címszerinti paraméterátadás történt, akkor bármelyik szint változtatja meg a paraméter értékét, az a hívó szintjén is változni fog 12 4
Rekurzió jellemzői Előnyök gyakran elegáns, jól érthető, áttekinthető kódot ad bizonyos feladatoknál (pl. rekurzív adatszerkezetek feldolgozása esetén) jóval egyszerűbbek a rekurzív megoldások Hátrányok gyakran áttekinthetetlen, ember számára nagyon nehezen értelmezhető kódot ad nagyszámú újrahívás esetén a nyomkövetés nehézkes, nehezen áttekinthető a függvényhívás általában meglehetősen költséges művelet, emiatt a rekurzív algoritmusok nem hatékonyak egy rekurzívan megadott algoritmusban gyakran észrevétlenek maradnak elhibázott döntések (bár ez nem a rekurzió, hanem a tervező hibája) Átalakítások később látni fogjuk, hogy a rekurzív és iteratív megoldások általában egymásba alakíthatók 13 Témakörök Rekurzió alapjai Rekurzív algoritmusok végrehajtása Visszalépéses keresés Programtranszformációk 14 Egy megoldásra váró feladat Egy építkezésen több egymástól független munkafázist kell elvégezni. Osszuk szét a munkákat az arra alkalmasak között (mindenki csak egyet vállalhat)! Géza Miklós Miklós András Zsolt Géza Miklós Klaudia András Zsolt Palika András Géza Szponzor Irányítás Alap Fal Engedély Lefizetés Végigpróbálgathatjuk az összes lehetséges változatot, (2 2 2 3 2 2 = 96 db), ezek túlnyomó többsége azonban nem megoldása a feladatnak Olyan algoritmust keresünk, ami a megoldás keresés során eleve nem folytat olyan utakat, amelyek nem vezethetnek megoldáshoz 15 5
Feladat általánosítása N darab részeredményt keresünk (E 1, E 2 E N ) Mindegyiknek ismerjük a véges értékkészletét (pl. E 1 - hez ennek mérete M 1, elemei: R 1,1, R 1,2, R 1,M1 ) M 4 =3 M 1 =2 M 2 =2 M 3 =2 R 4,1 M 5 =2 M 6 =2 R 2,1 R 3,1 R 4,2 R 1,2 R 1,1 R 6,2 R 5,1 R 6,1 R 2,2 R 3,2 R 4,3 R 5,2 E 1 E 2 E 3 E 4 E 5 E 6 A visszalépéses keresés olyan feladat típusoknál alkalmazható hatékonyan, amelyeknél egy tetszőleges szabállyal a várt eredmények egy részéről is meg lehet állapítani, hogy nem lehet egy jó megoldás része A példában ez a szabály az volt, hogy egy embert nem rendelhetünk két munkához 16 N=6 Visszalépéses keresés paraméterei Keresés bemenete: N részeredmények száma M i i. részeredmény értékkészletének mérete R i,j i. részeredmény j. lehetséges értéke Keresés kimenete: VAN van-e teljes megoldás E i az i. részeredmény értéke A feladattól függő szabályokat általában két függvény segítségével adjuk meg: ft(i, r) visszatérési értéke igaz, ha az i. részeredményként elfogadható az r érték (a mi példánkban ez mindig igaz lesz) fk(i, r, j, q) visszatérési értéke igaz, ha az i. helyen található r érték és a j. helyen található q érték nem zárják ki egymást (a mi példánkban akkor igaz, ha r q) 17 Rekurzív visszalépéses keresés Visszalépéses keresés egy lehetséges megvalósítása Eljárás Próbál(szint, címszerint VAN, E) i 0 Ciklus i i + 1 Ha ft(szint, i) akkor k = 1 Ciklus amíg (k < szint) és fk(szint, R szint,i, k, E k ) k k + 1 Ha k = szint akkor E szint R szint,i Ha szint = N akkor VAN igaz Próbál(szint + 1, VAN, E) Ciklus amíg (nem VAN) és (i < M szint ) VAN hamis; Próbál(1, VAN, E) 18 6
Megjegyzések A szakirodalomban többféle algoritmussal lehet találkozni, ezekről néhány gondolat: elképzelhető olyan elhelyezési szabály, amit nem lehet az elemek páronkénti ellenőrzésével megvalósítani (pl. maximum ketten dolgozhatnak ugyanazon a munkán), ez azonban kisebb módosítással megvalósítható (fk függvényeket hívó ciklus helyettesítése az összetett szabályt ellenőrző algoritmussal) az eredménybe gyakran nem magát az értéke várjuk, hanem csak annak az indexét gyakran az összes részeredmény ugyanabból az értékkészletből kaphat valamilyen értéket, ilyenkor lehetőségünk van egyszerűsítésre mivel az algoritmus sokszor hívja meg önmagát, növelheti a hatékonyságot, ha a VAN és E változókat nem paraméterként kezeljük Az algoritmusnak természetesen létezik nem rekurzív változata (lásd előző témakör) 19 Minden megoldás kiválogatása Az első megoldás után nem állunk meg, keressük a többit Eljárás Próbál(szint, címszerint VAN, E, EMIND) i 0 Ciklus i i + 1 Ha ft(szint, i) akkor k = 1 Ciklus amíg (k < szint) és fk(szint, R szint,i, k, E k ) k k + 1 Ha k = szint akkor E szint R szint,i Ha szint = N akkor EMIND EMIND E VAN igaz Próbál(szint + 1, VAN, E, EMIND) Ciklus amíg (nem VAN) és (i < M szint ) 20 Legoptimálisabb megoldás keresése Keresés helyett tulajdonképpen minimumkiválasztás Eljárás Próbál(szint, címszerint VAN, E, EMAX) i 0 Ciklus i i + 1 Ha ft(szint, i) akkor k = 1 Ciklus amíg (k < szint) és fk(szint, R szint,i, k, E k ) k k + 1 Ha k = szint akkor E szint R szint,i Ha szint = N akkor Ha (nem VAN) vagy (költség(e) < költség(emax)) akkor EMAX VAN igaz Próbál(szint + 1, VAN, E, EMAX) Ciklus amíg (nem VAN) és (i < M szint ) 21 E 7
8 királynő a sakktáblán Klasszikus feladat: helyezzünk el úgy 8 királynőt a sakktáblán, hogy azok ne üssék egymást A lehetséges elhelyezések száma meglehetősen nagy: 64 63 62 61 60 59 58 57 1,78 * 10 14 Viszont látható, hogy bizonyos kiinduló állapotokból (pl. A1 és A2 az első két királynő) felesleges tovább vizsgálódni, nem vezethet jó megoldáshoz Mivel nyilvánvaló, hogy minden oszlopban pontosan egy királynőt kell elhelyeznünk, feltehetjük úgy is a kérdést, hogy melyik oszlopban hol a királynő helye? A megismert algoritmus így már alkalmazható: N = 8 M i = 8 ; R i,j = j (i=1..8 ; j = 1..8) ft(i, r) = igaz fk(i, r, j, q) = akkor igaz, ha a sakk szabályai szerint az i,r és a j,q pozicióban lévő királynők nem ütik egymást 22 8 királynő a sakktáblán Egy lehetséges elhelyezés Segítség: (x 1,y 1 ) és (x 2, y 2 ) helyen álló királynők akkor ütik egymást, ha az alábbiak közül bármelyik teljesül: x 1 = x 2 y 1 = y 2 x 1 - x 2 = y 1 - y 2 23 Huszár útja a sakktáblán Klasszikus feladat: a sakktábla bármelyik mezőjéről be lehet-e járni egy huszárral az egész táblát úgy, hogy minden mezőt pontosan egyszer érintünk? Egy huszár 8 irányba tud lépni, így az ellenőrizendő kombinációk száma: kb. 8 64 6,28 * 10 57 Mivel itt nem elemeket kell elhelyeznünk, hanem egy mozgást kell modellezni, emiatt a keresendő eredmény az egymás utáni lépések iránya lesz A megismert algoritmus változtatás nélkül használható: N = 64 M i = 8 ; R i,j = a huszár j. lehetséges lépése (pl. 2 fel+1 jobbra) az ft és fk függvényeket ebben az esetben más formában (paraméterekkel) kell megadni, de a megoldás szempontjából szerepük hasonló: ft megadott helyre léphet-e a huszár (táblán belül marad?) fk az előző lépések nem zárják-e ki az új helyet? (járt már ott?) 24 8
Néhány további feladat Adott M darab őstermelő, akik fejenként KI i mennyiségű répát termesztenek. Adott N darab áruház, akik BE i mennyiségű répát igényelnek. Egy áruház csak egy őstermelőtől szeretne vásárolni (fordítva nincs ilyen kikötés) Adjunk meg egy lehetséges őstermelő-áruház kapcsolati rendszert, amennyiben ilyen létezik Őstermelőnként határozzuk meg a répa árát, majd optimalizáljuk a keresést az áruházak (minél kisebb kiadás), illetve az őstermelők igényei szerint is (minél nagyobb bevétel) Adott M darab T i tömegű tárgy, illetve egy hátizsák, amibe legfeljebb MAX tömegű tárgy fér. Adjunk egy optimális megoldást arra, hogy minél jobban kihasználjuk a hátizsák kapacitását! 25 Témakörök Rekurzió alapjai Rekurzív algoritmusok végrehajtása Visszalépéses keresés Programtranszformációk 26 Elemi konstrukciók függvények segítségével A strukturált programozásnál megismert három konstrukció egyszerűen felírható függvényekkel is Szekvencia f(x) = g Elágazás f(x) = Ciklus f(x) = h(x) g(x) h(x) g(x) ha p(x) ha p(x) ha p(x) h f i(x) ha p(x) Egyszerű szabályokat követve így megadhatjuk a programozási tételek rekurzív formáját S 1 S 2 Ha L akkor S 1 S 2 Ciklus amíg L S 27 9
R I Rekurzív formában megadott fv. Ha egy rekurzív függvény az alábbiak szerint számítható ki: g(f(i-1), f(i-2),, f(i-k)) ha i > K f(i) = h(i) ha 0 i < K Az iteratív megoldás: Eljárás f(n) Ciklus i 0-tól (K-1)-ig F[i] h[i] Ciklus i K-tól N-ig F[i] g(f[i-1], F[i-2],, F[i-K]) f F[N] Optimálisabb változat is készíthető, hiszen a tömbből mindig csak az utolsó K elemre van szükségünk 28 R I Jobbrekurzió átírása Jobbrekurzió általános esete (a rekurzív hívást követően már nincs szükség a függvény lokális változóira): Eljárás JobbRek(X, Y) Q(X, Y) Ha p(x, Y) akkor S(X, Y) JobbRek(f(X), Y) Az iteratív megoldás: Eljárás JobbRek(X, Y) Q(X, Y) Ciklus amíg p(x, Y) S(X, Y) X f(x) Q(X, Y) 29 R I Balrekurzió átírása 1. Balrekurzió során a rekurzív függvény meghívása után is szükség van a lokális változók értékeire, azok módosulhatnak is Balrekurzió általános esete: Eljárás BalRek(X, Y) Ha p(x, Y) akkor BalRek(f(X), Y) S(X, Y) T(X, Y) A hívás előtt is szerepelhet valamilyen művelet, az egyszerűség kedvéért ezzel nem foglalkozunk Tipikusan akkor célszerű ezt használni, ha egy sorozatot visszafelé szeretnénk feldolgozni (verem?) 30 10
R I Balrekurzió átírása 2. Balrekurzió egy lehetséges iteratív átirata: Eljárás BalRek(X, Y) N 0 Ciklus amíg p(x, Y) Verembe(X) X f(x) N N + 1 T(X, Y) Ciklus i 1-től N-ig Veremből(X) S(X, Y) Ha a sorozat elemszámát előre ismerjük, értelemszerűen nincs szükség az N számolásra Ha az f(x) függvénynek van inverze, egyszerűbb (veremnélküli) algoritmus is adható 31 Iteratív Rekurzív átalakítás Az elöl és hátultesztelős ciklusok átírása Eljárás ElölTeszt(X) Ciklus amíg P(X) S(X) Eljárás HátulTeszt(X) Ciklus S(X) Ciklus amíg P(X) Eljárás ElölTeszt(X) Ha p(x) akkor S(X) ElölTeszt(X) Eljárás HátulTeszt(X) S(X) Ha p(x) akkor HátulTeszt(X) Számlálós ciklus egyszerűen átírható elöltesztelőssé Amennyiben a ciklus előtt vagy után további műveletek szerepelnek, azokat célszerű egy másik függvényben (a rekurzió 0. szintjén ) elvégezni 32 Javasolt/felhasznált irodalom Pap, Szlávi, Zsakó: μlógia4 Módszeres programozás: Rekurzió ELTE TTK, 2004 S. Harris, J. Ross: Kezdőkönyv az algoritmusokról SZAK Kiadó, 2006 Wikipedia.org megfelelő szócikkek http://en.wikipedia.org/wiki/recursion http://en.wikipedia.org/wiki/backtracking 33 11