Probleme rezolvate pe categorii
Metoda greedy
Înainte de probleme Tiparul problemelor rezolvate prin metoda greedy
Problemele de greedy s-au dat la concursul de Titularizare, în 2004, 2017 și 2021, și la examenul de Definitivat, în 2018. Toate cer o valoare optimă: cele mai puține componente, cele mai multe numere, cele mai multe intervale. Toate se rezolvă construind soluția pas cu pas, cu alegerea cea mai bună în acel moment și fără revenire asupra alegerilor făcute.
Tiparul enunțului
Enunțul unei probleme de greedy are trei semne:
- se cere un număr minim sau maxim de elemente alese dintr-o mulțime, sau o descompunere care respectă o regulă;
- elementele se aleg după o condiție de compatibilitate cu cele deja alese: suma să depășească o valoare, intervalele să nu se intersecteze, suma parelor să rămână mai mare;
- la Titularizare se cere un algoritm eficient și se punctează separat explicarea metodei și justificarea eficienței.
Metoda se aplică în trei pași:
- se stabilește criteriul de alegere: ce element este „cel mai bun” la fiecare pas;
- datele se ordonează după criteriu, ca elementul cel mai bun să fie mereu următorul;
- datele se parcurg o singură dată, iar fiecare element se alege dacă este compatibil cu cele alese până atunci. O alegere făcută nu se mai schimbă.
| Problema | Ce se cere | Criteriul de alegere |
|---|---|---|
| 2004 | cele mai puține componente cu suma mai mare decât v | cea mai mare componentă rămasă |
| 2017 | cele mai multe numere, cu suma parelor mai mare | parele cele mai mari, imparele cele mai mici |
| 2018 | n scris ca sumă de termeni ai unui șir | cel mai mare termen care nu depășește ce a rămas |
| 2021 | cele mai multe intervale disjuncte | intervalul care se termină cel mai devreme |
Programul-șablon
Programul de mai jos are toate părțile tiparului. Problema aleasă este cea din 2021, cea mai cunoscută aplicație a metodei: intervalele se ordonează după extremitatea dreaptă și se aleg cele care încep după ultimul interval ales.
#include <iostream>
#include <fstream>
#include <algorithm>
using namespace std;
struct interval {
int st, dr;
};
interval v[10001];
int n;
/* 1. Criteriul de ordonare. */
bool comparaDr(interval a, interval b) {
return a.dr < b.dr;
}
int main() {
/* 2. Citirea datelor. */
ifstream f("titu2021.in");
f >> n;
for (int i = 1; i <= n; i++)
f >> v[i].st >> v[i].dr;
f.close();
/* 3. Ordonarea dupa criteriu. */
sort(v + 1, v + n + 1, comparaDr);
/* 4. O singura parcurgere, cu alegerea elementelor compatibile. */
int nr = 1;
int ultimDr = v[1].dr;
for (int i = 2; i <= n; i++)
if (v[i].st >= ultimDr) {
nr++;
ultimDr = v[i].dr;
}
/* 5. Afisarea rezultatului. */
cout << nr;
return 0;
}
Părțile programului
- Criteriul de ordonare
- subprogramul
comparaDrprimește două intervale și returnează adevărat dacă primul trebuie așezat înaintea celui de-al doilea - aici: intervalul care se termină mai devreme stă înainte
- subprogramul
- Citirea datelor
ifstream f("titu2021.in")deschide fișierul pentru citiref >> ncitește numărul de intervale, apoi extremitățile fiecăruia
- Ordonarea
sort(v + 1, v + n + 1, comparaDr)ordonează elementele de pe pozițiile 1, …, n ale tabloului v, în ordinea dată decomparaDr- funcția este din biblioteca
algorithmși lucrează în timp de ordinul n log n
- Parcurgerea, cu alegerea
- primul element, cel mai bun după criteriu, se alege întotdeauna
- se reține ce trebuie știut despre alegerile făcute: aici, unde se termină ultimul interval ales
- fiecare element următor
- se alege, dacă este compatibil cu cele alese
- altfel se trece peste el; nu se revine niciodată la el
- Afișarea numărului de elemente alese
Problema 1 · Titularizare 2004 · Subiectul II.2 · 20 puncte Numărul minim de componente cu suma mai mare decât v
Ce cere problema
Se aleg cât mai puține componente, astfel încât suma lor să fie mai mare decât v. Se afișează pozițiile lor în vector, nu valorile. Pentru exemplul din enunț:
| Numărul de ordine | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Componenta | 2 | −1 | 9.5 | 3 | 6 | 20 |
O singură componentă nu ajunge: cea mai mare este 20, mai mică decât 28. Două ajung: 20 + 9.5 =
29.5 > 28. Se afișează numerele lor de ordine, 3 6.
Ca suma să crească cât mai repede, fiecare componentă aleasă trebuie să fie cât mai mare. De aici criteriul greedy: se alege, la fiecare pas, cea mai mare componentă rămasă. Din criteriu rezultă și două situații în care alegerea se oprește:
- suma a depășit v: s-a găsit răspunsul;
- componenta următoare nu este pozitivă: ea nu mai mărește suma, deci nici cele de după ea. Dacă suma tot nu depășește v, problema nu are soluție.
Încearcă alegerea
Simulatorul ordonează componentele descrescător, păstrând numărul de ordine al fiecăreia, apoi le alege pe rând. Bara arată suma componentelor alese, iar linia roșie valoarea v. Componentele și v se pot schimba.
Programul și descrierea metodei
În stânga este programul, cu comentariile cerute de enunț. În dreapta este metoda de rezolvare, descrisă în limbaj natural, cu pașii așezați pe niveluri după structura programului.
#include <iostream>
using namespace std;
int n, poz[500];
double a[500], v;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
poz[i] = i; // numarul de ordine al componentei
}
cin >> v;
/* Metoda greedy: ca suma sa depaseasca v cu cat mai putini termeni,
se aleg componentele in ordinea descrescatoare a valorilor.
Se ordoneaza descrescator, prin selectie, pastrand numerele de ordine. */
for (int i = 1; i < n; i++)
for (int j = i + 1; j <= n; j++)
if (a[j] > a[i]) {
double x = a[i]; a[i] = a[j]; a[j] = x;
int p = poz[i]; poz[i] = poz[j]; poz[j] = p;
}
/* Se alege intai cea mai mare componenta, apoi se adauga, pe rand,
cea mai mare dintre cele ramase, pana cand suma depaseste v.
O componenta negativa sau nula nu mai mareste suma, deci acolo
alegerea se opreste. */
double s = a[1];
int k = 1;
while (k < n && s <= v && a[k + 1] > 0) {
k++;
s = s + a[k];
}
if (s > v)
for (int i = 1; i <= k; i++)
cout << poz[i] << " ";
else
cout << "nu este posibil";
return 0;
}
Programul
- se citesc n, cele n componente și v
- componentele sunt numere reale, deci se păstrează în variabile de tip
double - pentru fiecare componentă i se reține și numărul ei de ordine, în
poz[i]
- componentele sunt numere reale, deci se păstrează în variabile de tip
- se ordonează componentele descrescător, prin selecție
- pentru fiecare poziție i, de la 1 la n − 1
- pentru fiecare poziție j de după ea
- dacă
a[j] > a[i], cele două componente își schimbă locurile - odată cu ele își schimbă locurile și numerele lor de ordine, din
poz, ca fiecare componentă să-și păstreze numărul
- dacă
- pentru fiecare poziție j de după ea
- pentru fiecare poziție i, de la 1 la n − 1
- se alege prima componentă, cea mai mare: suma s pornește de la ea, iar k, numărul celor alese, de la 1
- cât timp mai sunt componente, suma nu depășește v și componenta următoare este pozitivă
- se alege componenta următoare: k crește cu 1, iar ea se adaugă la sumă
- dacă suma a depășit v
- se afișează numerele de ordine ale primelor k componente
- altfel
- se afișează că nu este posibil
Prima componentă se alege întotdeauna, chiar dacă v este negativ: enunțul cere componente alese, deci cel puțin una.
Problema 2 · Titularizare 2017 · Subiectul II.2 · 15 puncte Cele mai multe numere, cu suma parelor mai mare decât a imparelor
Ce cere problema
Se aleg cât mai multe numere din mulțime, cu două condiții:
- cel mult k numere pare;
- suma parelor alese este strict mai mare decât suma imparelor alese.
Pentru exemplul din enunț, cu k = 3:
| Numerele | Se aleg | Suma | |
|---|---|---|---|
| pare, de la cel mai mare | 12, 8, 6, 4, 2, 0 | 12, 8, 6 | 26 |
| impare, de la cel mai mic | 1, 3, 5, 7, 11, 31 | 1, 3, 5, 7 | 16 |
Se aleg 3 pare și 4 impare, deci 7 numere. Imparul următor, 11, nu se mai poate alege: suma imparelor ar deveni 27, iar 27 nu este mai mic decât 26.
Criteriul greedy are două părți:
- parele, de la cel mai mare: k pare cât mai mari dau suma cea mai mare, deci lasă loc pentru cât mai multe impare. Mai puține de k pare nu ajută: fiecare par în plus mărește și numărul elementelor alese, și suma parelor;
- imparele, de la cel mai mic: fiecare impar adaugă cât mai puțin la suma imparelor, deci încap cât mai multe.
Încearcă alegerea
Simulatorul alege întâi parele, de la cel mai mare, apoi imparele, de la cel mai mic. Bara albastră arată suma parelor, cea portocalie suma imparelor, iar linia roșie de pe ea pragul pe care suma imparelor nu are voie să-l atingă. Numerele și k se pot schimba.
Programul și descrierea metodei
În stânga este programul, așa cum se scrie pe foaia de examen. În dreapta este metoda de rezolvare, descrisă în limbaj natural, cu pașii așezați pe niveluri după structura programului.
#include <iostream>
#include <fstream>
using namespace std;
int ap[10001]; // ap[x] = 1 daca x se afla in multime
int main() {
ifstream f("titu.in");
int n, k, x;
f >> n >> k;
for (int i = 1; i <= n; i++) {
f >> x;
ap[x] = 1;
}
f.close();
/* Cele mai mari k numere pare: suma lor este cea mai mare posibila. */
int nr = 0, luatePare = 0;
long long sPare = 0, sImpare = 0;
for (x = 10000; x >= 0 && luatePare < k; x = x - 2)
if (ap[x]) {
luatePare++;
sPare = sPare + x;
}
nr = luatePare;
/* Numerele impare, de la cel mai mic, cat timp suma lor ramane
strict mai mica decat suma numerelor pare alese. */
for (x = 1; x <= 9999; x = x + 2)
if (ap[x]) {
if (sImpare + x < sPare) {
sImpare = sImpare + x;
nr++;
} else
break;
}
if (sPare == 0) // nicio selectie nu indeplineste conditia
nr = 0;
cout << nr;
return 0;
}
Programul
- se citesc n, k și elementele mulțimii
ifstream f("titu.in")deschide fișierul pentru citire- fiecare număr citit x se marchează în vectorul caracteristic:
ap[x] = 1
- se aleg parele, de la cel mai mare
- x pornește de la 10000, cel mai mare număr par posibil, și scade din 2 în 2
- cât timp s-au ales mai puțin de k pare
- dacă x se află în mulțime, se alege: crește numărul parelor alese, iar x se adaugă la suma parelor
- se aleg imparele, de la cel mai mic
- x pornește de la 1 și crește din 2 în 2
- dacă x se află în mulțime
- dacă suma imparelor, cu x adăugat, rămâne strict mai mică decât suma parelor, x se alege
- altfel alegerea se oprește: imparele următoare sunt și mai mari
breakiese din buclafor
- dacă suma parelor alese este 0, nicio alegere nu respectă condiția, deci rezultatul este 0
- se afișează numărul elementelor alese
Parcurgerea vectorului ap de la un capăt la altul dă numerele în ordine, fără o sortare separată.
Problema 3 · Definitivat 2018 · Subiectul I.3 · 15 puncte Un număr scris ca sumă de termeni ai unui șir
Cerința este rezolvată și pe pagina subiectului: Suma de termeni ai unui șir.
Ce cere problema
Numărul n se scrie ca sumă de termeni ai șirului, fiecare termen folosit de cel mult două ori. Criteriul greedy: se scade din n, de fiecare dată, cel mai mare termen care nu depășește ce a rămas. Pentru n = 484:
| Ce a rămas | Cel mai mare termen cel mult egal | După scădere |
|---|---|---|
| 484 | 248 | 236 |
| 236 | 121 | 115 |
| 115 | 58 | 57 |
| 57 | 27 | 30 |
| 30 | 27 | 3 |
| 3 | 2 | 1 |
| 1 | 1 | 0 |
Se scriu termenii în ordinea în care au fost găsiți, 248 121 58 27 27 2 1, adică exact șirul din
enunț. Termenii ies singuri în ordine descrescătoare, fiindcă ce a rămas scade la fiecare pas.
Termenii șirului se calculează după formulă, din cei doi dinaintea lor:
| i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| fᵢ | 1 | 2 | 1 + 3·2 − 2·1 = 5 | 12 | 27 | 58 | 121 | 248 | 503 |
Încearcă descompunerea
Se scrie n. Simulatorul arată termenii șirului cel mult egali cu n și scade, la fiecare pas, cel mai mare dintre ei care încape în ce a rămas. Sub fiecare termen folosit apare de câte ori a fost scris.
Programul și descrierea metodei
În stânga sunt subprogramul și programul principal, așa cum se scriu pe foaia de examen. În dreapta este metoda de rezolvare, descrisă în limbaj natural, cu pașii așezați pe niveluri după structura programului.
#include <iostream>
#include <fstream>
using namespace std;
/* Cel mai mare termen al sirului 1, 2, 5, 12, 27, ... aflat in [1, n]. */
int termen(int n) {
int a = 1, b = 2; // doi termeni consecutivi
if (n < 2)
return 1;
while (1 + 3 * b - 2 * a <= n) {
int c = 1 + 3 * b - 2 * a;
a = b;
b = c;
}
return b;
}
Subprogramul termen(n)
- a și b sunt doi termeni consecutivi ai șirului; pornesc de la primii doi, 1 și 2
- dacă n este 1, cel mai mare termen din [1, 1] este 1, deci se returnează 1
- cât timp termenul următor,
1 + 3·b − 2·a, nu depășește n- se calculează termenul următor, c
- perechea de termeni consecutivi înaintează: a primește b, iar b primește c
- la ieșirea din buclă, b nu depășește n, iar termenul următor îl depășește, deci b este cel mai mare termen din [1, n]; se returnează b
Termenii aproape se dublează de la unul la altul, deci până la 10⁹ sunt cel mult 30 de termeni, iar bucla face cel mult 30 de pași.
int main() {
int n;
cin >> n;
ofstream g("def.out");
/* Metoda greedy: se scade din n, de fiecare data, cel mai mare termen
al sirului care nu il depaseste. */
while (n > 0) {
int t = termen(n);
g << t << " ";
n = n - t;
}
g.close();
return 0;
}
Programul principal
- se citește n
- se deschide fișierul pentru scriere
ofstream g("def.out")creează fișieruldef.out, gol, și leagă variabilagde el
- cât timp n nu a ajuns la 0
- se află cel mai mare termen t care nu depășește n, prin apelul
termen(n) - se scrie t în fișier, urmat de un spațiu
g << t << " "scrie în fișier la fel cumcoutscrie pe ecran
- din n se scade t
- se află cel mai mare termen t care nu depășește n, prin apelul
- se închide fișierul
g.close()
Problema 4 · Titularizare 2021 · Subiectul II.2 · 15 puncte Numărul maxim de intervale disjuncte
Cerința este rezolvată și pe pagina subiectului: Numărul maxim de intervale disjuncte.
Ce cere problema
Se aleg cât mai multe intervale, astfel încât oricare două să nu aibă niciun punct comun. Pentru exemplul din enunț, se pot alege patru:
| Intervalul ales | Se termină în | Următorul începe în |
|---|---|---|
| (10, 150) | 150 | 150 |
| (150, 200) | 200 | 270 |
| (270, 300) | 300 | 301 |
| (301, 500) | 500 | — |
Intervalele sunt deschise: (10, 150) nu conține numărul 150, iar (150, 200) nu-l conține nici el. De aceea cele două nu au niciun punct comun, deși unul se termină exact unde începe celălalt. Două intervale deschise (a₁, b₁) și (a₂, b₂), cu b₁ ≤ b₂, sunt disjuncte dacă a₂ ≥ b₁.
Problema este problema spectacolelor din programă: fiecare interval este un spectacol, cu ora de început și ora de sfârșit, iar într-o sală se programează cât mai multe spectacole care nu se suprapun. Criteriul greedy: se alege intervalul care se termină cel mai devreme, fiindcă el lasă cel mai mult loc pentru celelalte.
Încearcă alegerea
Intervalele sunt desenate pe o axă. Simulatorul le ordonează după extremitatea dreaptă, apoi le ia pe rând: cele alese se fac verzi, cele care se intersectează cu ultimul ales se taie. Linia roșie arată unde se termină ultimul interval ales. Intervalele se pot schimba.
Programul și descrierea metodei
În stânga este programul, așa cum se scrie pe foaia de examen. În dreapta este metoda de rezolvare, descrisă în limbaj natural, cu pașii așezați pe niveluri după structura programului.
#include <iostream>
#include <fstream>
#include <algorithm>
using namespace std;
struct interval {
int st, dr;
};
interval v[10001];
int n;
bool comparaDr(interval a, interval b) {
return a.dr < b.dr;
}
int main() {
ifstream f("titu2021.in");
f >> n;
for (int i = 1; i <= n; i++)
f >> v[i].st >> v[i].dr;
f.close();
sort(v + 1, v + n + 1, comparaDr); // crescator dupa extremitatea dreapta
int nr = 1; // primul interval se alege
int ultimDr = v[1].dr;
for (int i = 2; i <= n; i++)
if (v[i].st >= ultimDr) { // incepe dupa ultimul interval ales
nr++;
ultimDr = v[i].dr;
}
cout << nr;
return 0;
}
Programul
- un interval se reține ca înregistrare, cu două câmpuri
struct interval { int st, dr; }definește tipul:steste extremitatea stângă,drcea dreaptă
- subprogramul
comparaDrspune ordinea: intervalul cu extremitatea dreaptă mai mică stă înainte - se citesc n și cele n intervale
ifstream f("titu2021.in")deschide fișierul pentru citire
- se ordonează intervalele crescător după extremitatea dreaptă
sort(v + 1, v + n + 1, comparaDr)ordonează pozițiile 1, …, n ale tabloului v dupăcomparaDr; funcția este din bibliotecaalgorithm
- se alege primul interval, cel care se termină cel mai devreme
- nr, numărul intervalelor alese, pornește de la 1
ultimDrreține unde se termină ultimul interval ales
- pentru fiecare interval următor, în ordinea obținută
- dacă începe cel mai devreme unde se termină ultimul ales, adică
v[i].st >= ultimDr- se alege: nr crește cu 1, iar
ultimDrprimește extremitatea lui dreaptă
- se alege: nr crește cu 1, iar
- altfel se intersectează cu ultimul ales și nu se alege
- dacă începe cel mai devreme unde se termină ultimul ales, adică
- se afișează nr
Extremitățile sunt din [−10⁹, 10⁹], deci încap în tipul int, al cărui maxim este 2 147 483 647.
Materialul acesta se citește pe educamp.ro și nu se tipărește.
S-a încheiat minutul de citit liber.
Vezi problemele rezolvate ca și cursant
Lista categoriilor, cu problemele date la examen în fiecare, se citește gratuit și integral. Pentru a citi rezolvările, te înscrii la cursul de pregătire.
Te înscrii, alegi în cont când începe cursul și plătești prima lună prin transfer bancar. Pentru a vedea mai întâi ce probleme s-au dat, poți reveni la lista categoriilor.
Acces la suportul de curs
25 RON / lună, fără ședințe
Ce cuprinde:
- Acces la toate lecțiile disponibile
- Suport de curs pentru tematica științifică și metodică
- Exemple, explicații și exerciții
- Acces la materialele noi adăugate pe platformă
- Posibilitatea de a adresa întrebări
Poți alege și variante cu ședințe online, în grup sau individuale. Le vezi pe pagina cursului.
Ai întrebări sau o problemă? Scrie-mi pe WhatsApp, la 0745 874 576.