Sunteți pe pagina 1din 3

Cateva probleme pentru testul urmator

In continuare, va prezint cateva tipuri de probleme ce ar putea sa apara la


testul urmator, a carui tema este complexitatea algoritmilor. In multe cazuri,
o sa apara o functie numita read :
read(x, n)
Semnificatia apelului este ca se citeste variabila x, a carei dimensiune este
egala cu n. Variabilele pot fi de doua tipuri: numere intregi si vectori. In cazul
unui numar intreg, daca dimensiunea variabilei este n, atunci numarul respectiv
este reprezentat pe n biti. In acest caz, consideram ca variabila x poate lua valori
intre 2n1 si 2n1 1. Pentru o variabila de tip vector, dimensiunea reprezinta
pur si simplu numarul de elemente pe care acesta le contine.
N-am apucat sa vorbesc cu Matei despre asta, dar din cum am observat ca
apare read folosit in diverse locuri, se considera ca citirea variabilelor are loc
in timp constant. Cu alte cuvinte, instructiunea read in sine nu o sa domine
niciodata timpul de executie al unui algoritm.
O alta observatie importanta este ca operatiile elementare asupra intregilor
se executa in timp constant, indiferent de dimensiunea lor. De exemplu, daca
adunam doua numere reprezentate pe n biti, adunarea are loc in (1), si nu
in (n), indiferent de valoarea lui n. Practic, numarul de biti pe care este
reprezentat un intreg este relevant doar pentru a obtine intervalul de valori pe
care acesta le poate contine.
Problemele constau in a evalua complexitatea unui algoritm, dat sub forma
de pseudocod, in functie de dimensiunea variabilelor de intrare. De exemplu:
read(a, n);
read(b, n);
if (a > 0)
{
s = 0;
while (a > b)
{
s++;
a--;
}
}
Care este complexitatea acestui algoritm ? Raspunsul este O(2n ). De ce ? In
afara de while, toate instructiunile ruleaza in timp constant. Complexitatea va
fi data deci de numarul maxim posibil de iteratii ale lui while. Care este acesta
? Pai in cel mai rau caz, a va avea valoarea maxim posibila pentru un intreg pe
n biti (adica 2n1 1), iar b va avea ce mai mica valoare posibila (adica 2n1 ).
Pentru ca a sa inceteze a fi mai mare decat b trebuie sa-l decrementam de a b
ori. Daca efectuam calculul, vedem ca a b = 2n1 1 (2n1 ) = 2n 1.
Rezulta ca bucla while poate produce 2n 1 iteratii in cel mai rau caz, deci
1

complexitatea algoritmului este O(2n ). Poate complexitatea algoritmului sa fie


(2n ) ? Raspunsul este nu, pentru ca el se poate incheia imediat atunci cand
b a, indiferent de valoarea lui n.
Un alt algoritm:
read(v, n);
read(u, m);
for (i = 0; i < n; i++)
{
for (j = 0; j < m; j++)
{
s = 0;
for (k = 0; k < m; k++)
{
if(k > sqrt(n))
break;
s += v[k] * u[k];
}
}
}
Care este complexitatea acestui algoritm ? Din start observam ca primele
doua bucle for duc la ceva ce incepe cu n m. Intrebarea putin mai dificila
este cate iteratii produce a treia bucla ? Daca ne luam doar dupa ce apare in
instructiunea for, am spune ca m, insa ea se poate termina
prematur din cauza
n. Din cauza aceasta,
instructiunii break, atunci cand k devine mai mare
decat

vom spune ca a treia bucla va producemin( n, m) iteratii, iar complexitatea


intregului algoritm va fi O(n m min( n, m)) (la fel de bine puteam sa punem
si in acest caz, dar nu cred ca o sa zica nimeni ca e gresit daca lasati cu O).
In cele din urma, iata si un algoritm unde nu mai apare instructiunea read.
Pseudocodul este urmatorul:
int f(int m, int a)
{
s = 0;
for (i = 0; i < 300; i++)
{
s += a;
a = a / 3;
}
if (m < 4)
return s;

if (m % 3)
return s + f(m - 3, a / 2);
else
return s + f(m / 3, a / 2);
}
Ce complexitate are algoritmul de mai sus ? Din moment ce nu mai apare
read, nu va mai conta pe cati biti sunt reprezentate valorile si ne vom referi la
ele ca atare. E important sa incepem prin a observa ca bucla for nu contribuie
semnificativ la complexitatea algoritmului. De ce ? Pai, numarul de pasi este
constant (300 mai precis), iar corpul buclei contine operatii ce se executa de
asemenea in timp constant. Astfel, putem spune ca toate instructiunile de pana
la al doilea if se executa in timp constant.
Urmatoarea observatie este ca parametrul a nu are nici el vreo influenta
asupra complexitatii; ea nu depinde decat de m, in functie de care se executa
apelurile recursive. Din moment ce, in afara recursivitatii, operatiile se executa
in (1), inseamna ca pentru a determina complexitatea algoritmului trebuie sa
vedem de cate ori se autoapeleaza functia in cel mai rau caz.
In cadrul celui de-al doilea if, prima ramura este mai costisitoare decat a
doua, deorece prima doar scade din valoarea lui m trei unitati, pe cand a doua
il reduce direct la o treime. Acestea fiind spuse, cel mai rau caz este acela in care
tot timpul ne incadram in prima dintre cele doua ramuri. Este posibil asa ceva ?
Raspunsul este da, pentru ca daca m mod 3 6= 0, atunci si (m 3) mod 3 6= 0.
Cu alte cuvinte, o data ce am intrat pe prima ramura a ultimei instructiuni if,
acolo vom ramane pentru toate apelurile recursive ce urmeaza. In acest caz,
formula recurentei poate fi scrisa ca T (m) = T (m 3) + 1. Daca o rezolvam,
obtinem complexitatea algoritmului ca fiind O(m).
O intrebare interesanta legata de acelasi algoritm este urmatoarea: ce se
intampla cu complexitatea algoritmului daca inlocuim linia
return s + f(m - 3, a / 2);
cu urmatoarea:
return s + f(m - 1, a / 2);
Cu alte cuvinte, se schimba complexitatea algoritmului in cazul in care avem
m 1 in loc de m 3 ? Raspunsul este da; complexitatea algoritmului devine
O(log m). De ce ? De data aceasta, pentru orice valoare ar avea m, dupa cel
mult doua apeluri recursive va deveni multiplu de 3, si se va intra pe ramura a
doua a ultimului if. In cel mai rau caz vom avea doua apeluri ale functiei care
il decrementeaza pe m, urmate de unul care il transforma in m
3 , si tot asa pana
cand ajungem in situatia in care m < 4. De cate ori se poate repeta secventa
descrisa anterior ? De cel mult log3 m ori, deci complexitatea algoritmului
devine O(log m).

S-ar putea să vă placă și