De ce contează?
La supermarket, casiera nu citește numele produsului literă cu literă ca să afle prețul. Trece codul de bare prin scaner: un șir lung devine instant un număr scurt. Două produse identice au același cod, iar cautarea în baza de date e fulgerătoare. Așa lucrezi și tu cu șirurile: în loc să le compari caracter cu caracter, le dai o „amprentă" numerică și compari amprentele.
Ideea-cheie
A compara două șiruri de lungime L caracter cu caracter costă O(L) în cazul cel
mai rău. Dacă faci asta de multe ori — într-o buclă peste N șiruri — ajungi rapid
la O(N·L) sau chiar O(N²·L). Tentant, dar lent.
Trucul de punte: reduci fiecare șir la o cheie și lucrezi cu cheia. Cheia poate
fi un număr (un hash) sau chiar șirul în sine pus într-un dicționar. Întrebarea
„l-am mai văzut pe acesta?" sau „de câte ori apare?" devine atunci o căutare în
unordered_set / unordered_map, adică ~O(1) amortizat per operație.
Cheia e o amprentă: dacă două șiruri au chei diferite, sigur sunt diferite. Dar
atenție la sensul invers — un hash pur poate da coliziuni: două șiruri diferite
cu același număr. De aceea, când contează corectitudinea, folosești o structură ce
compară exact (unordered_map<string,int> ține cheia ca șir și o verifică), sau
confirmi egalitatea după ce hash-urile coincid. Map-ul compară exact; hash-ul pur,
nu.
Cum alegi
Întrebarea pe care ți-o pune problema îți spune ce structură folosești. Trei tipare acoperă aproape tot:
- „L-am mai întâlnit?" →
unordered_set<string>. Inserezi fiecare șir; dacă era deja înăuntru, l-ai mai văzut. Așa numeri cuvinte distincte: dimensiunea mulțimii la final. - „De câte ori apare fiecare?" →
unordered_map<string,int>. Cheia e șirul, valoarea e numărul de apariții. Un singurmp[cuvant]++per cuvânt. - „Care șiruri sunt anagrame între ele?" → tot un
map, dar cheia nu e șirul brut. Două anagrame ("ascult"și"cultas") trebuie să primească aceeași cheie. Cheia bună: șirul cu literele sortate (ambele dau"acltsu"), sau vectorul de frecvențe al literelor. Grupezi după această cheie.
Reține criteriul: alegi cheia astfel încât exact lucrurile pe care vrei să le consideri egale să primească aceeași cheie — nici mai mult, nici mai puțin.
Folosește exploratorul de mai sus ca să vezi diferența: un set reține doar dacă un element există deja (răspunde da/nu), pe când un map reține și o valoare asociată (de exemplu un contor). Adaugă același element de două ori și observă: în set nu se schimbă nimic, în map contorul crește.
Iată cheia-de-anagramă în C++ — același tipar pe care îl reciclezi pentru numărat cuvinte sau grupat:
#include <iostream>
#include <unordered_map>
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
int main() {
// grupam anagramele dintr-o lista de cuvinte
vector<string> cuvinte = {"ascult", "cultas", "litera", "tarile", "altice"};
// cheia = cuvantul cu literele sortate; anagramele dau aceeasi cheie
unordered_map<string, vector<string>> grupuri;
for (const string& c : cuvinte) {
string cheie = c; // copie pe care o sortam
sort(cheie.begin(), cheie.end()); // "ascult" -> "acltsu"
grupuri[cheie].push_back(c); // map compara cheia EXACT, fara coliziuni
}
// afisam fiecare grup de anagrame
for (const auto& g : grupuri) {
cout << g.first << ":";
for (const string& c : g.second) cout << " " << c;
cout << "\n";
}
// grupul "acltsu" contine: ascult cultas
return 0;
}Costul: sortarea unei chei de lungime L e O(L·log L), plus o căutare ~O(1) în
map. Pentru N cuvinte ajungi la O(N·L·log L) — mult sub O(N²·L) pe care l-ai
fi plătit comparând fiecare cuvânt cu fiecare.
Ca să numeri cuvintele distincte dintr-un text, prima idee e: pentru fiecare cuvânt nou, plimbă-te prin toate cele deja văzute și compară-l, caracter cu caracter, cu fiecare.
La N cuvinte de lungime L, asta e O(N²·L). La N de ordinul zecilor de mii,
cu cuvinte de câteva litere, sunt miliarde de comparații de caractere — prea lent
pentru limita de timp.
Nu trebuie să compari cuvântul nou cu fiecare cuvânt vechi. Îl arunci într-un
unordered_set<string>: structura calculează singură o cheie și știe instant dacă
l-a mai văzut, în ~O(1) amortizat.
Parcurgi o singură dată, faci vazute.insert(cuvant) pentru fiecare, iar răspunsul
e vazute.size(). Total ~O(N·L): o trecere, nu o buclă dublă.
Două capcane clasice. Prima: te bazezi pe un hash pur calculat de tine (de
exemplu o sumă polinomială) și declari două șiruri egale doar pentru că hash-urile
coincid — fără confirmare. Asta te lasă vulnerabil la coliziuni; la egalitate
strictă, confirmă comparând efectiv șirurile sau folosește unordered_map<string,int>,
care păstrează cheia ca șir și o verifică exact. A doua: rămâi la == pe string
într-o buclă mare. Comparația e O(lungime), deci o buclă peste N șiruri devine
O(N·lungime) sau mai rău — exact ce voiai să eviți. Mută comparația într-un set
sau map și plătești ~O(1) amortizat per operație.