Tehnici de programare
Prezentarea principalelor paradigme și tehnici algoritmice avansate.
Bacalaureat
Divide et Impera: împarte, rezolvă, combină
Divide et Impera rezolvă o problemă în trei pași care se repetă recursiv:
- Divide: problema se împarte în subprobleme de același tip, dar de dimensiune mai mică (de regulă două jumătăți);
- Impera: subproblemele suficient de mici se rezolvă direct (cazul de bază al recursivității);
- Combină: soluțiile subproblemelor se asamblează în soluția problemei inițiale.
Condiția esențială, care se uită la examen: subproblemele trebuie să fie independente — rezolvarea uneia nu depinde de rezultatul alteia. Când subproblemele se suprapun (aceleași calcule apar de mai multe ori), Divide et Impera devine ineficientă și locul ei îl ia programarea dinamică.
Exemple canonice: căutarea binară (împarte intervalul în două, păstrează o singură jumătate), MergeSort (sortează recursiv cele două jumătăți, apoi le interclasează), QuickSort (partiționează în jurul pivotului, apoi sortează recursiv cele două zone), turnurile din Hanoi.
Capcana clasică de grilă: nu orice funcție recursivă înseamnă Divide et Impera. O recursivitate care reduce problema cu 1 (de la n la n-1, ca la suma elementelor) nu împarte problema în subprobleme comparabile — metoda cere împărțirea în bucăți semnificative, tipic înjumătățire, ceea ce duce la complexități cu log n.
Greedy: alegerea local optimă, fără revenire
Metoda Greedy construiește soluția pas cu pas: la fiecare pas alege elementul care pare cel mai bun în acel moment (optimul local), iar alegerea făcută nu se mai revizuiește niciodată. De aici vin și puterea, și slăbiciunea metodei:
- puterea: algoritmii Greedy sunt rapizi — de regulă o sortare inițială plus o singură parcurgere, deci O(n log n);
- slăbiciunea: optimul local nu garantează optimul global; Greedy dă soluția corectă doar pentru problemele la care se poate demonstra că strategia locală conduce la optim global.
Exemple la care Greedy funcționează demonstrabil: problema spectacolelor (selectarea numărului maxim de activități care nu se suprapun — se alege mereu activitatea care se termină cel mai devreme), interclasarea optimă, algoritmii Kruskal și Prim pentru arbore parțial de cost minim, Dijkstra pentru drumuri minime.
Contraexemplul care se cere la examen: problema restului cu monede arbitrare. Pentru monedele 1, 3, 4 și suma 6, Greedy alege 4 + 1 + 1 (trei monede), dar optimul este 3 + 3 (două monede). Concluzia de reținut: Greedy nu explorează alternative și nu revine asupra deciziilor — dacă problema cere garantat optimul și strategia locală nu e demonstrată, se folosește altă metodă (programare dinamică sau backtracking).
Backtracking: explorare sistematică cu revenire
Backtracking generează toate soluțiile posibile construindu-le element cu element, într-un vector soluție x[1..n]. La fiecare pas k se încearcă pe rând valorile candidate pentru x[k]:
- dacă valoarea satisface condițiile de continuare, se avansează la pasul k+1;
- dacă nicio valoare nu mai e validă, se revine (backtrack) la pasul k-1 și se încearcă acolo următoarea valoare.
Ideea care face metoda utilizabilă: condițiile de continuare taie din start ramurile care nu pot duce la soluție — nu se generează toate combinațiile și abia apoi se verifică, ci se abandonează prefixele invalide cât mai devreme. Fără această tăiere, backtracking degenerează în forță brută pură.
Probleme standard, de recunoscut instantaneu în enunț:
- permutări (n dame pe tabla de șah, aranjarea a n persoane pe n scaune);
- submulțimi și combinări (alegerea unui grup din n obiecte);
- produs cartezian (generarea tuturor șirurilor de lungime dată peste un alfabet);
- colorarea hărților, partiții, parcurgeri de labirint.
Complexitatea este exponențială (sau factorială la permutări) — de aceea backtracking se aplică doar la dimensiuni mici. Grilă frecventă: la generarea permutărilor lui n, numărul soluțiilor este n!; la submulțimi este 2^n. Cine le inversează pierde punctul fără drept de apel.
Programarea dinamică: subprobleme care se suprapun
Programarea dinamică se aplică problemelor de optimizare care au două proprietăți:
- substructura optimă: soluția optimă a problemei se construiește din soluțiile optime ale subproblemelor (principiul optimalității al lui Bellman);
- subprobleme suprapuse: aceleași subprobleme apar de mai multe ori în procesul de rezolvare.
Strategia: fiecare subproblemă se rezolvă o singură dată, iar rezultatul se memorează într-un tablou (de regulă numit d sau dp), de unde e refolosit ori de câte ori e nevoie. Se lucrează fie bottom-up (se completează tabloul de la cazurile mici spre problema mare), fie top-down cu memoizare (recursivitate care își notează rezultatele).
Exemplul didactic standard: șirul lui Fibonacci. Calculat recursiv naiv, F(n) cere timp exponențial pentru că F(n-2) se recalculează de nenumărate ori; cu memorare, timpul devine liniar. Probleme clasice de examen: subșirul crescător de lungime maximă, problema rucsacului (varianta discretă), numărul de drumuri într-o matrice, suma maximă pe un traseu în triunghi de numere.
Delimitarea față de celelalte metode — întrebare de teorie garantată: față de Divide et Impera, programarea dinamică tratează subprobleme care se suprapun (D&I cere subprobleme independente); față de Greedy, programarea dinamică examinează toate alternativele la fiecare pas și garantează optimul, în timp ce Greedy face o singură alegere locală.
Branch and Bound și alegerea metodei potrivite
Branch and Bound explorează, la fel ca backtracking, un arbore de soluții candidate, dar cu două diferențe:
- pentru fiecare ramură se calculează o margine (bound) — o estimare optimistă a celui mai bun rezultat care s-ar mai putea obține continuând pe acea ramură;
- dacă marginea este mai slabă decât cea mai bună soluție găsită deja, întreaga ramură se taie fără a mai fi explorată.
În plus, ordinea de explorare nu e neapărat în adâncime: se poate alege mereu nodul cel mai promițător (best-first). Metoda se folosește la probleme de optimizare grele (problema comis-voiajorului, rucsacul), unde backtracking simplu ar explora inutil ramuri evident proaste.
Cum alegi metoda la examen — schema de decizie care rezolvă majoritatea grilelor de teorie:
- se cer toate soluțiile sau o configurație care respectă restricții → backtracking;
- problema se sparge în subprobleme independente de același tip → Divide et Impera;
- optimizare cu subprobleme suprapuse → programare dinamică;
- optimizare unde alegerea locală duce demonstrat la optim global → Greedy;
- optimizare cu spațiu uriaș de căutare, unde se pot estima margini → Branch and Bound.
Confuzia frecventă: Branch and Bound nu e „backtracking mai rapid” în general — tăierea depinde de calitatea estimării; cu o margine proastă, explorează la fel de mult.
De reținut
- Divide et Impera
- tehnică ce împarte problema în subprobleme independente de același tip, le rezolvă recursiv și combină soluțiile; exemple: căutarea binară, MergeSort, QuickSort
- metoda Greedy
- tehnică ce construiește soluția pas cu pas, alegând la fiecare pas optimul local, fără a reveni asupra alegerilor făcute; garantează optimul global doar dacă acest lucru este demonstrat pentru problema dată
- backtracking
- tehnică ce generează soluțiile element cu element într-un vector soluție, abandonând prin revenire prefixele care nu satisfac condițiile de continuare
- condiții de continuare
- condițiile verificate la fiecare pas al backtracking-ului, care decid dacă valoarea curentă poate conduce la o soluție; ele taie ramurile invalide înainte de a fi explorate complet
- programare dinamică
- tehnică de optimizare pentru probleme cu substructură optimă și subprobleme suprapuse; fiecare subproblemă se rezolvă o singură dată, iar rezultatul se memorează și se refolosește
- principiul optimalității
- proprietatea conform căreia soluția optimă a unei probleme conține soluții optime ale subproblemelor sale; este condiția de aplicare a programării dinamice
- memoizare
- varianta top-down a programării dinamice: recursivitate care memorează rezultatul fiecărui apel, astfel încât fiecare subproblemă se calculează o singură dată
- Branch and Bound
- tehnică de explorare a arborelui de soluții în care fiecare ramură primește o estimare optimistă (margine); ramurile cu margine mai slabă decât cea mai bună soluție cunoscută se elimină fără explorare
- spațiul soluțiilor la backtracking
- numărul soluțiilor generate: n! la permutări, 2^n la submulțimi, aranjamente și combinări după formulele combinatoricii; explică natura exponențială a metodei
Greșeli frecvente
Greșit: Orice algoritm recursiv ar fi Divide et Impera
Corect: Divide et Impera cere împărțirea în subprobleme independente de dimensiuni comparabile (tipic înjumătățire); o recursie care doar scade n cu 1 este recursivitate simplă, nu Divide et Impera
Greșit: Greedy ar garanta întotdeauna soluția optimă pentru că alege mereu cel mai bun element
Corect: Greedy alege optimul local și nu revine; optimul global e garantat doar la problemele unde strategia e demonstrată — la restul cu monedele 1, 3, 4 pentru suma 6, Greedy dă 3 monede în loc de 2
Greșit: Programarea dinamică s-ar aplica atunci când subproblemele sunt independente
Corect: Exact pe dos: subproblemele independente țin de Divide et Impera; programarea dinamică e valoroasă tocmai când subproblemele se suprapun și rezultatele memorate se refolosesc
Greșit: La generarea permutărilor prin backtracking s-ar obține 2^n soluții
Corect: Permutările a n elemente sunt n!; 2^n este numărul submulțimilor unei mulțimi cu n elemente — cele două formule se confundă frecvent în grile
Greșit: Backtracking-ul ar genera toate combinațiile posibile și abia la final le-ar verifica validitatea
Corect: Backtracking verifică condițiile de continuare la fiecare pas și abandonează imediat prefixele invalide; generarea completă urmată de verificare este forța brută, tocmai ce evită metoda
Test — 6 întrebări ca la examen
1. Care dintre următorii algoritmi este o aplicare directă a metodei Divide et Impera?
- căutarea secvențială într-un vector
- sortarea prin interclasare (MergeSort)
- determinarea maximului printr-o singură parcurgere
- calculul sumei elementelor unui vector
Vezi răspunsul
sortarea prin interclasare (MergeSort). MergeSort împarte vectorul în două jumătăți, le sortează recursiv și combină rezultatele prin interclasare — exact cei trei pași ai metodei. Căutarea secvențială e tentantă doar prin contrast cu cea binară, dar parcurge liniar, fără nicio împărțire în subprobleme.
2. Trăsătura definitorie a metodei Greedy este:
- memorarea rezultatelor subproblemelor pentru refolosire
- revenirea la pasul anterior când soluția nu poate fi continuată
- alegerea optimului local la fiecare pas, fără revizuirea alegerilor
- împărțirea problemei în subprobleme independente
Vezi răspunsul
alegerea optimului local la fiecare pas, fără revizuirea alegerilor. Greedy face la fiecare pas alegerea care pare cea mai bună local și nu se mai întoarce asupra ei. Revenirea la pasul anterior este mecanismul backtracking-ului — distractorul clasic, pentru că ambele metode construiesc soluția pas cu pas, dar numai backtracking revine.
3. Pentru monedele cu valorile 1, 3 și 4, strategia Greedy (alege mereu cea mai mare monedă care încape) aplicată sumei 6 produce:
- soluția optimă, cu două monede
- o soluție cu trei monede, deși optimul este de două monede
- nicio soluție, pentru că 6 nu se poate forma
- o soluție cu patru monede
Vezi răspunsul
o soluție cu trei monede, deși optimul este de două monede. Greedy alege 4, apoi nu mai încape decât 1 și 1 — total trei monede; optimul este 3 + 3, două monede. Exemplul arată exact limita metodei: optimul local (moneda 4) strică optimul global. Suma 6 se poate forma evident, deci varianta a treia pică imediat.
4. Prin metoda backtracking se generează toate permutările a 5 elemente. Câte soluții se obțin?
- 32
- 25
- 120
- 3125
Vezi răspunsul
120. Numărul permutărilor a n elemente este n!, deci 5! = 120. Distractorul 32 = 2^5 corespunde numărului de submulțimi, confuzia standard între cele două probleme de generare; 3125 = 5^5 ar fi produsul cartezian.
5. Calculul recursiv naiv al lui F(n) din șirul Fibonacci este exponențial, dar devine liniar prin programare dinamică, deoarece:
- recursivitatea este înlocuită cu o structură repetitivă, ceea ce e întotdeauna mai rapid
- fiecare subproblemă se rezolvă o singură dată, iar rezultatul memorat se refolosește
- problema se împarte în subprobleme independente rezolvate separat
- se renunță la calculul exact în favoarea unei aproximări
Vezi răspunsul
fiecare subproblemă se rezolvă o singură dată, iar rezultatul memorat se refolosește. Câștigul vine din eliminarea recalculărilor: F(n-2), F(n-3) etc. apar de multe ori în arborele recursiv, dar memorate se calculează o singură dată. Prima variantă e capcana: simpla trecere la iterativ fără memorare nu schimbă numărul de calcule — memorarea e cheia, nu forma buclei.
6. O problemă cere numărul maxim de spectacole care pot fi vizionate integral, cunoscând intervalele lor de desfășurare. Metoda adecvată și criteriul corect sunt:
- Greedy, alegând mereu spectacolul care începe cel mai devreme
- Greedy, alegând mereu spectacolul care se termină cel mai devreme
- backtracking, pentru că se cer toate combinațiile de spectacole
- Divide et Impera, împărțind ziua în două jumătăți
Vezi răspunsul
Greedy, alegând mereu spectacolul care se termină cel mai devreme. Alegerea spectacolului care se termină cel mai devreme lasă loc maxim pentru restul — criteriu demonstrat optim. Prima variantă e distractorul puternic: un spectacol care începe devreme dar durează enorm blochează ziua. Backtracking ar da și el răspunsul, dar exponențial — enunțul cere doar maximul, nu toate combinațiile.
Deschide varianta interactivă — cu AI care îți explică