1 of 66

Dinamikus Programozás2024.10.02

2 of 66

Ismétlés

3 of 66

Oszd meg és uralkodj

Divide & Conquer (,,Oszd meg és uralkodj”) paradigma

4 of 66

Oszd meg és uralkodj

Felosztás: hogyan osztjuk a feladatot több részfeladatra

Uralkodás: a részfeladatokat rekurzív módon megoldjuk. Ha a részfeladatok mérete elég kicsi, akkor közvetlenül megoldja a részfeladatokat.

Összevonás: a részfeladatok megoldásait összevonjuk az eredeti feladat megoldásává

PL: vállalat adója ->részleg->munkások

5 of 66

Rekurzió

Faktoriális számítás: n! = n· (n - 1) · … · 2 · 1

Rekurzív összefüggés: n! = n · (n - 1)!

Alapeset: n = 1

Rekurzív eset: n > 1: n · (n - 1)!

6 of 66

Fibonacci

  • Keressük meg a Fibonacci sorozat (1, 1, 2, 3, 5, 8, 13...) N. elemét

  • Alapeset:

az első és a második elem értéke 1.

  • Rekurzív eset:

az N. elem az N-1 - edik és az N-2 - dik elemek összege.

7 of 66

Fibonacci

def fibo( n ) :

if (( n == 1 ) || (n == 0)):

return 1

else

return fibo(n-1) + fibo(n-2)

8 of 66

Lépcső

n=4

9 of 66

Lépcső

  • P(n) = P(n − 1) + P(n − 2), ha n ≥ 3 (utolsó lépésként egyet vagy kettőt léphetünk)

def P( n ) :

if ( n == 1 )

return 1

elseif ( n == 2 )

return 2

else

return P( n−1) + P( n−2)

10 of 66

Dinamikus programozás

11 of 66

Dinamikus programozás

Az eddigiekben egy-egy összetett feladat megoldására oszd-meg-és-uralkodj megoldást használtunk.

A rekurzív megoldás legnagyobb hátránya azonban az, hogy legtöbb esetben, ahogy növekszik az elemszámunk, vele együtt hatványozottan növekszik a megoldáshoz szükséges idő hossza és a lépések száma is.

12 of 66

Dinamikus programozás

Ennek a problémának a kiküszöbölésére ad hatékony megoldást a dinamikus programozás.

Ebben a tananyagban egy másik, szintén részproblémára bontáson alapuló, algoritmus családdal, a dinamikus programozással ismerkedünk meg.

13 of 66

Dinamikus programozás

A rekurzív algoritmusok sokszor azért lassúak, mert bizonyos részproblémákat többször is kiszámolnak.

Tipikusan ez történik, amikor optimalizálási feladatunk van és a megoldás kedvezményeket lépésről lépésre építjük fel.

14 of 66

Dinamikus programozás

Ezt úgy tudjuk elkerülni, ha az egyszer már kiszámolt rész megoldásokat eltároljuk, és később újra felhasználjuk azokat.

A több időt valójában most több tárra „cseréljük”, melynek eredményeként a rekurzív programok időkomplexitása drasztikusan csökkenthető.

15 of 66

Dinamikus programozás

A dinamikus programozás a részproblémákra bontás gyengeségeit két fő módszerrel próbálja orvosolni:

Az egyik módszer lényege, hogy programunk futása alatt

  • a megoldott rész problémák (optimális) megoldását eltároljuk,
  • ha ugyanezen részprobléma megoldására szükségünk lenne később, csak kiolvassuk a megoldást,
  • nem oldjuk meg újra a részfeladatot

(TÁBLÁZATKITÖLTÉS módszere)

16 of 66

Dinamikus programozás

A második módszer:

Általában amikor a részproblémákat többször is szükséges megoldani, akkor az összes lehetséges „kis” részproblémát ki kell számolni, hogy az eredeti nagy probléma megoldása kiszámítható legyen.

17 of 66

Dinamikus programozás

Ezért a dinamikus programozásban tipikusan:

  • a legkisebb problémától indulunk,
  • minden egyes nagyobb részprobléma megoldását kiszámítjuk a kisebb részfeladatok megoldásainak felhasználásával,
  • egészen addig míg az eredeti/legnagyobb feladatig el nem jutunk

(LENTRŐL FELFELÉ ÉPÍTKEZÉS módszere).

18 of 66

Dinamikus programozás

Vegyük észre, hogy ez ellentétes az oszd-meg-és-uralkodj algoritmusok filozófiájával, ahol mindig az eredeti problémát bontjuk kisebb és kisebb problémákra

(FELÖLRŐL LEFELÉ ÉPÍTKEZÉS módszere)

19 of 66

Dinamikus programozás

A Dinamikus programozás gyakorlatilag „ugyanaz” mint a oszd meg és uralkodj módszer/rekurzió.

Ám még előzőekben sok egymásba ágyazás/időigény mellett dolgoztunk, most a memóriában történő tároláson van a hangsúly.

20 of 66

Dinamikus programozás

Nézzünk meg egy példát:

Rendezzük a következő számsort 8, 4, 1, 5, 3, 2, 6, 7 !

Próbáljunk oszd meg és uralkodj logikával gondolkodni

21 of 66

Dinamikus programozás

Összefésülő (merge)rendezés:

A tömböt két részre vágjuk (lehetőleg fele-fele arányban), a részeket külön-külön rendezzük (rekurzió!), majd a rendezett részeket összefésüljük.

22 of 66

Dinamikus programozás

Probléma: Rekurzív algoritmusok -> többször is kiszámolnak.

Megoldás: A részmegoldások -> eltároljuk es újra felhasználjuk.

A több időt több tárra ,,cseréljük" -> exponenciális idő -> polinomidő

Ehhez viszont el kell tároljuk a rész problémák megoldását.

23 of 66

Dinamikus programozás

A megoldás lépései általában:

Adjuk meg a rekurzív megoldást (alap- és rekurzív eset).

A rekurzió alapján építsük fel ,,lentről felfelé”: az algoritmus kezdjen az alapesettel, majd a teljes megoldásig egy jó sorrendben számolja ki a rész problémákat.

24 of 66

Fibonacci

def fib( n ) :

if (( n == 2 ) || (n == 1)):

result = 1

else

result = fib(n-1) + fib(n-2)

return result

25 of 66

Táblázatkitöltés

A részeredmények (TÁBLÁZATKITÖLTÉS) tárolásának módszere:

A részproblémára bontási algoritmusokat kibővítjük egy- vagy kétdimenziós tömbbel, melyet a részeredmények tárolására használunk fel.

26 of 66

Táblázatkitöltés

Amikor egy részproblémát kell megoldanunk először megvizsgáljuk, hogy a keresett érték megtalálható-e a részeredmények között.

Ha megtalálható, akkor felhasználjuk, egyébként kiszámoljuk, majd eltároljuk.

27 of 66

Fibonacci

def fib( n ) :

if (( n == 2 ) || (n == 1)):

result = 1

else

result = fib(n-1) + fib(n-2)

return result

Rendkívül gyenge a hatásfoka, nagyon sokáig fut.

Nézzünk egy példát: fib(5)

28 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

Fibonacci

29 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

Fibonacci

30 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1

Fibonacci

31 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1

Fibonacci

32 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1

Fibonacci

33 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1

Fibonacci

34 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1 1 2

Fibonacci

35 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1 1 2

Fibonacci

36 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1 1 2 4

Fibonacci

37 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1 1 2 4

Fibonacci

38 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1 1 2 4 5

Fibonacci

39 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1 1 2 4 5

Fibonacci

40 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1 1 2 4 5

Fibonacci

41 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1 1 2 4 5

Fibonacci

42 of 66

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

1 1 2 4 5

Fibonacci

43 of 66

  • Sok számot többször ki kell számolnunk, emiatt sok az ismételt lépés.

  • 5 számnál ez nem tűnhet nagy valaminek, de ennek a megoldásnak a növekedése

  • A dinamikus programozás elve pedig azt „kérdezi”, hogy miért nem tároljuk el az ismétlődő értékeket.

Fibonacci

44 of 66

def fib( n, memo ) :

if memo[n] != null:

return memo[n]

if (( n == 2 ) || (n == 1)):

result = 1

else

result = fib(n-1) + fib(n-2)

memo[n] = result

return result

  • Nézzük meg, hogyan nézne ki a példánk, ha eltárolnánk az értékeket.

memo = 1 1 2 3

T(n) = 2n + 1 …… O(n)

Fibonacci

45 of 66

def fib( n, memo ) :

if memo[n] != null:

return memo[n]

if (( n == 2 ) || (n == 1)):

result = 1

else

result = fib(n-1) + fib(n-2)

memo[n] = result

return result

  • fib(5)

memo =

Fibonacci

46 of 66

def fib( n, memo ) :

if memo[n] != null:

return memo[n]

if (( n == 2 ) || (n == 1)):

result = 1

else

result = fib(n-1) + fib(n-2)

memo[n] = result

return result

  • fib(5)

memo =

Fibonacci

47 of 66

def fib( n, memo ) :

if memo[n] != null:

return memo[n]

if (( n == 2 ) || (n == 1)):

result = 1

else

result = fib(n-1) + fib(n-2)

memo[n] = result

return result

  • fib(5)

memo = 1

Fibonacci

48 of 66

Fibonacci

def fib( n, memo ) :

if memo[n] != null:

return memo[n]

if (( n == 2 ) || (n == 1)):

result = 1

else

result = fib(n-1) + fib(n-2)

memo[n] = result

return result

  • fib(5)

memo = 1 1

Fibonacci

49 of 66

def fib( n, memo ) :

if memo[n] != null:

return memo[n]

if (( n == 2 ) || (n == 1)):

result = 1

else

result = fib(n-1) + fib(n-2)

memo[n] = result

return result

  • fib(5)

memo = 1 1 2

Fibonacci

50 of 66

def fib( n, memo ) :

if memo[n] != null:

return memo[n]

if (( n == 2 ) || (n == 1)):

result = 1

else

result = fib(n-1) + fib(n-2)

memo[n] = result

return result

  • fib(5)

memo = 1 1 2 3 5

Fibonacci

51 of 66

Alulról felfelé építkezés

A TÁBLÁZATKITÖLTÉSEN alapuló dinamikus programozási megoldásnál még gyorsabb lehet, ha nem FELÜLRŐL LEFELÉ, rekurzív módon töltjük ki a táblázatot, hanem az ALULRÓL FELFELÉ ÉPÍTKEZÉS módszerével:

52 of 66

Alulról felfelé építkezés

-Hozzuk létre a megfelelő méretű táblázatot, amiben minden cella egy része problémának felel meg.

-Töltsük az alapesetek megoldásait.

-„alulról felfelé” az alapesetekből kiindulva, a teljes megoldásig számoljuk ki a részproblémák megoldásait a már korábban kiszámolt megoldások alapján

53 of 66

Lépcső

Jelölje P(n) azt a számot, hányféleképpen mehetünk fel egy n lépcsőfokból álló lépcsőn, ha egyszerre csak 1 vagy 2 lépcsőfokot léphetünk.

Adjunk egy dinamikus programozási eljárást a P(n) érték kiszámítására!

P(6) = ?

54 of 66

Lépcső

Az előző órán a rekurzív algoritmushoz megadtuk az összefüggéseket:

Alapeset:

  • P(1) = 1 (ha egy lépcső van, egyféleképpen mehetünk)
  • P(2) = 2 (ha két lépcső van, vagy kétszer egyet lépünk, vagy egyszer kettőt)

Rekurzív eset:

  • P(n) = P(n − 1) + P(n − 2), ha n ≥ 3 (utolsó lépésként egyet vagy kettőt léphetünk)

55 of 66

Lépcső

Ezek alapján a következő dinamikus programozási eljárást adhatjuk meg, ahol egy T tömböt töltünk ki, T[i] a P(i) értékét tartalmazza:

56 of 66

Lépcső

Nézzünk egy példát: egy 8 magas lépcsőre szeretnénk felmenni.

Sok lépést spórolunk meg az előző megoldáshoz képest!

57 of 66

Melyik a jobb…?

Melyik a jobb megoldás?

58 of 66

Melyik a jobb…?

Melyik a jobb megoldás?

59 of 66

Sakk

Jelölje R(k; n) azt a számot, hányféleképpen juthatunk egy k x n méretű sakktábla bal alsó sarokból a jobb felső sarkába, ha csak a jobbra vagy a felfelé szomszédos mezőre léphetünk. Adjunk meg két dinamikus programozási eljárást (egyet négyzetes, egyet lineáris táblázatkitöltéssel) az R(k; n) érték kiszámítására!

R(3,4) = ?

60 of 66

Sakk

Megoldás:

Az előző órán megadtuk a rekurzív algoritmusokhoz az összefüggéseket.

A következő összefüggések állnak fent:

Alapeset:

  • R(k, 1) = 1 (csak felfelé mehetünk)
  • R(1, n) = 1 (csak jobbra mehetünk)

Rekurzív eset:

  • R(k, n) = R(k, n−1)+R(k−1, n), ha n, k ≥ 2 (az első lépés jobbra vagy felfelé történhet)

61 of 66

Sakk

Ezek alapján a következő dinamikus programozási eljárást adhatjuk meg, T[i, j] az R(i, j) értékét tartalmazza:

62 of 66

Sakk

63 of 66

Táblázat

Feladat

Adott egy k×n-es tábla. Minden mezőre meg van adva egy Cij pozitív szám, ami

a mezőről begyűjthető érték.

Egy játékos a bal alsó sarokból szeretne eljutni a jobb felső sarokba úgy, hogy csak jobbra és felfelé léphet a szomszédos mezőre.

Az útja során összegyűjtheti a mezőkről az értékeket.

Mennyi értéket tudunk összeszedni maximálisan?

Hogyan határoznánk meg ezt az utat, amin a maximális értéket szedhetjük össze?

j

i

?

64 of 66

Táblázat

Megoldás

Az előző algoritmust annyiban kell módosítanunk, hogy nem a lépések számát, hanem az addig összeszedhető maximum értéket kell eltároljuk.

65 of 66

Táblázat

Sakk vs Pontok

66 of 66

Ajánlott kitekintő