Cost d'algorismes
Comptes quantes operacions fa un algorisme segons la mida de l'entrada, en el cas pitjor, i et quedes amb el terme que creix més. Així pots comparar algorismes sense cronometrar-los.
Conceptes clau
- Mida de l'entrada i cas pitjor
- Notació , i
- Ordres de creixement habituals
- Cost de seqüències, condicionals i bucles
- Cost de les operacions de la STL
| Cost | Operacions | Temps (109 op/s) |
|---|
Per què?
Tens dos programes que resolen el mateix problema. Amb l’exemple de l’enunciat, tots dos responen a l’instant. Amb les dades reals (un milió d’elements), un triga una dècima de segon i l’altre tres hores. Al Jutge, el segon dona «temps límit excedit».
Cronometrar no és una bona manera de saber-ho abans: depèn de l’ordinador, del compilador i de l’exemple que provis. El que necessites és una manera de predir com creix el temps quan creixen les dades. Això és el cost d’un algorisme.
La idea: comptar i quedar-se amb el que creix més
Per analitzar un algorisme:
- Decideix què és la mida de l’entrada, : el nombre d’elements d’un vector, d’una llista o d’un arbre; el valor d’un número; el nombre de dígits…
- Compta quantes operacions elementals fa en funció de . Una operació elemental és qualsevol cosa que costa sempre el mateix: una assignació d’un
int, una suma, una comparació, accedir av[i], unpushd’una pila… - Mira el cas pitjor: l’entrada de mida que fa treballar més l’algorisme. (Una cerca on l’element no hi és, per exemple.)
- Queda’t amb el terme que creix més ràpid i oblida les constants.
Per què oblidar les constants? Perquè depenen de la màquina i del compilador, i perquè quan és gran, el terme dominant ho decideix tot. Si un algorisme fa operacions, amb el són , i la resta, uns : menys d’una deumil·lèsima part. Diem que el cost és .
Prova-ho a l’eina «Com creix el cost» de dalt de tot: compara les funcions per a cada vegada més grans.
La notació asimptòtica
Per dir «creix com » amb precisió hi ha tres notacions. Si és el nombre d’operacions i una funció de referència:
Per exemple, , i també és (és cert, però no és la millor fita) i . Per veure que és , n’hi ha prou de trobar unes constants: amb i , , perquè per a ja tens . I és amb i .
A PRO2 fem servir sobretot : diu el cost exacte, llevat de constants.
Unes quantes regles que es fan servir sempre:
- Les constants no compten: .
- En una suma, guanya el terme més gran: .
- La base dels logaritmes no compta (canviar de base és multiplicar per una constant): , i s’escriu .
Els ordres de creixement habituals
| Cost | Nom | Exemple | Temps amb |
|---|---|---|---|
| constant | top() d’una pila, v[i] |
instantani | |
| logarítmic | cerca dicotòmica, operacions d’un map |
instantani (unes 20 operacions) | |
| lineal | recórrer un vector, cerca seqüencial | 0,01 s | |
| quasi lineal | ordenar (sort, ordenació per fusió) |
0,2 s | |
| quadràtic | comparar totes les parelles | unes 3 hores | |
| exponencial | provar tots els subconjunts | mai (ja amb , segles) |
(Els temps suposen uns passos per segon, que és un ordre de magnitud raonable.)
La lliçó: la diferència entre ordres és enorme, i cap millora de constants ni cap ordinador més ràpid no la compensa. Passar d’un algorisme a un de val més que un ordinador mil vegades més ràpid.
Com es calcula el cost
Seqüència
Si fas una cosa que costa i després una altra que costa , el total és . En notació , això és el més gran dels dos: .
Condicional
if (B) S1; else S2; costa el que costa avaluar B més, en el cas pitjor, el més car de S1 i S2.
Bucle
Suma el cost de cada volta. Si totes costen el mateix, és nombre de voltes × cost d’una volta.
for (int i = 0; i < n; ++i) s += v[i]; // n voltes de Θ(1): Θ(n)
Amb bucles niuats, el de dins s’executa sencer a cada volta del de fora:
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j) ... // n · n voltes: Θ(n²)
Si el de dins depèn del de fora, cal sumar:
for (int i = 0; i < n; ++i)
for (int j = 0; j < i; ++j) ... // 0 + 1 + ... + (n-1) = n(n-1)/2: Θ(n²)
I si la variable del bucle es multiplica o es divideix, en lloc de sumar-se, el nombre de voltes és logarítmic:
for (int i = 1; i < n; i *= 2) ... // i = 1, 2, 4, 8, ...: unes log₂ n voltes, Θ(log n)
Crides a funcions i operacions
Una crida costa el que costa la funció cridada amb aquells paràmetres. Compte amb les operacions que semblen elementals però no ho són:
| Operació | Cost |
|---|---|
push, pop, top, front, back, empty, size (pila, cua, llista) |
|
insert(it, x), erase(it) en una list |
|
insert, erase en un vector (no al final) |
|
push_back, pop_back, v[i], size en un vector |
(push_back, de mitjana) |
m[k], find, count, insert, erase en un map o set |
|
BinTree: left, right, value, construir, copiar |
|
Copiar un vector, una list, un map… (o passar-lo per valor) |
|
sort(v.begin(), v.end()) |
Un exemple: tots diferents
Vols saber si tots els elements d’un vector són diferents. La manera directa: comparar cada element amb tots els que té a la dreta.
// Pre: cert. Post: retorna si tots els elements de v són diferents.
bool tots_diferents(const vector<int>& v) {
int n = v.size();
bool dif = true;
int i = 0;
while (i < n and dif) {
int j = i + 1;
while (j < n and dif) {
if (v[i] == v[j]) dif = false;
++j;
}
++i;
}
return dif;
}
Cas pitjor: quan tots són diferents (no s’atura mai abans). El bucle de dins fa voltes per a cada : en total . Cost .
Amb un set, cada element es busca entre els que ja s’han vist en :
bool tots_diferents(const vector<int>& v) {
set<int> vistos;
bool dif = true;
int i = 0;
while (i < int(v.size()) and dif) {
if (vistos.count(v[i]) == 1) dif = false;
else vistos.insert(v[i]);
++i;
}
return dif;
}
voltes de : . (Una altra opció amb el mateix cost: ordenar una còpia i mirar si hi ha dos consecutius iguals.)
Errors habituals
Exercicis
ExerciciSimplifica
Digues a quin pertany cada funció:
Mostra la solució
- .
- : el logaritme creix més a poc a poc que .
- : creix més que .
- : qualsevol exponencial acaba superant qualsevol polinomi (encara que amb triga a passar).
- : és .
- : .
ExerciciQuantes voltes?
Digues el cost de cada fragment en funció de (suposa que ... és ):
// a)
for (int i = 0; i < n; ++i)
for (int j = 0; j < 10; ++j) ...
// b)
for (int i = 1; i < n; i *= 2)
for (int j = 0; j < n; ++j) ...
// c)
for (int i = 0; i < n; ++i)
for (int j = i; j < n; ++j) ...
// d)
int k = n;
while (k > 1) k /= 3;
// e)
for (int i = 0; i < n; ++i)
for (int j = 1; j < n; j *= 2) ...Mostra la solució
- a) . El de dins fa sempre 10 voltes: és una constant.
- b) . El de fora fa unes voltes, i el de dins, .
- c) . El de dins fa voltes: .
- d) .
kes divideix per 3 a cada volta: voltes, que és . - e) . Per a cada , el de dins fa unes voltes.
ExerciciQuin és el cost?
Digues el cost de cada funció. v té elements.
// a)
int f(vector<int> v) { return v[0]; }
// b)
bool g(const list<int>& l, int x) {
for (int y : l) if (y == x) return true;
return false;
}
// c)
void h(vector<int>& v) {
while (not v.empty()) v.erase(v.begin());
}
// d)
int k(const map<string, int>& m, const vector<string>& claus) {
int s = 0;
for (const string& c : claus) s += m.at(c);
return s;
}Mostra la solució
- a) , encara que només llegeixi un element: el vector es passa per valor, i copiar-lo costa . Amb
const vector<int>&, seria . - b) en el cas pitjor (quan
xno hi és). - c) . Cada
erase(v.begin())desplaça tots els elements que queden: . (Esborrant des del final ambpop_backseria , iv.clear()també.) - d) Si
clausté elements i elmapen té : . (Suposant que comparar dues claus és constant; ambstringllargues, també depèn de la seva llargada.)
ExerciciQuin triaries?
Tens tres algorismes per al mateix problema, amb costos , i , però el lineal té una constant molt gran: fa operacions, mentre que els altres fan i . Quin és millor per a ? I per a ?
Mostra la solució
- Amb , el millor és el , i el lineal és el pitjor. Per a entrades petites, les constants poden importar més que l’ordre.
- Amb , el quadràtic queda fora de joc (unes 3 hores contra pocs segons). Entre els altres dos, encara guanya el , perquè . Però si continua creixent, a partir de (és a dir, , una mida absurda) guanyaria el lineal.
La notació asimptòtica diu qui guanya quan és prou gran. Per decidir en un cas concret, de vegades cal mirar també les constants.
ExerciciMés ràpid que els dos bucles
Tens tots_diferents amb dos bucles, de cost . Escriu-ne una versió de que ordeni una còpia del vector, i digues quant costa cada pas.
Mostra la solució
Si el vector està ordenat, els elements iguals queden junts: n’hi ha prou de mirar si dos de consecutius són iguals.
// Pre: cert. Post: retorna si tots els elements de v són diferents.
bool tots_diferents(const vector<int>& v) {
vector<int> w = v; // còpia: Θ(n)
sort(w.begin(), w.end()); // ordenar: Θ(n log n)
bool dif = true;
int i = 1;
// Inv: 1 <= i, dif = (w[0..i-1] no té dos consecutius iguals)
while (i < int(w.size()) and dif) { // com a molt n - 1 voltes de Θ(1): Θ(n)
if (w[i - 1] == w[i]) dif = false;
++i;
}
return dif;
}Total: . Domina el pas més car. Fixa’t que la còpia és necessària: v és const, i ordenar-lo canviaria l’entrada.