pa1-06-podproblemyrekurze.odp

Download Report

Transcript pa1-06-podproblemyrekurze.odp

Příprava studijního programu Informatika je podporována
projektem financovaným z Evropského sociálního fondu a rozpočtu
hlavního města Prahy.
Praha & EU: Investujeme do vaší budoucnosti
Rozklad problému na podproblémy,
rekurze
BI-PA1 Programování a algoritmizace 1, ZS 2014-2015
Katedra teoretické informatiky
© Miroslav Balík
Fakulta informačních technologií
České vysoké učení technické
Rozklad problému na podproblémy
• Postupný návrh programu rozkladem problému na
podproblémy
•
•
•
•
zadaný problém rozložíme na podproblémy
pro řešení podproblémů zavedeme abstraktní příkazy
s pomocí abstraktních příkazů sestavíme hrubé řešení
abstraktní příkazy realizujeme pomocí metod
• Rozklad problému na podproblémy ilustrujme na příkladu
hry NIM
• Pravidla:
• hráč zadá počet zápalek ( např. od 15 do 35 )
• pak se střídá se strojem v odebírání; odebrat lze 1, 2 nebo 3 zápalky,
• prohraje ten, kdo odebere poslední zápalku.
• Dílčí podproblémy:
• zadání počtu zápalek
• odebrání zápalek hráčem
• odebrání zápalek strojem
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
2/28
Hra NIM
•
Hrubé řešení:
int pocet;
v hrubém řešení použijeme typ bool,
bool stroj = false;
jehož hodnotami jsou true a false
“zadání počtu zápalek“
do {
if (stroj) “odebrání zápalek strojem“
else “odebrání zápalek hráčem“
stroj = !stroj;
} while (pocet > 0);
if (stroj) “vyhrál stroj“
else “vyhrál hráč“
• Podproblémy „zadání počtu zápalek“, „odebrání zápalek strojem“ a
„odebrání zápalek hráčem“ budeme realizovat procedurami, proměnné
pocet a stroj budou globálními (pro procedury nelokálními) proměnnými
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
3/28
Hra NIM
/* prog6-nim.c */
#include <stdio.h>
#include <stdlib.h>
#define bool char
#define true 1
#define false 0
co je typ bool ?
co je to false ?
int pocet;
Vysvětlení na dalším slajdu
bool stroj = false; /* zacina hrac */
void zadaniPoctu(void) {
do {
printf("zadejte pocet zapalek (od 15 do 35): ");
scanf("%d", &pocet);
} while (pocet<15 || pocet>35);
}
void bereHrac(void) {
int x; bool chyba;
do {
chyba = false;
printf("pocet zapalek je %d\n", pocet);
printf("kolik odeberete: "); scanf("%d", &x);
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
4/28
Hra NIM
}
if (x<1) { printf("prilis malo\n"); chyba = true; }
else if (x>3 || x>pocet) { printf("prilis mnoho\n"); chyba = true; }
} while (chyba); pocet -= x;
void bereStroj(void) {
printf("pocet zapalek je %d\n", pocet);
int x = (pocet-1)%4;
if (x==0) x = 1;
printf("odebiram %d\n", x);
pocet -= x;
}
int main(void) {
zadaniPoctu();
do {
if (stroj) bereStroj();
else bereHrac();
stroj = !stroj;
} while (pocet>0);
if (stroj) printf("vyhral jsem\n");
else printf("vyhral jste, gratuluji\n");
return 0;
}
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
5/28
Direktiva define
/* prog6-nim.c */
#define bool char
#define true 1
#define false 0
int pocet;
bool stroj = false;
direktiva pro předprocesor
každý výskyt tohoto identifikátoru
bude nahrazen tímto textem
místo false bude 0
místo bool bude char
takže překladač zpracuje deklaraci proměnné stroj takto
char stroj = 0;
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
6/28
Hra NIM
• Pravidla pro odebírání zápalek strojem, která vedou k
vítězství (je-li to možné):
• počet zápalek nevýhodných pro protihráče je 1, 5, 9, atd., obecně 4n+1,
kde n >=0,
• stroj musí z počtu p zápalek odebrat x zápalek tak, aby platilo
p – x = 4n + 1
• z tohoto vztahu po úpravě a s ohledem na omezení pro x dostaneme
x = (p – 1) % 4
• vyjde-li x=0, znamená to, že okamžitý počet zápalek je pro stroj
nevýhodný a bude-li protihráč postupovat správně, stroj prohraje.
void bereStroj(void) {
printf("pocet zapalek je %d\n", pocet);
int x = (pocet-1)%4;
if (x==0) x = 1;
printf("odebiram %d\n", x);
pocet -= x;
}
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
7/28
Rekurze
•
Definice ze slovníku (pozor vtip)
Rekurze
viz „Rekurze“
• ….nekonečná rekurze, lépe:
pokud neznáte význam tohoto pojmu,
pokračujte pojmem „Rekurze“
•
Rekurze - algoritmus, který volá v
průběhu svého běhu sama sebe
Příklad: výpočet faktoriálu: n!
0! = 1,
1! = 1, pro záporné číslo x budiž x! = 1
pro n>1 n! = n(n-1)!
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
8/28
Faktoriál pomocí rekurze a iterace
• n! = 1
• n! = n*(n-1)!
pro n1
pro n>1
int fakt(int n) {
int fakt(int n) {
int fakt(int n) {
int f = 1;
if (n<=1) return 1;while (n>1){
if (n<=1) return 1;
return n * fakt(n-1)
else return n * fakt(n-1);
f *= n;
}
}
n--;
}
return f;
int fakt(int n) { }
return n<=1?1:n * fakt(n-1); // ternární operátor
}
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
9/28
Iterační alg. – NSD(), připomenutí
int nsd(int x, int y) {
int zbytek;
while ( y != 0 ) {
zbytek = x % y; x = y; y = zbytek;
}
Kolikrát se provede tělo cyklu while ?
return x;
}
Platí: Je-li x  y (> 0), pak x mod y < x/2
(55 88, 88 55, 55 33, 33 22, 22 11, 11 11, 11 0)
•
Důkaz:
– buď je y  x/2
– nebo je y > x/2
•
y
y
y
x/2
x/2
y
x
x mod y
y
x
x mod y
Nechť n je počáteční hodnota y. Každé dva průchody cyklem se y
zmenší na polovinu, takže na hodnotu 0 dospěje nejpozději po 2.log2 n
průchodech.
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
10/28
Rekurzivní algoritmus – NSD() I
• Platí: je-li x, y > 0, pak nsd(x, y):
je-li x = y, pak nsd(x,y) = x
je-li x > y, pak nsd(x,y) = nsd(x%y,y)
je-li x < y, pak nsd(x,y) = nsd(x,y%x)
int nsd(int x, int y) {
if (x==y) return x;
–
else if (x>y) return nsd(x%y, y);
else return nsd(x, y%x);
}
Rekurze
int nsd(int x, int y) {
int zbytek;
while( y != 0 ) {
zbytek = x % y; x = y; y = zbytek;
}
return x;
}
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
Iterace
11/28
Rekurze a rozklad problému na
podproblémy
• Příklad:
Program, který přečte posloupnost čísel zakončenou nulou a
vypíše ji obráceně
• Rozklad problému:
• zavedeme abstraktní příkaz „obrať posloupnost“
• příkaz rozložíme do tří kroků:
– přečti číslo
(“a ulož si ho”)
– if (přečtené číslo není nula) „obrať posloupnost“
(“zbytek!!”)
– vypiš číslo
(“uložené”)
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
12/28
Příklad rekurze „Obrať posloupnost“
„obrať posloupnost“
– přečti číslo
– if (přečtené číslo není nula) „obrať posloupnost, tj. zbytek“
– vypiš číslo
2
cti x
4
if()…
cti x
6
if()…
cti x
8
if()…
cti x
24680
08642
0
if()…
4
2
piš x
6
piš x
cti x
8
piš x
if()…
piš x
0
piš x
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
13/28
Příklad rekurze - obrat()
/* prog6-obrat.c */
#include <stdio.h>
#include <stdlib.h>
void obrat(void) {
int x;
scanf("%d", &x);
if (x!=0) obrat();
printf("%d ", x);
}
int main(void) {
printf("zadejte posloupnost celych cisel zakoncenou nulou\n" );
obrat();
printf("\n");
return 0;
}
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
14/28
Příklad rekurze - Hanojské věže
1
2
3
• Úkol: přemístit disky na druhou jehlu s použitím třetí
pomocné jehly, přičemž musíme dodržovat tato pravidla:
- v každém kroku můžeme přemístit pouze jeden disk, a to vždy z jehly na
jehlu (disky nelze odkládat mimo jehly),
- není možné položit větší disk na menší.
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
15/28
Příklad rekurze - Hanojské věže
1
•
•
2
3
Zavedeme abstraktní příkaz: přenes_věž(n,1,2,3), který interpretujeme
jako "přenes n disků z jehly 1 na jehlu 2 s použitím jehly 3".
Pro n>0 lze příkaz rozložit na tři jednodušší příkazy
– přenes_věž(n-1,1,3,2)
– "přenes disk z jehly 1 na jehlu 2",
– přenes_věž(n-1,3,2,1)
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
16/28
Hanojské věže
/* prog6-hanoj.c */ /* hanojske veze */
#include <stdio.h>
#include <stdlib.h>
void prenesVez(int vyska, int odkud, int kam, int pomoci) {
if (vyska>0) {
prenesVez(vyska-1, odkud, pomoci, kam);
printf("prenes disk z %d na %d\n", odkud, kam);
prenesVez(vyska-1, pomoci, kam, odkud);
}
}
int main(void) {
int pocetDisku;
printf("zadejte pocet disku: ");
scanf("%d", &pocetDisku);
if (pocetDisku<=0) printf("pocet disku musi byt kladne cislo\n");
else prenesVez(pocetDisku, 1, 2, 3);
return 0;
}
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
17/28
Příklad rekurze - Hanojské věže
pVez(3, 1, 2, 3)
pVez(2, 1, 3, 2)
(1, 2)
pVez(2, 3, 2, 1)
pVez(1,1,2,3) (1,3) pVez(1,2,3,1)
pVez(1,3,1,2) (3,2) pVez(1,1,2,3)
pVez(0,1,3,2) (1, 2) pVez(0,3,2,1)
(2, 3)
void prenesVez(int vyska, int odkud, int kam, int pomoci) {
if (vyska>0) {
prenesVez(vyska-1, odkud, pomoci, kam);
printf("prenes disk z %d na %d\n", odkud, kam);
prenesVez(vyska-1, pomoci, kam, odkud);
}
}
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
(3, 1)
(1, 2)
3
přenes disk z 1 na 2
přenes disk z 1 na 3
přenes disk z 2 na 3
přenes disk z 1 na 2
přenes disk z 3 na 1
přenes disk z 3 na 2
přenes disk z 1 na 2
18/28
Obecně k rekurzivitě
•
•
Rekurzivní funkce (procedury) jsou přímou realizací rekurzivních
algoritmů
Rekurzivní algoritmus předepisuje výpočet „shora dolů“ v závislosti na
velikosti (složitosti) vstupních dat:
• pro nejmenší (nejjednodušší) data je výpočet předepsán přímo
• pro obecná data je výpočet předepsán s využitím téhož algoritmu pro
menší (jednodušší) data
•
•
•
•
Výhodou rekurzivních funkcí (procedur) je jednoduchost a přehlednost
Nevýhodou může být časová náročnost způsobená např. zbytečným
opakováním výpočtu
Řadu rekurzívních algoritmů lze nahradit iteračními, které počítají
výsledek „zdola nahoru“, tj, od menších (jednodušších) dat k větším
(složitějším)
Pokud algoritmus výpočtu „zdola nahoru“ nenajdeme (např. při řešení
problému Hanojských věží), lze rekurzivitu odstranit pomocí tzv.
zásobníku
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
19/28
Fibonacciho posloupnost - historie
•
•
•
•
•
•
•
•
•
•
•
Pingala (Chhandah-shāstra, the Art of Prosody, 450 or 200 BC)
Leonardo Pisano (Leonardo z Pisy), známý také jako Fibonacci (cca
1175–1250) - králíci
Henry E. Dudeney (1857 - 1930) - krávy
„Jestliže každá kráva vyprodukuje své první tele (jalovici) ve věku dvou
let a poté každý rok jednu další jalovici, kolik budete mít krav za 12 let,
jestliže Vám žádná nezemře?“
rok počet krav (jalovic)
•…..
1
1
•12
144
2
1
•…..
3
2
•50 20 365 011 074 (20 miliard)
4
3
počet krav = počet krav vloni +
5
5
počet narozených (odpovídá počtu krav předloni)
6
8
fn = fn-1 + fn-2
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
20/28
Fibonacciho posloupnost
• Platí:
f0 = 1
f1 = 1
fn = fn-1 + fn-2
pro n > 1
Rekurzivní funkce:
int fib(int i) {
if (i<2) return 1;
return fib(i-1)+fib(i-2);
}
Rekurze je hezká - zápis „odpovídá“ rekurentní definici. Je
ale i efektivní?
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
21/28
Fibonacciho posloupnost - iteračně
• Platí:
f0 = 1
f1 = 1
fn = fn-1 + fn-2 ,pro n > 1
Iteračně:
fibNMinus2
fibN
i
1
1
1
1
+
2
2
1
2
+
3
3
int fib(int n) {
2
int i, fibNMinus2=1;
fibNMinus1=1, fibN=1;
3
for (i=2; i<=n; i++) {
fibNMinus2 = fibNMinus1;
fibNMinus1 = fibN;
fibN = fibNMinus1 + fibNMinus2;
}
return fibN;
}
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
fibNMinu1
1
3
+
5
4
5
+
8
5
Složitost:
3*n
22/28
Složitost výpočtu Fibonacciho čísla
• Iterační metoda: 3*n
• Rekurzivní metoda:
příklad pro fib(10):
fib(10)
+
fib(9)
fib(8)
+
fib(7)
fib(8)
fib(7)
fib(7)+ fib(6) fib(6)+ fib(5) fib(6) + fib(5)
f50
+
fib(6)
fib(5) + fib(4)
20 365 011 074 (20 miliard)
Složitost je exponenciální!!!!
int fib(int i) {
if (i<2) return 1;
return fib(i-1)+fib(i-2);
}
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
23/28
Složitost výpočtu Fibonacciho čísla 2
• Iterační metoda: 3*n
• Rekurzivní výpočet ~ 2n
• Přímý výpočet (Johannes Kepler)
• zlatý řez (golden ratio)
≈1,6180339887498948482045868343656
1 5

,
2
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
F ( n) 

n
5

1 

n
5
24/28
Příklad rekurze, základní schéma –
součin
void main ( void ) {
int x, y;
……;
printf (”%d %d”, souI(x, y), souR(x,y));
}
int souI ( int s, int t ) {
int i, sI=0;
for (i = 0; i < s; i++)
sI = sI + t;
return sI;
}
int souR(int s, int t) {
int sR;
if (s > 0) sR = souR(s - 1,t)+t;
else sR = 0;
return sR;
}
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
pro zájemce
25/28
Rozklad na prvočinitele
• Rozklad přirozeného čísla n na součin prvočísel
• Řešení:
• dělit 2, pak 3, atd. , a dalšími prvočísly, … n-1
• každé dělení beze zbytku dodá jednoho prvočinitele
Příklad:
60/2=>30/2=>15/3=>5/5
60 má prvočinitele 2, 2, 3, 5
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
pro zájemce
26/28
Rozklad na prvočinitele - iterací
int rozklad (int x, int d) {
while (d < x && x % d != 0) d++;
printf ("%d \n",d);
return d;
}
void main (void ) {
int x;
int d = 2;
printf (“zadejte prirozene cislo:\n");
scanf ("%d ",&x);
if (x < 1) {
printf (“cislo neni prirozene\n");
return;
}
while (d < x) {
zadejte přirozené číslo: 144
d = rozklad(x, d);
22223
x = x/d;
}
}
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
pro zájemce
27/28
Rozklad na prvočinitele - rekurzí
void rozklad (int x, int d ) {
if (d < x) {
while (d < x && x % d != 0) d++;
printf ("%d ",d);
rozklad (x / d, d);
}
}
void main(void) {
int x;
printf (“zadejte prirozene cislo:\n");
scanf ("%d ",&x)
if (x < 1) {
printf (“cislo neni prirozene\n");
return;
}
rozklad(x, 2);
zadejte přirozené číslo: 144
}
22223
Ing. Miroslav Balík, Ph.D. - BI-PA1- 06
pro zájemce
28/28