Greedy: Restul cu monede
Avem un set de monede
c[1], c[2], ..., c[k]și o sumăS. Cum dăm restul folosind minimum de monede?
E exemplul clasic care arată și unde greedy reușește, și unde eșuează - un studiu de caz perfect.
Greedy lacom
Sortează monedele descrescător. La fiecare pas, ia cea mai mare monedă care încape în restul rămas.
sortează descrescător c[]
total = 0
pentru fiecare monedă c din c[]:
cnt = S / c
folosesc cnt monede de c
S = S - cnt * c
total = total + cnt
Exemplu cu monede românești
Monede: 1, 5, 10, 50, 100, 500 (bani /
lei).
Suma S = 287:
| Pas | Restul | Cea mai mare monedă ≤ rest | Câte | Folosesc |
|---|---|---|---|---|
| 0 | 287 | - | - | - |
| 1 | 287 | 100 | 2 | 200 |
| 2 | 87 | 50 | 1 | 50 |
| 3 | 37 | 10 | 3 | 30 |
| 4 | 7 | 5 | 1 | 5 |
| 5 | 2 | 1 | 2 | 2 |
Total:
2 + 1 + 3 + 1 + 2 = 9 monede. Optim ✓.
Cod (greedy)
#include <fstream>
#include <algorithm>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
const int KMAX = 105;
int c[KMAX];
int k, S;
bool descrescator(int a, int b) {
return a > b;
}
int main() {
fin >> k >> S;
for (int i = 1; i <= k; i++) {
fin >> c[i];
}
sort(c + 1, c + k + 1, descrescator);
int total = 0;
for (int i = 1; i <= k; i++) {
int cnt = S / c[i];
total = total + cnt;
S = S - cnt * c[i];
}
if (S > 0) {
fout << "imposibil"; // dacă nu există monedă de 1
} else {
fout << total;
}
return 0;
}Complexitate: O(K log K) (sortarea) + O(K) (iterare) = O(K log K).
CÂND greedy funcționează: monede „canonice”
Un set de monede e canonic dacă greedy dă
optimul pentru orice sumă
S ≥ 0.
Exemple de seturi canonice
- Monedele românești / euro / dolari:
{1, 2, 5, 10, 20, 50, 100, ...}✓ - Monede „de putere a lui c”:
{1, c, c², c³, ...}pentru oricec ≥ 2✓ {1, 5, 10, 25}(vechiul sistem american) ✓
Test rapid (informal)
Dacă, pentru fiecare monedă c[i], valoarea ei e
cel puțin dublul monedei anterioare,
probabil e canonic. (Nu e o garanție
matematică, dar e o regulă empirică folositoare.)
CÂND greedy EȘUEAZĂ: monede non-canonice
Contraexemplul clasic
Monede: {1, 6, 10}. Sumă
S = 12.
Greedy:
- Cea mai mare ≤ 12:
10→ folosesc 1 monedă, rămâne 2. - Cea mai mare ≤ 2:
1→ folosesc 2 monede. - Total:
1 + 2 = 3 monede.
Optim: - 6 + 6 = 2 monede.
Greedy dă 3, optimul e 2. Greedy greșește.
Alt exemplu
Monede: {1, 7, 10}, sumă 14.
Greedy: 10 + 1 + 1 + 1 + 1 = 5 monede.
Optim: 7 + 7 = 2 monede.
De ce eșuează?
Pentru că în sistemele non-canonice, alegerea celei mai mari monede nu lasă mereu ce e mai bun pentru rest. Există combinații „inteligente” cu monede mai mici care dau totalul mai eficient.
Greedy nu „vede înainte” - e prizoner al opțiunii imediate.
Soluția generală (DP)
Când greedy nu funcționează, folosim programare dinamică - răspunsul corect garantat pentru orice set de monede:
const int INF = 1e9;
const int SMAX = 1000005;
int dp[SMAX]; // dp[s] = nr minim de monede pentru suma s
void minMonedeDP(int S, int c[], int k) {
for (int s = 0; s <= S; s++) {
dp[s] = INF;
}
dp[0] = 0;
for (int s = 1; s <= S; s++) {
for (int i = 1; i <= k; i++) {
int x = c[i];
if (x <= s && dp[s - x] != INF) {
if (dp[s - x] + 1 < dp[s]) {
dp[s] = dp[s - x] + 1;
}
}
}
}
// dp[S] e răspunsul (sau INF dacă imposibil)
}Complexitate: O(S · K). Pentru
S = 10⁶, K = 10 → 10⁷ operații, OK.
Greedy vs DP - când alegi pe care?
| Criteriu | Greedy | DP |
|---|---|---|
| Set de monede canonic | ✓ | ✓ (overkill) |
| Set de monede arbitrar | ✗ (poate greși) | ✓ |
S foarte mare (10⁹+) |
✓ (rapid) | ✗ (memorie) |
S mic (10⁶), set arbitrar |
✗ | ✓ |
Regulă practică: dacă enunțul specifică monedele standard (1, 5, 10, …), folosește greedy. Dacă monedele sunt arbitrare (citite din input), folosește DP.
Variantă: numărul de moduri (nu minim)
În câte moduri distincte putem da rest
Scu monedele date?
Greedy nu se potrivește aici - e o problemă de numărare, nu de optim. Soluție: DP.
long long dp[SMAX]; // dp[s] = nr de moduri să formăm s
dp[0] = 1;
for (int i = 1; i <= k; i++) { // pentru fiecare tip de monedă
int x = c[i];
for (int s = x; s <= S; s++) {
dp[s] = dp[s] + dp[s - x];
}
}
// dp[S] = numărul de moduri(Ordinea buclelor contează - exterior pe monede, interior pe sume - pentru a evita numărarea aceleiași combinații în ordine diferită.)
Greșeli frecvente
1. Folosirea greedy fără verificarea setului de monede
Mereu întreabă: e setul „canonic”? Dacă nu ești sigur, DP.
2. Sortare crescătoare în loc de descrescătoare
Greedy începe cu cea mai mare monedă. Dacă sortezi crescător, iei cele mici → folosești mult prea multe monede.
3. Uitarea cazului „imposibil”
Dacă suma rămasă > 0 după ce ai consumat toate monedele și nu există moneda de 1, e imposibil. Tratează:
if (S > 0) fout << "imposibil";4. DP cu memorie excesivă
Pentru S = 10⁹, DP nu încape. Dacă setul e
canonic, greedy funcționează în O(K). Dacă nu e canonic și
S e enorm, problema e mai grea (necesită optimizări
speciale).
5. Confuzia „minim de monede” cu „număr de moduri”
Sunt probleme diferite. Verifică ce întreabă enunțul.
Ce să reții
- Greedy pentru rest: sortează descrescător, ia cea mai mare monedă posibilă la fiecare pas.
- Funcționează doar pentru seturi „canonice” (monede românești, euro, dolari).
- Eșuează pentru seturi arbitrare (ex.
{1, 6, 10}→S = 12greșește). - Pentru orice set de monede, DP O(S · K) garantează optimul.
- DP-ul are și varianta „numărul de moduri” cu inversare a ordinii buclelor.
- Lecția generală: greedy e periculos când nu poți demonstra că e corect. Mereu testează pe contraexemple.