Salta al contingut
Llegat fiber

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ó OO, Ω\Omega i Θ\Theta
  • Ordres de creixement habituals
  • Cost de seqüències, condicionals i bucles
  • Cost de les operacions de la STL
Prova-hoCom creix el cost
CostOperacionsTemps (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:

  1. Decideix què és la mida de l’entrada, nn: 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…
  2. Compta quantes operacions elementals fa en funció de nn. Una operació elemental és qualsevol cosa que costa sempre el mateix: una assignació d’un int, una suma, una comparació, accedir a v[i], un push d’una pila…
  3. Mira el cas pitjor: l’entrada de mida nn que fa treballar més l’algorisme. (Una cerca on l’element no hi és, per exemple.)
  4. 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 nn és gran, el terme dominant ho decideix tot. Si un algorisme fa 3n2+100n+50003n^2 + 100n + 5000 operacions, amb n=106n = 10^6 el 3n23n^2 són 3⋅10123 \cdot 10^{12}, i la resta, uns 10810^8: menys d’una deumil·lèsima part. Diem que el cost és Θ(n2)\Theta(n^2).

Prova-ho a l’eina «Com creix el cost» de dalt de tot: compara les funcions per a nn cada vegada més grans.

La notació asimptòtica

Per dir «creix com n2n^2» amb precisió hi ha tres notacions. Si f(n)f(n) és el nombre d’operacions i g(n)g(n) una funció de referència:

Per exemple, 3n2+100n+5000∈Θ(n2)3n^2 + 100n + 5000 \in \Theta(n^2), i també és O(n3)O(n^3) (és cert, però no és la millor fita) i Ω(n)\Omega(n). Per veure que és O(n2)O(n^2), n’hi ha prou de trobar unes constants: amb c=4c = 4 i n0=200n_0 = 200, 3n2+100n+5000≤4n23n^2 + 100n + 5000 \le 4n^2, perquè per a n≥200n \ge 200 ja tens n2≥100n+5000n^2 \ge 100n + 5000. I és Ω(n2)\Omega(n^2) amb c=3c = 3 i n0=1n_0 = 1.

A PRO2 fem servir sobretot Θ\Theta: diu el cost exacte, llevat de constants.

Unes quantes regles que es fan servir sempre:

  • Les constants no compten: Θ(5n)=Θ(n)\Theta(5n) = \Theta(n).
  • En una suma, guanya el terme més gran: Θ(n2+nlog⁡n)=Θ(n2)\Theta(n^2 + n \log n) = \Theta(n^2).
  • La base dels logaritmes no compta (canviar de base és multiplicar per una constant): Θ(log⁡2n)=Θ(log⁡10n)\Theta(\log_2 n) = \Theta(\log_{10} n), i s’escriu Θ(log⁡n)\Theta(\log n).

Els ordres de creixement habituals

Cost Nom Exemple Temps amb n=106n = 10^6
Θ(1)\Theta(1) constant top() d’una pila, v[i] instantani
Θ(log⁡n)\Theta(\log n) logarítmic cerca dicotòmica, operacions d’un map instantani (unes 20 operacions)
Θ(n)\Theta(n) lineal recórrer un vector, cerca seqüencial 0,01 s
Θ(nlog⁡n)\Theta(n \log n) quasi lineal ordenar (sort, ordenació per fusió) 0,2 s
Θ(n2)\Theta(n^2) quadràtic comparar totes les parelles unes 3 hores
Θ(2n)\Theta(2^n) exponencial provar tots els subconjunts mai (ja amb n=60n = 60, segles)

(Els temps suposen uns 10810^8 passos per segon, que és un ordre de magnitud raonable.)

operacionsn148121664log nnn log nn²2ⁿ
Operacions que fa cada ordre de creixement quan n va d'1 a 16 (el gràfic es talla a 64 operacions). Fixa't en com 2ⁿ i n² surten de l'escala abans que les altres.

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 Θ(n2)\Theta(n^2) a un de Θ(nlog⁡n)\Theta(n \log n) 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 C1C_1 i després una altra que costa C2C_2, el total és C1+C2C_1 + C_2. En notació Θ\Theta, això és el més gran dels dos: Θ(n)+Θ(n2)=Θ(n2)\Theta(n) + \Theta(n^2) = \Theta(n^2).

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) Θ(1)\Theta(1)
insert(it, x), erase(it) en una list Θ(1)\Theta(1)
insert, erase en un vector (no al final) Θ(n)\Theta(n)
push_back, pop_back, v[i], size en un vector Θ(1)\Theta(1) (push_back, de mitjana)
m[k], find, count, insert, erase en un map o set Θ(log⁡n)\Theta(\log n)
BinTree: left, right, value, construir, copiar Θ(1)\Theta(1)
Copiar un vector, una list, un map… (o passar-lo per valor) Θ(n)\Theta(n)
sort(v.begin(), v.end()) Θ(nlog⁡n)\Theta(n \log n)

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 n−1−in - 1 - i voltes per a cada ii: en total (n−1)+(n−2)+⋯+0=n(n−1)2(n-1) + (n-2) + \dots + 0 = \frac{n(n-1)}{2}. Cost Θ(n2)\Theta(n^2).

Amb un set, cada element es busca entre els que ja s’han vist en Θ(log⁡n)\Theta(\log n):

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;
}

nn voltes de Θ(log⁡n)\Theta(\log n): Θ(nlog⁡n)\Theta(n \log n). (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 Θ\Theta pertany cada funció:

  1. 5n3+2n2+75n^3 + 2n^2 + 7
  2. n+log⁡nn + \log n
  3. 3nlog⁡n+100n3n \log n + 100n
  4. 2n+n102^n + n^{10}
  5. n(n+1)2\frac{n(n+1)}{2}
  6. log⁡(n2)\log(n^2)
Mostra la solució
  1. Θ(n3)\Theta(n^3).
  2. Θ(n)\Theta(n): el logaritme creix més a poc a poc que nn.
  3. Θ(nlog⁡n)\Theta(n \log n): nlog⁡nn \log n creix més que nn.
  4. Θ(2n)\Theta(2^n): qualsevol exponencial acaba superant qualsevol polinomi (encara que amb n10n^{10} triga a passar).
  5. Θ(n2)\Theta(n^2): és 12n2+12n\frac{1}{2}n^2 + \frac{1}{2}n.
  6. Θ(log⁡n)\Theta(\log n): log⁡(n2)=2log⁡n\log(n^2) = 2 \log n.

ExerciciQuantes voltes?

Digues el cost de cada fragment en funció de nn (suposa que ... és Θ(1)\Theta(1)):

// 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) Θ(n)\Theta(n). El de dins fa sempre 10 voltes: és una constant.
  • b) Θ(nlog⁡n)\Theta(n \log n). El de fora fa unes log⁡2n\log_2 n voltes, i el de dins, nn.
  • c) Θ(n2)\Theta(n^2). El de dins fa n−in - i voltes: n+(n−1)+⋯+1=n(n+1)2n + (n-1) + \dots + 1 = \frac{n(n+1)}{2}.
  • d) Θ(log⁡n)\Theta(\log n). k es divideix per 3 a cada volta: log⁡3n\log_3 n voltes, que és Θ(log⁡n)\Theta(\log n).
  • e) Θ(nlog⁡n)\Theta(n \log n). Per a cada ii, el de dins fa unes log⁡2n\log_2 n voltes.

ExerciciQuin és el cost?

Digues el cost de cada funció. v té nn 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) Θ(n)\Theta(n), encara que només llegeixi un element: el vector es passa per valor, i copiar-lo costa Θ(n)\Theta(n). Amb const vector<int>&, seria Θ(1)\Theta(1).
  • b) Θ(n)\Theta(n) en el cas pitjor (quan x no hi és).
  • c) Θ(n2)\Theta(n^2). Cada erase(v.begin()) desplaça tots els elements que queden: (n−1)+(n−2)+⋯=Θ(n2)(n-1) + (n-2) + \dots = \Theta(n^2). (Esborrant des del final amb pop_back seria Θ(n)\Theta(n), i v.clear() també.)
  • d) Si claus té kk elements i el map en té mm: Θ(klog⁡m)\Theta(k \log m). (Suposant que comparar dues claus és constant; amb string llargues, també depèn de la seva llargada.)

ExerciciQuin triaries?

Tens tres algorismes per al mateix problema, amb costos Θ(n2)\Theta(n^2), Θ(nlog⁡n)\Theta(n \log n) i Θ(n)\Theta(n), però el lineal té una constant molt gran: fa 1000n1000n operacions, mentre que els altres fan n2n^2 i 10nlog⁡2n10 n \log_2 n. Quin és millor per a n=100n = 100? I per a n=106n = 10^6?

Mostra la solució
n2n^2 10 nlog⁡2n10, n \log_2 n 1000 n1000, n
n=100n = 100 10410^4 ≈6.600\approx 6.600 10510^5
n=106n = 10^6 101210^{12} ≈2⋅108\approx 2 \cdot 10^8 10910^9
  • Amb n=100n = 100, el millor és el Θ(nlog⁡n)\Theta(n \log n), i el lineal és el pitjor. Per a entrades petites, les constants poden importar més que l’ordre.
  • Amb n=106n = 10^6, el quadràtic queda fora de joc (unes 3 hores contra pocs segons). Entre els altres dos, encara guanya el Θ(nlog⁡n)\Theta(n \log n), perquè 10log⁡2n≈200<100010 \log_2 n \approx 200 < 1000. Però si nn continua creixent, a partir de log⁡2n>100\log_2 n > 100 (és a dir, n>2100n > 2^{100}, una mida absurda) guanyaria el lineal.

La notació asimptòtica diu qui guanya quan nn é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 Θ(n2)\Theta(n^2). Escriu-ne una versió de Θ(nlog⁡n)\Theta(n \log n) 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: Θ(n)+Θ(nlog⁡n)+Θ(n)=Θ(nlog⁡n)\Theta(n) + \Theta(n \log n) + \Theta(n) = \Theta(n \log n). Domina el pas més car. Fixa’t que la còpia és necessària: v és const, i ordenar-lo canviaria l’entrada.