🐍 Python de la zero — Partea 8

Algoritmi Greedy, optimizare și probleme clasice de selecție

Prerechizite: părțile 1–7: condiții, repetări, șiruri, liste, matrice, funcții, recursivitate, programare dinamică și grafuri.

🎯 Obiective

Să înțelegi ideea de alegere locală.
Să recunoști probleme potrivite pentru Greedy.
Să ordonezi datele înainte de selecție.
Să explici de ce o alegere produce o soluție bună.
Să aplici Greedy la intervale, monede și activități.
Să legi Greedy de algoritmii pe grafuri.

1. Recapitulare: ce știm deja?

În părțile anterioare am învățat să lucrăm cu variabile, condiții și repetări, apoi cu șiruri, liste și matrice. Am învățat funcții, recursivitate, programare dinamică și grafuri.

Date
→
Algoritm
→
Soluție
→
Verificare
Regulă: înainte de cod, formulăm clar ce trebuie optimizat și ce condiții trebuie respectate.

2. Ce înseamnă optimizare?

Într-o problemă de optimizare există mai multe soluții posibile și vrem una cât mai bună.

Exemplu: avem activități și vrem să alegem cât mai multe fără să se suprapună.

3. Ideea algoritmului Greedy

Greedy înseamnă să construim soluția pas cu pas, alegând la fiecare pas o variantă care pare cea mai bună în acel moment.

O schemă generală este:

solutie = ...

pregatim datele

cat timp mai avem alegeri:
    alegem varianta cea mai buna local
    daca alegerea este permisa:
        o pastram
Atenție: o alegere locală bună nu garantează în orice problemă o soluție globală optimă. Trebuie să știm sau să demonstrăm că strategia este corectă pentru problema respectivă.

4. Greedy cu sortare

Multe probleme Greedy devin simple după ce sortăm elementele după un criteriu.

a.sort()

for x in a:
    # analizăm x
    ...

Sortarea trebuie făcută după criteriul care susține strategia. Nu există o singură ordine corectă pentru toate problemele.

5. Problema activităților

Avem activități cu moment de început și sfârșit. Vrem să alegem cât mai multe activități care nu se suprapun.

Strategia Greedy corectă: alegem activitatea care se termină cel mai devreme, apoi căutăm următoarea activitate care începe după terminarea celei alese.

activitati.sort(key=lambda x: x[1])

ultima = -1
numar = 0

for activitate in activitati:
    inceput = activitate[0]
    sfarsit = activitate[1]

    if inceput >= ultima:
        numar = numar + 1
        ultima = sfarsit

print(numar)
Observație: aici folosim o funcție anonimă prin lambda doar pentru criteriul de sortare. Dacă acest concept nu a fost încă studiat în detaliu, el trebuie tratat ca o extensie a capitolului despre funcții, nu memorat mecanic.

6. Problema monedelor

Avem valori de monede și vrem să obținem o sumă folosind cât mai puține monede. Strategia „aleg mereu cea mai mare monedă posibilă” funcționează pentru anumite sisteme de monede, dar nu pentru orice sistem.

monede = [10, 5, 1]
suma = 28
numar = 0

for moneda in monede:
    while suma >= moneda:
        suma = suma - moneda
        numar = numar + 1

print(numar)
Exemplu de atenție: pentru monedele [1, 3, 4] și suma 6, Greedy alege 4 + 1 + 1, adică 3 monede, dar soluția optimă este 3 + 3, adică 2. Pentru astfel de cazuri este potrivită programarea dinamică.

7. Greedy și programarea dinamică

GreedyProgramare dinamică
Alege imediat o variantă.Compară soluții ale subproblemelor.
De obicei este simplu și rapid.Poate necesita mai multă memorie.
Are nevoie de o strategie corectă.Folosește relații între subprobleme.
Nu este corect în orice problemă.Poate rezolva probleme în care Greedy eșuează.

8. Greedy în grafuri

În partea 7 am introdus grafurile și drumurile. Unele probleme de grafuri au soluții Greedy.

Dijkstra

Pentru muchii cu costuri nenegative, Dijkstra alege repetat vârful nevizitat cu distanța cunoscută cea mai mică și încearcă să îmbunătățească distanțele vecinilor.

distanta[start] = 0

repetam:
    alegem v care are distanta minima
    il marcam

    pentru fiecare vecin u al lui v:
        nou = distanta[v] + cost(v, u)
        daca nou < distanta[u]:
            distanta[u] = nou
Important: Dijkstra nu trebuie folosit cu muchii de cost negativ.

9. Arbore de acoperire de cost minim

Un graf neorientat conex poate avea un arbore care leagă toate vârfurile fără cicluri. Dacă vrem cost total minim, căutăm un arbore de acoperire minim.

Kruskal — ideea

  1. Sortăm muchiile crescător după cost.
  2. Luăm muchia cea mai ieftină care nu formează un ciclu.
  3. Continuăm până când avem suficiente muchii pentru a lega toate vârfurile.
Implementarea eficientă a verificării componentelor folosește o structură numită Disjoint Set Union (DSU). În această parte o introducem conceptual; implementarea completă poate fi tratată într-o secțiune avansată.

10. Cum verificăm o strategie Greedy?

  1. Definim clar ce alegere facem.
  2. Arătăm că alegerea este permisă.
  3. Explicăm de ce alegerea nu ne strică posibilitatea de a obține o soluție optimă.
  4. Repetăm argumentul pentru pașii următori.

Nu este suficient să spunem „pare cea mai bună”. Trebuie să existe un motiv matematic sau algoritmic.

11. Exerciții rezolvate

Exercițiul 1 — Alegerea activităților

Activități: (1,3), (2,5), (4,7), (6,8), (8,9). Alegem după sfârșitul cel mai mic.

activitati = [(1, 3), (2, 5), (4, 7), (6, 8), (8, 9)]
activitati.sort(key=lambda x: x[1])

ultima = -1
numar = 0

for a in activitati:
    if a[0] >= ultima:
        numar = numar + 1
        ultima = a[1]

print(numar)

Se aleg (1,3), (4,7), (8,9), deci răspunsul este 3.

Exercițiul 2 — Monede

Monedele sunt 25, 10, 5, 1. Pentru suma 63, alegem de fiecare dată cea mai mare monedă posibilă.

monede = [25, 10, 5, 1]
suma = 63
numar = 0

for moneda in monede:
    while suma >= moneda:
        suma = suma - moneda
        numar = numar + 1

print(numar)

Se folosesc 2×25, 1×10, 1×1, 1×1, 1×1: în total 6 monede.

Exercițiul 3 — De ce Greedy poate eșua?

Pentru [1,3,4] și suma 6, Greedy alege 4, apoi 1 și 1. Obținem 3 monede. Dar 3 + 3 folosește doar 2. Concluzia: strategia Greedy nu este universală.

Exercițiul 4 — Alegerea maximului dintre variante

Avem valori [7, 2, 9, 4, 6]. Dacă problema cere doar valoarea maximă, nu avem nevoie de Greedy: folosim parcurgerea listei.

a = [7, 2, 9, 4, 6]
maxim = a[0]

for x in a:
    if x > maxim:
        maxim = x

print(maxim)

Ideea importantă este să nu folosim o tehnică complicată când o metodă simplă este suficientă.

Exercițiul 5 — Ordinea contează

Pentru o problemă în care trebuie să alegem intervale după momentul de sfârșit, sortarea după început poate produce o alegere greșită. Criteriul de sortare trebuie justificat de strategie.

Exercițiul 6 — Alegerea unui drum

Într-un graf cu costuri nenegative, Dijkstra menține pentru fiecare vârf cea mai bună distanță cunoscută și actualizează vecinii după regula:

nou = distanta[v] + cost

Dacă nou este mai mică decât distanța actuală, o înlocuim.

12. Exerciții propuse

1. Selectează maximum de activități neintersectate.
2. Modifică problema activităților astfel încât să afișezi și activitățile alese.
3. Rezolvă problema monedelor pentru [50,20,10,5,1].
4. Construiește un contraexemplu pentru o strategie Greedy dată.
5. Compară Greedy cu programarea dinamică pentru monedele [1,3,4].
6. Pentru mai multe intervale, găsește numărul maxim de intervale compatibile.
7. Alege obiecte după un criteriu de valoare pentru o variantă simplificată a problemei rucsacului.
8. Explică de ce problema rucsacului 0/1 nu se rezolvă în general prin Greedy.
9. Aplică ideea lui Dijkstra pe un graf mic.
10. Afișează distanța minimă de la un vârf la toate celelalte.
11. Construiește manual pașii algoritmului Kruskal pentru un graf mic.
12. Compară Kruskal și ideea lui Prim la nivel de strategie.
13. Identifică în enunț dacă se cere minimizare sau maximizare.
14. Pentru 5 probleme date, spune dacă ai încerca Greedy, DP sau căutare exhaustivă și justifică.
15. Scrie un program care verifică dacă o listă de activități este compatibilă.
16. Găsește o strategie Greedy pentru a acoperi cât mai multe cereri cu resurse limitate.
17. Rezolvă o problemă în care trebuie să alegi cele mai ieftine muchii fără cicluri.
18. Construiește un exemplu în care alegerea celui mai mare element la fiecare pas nu produce soluția optimă.

13. Greșeli frecvente

14. Checklist final

Pot...✓
explica ce este o problemă de optimizare?☐
explica ideea Greedy?☐
alege și justifica un criteriu de sortare?☐
rezolva problema activităților?☐
explica când Greedy eșuează?☐
diferenția Greedy de programarea dinamică?☐
explica ideea Dijkstra?☐
explica ideea lui Kruskal?☐
justifica o strategie, nu doar să o memorez?☐

15. Legătura cu următoarea parte

După Greedy, următorul pas natural este aprofundarea algoritmilor avansați: complexitate, tehnici de căutare și optimizare, structuri de date mai eficiente și implementări mai rapide ale algoritmilor pe grafuri.

Grafuri
→
Greedy
→
Optimizare
→
Algoritmi avansați