Documente Academic
Documente Profesional
Documente Cultură
Recurs IV It Ate
Recurs IV It Ate
PROIECT ATESTAT
INFORMATICA
TEMA:
RECURSIVITATE
CUPRINS
1. Despre recursivitate
pag. 4
2. Apelul recursiv ..
pag. 5
3. Clasificarea recursivitatii .
..... pag. 20
4. Divide et impera ..
..... pag. 21
5. Exerciti si probleme
.. pag. 22
6. Bibliografie
.
pag. 33
Despre
Recursivitate
In limbajele de programare, recursivitatea e un concept fundamental care le extinde n
deoarece permite sa
folosind una sau mai multe probleme de acelasi tip, dar mai simple. Ea e astfel strans
legata de principiul descompunerii n subprobleme (divide et impera ) n proiectarea
solutiilor.
Definitie si exemple
O notiune e recursiva daca e folosita n propria sa definitie. Cunoastem un exemplu
din matematica:
sirurile recurente , unde un termen al sirului e definit printr-o relatie n raport cu
termenii anteriori. Exemple sunt:
progresia aritmetica: x0 = a, xn = xn1 + p pentru n > 0.
progresia geometrica: x0 = b, xn = q xn1 pentru n > 0.
Acestea sunt recurente de ordinul I, n care termenul definit depinde doar de
termenul imediat anterior. Alte recurente mai complexe sunt:
sirul lui Fibonacci: F0 = F1 = 1, Fn = Fn1 + Fn2 pentru n 2 (un sir recurent
de ordinul II)
coeficientii binomiali: C 0 = C n = 1 pentru n 0,
Ck = Ck
Acest
baza pentru primul termen (sau primii cativa), si relatia recursiva propriu-zisa pentru
termenul general. Aceasta sugereaza ca putem folosi operatorul conditional pentru a
4
exprima o definitie recursiva, similar cu functiile definite anterior pe mai multe cazuri.
Consideram ca exemplu functia putere (cu baza reala si exponent natural), care
poate fi privita recursiv ca progresie geometrica, indicele fiind exponentul: puterea de
exponent n (termenul de ordin n) este baza (ratia) nmultita cu puterea de exponent n1.
Grupam n definitie cazul de baza (termenul initial, 1) si termenul general n felul
urmator:
x xn1 altfel (n > 0)
de functie
valoarea functiei
sqr(int
x)
{
printf("calculam patratul lui %d\n", x);
return x * x;
}
int main(void)
{
printf("sqr(3 * sqr(2)) = %d\n", sqr(3*sqr(2)));
return 0;
}
calculam patratul lui
2 calculam patratul lui
12 sqr(3 * sqr(2)) =
144
Rezultatul rularii programului (prezentat sub textul sursa) evidentiaza si pentru
apelul printf din main evaluarea argumentelor nainte de nceperea executiei functiei.
Evaluarea argumentului al doilea produce cele doua linii tiparite n apelurile la sqr.
Aceste linii apar nainte de a scrie chiar si portiunea de text obisnuit din primul argument
(formatul), scriere n care consta tocmai executia functiei printf.
Returnarea valorilor
ncheie; orice alte instructiuni care urmeaza n corpul functiei nu se mai executa.
Daca executia unei functii se termina prin atingerea ultimei acolade } fara a executa o
instructiune return, iar programul utilizeaza valoarea functiei, efectul e nedefinit
(programul se comporta imprevizibil).
Apelul recursiv
se repeta cu pwr(5,
calculeaza direct: 1. Din acest apel se revine n locul unde a fost facut: la evaluarea lui 5
* pwr(5,
1) returneaza valoarea 5
2) pentru a calcula
valoarea 125 a
lui
pwr(5,
3).
pwr(5, 3)
apel
125
5 * pwr(5,
2)
apel
25
5 * pwr(5,
1)
apel
5
5 * pwr(5, 0)
apel
1
1
Urmarind secventa de apel, rezulta ca la un moment dat pot fi n executie mai multe
apeluri diferite la aceeasi functie. Fiecare apel reprezinta o instanta (copie) distincta
a functiei, cu propriile valori de parametri, cele primite n momentul apelului. La fel se
ntampla si n calculul pe hartie, prin desfasurarea formulei de recurenta:
pentru a calcula pe 53 , nlocuim pe x cu 5 si n cu 3. Trebuie calculat 52 : aplicand din
nou formula nlocuim pe n cu 2; aceasta nu afecteaza nsa instanta initiala a
problemei (53 ), unde n este n continuare 3.
3
genereaza un singur nou apel, pana la oprirea pentru cazul de baza. In acel moment,
sunt active toate apelurile, n numar de 4. Revenirea se face n ordine inversa fata de
cea de apel: din fiecare apel se revine n instanta care a efectuat apelul. Aici, executia
se reia n contextul
dinainte de apel: adica din locul n care a fost facut apelul (unde e folosita valoarea
returnata), si cu acele valori ale parametrilor corespunzand instantei respective.
Ca la orice apel de functie, informatia se transmite spre functia apelata prin
parametri, si napoi spre locul de apel (functia apelanta ) prin rezultat. In exemplul dat se
creeaza un lant n care la revenire, valoarea returnata de fiecare apel e folosita n
instanta apelanta pentru calculul propriului rezultat, care e transmis mai departe napoi
spre locul de apel. Rezultatul final e astfel efectul unui calcul n care cate un pas e efectuat
de fiecare din instantele apelate. Aceasta
acolade: xn
xn
n=0
+p n>0
la stanga
parametri
::=
void | lista-param
lista-param
::=
Acest mod de a descrie regulile sintactice, adica gramatica unui limbaj se numeste forma
Backus-Naur (BNF).
Putem defini recursiv si alta notiune fundamentala, expresia. Din cele prezentate
pana acum:
expresie
argumente
::= s | lista-argumente
n=0
si putem transcrie direct
Avem:
n! =
n C: (n 1)! n
altfel (n > 0)
unsigned fact(unsigned n)
{
return n == 0 ? 1 : fact(n-1) * n;
}
In evaluarea lui fact pentru n > 0, ultima operatie efectuata e nmultirea cu n,
apelul
fact(n-1) (si
celelalte apeluri
e indicata de paranteze n
folosind asociativitatea
nmultirii, pentru a efectua cat mai multe nmultiri ndata ce factorii devin
disponibili: n! = 1 (2 (. . . ((n 1) n)))
Transcriind n C, am dori sa efectuam nmultirea (n-1)*n n cadrul apelului
functiei pentru n-1,
nainte de a calcula recursiv factorialul pentru n-2.
nmulti
apoi rezultatul lui (n-1)*n cu n-2, etc. Pentru aceasta, avem nevoie sa
transmitem la fiecare apel recursiv pe langa valoarea lui n si rezultatul nmultirilor deja
efectuate. Obtinem astfel:
unsigned fact_r(unsigned n, unsigned
r)
{
return n == 0 ? r : fact_r(n-1, r * n);
}
13
calculat anterior.
In a doua, rezultatul partial e transmis n adancime ca parametru, si actualizat nainte
de fiecare apel recursiv.
Exista nsa situatii n care parte din prelucrare se efectueaza nainte de fiecare apel
recursiv, fiind
necesar sa transmitem n jos la naintarea n recursivitate valori ce vor fi folosite
ulterior n calcule.
l transforma n numarul cu aceleasi cifre zecimale dar n ordine inversa. Scriem solutia
pornind de la un exemplu: 1472. Ultima cifra, 2, devine prima cifra a rezultatului. Punem
ultima cifra ramasa din 147 dupa 2, obtinand
27 = 2 10 + 7. Din numarul ramas, 14, plasam ultima cifra dupa 27, obtinand 27
10 + 4 = 274, etc.
14
divizor comun a doi ntregi pozitivi e un exemplu clasic de algoritm exprimat recursiv, n
care o problema e rezolvata prin reducerea la o instanta mai simpla a aceleiasi
probleme. Exprimat informal, algoritmul e:
daca numerele sunt egale, rezultatul e chiar valoarea lor
comuna
15
altfel, se scade cel mai mic numar din cel mai mare, si se repeta procedura cu
noile numere.
Exprimarea din urma (se repeta procedura) indica abordarea recursiva: solutia
se obtine re- zolvand aceeasi problema pentru valori noi ale numerelor (mai mici, deci
dupa un numar finit de pasi se ajunge la cazul de baza).
Putem scrie deci pe cazuri:
cmmdc(a, b) =
a
a= b
cmmdc(a b, b) a > b
cmmdc(a, b a)
altfel (a
< b)
si transcrie direct n C:
unsigned cmmdc(unsigned a, unsigned b)
{
return a == b ? a
: a > b ? cmmdc(a - b, b)
: cmmdc(a, b - a);
}
Desi am transpus direct din cuvinte n formula recursiva si apoi n cod, a ramas
netratat un aspect:
enuntul initial e dat pentru numere pozitive, iar tipul unsigned permite si valoarea 0.
Apelarea (chiar
si accidentala) a functiei scrise mai sus cu un parametru nul va duce la o secventa
infinita de apeluri recursive, deoarece scazand 0 celalalt numar nu se modifica si
reluam acelasi apel (pana cand, n functie de mediul de rulare, programul se va
termina probabil fortat epuizand resursele de memorie).
Este important ca functiile pe care le scriem sa fie robuste si sa nu produca erori
neprevazute si catas- trofale. Ca atare, rescriem functia tinand cont ca 0 e divizibil cu
orice numar, si deci cmmdc(a, 0) = cmmdc(0, a) = a:
unsigned cmmdc(unsigned a, unsigned
b)
{
return b == 0 ? a : a == 0 ? : b
: a > b ? cmmdc(a - b, b)
16
: cmmdc(a, b - a);
}
In aceasta scriere, cazul a = b = 0 va intra pe ultima ramura, avand ca efect apelul
cmmdc(a, 0)
care va returna a.
sn = sn
tn
17
si
(n > 0)
Avand o formula de calcul direct pentru termenul tn al seriei (exprimat deci ca functie
de n), putem transforma direct formula de mai sus ntr-o functie recursiva s, care
apeleaza pentru calcul functia t. Pentru exemplul simplu tn = 1/n (pentru n > 1, iar t0 =
0), putem scrie urmatorul program:
#include <math.h>
#include <stdio.h>
double t(unsigned n)
{
return n == 0 ? 0 : 1.0/n;
}
double s(unsigned n)
{
return n == 0 ? t(0) : s(n-1) + t(n);
}
int main(void)
{
logaritmul natural
al lui n. Mai
precis,
n
lim (
1/k ln n) = c 0.5772...
k=1
Tipul double folosit n program reprezinta, ca si float, numere reale, dar cu precizie
mai buna (de aici si numele), si e recomandabil n calcule pentru a micsora acumularea
erorilor de rotunjire.
declarate n math.h (cum e si functia log pentru logaritmul natural) si tipul implicit
pentru constantele reale. Scrierea 1.0/n pentru termenul matematic 1/n e necesara pentru
a obtine mpartire
reala :
operandul 1.0
19
x. O
cunoscuta
formula matematica (poate fi derivata din metoda lui Newton, dar n acest caz particular e
mult mai veche) ne da secventa de aproximari: an+1
}
int main(void)
{
printf("%f\n", rad(2, 1));
return 0;
}
Functia standard fabs, declarata n math.h returneaza valoarea absoluta a unui numar real
(double), spre deosebire de functia abs (din stdlib.h) aplicabila doar la numere ntregi.
Logica comparatiei
este urmatoarea: a si x/a se afla ntotdeauna de o parte si de alta a valorii exacte
x.
Deci, daca
intervalul dintre ele e mai mic decat precizia dorita (n exemplul dat, 106 ), oricare din ele
va fi o aproximatie de precizie suficienta.
Clasificarea recursivitatii
Recursivitate liniar.
Este forma cea mai simpl de recursivitate i const dintrun singur apel recursiv. De
exemplu pentru calculul factorialului avem:
f(0)=1
cazul de baz
f(n)=n*f(n-1)
cazul general
Se vede c dac timpul necesar execuiei cazului de baz este a i a cazului general este b, atunci timpul
total de calcul este: a+b*(n1) = O(n), adic avem complexitate liniar.
Recursivitate binar.
Recursivitatea binar presupune existena a dou apeluri recursive. Exemplul tipic l constituie
irul lui Fibonacci:
long fibo(int n)
{
if(n<=2) return 1;
return fibo(n1) +fibo(n2);
}
Funcia este foarte ineficient, avnd complexitate exponenial, cu multe apeluri recursive repetate.
Se poate nlocui dublul apel recursiv printrun singur apel recursiv. Pentru aceasta inem seama c dac a,
b i c sunt primii 3 termeni din irul Fibonacci, atunci calculul termenul situat la distania n n raport cu
a, este situat la distana n1 fa de termenul urmtor b.
Necesitatea cunoaterii termenului urmtor impune prezena a doi termeni n lista de parametri.
Ultima etap poate lipsi (n anumite cazuri), soluia problemei rezultnd direct din soluiile
subproblemelor.
Metoda divizrii se exprim natural n mod recursiv: rezolvarea problemei const n rezolvarea unor
instane ale problemei de dimensiuni mai reduse
void DivImp(int dim, problema p, solutie *s){
int dim1, dim2; problema
p1, p2; solutie s1, s2;
if(dim > prag){
Separa(dim,p,&dim1,&p1,&dim2,&p2);
DivImp(dim1, p1, &s1);
DivImp(dim2, p2, &s2);
Combina(s1, s2, s);
}
else
RezDirect(dim, p, s);
}
Pentru evaluarea complexitii unui algoritm dezvoltat prin metoda divide et impera se rezolv o ecuaie
recurent avnd forma general: T(n)=a.T(n/b) + f(n), n care a >= 1, reprezint numrul
subproblemelor, iar n/b este dimensiunea subproblemelor, b > 1.
f(n) reprezint costul divizrii problemei n subprobleme i a combinrii soluiilor
subproblemelor pentru a forma soluia problemei.
Soluia acestei ecuaii recurente este dat de teorema master
Probleme si exercitii
Se citeste un vector cu n elemente numere naturale (n<=100).
Sa se inlocuiasca fiecare element al vectorului cu suma cifrelor cu aceeasi paritate ca
si indicele elementului. Indexarea elementelor incepe de la 1.
Se vor scrie si folosi functii recursive pentru:
- citirea vectorului
- afisarea vectorului
- calculul sumei cifrelor de o anumita paritate
- inlocuirea ceruta
Exemplu: Pentru datele de mai jos
7
223 435 6667 24 55 662 122
Sirul rezultat este
3 4 7 6 10 14 1
REZOLVARE:
#include <iostream>
using namespace std;
}
}
int sumcifp(int n, int p)
{
if(n==0) return 0;
else if(n%2==p) return sumcifp(n/10,p)+n%10;
else return sumcifp(n/10,p);
}
void inlocuire(int A[], int n)
{
if(n>0)
{
inlocuire(A,n-1);
A[n]=sumcifp(A[n],n%2);
}
}
void afisare(int A[], int n)
{
if(n>0)
{
afisare(A,n-1);
cout<<A[n]<<" ";
}
}
int main()
{
int A[101],n;
cin>>n;
citire(A,n);
inlocuire(A,n);
afisare(A,n);
return 0;
}
Se citeste un numar natural n cu cel mult 9 cifre. Afisati numarul de cifre distincte ale
lui
n.
Se
vor
folosi
exclusiv
subprograme
Exemplu:
Pentru n=38837 se afiseaza 3 (cifrele distinte sunt 3,7 si 8).
REZOLVARE:
#include <iostream>
using namespace std;
int apcif(int n, int c)
{//numarul de aparitii ale lui c in n
if(n<=9) return n==c;
else if(n%10==c) return apcif(n/10,c)+1;
else return apcif(n/10,c);
}
int dist(int n, int c)
{//numara cifrele distincte ale lui n
if(c==-1) return 0;
else if(apcif(n,c)) return dist(n,c-1)+1;
else return dist(n,c-1);
}
int main()
{
int n;
cin>>n;
cout<<dist(n,9);
return 0;
}
sau
#include <iostream>
using namespace std;
recursive.
de
mai
jos.
Se
vor
folosi
exclusiv
subprograme
recursive.
Exemplu:
Pentru
*
***
*****
***
*
REZOLVARE:
#include <iostream>
using namespace std;
n=3
se
afiseaza
}
Se citeste un numar natural n. Sa se descompuna ca suma de puteri crescatoare ale
lui 2. Se vor folosi doar prelucrari/calcule realizate cu ajutorul functiilor implementate
recursiv.
Exemplu: Pentru n=84 va afisa 4 16 64 (84 se descompune ca 4+16+64)
REZOLVARE:
#include <iostream>
using namespace std;
unsigned int pm2(int n, unsigned int p)
{
if(p*2>n) return p;
else return pm2(n,p*2);
}
void puteri2(int n)
{
if(n>0)
{
unsigned int p=pm2(n,1);
puteri2(n-p);
cout<<p<<" ";
}
}
int main()
{
int n;
cin>>n;
puteri2(n);
return 0;
}
sau
#include <iostream>
O harta este data intr-o matrice n*m in care valorile 1 reprezinta uscatul, iar valorile
0 reprezinta apa. Doua zone de uscat se considera ca fac parte din acelasi continent
daca sunt vecine pe linie sau pe coloana. Determinati numarul de continente de pe
harta si care este aria (numarul de valori de 1) maxima dintre ariile continentelor.
Exemplu:
harta.in
6
harta.out
(numarul
de
continente)
}
fout<<c<<endl;
int max=0;
for(int cul=2;cul<=c+1;cul++)
{
int s=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(a[i][j]==cul) s++;
if(s>max)
max=s;
}
fout<<max;
fin.close();
fout.close();
return 0;
}
Scrieti o functie recursiva litera cu doi parametri s si c unde s e un cuvant, iar c este o
litera si care returneaza de cate ori apare litera c in cuvantul s.
b) Scrieti o functie recursiva litere cu trei parametri s, n si c unde s e un vector ce
memoreaza cel mult 20 de cuvinte, n e numar natural reprezentand numarul de
cuvinte din vectorul s, iar c este o litera si care returneaza de cate ori apare litera c in
total in cele n cuvinte din vectorul s (va folosi functia litera).
c) Se citeste un numar n si un vector s de n cuvinte. Folosind functia litere,
determinati si afisati literele care apar de un numar maxim de ori in cuvintele din
vectorul s.
Exemplu: n=3, s={"ana", "are", "mere"} => a e
REZOLVARE:
#include <iostream>
#include <cstring>
using namespace std;