Pagina Modulului 5
Ora 40 - Modulul 5

Exerciții de analiză comparativă

Alege cel mai eficient algoritm: 2 implementări, aceeași problemă, rezultate diferite în performanță

45min teorie + 15min probleme PbInfo + exerciții practice
Competența 4.2
Săptămâna 7
Sigla liceului
TEORIE - 45 minute

Ce facem astăzi?

Analizăm probleme concrete cu 2 soluții diferite. Tu decizi care e mai eficientă și de ce!

Numărare pare: 2 metode
Verificare prim: 2 metode
Calcul putere: 2 metode
Alegerea algoritmului potrivit

Dicționar: Analiză comparativă ↔ C++

ConceptCum se citeșteÎn C++Observație
n / 2 — formulă directă„n supra 2"int cont = n / 2;O(1) — un singur pas
√n — radical„rădăcina pătrată din n"d * d <= nevită sqrt() — mai rapid
a^n — ridicare la putere„a la puterea n"rez *= baza;sucesiv O(n) sau rapid O(log n)
Fibonacci recursiv vs iterativ„al n-lea termen"fib(n-1)+fib(n-2) vs forrecursiv O(2ⁿ) vs iterativ O(n)
Exponențiere rapidă„baza la exponent prin înjumătățire"while(exp > 0) { exp /= 2; baza *= baza; }log₂(n) pași
Overflow„depășire de tip"long long rez;max ≈ 9.2 × 10⁸
Contor de pași„numărătoare operații"int pasi = 0; pasi++;măsoară eficiența

Analogie: Cutia de scule

Imaginează-ți că trebuie să înșurubezi un șurub:

Ai două opțiuni: o șurubelniță manuală (funcționează, dar lent) sau un bormaș electric (rapid și eficient). Ambele fac treaba, dar bormașul termină de 100 de ori mai repede.

La fel în programare: două algoritmi pot rezolva aceeași problemă, dar diferența de viteză poate fi enormă. Un programator bun alege întotdeauna „bormașul" — algoritmul mai eficient.

Problema 1: Numărare numere pare

Enunț: Câte numere pare sunt de la 1 la n?

Metoda A: Parcurgere cu for
int cont = 0;
for (int i = 1; i <= n; i++)
    if (i % 2 == 0) cont++;
// n pași
Metoda B: Formulă directă
int cont = n / 2;
// 1 singur pas!
nMetoda A (pași)Metoda B (pași)Câștig
1010110x
1.0001.00011.000x
1.000.0001.000.00011.000.000x

Lecție: Când există o formulă matematică directă, aceasta este întotdeauna mai eficientă decât parcurgerea.

Numere pare de la 1 la n = n / 2 (formulă directă, O(1))
int cont = n / 2; // un singur pas, indiferent de n
Se citește: „cont egal n împărțit la 2" — nu trebuie buclă, formula dă rezultatul instant

Problema 2: Verificare număr prim

Enunț: Este n prim?

Metoda A: Verificăm toți divizorii de la 2 la n-1
bool prim = true;
for (int d = 2; d < n; d++)
    if (n % d == 0) { prim = false; break; }
// Până la n-1 pași
Metoda B: Verificăm doar până la √n
bool prim = (n >= 2);
for (int d = 2; d * d <= n; d++)
    if (n % d == 0) { prim = false; break; }
// Până la √n pași
nMetoda A (max pași)Metoda B (max pași)
1009810
10.0009.998100
1.000.000999.9981.000
Verificare prim: testăm doar 2 ≤ d ≤ √n (dacă n = d × k, atunci min(d,k) ≤ √n)
for(int d = 2; d * d <= n; d++) if(n % d == 0) prim = false;
Se citește: „pentru d de la 2 cât timp d ori d mai mic sau egal cu n" — O(√n) în loc de O(n)
// PSEUDOCOD: Verificare număr prim (metoda √n) SET prim TRUE IF n < 2 THEN prim FALSE FOR d 2 WHILE d × d n IF n MOD d = 0 prim FALSE BREAK // găsit divizor, nu mai trebuie END IF END FOR PRINT prim // O(√n) pași

Problema 3: Calculul puterii a^n

Metoda A: Înmulțiri succesive
long long rez = 1;
for (int i = 0; i < n; i++)
    rez *= a;
// n pași
Metoda B: Ridicare rapidă la putere
long long rez = 1, baza = a;
int exp = n;
while (exp > 0) {
    if (exp % 2 == 1) rez *= baza;
    baza *= baza;
    exp /= 2;
}
// log₂(n) pași!
Exemplu: 2^16

Metoda A: 16 înmulțiri

Metoda B: 2→4→8→16→32→64→128→256→...→65536 = doar 4 pași!

Exponențiere rapidă: a^n în O(log₂ n) pași — dacă n e par: a^n = (a)^(n/2)
while(exp > 0) { if(exp % 2 == 1) rez *= baza; baza *= baza; exp /= 2; }
Se citește: „cât timp exponentul > 0: dacă e impar, înmulțim; ridicăm baza la pătrat; înjumătățim exponentul"
// PSEUDOCOD: Ridicare rapidă la putere FUNCTION putereRapida(a, n) rez 1 baza a exp n WHILE exp > 0 IF exp MOD 2 = 1 // exponent impar rez rez × baza END IF baza baza × baza // ridicare la pătrat exp exp / 2 // înjumătățim END WHILE RETURN rez END FUNCTION
Trace: Exponențiere rapidă 2^10
Pasexpbazarezexp%2Acțiune
01021inițializare
1102→410 (par)baza=4, exp=5
254→161×4=41 (impar)rez×=4, baza=16, exp=2
3216→25640 (par)baza=256, exp=1
41256→...4×256=10241 (impar)rez×=256 → STOP

4 pași în loc de 10 (metoda simplă). 2^10 = 1024 ✓

Problema 4: Fibonacci — recursiv vs iterativ

Metoda A: Recursiv (lent!)
int fib(int n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);  // se apelează de 2 ori!
}
// ~2^n apeluri — crește EXPONENȚIAL
Metoda B: Iterativ (rapid)
int fib(int n) {
    if (n <= 1) return n;
    int a = 0, b = 1;
    for (int i = 2; i <= n; i++) {
        int c = a + b;
        a = b; b = c;
    }
    return b;
}
// n-1 pași — crește LINIAR
Comparație nr. apeluri/operații:
nRecursiv (apeluri)Iterativ (pași)Diferență
101779~20x
2021.89119~1.150x
302.692.53729~93.000x
40331.160.28139~8.500.000x

Lecție: Recursivitatea e elegantă, dar poate fi extrem de lentă dacă recalculează aceleași valori. Fibonacci recursiv recalculează fib(5) de zeci de ori!

Fibonacci recursiv: O(2ⁿ) — arbore exponențial | Iterativ: O(n) — o singură buclă
// Iterativ: for(int i=2; i<=n; i++) { int c = a+b; a=b; b=c; } return b;
Se citește: „pentru i de la 2 la n: c egal a plus b, a ia b, b ia c" — doar 3 variabile, n-1 pași
Trace: Fibonacci iterativ, n=7
Pas (i)abc = a+bfib(i)
01fib(0)=0, fib(1)=1
2111fib(2)=1
3122fib(3)=2
4233fib(4)=3
5355fib(5)=5
6588fib(6)=8
781313fib(7)=13

6 pași (n-1). Recursivul ar fi făcut 41 de apeluri pentru n=7!

Greșeli frecvente în analiză

Eroare #1: Overflow la putere

Calculul 2^64 depășește long long! Pentru numere foarte mari, trebuie modular arithmetic: rez = (rez * baza) % MOD;

Eroare #2: Comparare doar pe cazuri mici

La n=10, ambele metode par rapide. Diferența se vede la n=1.000.000+. Testează mereu cu date mari pentru a vedea diferența reală.

Verificare: Alege algoritmul mai eficient

1. Numeri numerele pare de la 1 la 1.000.000. Ce metodă alegi?

2. Verifici dacă 999.983 e prim. Verifici până la:

3. Calculezi 3^1000. Câți pași face metoda rapidă?

4. Fibonacci(30) recursiv face ~2.7 milioane de apeluri. De ce?

5. CMMDC(1000000, 999997): Euclid face ~20 pași, for face ~999.997. De ce Euclid e mai rapid?

Extensie: cum alegi testele pentru o comparație corectă

O comparație între doi algoritmi este relevantă doar dacă îi testezi pe aceleași date și pe cazuri variate. Pentru analiza comparativă, nu este suficient să rulezi un singur exemplu mic.

Set minim de teste
  1. Caz mic: verifică ușor manual rezultatul.
  2. Caz mediu: observă diferența dintre metode.
  3. Caz mare: vezi clar cum crește timpul sau numărul de pași.
  4. Caz limită: n = 0, n = 1, valori egale sau element inexistent, după problemă.
Ce compariMăsură utilăExemplu
formulă vs buclănumăr de operațiisuma 1+...+n
secvențial vs binarnumăr de comparațiicăutarea unei valori
metodă naivă vs eficientătimp / pașiverificare prim sau CMMDC

Întrebări frecvente (FAQ)

Mereu trebuie să aleg metoda mai rapidă?

Nu neapărat! Pentru n mic (sub 100), diferența e neglijabilă. Alege codul mai ușor de citit dacă performanța nu contează. Dar la n mare, eficiența devine critică.

Cum măsor câți pași face algoritmul?

Adaugă o variabilă int pasi = 0; și incrementeaz-o (pasi++;) la fiecare operație semnificativă (comparație, calcul). La final, afișează pasi.

Ce înseamnă O(n) vs O(log n)?

O(n) = nr. pași crește proporțional cu n (de 10x mai mare n → 10x mai mulți pași). O(log n) = nr. pași crește lent (de 10x mai mare n → doar +3 pași). Formula directă e O(1) = mereu 1 pas!

De ce Fibonacci recursiv e atât de lent?

Recursivul formează un arbore exponențial. fib(5) apelează fib(4) și fib(3). fib(4) apelează fib(3) din nou! fib(3) se calculează de 2 ori, fib(2) de 3 ori. La fib(40), fib(2) se calculează de ~63 milioane de ori.

PROBLEME PBINFO - 15 minute

PbInfo - Masurare cu clock

Timp de executie

Enunț:

Cerință: Se citeste n. Calculeaza suma 1+2+...+n si afiseaza timpul aproximativ de executie.

Date de intrare:

n.

Date de ieșire:

Suma si timpul.

Exemplu:

Intrare:

100000

Ieșire:

5000050000 timp

Explicație: Timpul depinde de calculator.

Cum gândim soluția:
  1. Pornim cronometrul cu clock().
  2. Rulam bucla.
  3. Oprim cronometrul si calculam diferenta.

Tip: Timpul este orientativ, nu o valoare fixa. clock() masoara aproximativ timpul de executie pe calculatorul tau, deci poate iesi diferit pe alt calculator.

Scrie soluția în C++:
Soluție corectă:
#include <iostream>
#include <ctime>
using namespace std;

int main() {
    int n;
    cin >> n;
    clock_t start = clock();
    long long s = 0;
    for (int i = 1; i <= n; i++)
        s += i;
    clock_t stop = clock();
    cout << s << " " << double(stop - start) / CLOCKS_PER_SEC;
}

PbInfo - Cautare secventiala

Comparatii

Enunț:

Cerință: Se citeste n, apoi n numere si x. Afiseaza pozitia lui x sau -1.

Date de intrare:

n, sirul, x.

Date de ieșire:

Pozitia sau -1.

Exemplu:

Intrare:

5
4 8 2 9 1
2

Ieșire:

3

Explicație: 2 se afla pe pozitia 3.

Cum gândim soluția:
  1. Parcurgem sirul de la stanga la dreapta.
  2. Daca gasim x, memoram pozitia.
  3. Daca nu gasim, afisam -1.

Tip: Cautarea secventiala nu cere sir sortat.

Scrie soluția în C++:
Soluție corectă:
#include <iostream>
using namespace std;

int main() {
    int n, x, poz = -1;
    cin >> n;
    int v[1001];
    for (int i = 1; i <= n; i++)
        cin >> v[i];
    cin >> x;
    for (int i = 1; i <= n; i++) {
        if (v[i] == x && poz == -1) {
            poz = i; // prima pozitie pe care apare x
        }
    }
    cout << poz;
}

PbInfo - Cautare binara

Algoritm eficient

Enunț:

Cerință: Se citeste un sir sortat si x. Afiseaza DA daca x apare in sir.

Date de intrare:

n, sir sortat, x.

Date de ieșire:

DA sau NU.

Exemplu:

Intrare:

5
1 3 5 7 9
7

Ieșire:

DA

Explicație: 7 se gaseste prin injumatatiri succesive.

Cum gândim soluția:
  1. Setam stanga=1 si dreapta=n.
  2. Calculam mijlocul.
  3. Micsoram intervalul dupa comparatie.

Tip: Cautarea binara cere sir sortat.

Scrie soluția în C++:
Soluție corectă:
#include <iostream>
using namespace std;

int main() {
    int n, x, v[1001];
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> v[i];
    cin >> x;
    int st = 1, dr = n;
    bool ok = false;
    while (st <= dr) {
        int m = (st + dr) / 2;
        if (v[m] == x) {
            ok = true;
            break;
        }
        if (v[m] < x)
            st = m + 1;
        else
            dr = m - 1;
    }
    cout << (ok?"DA":"NU");
}
EXERCIȚII PRACTICE - 2 sarcini / 20 minute

Exercițiul Practic: Analiză comparativă

Compară metode de rezolvare prin formule directe, bucle, căutare și măsurarea pașilor executați. Numărul de pași este mai important decât timpul exact, pentru că timpul depinde de calculator.

De ce long long? Suma 1+2+...+n crește repede. Pentru n = 100000, suma este 5000050000, mai mare decât limita unui int obișnuit.

Sarcina 1: Suma 1+2+...+n
Pași de rezolvare
  1. Metoda A: for(i=1; i<=n; i++) suma += i; — O(n) pași
  2. Metoda B: suma = n * (n+1) / 2; — O(1), un singur pas
  3. Cronometrează ambele cu clock() pentru n=1.000.000
  4. Afișează: rezultat, timp. Folosește long long pentru n mare!

Implementează 2 metode: (A) for cu sumă, (B) formula n*(n+1)/2. Compară nr. de operații pentru n=1.000.000.

Suma 1+2+…+n = n·(n+1)/2 — formulă directă, O(1) vs for O(n)
long long suma = (long long)n * (n+1) / 2; // un singur pas!
Se citește: „suma egal n ori n plus 1 supra 2" — formula lui Gauss, nu trebuie buclă
// PSEUDOCOD: Suma 1..n — două metode comparate // Metoda A — for, O(n) sumaA 0 PENTRU i 1 PÂNĂ LA n sumaA sumaA + i SFÂRȘIT PENTRU // Metoda B — formula Gauss, O(1) sumaB n · (n + 1) / 2 SCRIE sumaA, sumaB // ambele dau același rezultat

De ce funcționează? Formula lui Gauss (n·(n+1)/2) dă suma primelor n numere naturale într-un singur pas O(1). For-ul parcurge fiecare număr → O(n) pași. La n=1.000.000, formula e de ~1.000.000× mai rapidă.

#include <iostream>   // cin, cout
#include <ctime>      // clock(), CLOCKS_PER_SEC
using namespace std;

int main(){
    long long n = 1000000;          // n mare pt diferență vizibilă

    // ── Metoda A: for O(n) ──
    clock_t s1 = clock();           // start cronometru
    long long suma1 = 0;
    for(long long i = 1; i <= n; i++)
        suma1 += i;                 // adunăm fiecare i
    clock_t e1 = clock();           // stop cronometru
    
    // ── Metoda B: formula O(1) ──
    clock_t s2 = clock();
    long long suma2 = n * (n+1) / 2; // Gauss: un singur pas!
    clock_t e2 = clock();
    
    cout << "For: " << suma1 << " in " 
         << double(e1-s1)/CLOCKS_PER_SEC << "s" << endl;
    cout << "Formula: " << suma2 << " in "
         << double(e2-s2)/CLOCKS_PER_SEC << "s" << endl;
    // Ambele dau 500000500000, dar for e mult mai lent
    return 0;
}
Sarcina 2: Numere prime până la n
Pași de rezolvare
  1. For exterior: for(num = 2; num <= n; num++)
  2. For interior (optimizat): for(d = 2; d*d <= num; d++)
  3. Numără comparațiile totale cu un contor global
  4. Afișează: câte prime, câte comparații totale (așteptat ~114.000 pt n=10.000)

Generează toate numerele prime până la n=10.000. Folosește metoda √n. Numără câte verificări faci.

Prime până la n: pentru fiecare num, testăm d de la 2 la √num → total ≈ O(n·√n) comparații
for(int d = 2; d*d <= num; d++) { totalComp++; if(num%d==0) { prim=false; break; } }
Se citește: „pentru d cât timp d ori d ≤ num" — fiecare număr e testat doar până la rădăcina lui
// PSEUDOCOD: Generare prime 2..n cu verificare √num totalComp 0 nrPrime 0 PENTRU num 2 PÂNĂ LA n prim TRUE PENTRU d 2 CÂT TIMP d × d num totalComp totalComp + 1 DACĂ num MOD d = 0 prim FALSE BREAK SFÂRȘIT DACĂ SFÂRȘIT PENTRU DACĂ prim ATUNCI nrPrime nrPrime + 1 SFÂRȘIT PENTRU SCRIE nrPrime, totalComp

De ce funcționează? Dacă un număr num are un divizor d, atunci num/d e tot divizor și min(d, num/d) ≤ √num. Deci e suficient să testăm până la √num. La n=10.000, asta reduce comparațiile de la milioane la ~114.000.

#include <iostream>
using namespace std;

int main(){
    int n = 10000;             // limita superioară
    int totalComp = 0;         // contor global comparații
    int nrPrime = 0;           // câte prime am găsit
    for(int num = 2; num <= n; num++){
        bool prim = true;
        // Verificăm doar până la √num (d*d ≤ num)
        for(int d = 2; d*d <= num; d++){
            totalComp++;       // numărăm fiecare test
            if(num % d == 0){  // găsit divizor → nu e prim
                prim = false;
                break;         // nu mai testăm restul
            }
        }
        if(prim) nrPrime++;    // dacă n-am găsit divizor → prim
    }
    cout << "Prime gasite: " << nrPrime << endl;      // 1229
    cout << "Total comparatii: " << totalComp << endl; // ~114.000
    return 0;
}

Descarcă toate exercițiile și soluțiile