Stiva
În multe probleme în care lucrăm cu secvențe de valori, suntem nevoiți să procesăm valorile pe rând, asemenea unui teanc de obiecte. Pentru a formaliza acest proces, vom avea nevoie de o structură de date potrivită. În informatică, numim această structură de date stivă.
Noțiuni introductive
Ce este o stivă?
Stiva (în engleză, stack) este o structură de date liniară abstractă, pentru care sunt definite operațiile de adăugare a unui element și eliminare a unui element și aceste operații se realizează la un singur capăt al structurii, numit vârful stivei. În timpul operațiilor cu stiva avem acces numai la elementul din vârful stivei.
Operațiile pe care o stivă le poate efectua în timp constant sunt:
- push(x): Adaugă valoarea pe vârful stivei.
- top(): Spune care este valoarea de pe vârful stivei.
- pop(): Scoate elementul din vârful stivei.
- empty(): Spune dacă stiva este goală.
Observație
Valorile vor fi procesate conform principiului LIFO, adică last in, first out.
Probleme de bază
Problema stack
Această problemă ne cere să implementăm exact operațiile descrise mai sus.
Pentru a implementa aceste operații, avem două variante posibile:
Implementarea folosind un vector obișnuit
Pentru a implementa aceste operații fără a folosi vreo structură de date dinamică, putem face asta ținând un contor cu numărul de valori care se află la acel moment în stivă, astfel operațiile de ștergere și de verificare a dimensiunii stivei se fac raportându-ne la variabila , iar adăugarea valorii se face pur și simplu crescând valoarea lui .
#include <iostream>
using namespace std;
int stk[100001];
int main() {
int t;
cin >> t;
int pos = 0;
while (t--) {
int tip;
cin >> tip;
if (tip == 1) {
cin >> stk[pos++];
} else {
if (tip == 2) {
pos--;
} else {
if (tip == 3) {
cout << stk[pos - 1] << '\n';
} else {
if (pos == 0) {
cout << "YES" << '\n';
} else {
cout << "NO" << '\n';
}
}
}
}
}
return 0;
}Implementarea folosind std::stack
Stiva poate fi implementată și cu funcțiile din STL. Pentru mai multe detalii, vedeți implementarea și funcțiile descrise aici.
Se poate observa faptul că avem nevoie de biblioteca <stack> pentru aceste funcții.
#include <iostream>
#include <stack>
using namespace std;
int main() {
int t;
cin >> t;
stack<int> st;
while (t--) {
int tip;
cin >> tip;
if (tip == 1) {
int val;
cin >> val;
st.push(val);
} else {
if (tip == 2) {
st.pop();
} else {
if (tip == 3) {
cout << st.top() << '\n';
} else {
if (st.empty()) {
cout << "YES" << '\n';
} else {
cout << "NO" << '\n';
}
}
}
}
}
return 0;
}Problema stack_max_min
Problema ne dă un șir de numere și 4 întrebări pentru câte o poziție:
- Cel mai apropiat indice la stânga, unde elementul este mai mare decât poziția din întrebare.
- Cel mai apropiat indice la stânga, unde elementul este mai mic decât poziția din întrebare.
- Cel mai apropiat indice la dreapta, unde elementul este mai mare decât poziția din întrebare.
- Cel mai apropiat indice la dreapta, unde elementul este mai mic decât poziția din întrebare.
Vom precalcula, pentru fiecare element, răspunsul la fiecare tip de întrebare. Aici vom descrie algoritmul doar pentru primul tip, deoarece celelalte se rezolvă analog.
Vom parcurge vectorul de la stânga la dreapta, iar pe o stivă vom reține indicii cu elemente mai mici sau egale cu elementul curent. Cu alte cuvinte, pentru fiecare element, scoatem de pe stivă toate elementele mai mici sau egale cu el. Dacă stiva este goală, atunci răspunsul este , altfel este indicele elementului de pe vârful stivei. Apoi, îl adăugăm pe el însuși în stivă.
Observație
Pe stivă vom reține indici, nu valori. Acest lucru va fi valabil pentru o mare parte din problemele de stivă pe care le rezolvați.
Exemplu
Vom face o simulare a acestui algoritm, folosindu-ne de exemplul din problemă, . Ca în problemă, vectorul va fi indexat de la 0.
- Suntem la indicele 0, . Răspunsul va fi -1.
- Suntem la indicele 1, , dar îl scoatem, iar apoi . Răspunsul va fi .
- Suntem la indicele 2, , dar îl scoatem, iar apoi . Răspunsul va fi .
- Suntem la indicele 3, , dar îl scoatem, iar apoi . Răspunsul va fi .
- Suntem la indicele 4, . Răspunsul va fi 3.
- Suntem la indicele 5, , dar îl scoatem pe 4. Răspunsul va fi 3.
- Suntem la indicele 6, . Răspunsul va fi 5.
- Suntem la indicele 7, . Răspunsul va fi 6.
- Suntem la indicele 8, . Răspunsul va fi 7.
- Suntem la indicele 9, , dar le scoatem pe toate, iar apoi . Răspunsul va fi .
Această rezolvare are complexitatea , pentru că fiecare element va fi pus pe stivă și scos, deci se vor face cel mult două operații pentru fiecare.
Notă
Pentru a înțelege de ce complexitatea este liniară, puteți citi aici mai multe detalii.
Detalii de implementare: vom reține o matrice care va reprezenta răspunsul la o întrebare de tipul . De asemenea, vom folosi o santinelă, care va fi o valoare care va fi mereu mai mică (sau mai mare, în funcție de caz) decât orice valoare din vector. Pentru mai multe detalii, vezi implementarea de mai jos.
#include <iostream>
using namespace std;
#define MAXN 200000
#define MAXTIP 4
#define INFINIT 2000000000
int v[MAXN + 2], raspuns[MAXTIP][MAXN + 1], stiva[MAXN + 1];
int main() {
ios_base::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n, sp;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> v[i];
}
// santinela
v[0] = INFINIT;
stiva[0] = 0;
sp = 1;
// intrebari de tip 1
for (int i = 1; i <= n; i++) {
// scoatem elementele mai mici sau egale
while (v[stiva[sp - 1]] <= v[i]) {
sp--;
}
raspuns[0][i] = stiva[sp - 1]; // primul element mai mare
stiva[sp++] = i; // adaugam i in stiva
}
// santinela
v[0] = 0;
stiva[0] = 0;
sp = 1;
// intrebari de tip 2
for (int i = 1; i <= n; i++) {
// scoatem elementele mai mari sau egale
while (v[stiva[sp - 1]] >= v[i]) {
sp--;
}
raspuns[1][i] = stiva[sp - 1]; // primul element mai mic
stiva[sp++] = i;
}
// santinela
v[n + 1] = INFINIT;
stiva[0] = n + 1;
sp = 1;
// intrebari de tip 3
for (int i = n; i >= 1; i--) {
// scoatem elementele mai mici sau egale
while (v[stiva[sp - 1]] <= v[i]) {
sp--;
}
raspuns[2][i] = stiva[sp - 1]; // primul element mai mare
stiva[sp++] = i;
}
// santinela
v[n + 1] = 0;
stiva[0] = n + 1;
sp = 1;
// intrebari de tip 4
for (int i = n; i >= 1; i--) {
// scoatem elementele mai mari sau egale
while (v[stiva[sp - 1]] >= v[i]) {
sp--;
}
raspuns[3][i] = stiva[sp - 1];
stiva[sp++] = i;
}
int q;
cin >> q;
while (q--) {
int tip, poz;
cin >> tip >> poz;
cout << raspuns[tip - 1][poz + 1] - 1 << "\n";
}
return 0;
}Probleme rezolvate
Problema skyline
Pentru a rezolva această problemă, va trebui să aflăm pentru fiecare valoare care este cea mai apropiată poziție de la stânga și de la dreapta cu o înălțime mai mică decât cea curentă.
După ce aflăm aceste valori, tot ce trebuie să facem este să folosim sume parțiale pentru a calcula aria cerută pentru fiecare poziție.
#include <fstream>
using namespace std;
ifstream fin("skyline.in");
ofstream fout("skyline.out");
const int nmax = 40000;
int length[5 + nmax], height[5 + nmax], l[5 + nmax], stk[5 + nmax];
// l[i] = cel mai mare j < i pentru care height[j] < height[i]
int main() {
int n;
fin >> n;
int ptr = 0;
for (int i = 1; i <= n; i++) {
fin >> height[i] >> length[i];
length[i] += length[i - 1]; // fac sume partiale pe length[]
while (ptr > 0 && height[stk[ptr - 1]] >= height[i]) {
ptr--;
}
if (ptr == 0) {
l[i] = 0;
} else {
l[i] = stk[ptr - 1];
}
stk[ptr++] = i;
}
ptr = 0;
long long ans = 0;
for (int i = n; i >= 1; i--) {
while (ptr > 0 && height[stk[ptr - 1]] >= height[i]) {
ptr--;
}
int r; // r = cel mai mic j > i pentru care height[j] < height[i]
if (ptr == 0) {
r = n + 1;
} else {
r = stk[ptr - 1];
}
stk[ptr++] = i;
ans = max(ans, 1ll * height[i] * (length[r - 1] - length[l[i]]));
}
fout << ans << '\n';
return 0;
}Problema unific
Mai întâi, vom defini urmatoarele două functii:
- dacă putem unifica si , 0 altfel
- rezultatul unificării dintre si
Sa simplificăm enunțul astfel: Găsim primul pentru care (dacă nu există atunci terminăm procedeul). Setăm la si scoatem din șir. Acum, dacă și continuăm căutarea de la . Altfel, setăm la și continuăm tot așa.
Observăm că noi trebuie să scoatem elemente din șir, iar acest lucru nu este ușor într-un vector. Așa că, atunci când ajungem la , vom menține într-o stivă grupurile care s-au format până la . Apoi, cât timp stiva nu este goală, vom încerca să unificam vârful stivei cu . Dacă atunci setăm la și scoatem vârful. Altfel, adaugăm in stiva și continuăm cu .
Sursa de 100 de puncte:
#include <fstream>
const int MAXN = 100'000;
const int MAXCF = 10;
const int FARA_CIFRE = -1;
std::ifstream fin("unific.in");
std::ofstream fout("unific.out");
int n, sp, fr[MAXCF], fra[MAXCF], frb[MAXCF];
long long v[MAXN], stiva[MAXN];
void readArray() {
int i;
fin >> n;
for (i = 0; i < n; i++) {
fin >> v[i];
}
}
void getMostFrequent() {
int i, max;
long long val;
for (i = 0; i < n; i++) {
val = v[i]; // vrem sa pastram valoarea
while (val > 0) {
fr[val % 10]++;
val /= 10;
}
}
max = 0;
for (i = 1; i < MAXCF; i++) {
if (fr[i] > fr[max]) {
max = i;
}
}
fout << max << "\n";
}
void getDigitFrequencies(long long val, int fr[MAXCF]) {
do { // tratam si cazul val = 0
fr[val % 10]++;
val /= 10;
} while (val > 0);
}
int canJoin(long long a, long long b) {
int i;
for (i = 0; i < MAXCF; i++) {
fra[i] = frb[i] = 0;
}
getDigitFrequencies(a, fra);
getDigitFrequencies(b, frb);
i = 0; // cautam prima cifra care apare la ambii
while (i < MAXCF && (fra[i] == 0 || frb[i] == 0)) {
i++;
}
return i < MAXCF; // daca am gasit vreuna
}
long long removeCommonDigits(long long val, int other_fr[]) {
long long p, rez;
int cf, has_digits;
p = 1;
while (p * 10 <= val) {
p *= 10;
}
rez = has_digits = 0;
while (p > 0) {
cf = val / p % 10;
if (other_fr[cf] == 0) { // daca nu e comuna
rez = rez * 10 + cf; // adaugam cifra
has_digits = 1;
}
p /= 10;
}
return has_digits ? rez : FARA_CIFRE;
}
// consideram ca am aplicat inainte canJoin(a, b)
// asta inseamna ca fra si frb sunt calculate deja
long long join(long long a, long long b) {
long long p, rez;
a = removeCommonDigits(a, frb); // numarul nou al lui a
b = removeCommonDigits(b, fra); // numarul nou al lui b
if (a != FARA_CIFRE
|| b != FARA_CIFRE) { // ambele dispar daca ambele n-au cifre
if (a == FARA_CIFRE) { // nu mai conteaza ca nu are cifre
a = 0;
}
p = 1;
while (p <= b) {
p *= 10;
}
if (b == 0) {
p = 10; // si cifra asta trebuie luata in considerare
}
if (b == FARA_CIFRE) { // setam la 0 ca sa nu ne afecteze rezultatul
b = 0;
}
a = a * p + b; // lipim numerele
}
return a;
}
void unifyArray() {
int i;
for (i = 0; i < n; i++) {
while (sp > 0 && v[i] >= 0 && canJoin(stiva[sp - 1], v[i])) {
v[i] = join(stiva[sp - 1], v[i]);
sp--; // scoatem varful din stiva
}
if (v[i] != FARA_CIFRE) { // daca mai are cifre
stiva[sp++] = v[i]; // adaugam elementul in stiva
}
}
fout << sp << "\n"; // cate sunt
for (i = 0; i < sp; i++) {
fout << stiva[i] << " ";
}
fout << "\n";
}
int main() {
readArray();
getMostFrequent();
unifyArray();
return 0;
}Problema swap
Notam cu șirul de paranteze. Pentru punctul a), în timp ce parcurgem șirul de paranteze, vom menține o stivă care va conține indicii parantezelor deschise cărora nu le-am găsit încă o pereche. De fiecare dată când dăm peste o paranteză deschisa, îi adaugăm indicele în stivă, iar atunci când dăm peste o paranteză închisă, va fi perechea ei. Adunăm la răspuns , scoatem vârful din stivă și continuăm cu . Punctele b) și c) pot fi rezolvate împreună. Deducem următoarele trei cazuri:
- , unde și sunt parantezele interschimbate. Atunci, răspunsul va rămâne exact la fel, deci nu este o operație swap validă
- și . În acest caz, există un și un astfel încât, , și , respectiv formau perechi. Costurile lor însumate vor fi. Când interschimbăm cu obținem perechileși, ale căror costuri însumate dau . Deci răspunsul a crescut cu 2, ceea ce înseamnă că nu este o operație swap validă.
- și . Dacă nu există nici un astfel încât și perechea lui (pe care o notăm cu ) să fie mai mare ca , atunci operația nu ar fi validă, deoarece nu am avea pereche pentru dacă . Mai întai, avem perechile și . După cum am văzut la cazul 2, răspunsul ar fi mai mic cu 2 dacă perechile ar fi și (i + 1, b)\. Deci operația swap este validă.
Observăm că singurul caz în care operația swap este validă este atunci când și există un , , a cărui pereche este mai mare ca . Acest lucru se poate simplifica astfel: Căutăm un care respectă următoarele condiții:
- Înainte să scoatem perechea lui , stiva trebuie să aiba cel puțin 2 elemente
- Vârful stivei trebuie să aibă valoarea .
Deci noi, trebuie să numărăm câți respectă cele trei condiții. Dacă găsim vreunul, răspunsul este , unde este răspunsul de la punctul a. Altfel, răspunsul este .
Sursa de 100 de puncte:
#include <fstream>
const int MAXN = 90'000;
const char DESCHISA = '(';
const char INCHISA = ')';
std::ifstream fin("swap.in");
std::ofstream fout("swap.out");
int stiva[MAXN], sp;
void calcAnswer() {
int i, n, ch, cate;
long long rez;
fin >> n;
rez = cate = 0;
while ((ch = fin.get()) != '(')
; // asta va fi mereu primul caracter
for (i = 0; i < n; i++) {
if (ch == DESCHISA) {
stiva[sp++] = i; // il adaugam in stiva
} else { // INCHISA
rez += i - stiva[sp - 1]; // distanta pana la pereche
if (sp >= 2
&& stiva[sp - 1] == i - 1) { // conditia sa fie valida operatia
cate++;
}
sp--; // scoatem perechea din stiva
}
ch = fin.get();
}
fout << rez << "\n" << (cate > 0 ? rez - 2 : -1) << "\n" << cate << "\n";
}
int main() {
calcAnswer();
return 0;
}Problema Ehab and Prefix MEXs
Să presupunem că suntem la un indice și am reușit să construim tot prefixul . Dacă , atunci poate fi orice valoare mai mare ca . Vom ține elementele care au într-o stivă, deoarece ele pot lua orice valoare mai mare ca . Când , vom putea folosi elementele din stivă pentru a acoperi intervalul . Dacă în stivă sunt mai puțin de elemente, atunci nu putem forma prefixul și atunci răspunsul va fi . Altfel, setăm la . Apoi luăm primele elemente, le setăm la și apoi le scoatem din stivă. Restul elementelor, care au rămas în stivă după ce am trecut prin tot șirul, pot fi setate la sau la orice valoare mai mare ca .
Codul de Accepted:
#include <iostream>
const int MAXN = 100'000;
const int CANNOT = -1;
int n, a[MAXN + 1], answer[MAXN + 1], stiva[MAXN];
void readArray() {
int i;
std::cin >> n;
for (i = 1; i <= n; i++) {
std::cin >> a[i];
}
}
void buildAnswer() {
int i, sp, pot, j;
sp = 0;
pot = i = 1;
while (i <= n && pot) {
if (a[i] == a[i - 1]) {
stiva[sp++] = i; // il adaugam in stiva
} else if (a[i] - a[i - 1] > sp + 1) { // nu avem destule elemente
pot = 0;
} else {
answer[i] = a[i - 1];
for (j = a[i - 1] + 1; j < a[i]; j++) {
// folosim elementul si il scoatem
answer[stiva[--sp]] = j;
}
}
i++;
}
while (sp > 0) {
// setam restul elementelor la ceva care nu conteaza
answer[stiva[--sp]] = n + 1;
}
if (pot == 0) {
answer[0] = CANNOT;
}
}
void writeAnswer() {
int i;
if (answer[0] == CANNOT) {
std::cout << CANNOT << "\n";
} else {
for (i = 1; i <= n; i++) {
std::cout << answer[i] << " ";
}
std::cout << "\n";
}
}
int main() {
readArray();
buildAnswer();
writeAnswer();
return 0;
}Concluzii
După cum se poate observa, odată ce deprindeți tehnicile folosite la problemele prezentate mai sus, soluțiile devin mult mai ușor de conceput, existând foarte multe similarități între problemele care implică folosirea stivei.
Probleme suplimentare
- Advertisement - CSES (O versiune mai ușoară a problemei skyline)
- Maximum Building I - CSES (O versiune putin mai grea a problemei skyline)
- inundație - ONI 2022 VI (Cerințele 2 și 3 pot fi rezolvate folosind o stivă, necesită și căutare binară).
- fuziune - ONI 2023 Baraj Juniori (Problemă asemănătoare cu unific, dar necesită lucru cu numere mari și numere prime)
- șiruri - ONI 2022 VI (Problemă asemănătoare cu unific)
- tower - Shumen 2016 Juniori (Nu vă speriați că este de la Shumen, problema este doar o aplicație la stack_max_min)
- maxp - ONI 2013 VIII (O altă aplicație la problema stack_max_min)
- changemin - ONI 2022 X (O aplicație similară cu stack_max_min)
- reactii - ONI 2009 X (Problemă asemănătoare cu unific)
- cladiri - Lot 2007 seniori
- dag - ONI 2019 Baraj Seniori (Problemă care se folosește de tehnica de la stack_max_min)
- leftmax - OJI 2020 X (Problemă care se folosește de tehinca de la stack_max_min)
- Alte probleme cu stiva de pe kilonova
Bibliografie și lectură suplimentară
- std::stack - cppreference
- Articolul de pe USACO Guide despre stivă
- Stiva - CPPI Sync
- Un video despre stivă, pentru a vă ajuta să înțelegeți mai bine acest concept.
- Un video despre analiza amortizată, vă va ajuta să înțelegeți mai bine rezolvarea problemei stack_max_min și de ce are complexitatea
- Stiva - pbinfo
- Algopedia - Stive
- Algopedia - Analiza amortizată, mai multe detalii despre problema stack_max_min