Biblioteca
Tutora
BibliotecaInformatică și TIC › clasa a XII-a

Tehnici de programare

Aprofundarea metodelor algoritmice avansate pentru rezolvarea problemelor complexe de programare.

Bacalaureat

Metoda Greedy — alegerea local optimă

Greedy (lacom) construiește soluția pas cu pas, alegând la fiecare pas varianta care pare cea mai bună la momentul respectiv (optimul local), fără a reveni vreodată asupra alegerilor făcute. De aici vin cele două trăsături care se cer la examen:

Schema generală: se sortează sau se ordonează candidații după un criteriu de selecție, apoi se parcurg în ordine și fiecare candidat se adaugă la soluție doar dacă nu strică validitatea acesteia.

Exemple clasice unde Greedy dă optimul: problema spectacolelor (selectarea numărului maxim de activități care nu se suprapun — criteriul corect este ora de terminare, nu durata), problema rucsacului fracționar (obiectele pot fi tăiate — se aleg în ordinea descrescătoare a raportului valoare/greutate), plata unei sume cu număr minim de bancnote pentru sisteme monetare canonice, algoritmii Kruskal și Prim pentru arborele parțial de cost minim.

Confuzia clasică: la rucsacul 0/1 (obiectele nu pot fi fracționate) Greedy poate rata optimul — acolo se folosește programarea dinamică. La examen, când se cere să justifici de ce merge Greedy, argumentul este că optimul local conduce la optim global pentru acea problemă anume, nu în general.

Divide et Impera — împarte și stăpânește

Divide et Impera rezolvă o problemă prin trei etape care se enunță exact așa la examen:

Condiția esențială: subproblemele trebuie să fie independente — nu se suprapun. Dacă aceleași subprobleme apar de mai multe ori (de exemplu la șirul lui Fibonacci), Divide et Impera devine ineficientă și se trece la programare dinamică.

Exemple canonice: căutarea binară (O(log n)) — la fiecare pas se elimină jumătate din interval; MergeSort (O(n log n)) — împarte vectorul în două, sortează recursiv și interclasează; QuickSort — partiționează în jurul unui pivot (elementele mai mici la stânga, mai mari la dreapta), apoi sortează recursiv cele două zone; turnurile din Hanoi.

Capcană frecventă la scrierea recursivității: lipsa sau formularea greșită a cazului de bază duce la recursivitate infinită și depășirea stivei (stack overflow). La căutarea binară, condiția de oprire corectă este st > dr (interval vid), iar mijlocul se calculează cu m = (st + dr) / 2 pe indici întregi.

Backtracking — parcurgerea sistematică cu revenire

Backtracking generează toate soluțiile posibile ale unei probleme construind soluția ca un vector x[1..n], element cu element. Pentru fiecare poziție k se încearcă pe rând toate valorile candidate; o valoare este acceptată doar dacă trece de condițiile de continuare (validare). Dacă nicio valoare nu mai e validă pe poziția k, algoritmul revine (se întoarce) la poziția k-1 și încearcă acolo următoarea valoare — de aici numele.

Elementele care se cer la examen:

Probleme standard: generarea permutărilor (x[k] diferit de toate valorile anterioare), aranjamente și combinări (la combinări se impune suplimentar x[k] > x[k-1], ca să nu se genereze aceeași mulțime de mai multe ori), problema celor n regine (regine pe linii diferite, coloane diferite și diagonale diferite — diagonala se verifică prin |x[i] - x[k]| diferit de k - i), colorarea hărților, parcurgerea labirintului.

Greșeala tipică de raționament: backtracking NU se oprește la prima soluție decât dacă i se cere explicit; implicit el explorează tot spațiul soluțiilor.

Programarea dinamică — subprobleme care se suprapun

Programarea dinamică se aplică problemelor de optimizare care au două proprietăți — amândouă se pot cere ca justificare:

Ideea centrală: fiecare subproblemă se rezolvă o singură dată, iar rezultatul se memorează într-un tablou (tabelul de programare dinamică), de unde se refolosește. Astfel o recursivitate exponențială devine algoritm polinomial.

Cele două abordări: bottom-up (se completează tabelul de la cazurile mici spre problema mare, iterativ) și top-down cu memoizare (recursivitate plus tablou în care se rețin rezultatele deja calculate).

Exemplul didactic standard este Fibonacci: recursiv pur are complexitate exponențială pentru că F(n-2) se recalculează de multe ori; cu un vector completat de la stânga la dreapta devine O(n). Alte probleme de examen: numărul de drumuri într-o matrice (fiecare celulă se obține din suma vecinilor de sus și din stânga), subșirul crescător de lungime maximă, rucsacul 0/1 (tabel cu linii pentru obiecte și coloane pentru capacități), triunghiul de sumă maximă.

Distincția față de Divide et Impera este întrebare de teorie: ambele descompun problema, dar la Divide et Impera subproblemele sunt independente, pe când programarea dinamică tratează exact cazul în care ele se suprapun. Distincția față de Greedy: Greedy face o singură alegere pe pas și nu o mai schimbă; programarea dinamică evaluează toate alegerile și o reține pe cea optimă.

Branch and Bound și alegerea tehnicii potrivite

Branch and Bound (ramifică și mărginește) este o rafinare a explorării spațiului de soluții pentru probleme de optimizare: spațiul stărilor se organizează ca un arbore, iar pentru fiecare ramură se calculează o margine (bound) — o estimare optimistă a celui mai bun rezultat care se mai poate obține 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 fi explorată.

Diferențe care se cer în comparațiile de la examen:

Aplicații clasice: problema comis-voiajorului, rucsacul 0/1 pe date mari, jocul de tip puzzle glisant (taquin).

Cum alegi tehnica la o problemă de examen — ghid rapid:

Capcana de punctaj: a numi „Greedy” un algoritm care de fapt reține toate variantele într-un tabel — acela este programare dinamică; Greedy nu păstrează decât alegerea curentă.

De reținut

metoda Greedy
tehnică ce construiește soluția pas cu pas, alegând la fiecare pas optimul local după un criteriu de selecție, fără a reveni asupra alegerilor; rapidă, dar nu garantează optimul global pentru orice problemă
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
backtracking
tehnică ce construiește soluția ca un vector, element cu element, cu condiții de continuare, și revine la elementul anterior când nicio valoare nu mai este validă; generează toate soluțiile, în ordine lexicografică
condiții de continuare
condițiile verificate la plasarea unei valori pe o poziție a vectorului soluție în backtracking, care elimină din start ramurile ce nu pot conduce la o soluție validă
programare dinamică
tehnică de optimizare pentru probleme cu substructură optimă și subprobleme suprapuse; fiecare subproblemă se rezolvă o singură dată, iar rezultatul se memorează într-un tablou și se refolosește
memoizare
varianta top-down a programării dinamice: funcția recursivă reține într-un tablou rezultatele subproblemelor deja calculate și le returnează direct la apelurile următoare
substructură optimă
proprietatea unei probleme prin care soluția sa optimă este formată din soluțiile optime ale subproblemelor sale
Branch and Bound
tehnică de optimizare care explorează arborele stărilor și taie ramurile ale căror margini (estimări optimiste) sunt mai slabe decât cea mai bună soluție deja găsită
pivot
elementul față de care se partiționează vectorul în QuickSort: valorile mai mici trec în stânga lui, cele mai mari în dreapta, apoi cele două zone se sortează recursiv
caz de bază
situația în care o problemă rezolvată recursiv este suficient de mică pentru a fi rezolvată direct, oprind recursivitatea; lipsa lui duce la depășirea stivei

Greșeli frecvente

Greșit: Aplicarea metodei Greedy la rucsacul 0/1 și afirmarea că soluția obținută este sigur optimă
Corect: Greedy garantează optimul doar la rucsacul fracționar; la rucsacul 0/1 poate rata optimul, iar rezolvarea corectă este programarea dinamică
Greșit: La problema spectacolelor se sortează activitățile după durată sau după ora de început
Corect: Criteriul Greedy corect este ora de terminare: alegând mereu activitatea care se termină cel mai devreme, rămâne timp maxim pentru restul
Greșit: Confundarea backtracking-ului cu Divide et Impera pentru că ambele folosesc recursivitate
Corect: Divide et Impera împarte problema în subprobleme independente și combină rezultatele; backtracking construiește un vector soluție element cu element și revine la poziția anterioară când nu mai există candidați valizi
Greșit: La generarea combinărilor se folosesc aceleași condiții ca la permutări, deci apar mulțimi duplicate
Corect: La combinări se impune x[k] > x[k-1] (valori strict crescătoare în vectorul soluție), astfel încât fiecare submulțime să fie generată o singură dată
Greșit: Se afirmă că programarea dinamică și Divide et Impera sunt totuna
Corect: Divide et Impera presupune subprobleme independente; programarea dinamică se aplică tocmai când subproblemele se suprapun și rezultatele lor se memorează pentru refolosire

Test — 6 întrebări ca la examen

1. Ce caracterizează metoda Greedy?
  1. Revine asupra alegerilor făcute atunci când soluția nu este validă
  2. Alege la fiecare pas optimul local și nu revine niciodată asupra alegerii
  3. Memorează rezultatele subproblemelor într-un tablou
  4. Împarte problema în două jumătăți independente
Vezi răspunsul
Alege la fiecare pas optimul local și nu revine niciodată asupra alegerii. Greedy face la fiecare pas alegerea care pare cea mai bună local și nu o mai schimbă. Prima variantă descrie backtracking-ul — distractorul tentant, pentru că ambele construiesc soluția pas cu pas, dar numai backtracking-ul are mecanism de revenire.
2. Care dintre următorii algoritmi este un exemplu de Divide et Impera?
  1. Căutarea secvențială
  2. Sortarea prin selecție
  3. Sortarea prin interclasare (MergeSort)
  4. Algoritmul lui Kruskal
Vezi răspunsul
Sortarea prin interclasare (MergeSort). MergeSort împarte vectorul în două jumătăți, le sortează recursiv și interclasează rezultatele — exact cele trei etape Divide, Impera, Combină. Kruskal e distractorul atrăgător pentru că e un algoritm celebru, dar el este Greedy: alege mereu muchia de cost minim care nu formează ciclu.
3. Un algoritm backtracking generează permutările mulțimii {1, 2, 3} încercând candidații în ordine crescătoare. A treia soluție generată este:
  1. 2 1 3
  2. 1 3 2
  3. 3 1 2
  4. 2 3 1
Vezi răspunsul
2 1 3. Soluțiile apar în ordine lexicografică: 1 2 3, apoi 1 3 2, apoi 2 1 3 — deci a treia este 2 1 3. Cine alege 1 3 2 a numărat de la a doua soluție, uitând că prima permutare generată este chiar 1 2 3.
4. La problema celor n regine, condiția |x[i] - x[k]| ≠ k - i verifică faptul că reginele i și k:
  1. nu se află pe aceeași linie
  2. nu se află pe aceeași coloană
  3. nu se află pe aceeași diagonală
  4. nu sunt alăturate
Vezi răspunsul
nu se află pe aceeași diagonală. Două regine sunt pe aceeași diagonală exact când diferența coloanelor egalează în modul diferența liniilor, deci condiția exclude atacul pe diagonală. Coloana — distractorul frecvent — se verifică separat, prin x[i] ≠ x[k], iar liniile diferă din construcție (regina k stă pe linia k).
5. Calculul recursiv pur al lui F(n) din șirul lui Fibonacci este ineficient deoarece:
  1. recursivitatea nu se oprește niciodată
  2. aceleași subprobleme sunt recalculate de un număr exponențial de ori
  3. rezultatele intermediare nu încap în memorie
  4. șirul lui Fibonacci nu poate fi calculat decât iterativ
Vezi răspunsul
aceleași subprobleme sunt recalculate de un număr exponențial de ori. F(n) apelează F(n-1) și F(n-2), iar F(n-2) se recalculează și din F(n-1) — subproblemele se suprapun și numărul de apeluri crește exponențial; memorarea rezultatelor (programare dinamică) reduce totul la O(n). Recursivitatea se oprește corect la cazurile de bază F(0) și F(1), deci prima variantă e falsă.
6. Pentru o problemă de optimizare se observă că soluția optimă se compune din soluții optime ale subproblemelor, iar subproblemele apar în mod repetat în descompunere. Tehnica adecvată este:
  1. metoda Greedy, pentru că problema cere un optim
  2. Divide et Impera, pentru că problema se descompune în subprobleme
  3. programarea dinamică, pentru că subproblemele suprapuse se rezolvă o singură dată și se memorează
  4. backtracking, pentru că trebuie explorate toate variantele
Vezi răspunsul
programarea dinamică, pentru că subproblemele suprapuse se rezolvă o singură dată și se memorează. Cele două proprietăți enunțate sunt exact substructura optimă și subproblemele suprapuse — semnătura programării dinamice. Divide et Impera este distractorul cel mai tentant, dar el cere subprobleme independente; când ele se repetă, recalcularea îl face exponențial.
Deschide varianta interactivă — cu AI care îți explică
← Elemente de grafică pe calculatorGrafuri →
BiologieChimieEconomieFilosofieFizicăGeografieInformatică și TICIstorieLogică și argumentareMatematicăPsihologieLimba și literatura română