Salta al contingut
Llegat fiber

Consolidació

Repàs de tot el tema en una sola pàgina: com es calcula el cost d'un algorisme iteratiu o recursiu, els patrons de les recurrències i les tècniques per fer un algorisme més eficient. I exercicis en format d'examen.

Conceptes clau

  • Cost d'algorismes
  • Cost de programes recursius
  • Millores d'eficiència
  • Errors habituals

El tema en una pàgina

Capítol La idea clau
Cost d’algorismes Comptar operacions en el cas pitjor i quedar-se amb el terme dominant: Θ\Theta. Seqüència → el màxim; bucle → voltes × cost; compte amb les operacions amagades (còpies, insert en vectors).
Cost de programes recursius Una recurrència T(n)T(n). Substracció (n−cn - c) o divisió (n/bn / b), i la taula. En arbres, sovint és més fàcil comptar la feina per node.
Millores d’eficiència No recalcular: retornar més resultats (immersió per resultats), passar dades ja calculades (immersió per dades), o actualitzar en lloc de recalcular dins dels bucles.
543210121032101
L'arbre de crides de fib(5): les crides en corall es repeteixen. Dues crides amb n − 1 i n − 2 fan un arbre exponencial; guardar resultats o fer una immersió el torna lineal.

Les recurrències de PRO2, en una taula

Recurrència Cost On surt
T(n)=T(n−1)+Θ(1)T(n) = T(n-1) + \Theta(1) Θ(n)\Theta(n) recursió lineal sobre un vector o un número
T(n)=T(n−1)+Θ(n)T(n) = T(n-1) + \Theta(n) Θ(n2)\Theta(n^2) recursió que a cada pas recorre el que queda
T(n)=2T(n−1)+Θ(1)T(n) = 2T(n-1) + \Theta(1) Θ(2n)\Theta(2^n) dues crides amb n−1n - 1 (repetir una crida)
T(n)=T(n/2)+Θ(1)T(n) = T(n/2) + \Theta(1) Θ(log⁡n)\Theta(\log n) dicotòmica, potència ràpida
T(n)=T(n/2)+Θ(n)T(n) = T(n/2) + \Theta(n) Θ(n)\Theta(n) «dicotòmica» sobre una llista
T(n)=2T(n/2)+Θ(1)T(n) = 2T(n/2) + \Theta(1) Θ(n)\Theta(n) recorregut d’un arbre equilibrat, potència amb la crida repetida
T(n)=2T(n/2)+Θ(n)T(n) = 2T(n/2) + \Theta(n) Θ(nlog⁡n)\Theta(n \log n) mergesort

Errors habituals de tot el tema

  • Passar contenidors per valor sense voler: cada crida copia, Θ(n)\Theta(n).
  • Operacions amagades dins dels bucles: v.insert, v.erase(v.begin()), còpies, crides a funcions que recorren alguna cosa.
  • Repetir crides recursives amb els mateixos paràmetres.
  • Cridar dins d’una recursió sobre arbres una funció que recorre el subarbre (mida, alcada…): Θ(n2)\Theta(n^2) en el cas pitjor (un arbre en línia), Θ(nlog⁡n)\Theta(n \log n) si és equilibrat.
  • Confondre cas pitjor i cas concret, o donar una fita OO que no és ajustada quan es demana el cost.
  • Millorar abans de tenir una solució correcta.

Exercicis d’examen

ExerciciQuin cost té?

Digues el cost, en el cas pitjor, de cada fragment. l és una list<int>, v un vector<int> i m un map<int, int>, tots amb nn elements.

// a)
for (int x : l) m[x]++;

// b)
for (int i = 0; i < n; ++i) v.insert(v.begin(), i);

// c)
list<int>::iterator it = l.begin();
while (it != l.end()) {
    if (*it % 2 == 0) it = l.erase(it);
    else ++it;
}

// d)
int k = 0;
for (int i = 0; i < n; ++i)
    if (m.count(i) == 1) k += m[i];
Mostra la solució
  • a) Θ(nlog⁡n)\Theta(n \log n): nn accessos a un map, cadascun Θ(log⁡n)\Theta(\log n) (la mida del map creix fins a nn).
  • b) Θ(n2)\Theta(n^2): inserir al principi d’un vector desplaça tots els elements que hi ha. Com que v ja en té nn, cada inserció en desplaça entre nn i 2n−12n - 1: en total, n+(n+1)+⋯+(2n−1)=Θ(n2)n + (n+1) + \dots + (2n-1) = \Theta(n^2).
  • c) Θ(n)\Theta(n): erase en una llista és Θ(1)\Theta(1).
  • d) Θ(nlog⁡n)\Theta(n \log n): nn voltes amb un count i potser un [], tots dos Θ(log⁡n)\Theta(\log n).

ExerciciRecurrències

Escriu la recurrència i el cost de cada funció recursiva:

// a) (n = d - e)
int f(const vector<int>& v, int e, int d) {
    if (d - e <= 1) return d - e;
    int m = (e + d) / 2;
    return f(v, e, m) + f(v, m, d);
}

// b)
void g(int n) {
    if (n > 0) {
        for (int i = 0; i < n; ++i) cout << '*';
        g(n / 2);
    }
}

// c)
int h(int n) {
    if (n <= 1) return 1;
    return h(n - 1) + h(n - 2) + h(n - 3);
}
Mostra la solució
  • a) T(n)=2T(n/2)+Θ(1)T(n) = 2T(n/2) + \Theta(1): divisió, a=2a = 2, b=2b = 2, k=0<1k = 0 < 1: Θ(n)\Theta(n).
  • b) T(n)=T(n/2)+Θ(n)T(n) = T(n/2) + \Theta(n): divisió, a=1a = 1, b=2b = 2, k=1>0k = 1 > 0: Θ(n)\Theta(n). (Escriu n+n/2+n/4+⋯<2nn + n/2 + n/4 + \dots < 2n asteriscs.)
  • c) T(n)=T(n−1)+T(n−2)+T(n−3)+Θ(1)T(n) = T(n-1) + T(n-2) + T(n-3) + \Theta(1). Com a mínim és 3T(n−3)3T(n-3), que ja és exponencial: Θ(cn)\Theta(c^n) per a una certa c>1c > 1 (de fet, c≈1,84c \approx 1{,}84).

ExerciciParelles que sumen k

Tens un vector ordenat d’enters diferents i un enter kk. Vols comptar quantes parelles de posicions i<ji < j tenen v[i]+v[j]=kv[i] + v[j] = k. Amb [1,2,4,5,7,8,11][1, 2, 4, 5, 7, 8, 11] i k=9k = 9, n’hi ha 3 (1+81 + 8, 2+72 + 7, 4+54 + 5).

  1. Escriu la solució directa i digues-ne el cost.
  2. Fes-ne una de Θ(n)\Theta(n) aprofitant que el vector està ordenat. Pista: dos índexs, un a cada extrem.
Mostra la solució

1. Totes les parelles:

int parelles_lent(const vector<int>& v, int k) {
    int compte = 0;
    for (int i = 0; i < int(v.size()); ++i)
        for (int j = i + 1; j < int(v.size()); ++j)
            if (v[i] + v[j] == k) ++compte;
    return compte;
}

Θ(n2)\Theta(n^2).

2. Un índex i al principi i un j al final. Mira s=v[i]+v[j]s = v[i] + v[j]:

  • si s<ks < k, v[i]v[i] és massa petit fins i tot amb el més gran que queda (v[j]v[j]): cap parella amb v[i]v[i] funciona. Avança i.
  • si s>ks > k, v[j]v[j] és massa gran fins i tot amb el més petit que queda: retrocedeix j.
  • si s=ks = k, n’hem trobat una. Com que els elements són diferents, ni v[i]v[i] ni v[j]v[j] poden formar cap altra parella: movem tots dos.
// Pre: v ordenat estrictament creixent.
// Post: retorna quantes parelles i < j tenen v[i] + v[j] = k.
int parelles(const vector<int>& v, int k) {
    int compte = 0;
    int i = 0, j = int(v.size()) - 1;
    // Inv: compte = parelles que fan k amb algun element fora de v[i..j];
    //      les parelles que queden són dins de v[i..j]
    while (i < j) {
        int s = v[i] + v[j];
        if (s == k) {
            ++compte;
            ++i;
            --j;
        }
        else if (s < k) ++i;
        else --j;
    }
    return compte;
}

Cada volta acosta i i j (fita: j−ij - i): com a molt nn voltes, Θ(n)\Theta(n).

ExerciciFill esquerre més alt

Escriu una funció que compti quants nodes d’un arbre binari tenen el fill esquerre més alt que el dret. Primer fes-la directament amb alcada i digues-ne el cost; després millora-la.

Mostra la solució

Directa: a cada node, alcada(a.left()) > alcada(a.right()). Cada node és recorregut per alcada una vegada per cada avantpassat: Θ(n2)\Theta(n^2) en el cas pitjor (un arbre en línia).

Millorada, amb una immersió per resultats que retorna també l’alçada:

// Pre: cert. Post: retorna quants nodes de a tenen el fill esquerre més alt que el dret;
//      h és l'alçada de a.
int mes_alts_esquerra(const BinTree<int>& a, int& h) {
    if (a.empty()) {
        h = 0;
        return 0;
    }
    int he, hd;
    int n = mes_alts_esquerra(a.left(), he) + mes_alts_esquerra(a.right(), hd);
    h = 1 + max(he, hd);
    if (he > hd) ++n;
    return n;
}

Θ(n)\Theta(n). És el mateix patró que l’arbre equilibrat: sempre que una funció sobre arbres necessiti una propietat dels subarbres que es calcula recursivament, fes que la recursió la retorni.

ExerciciRècords en un arbre

Escriu una funció que compti quants nodes d’un arbre binari d’enters tenen un valor més gran que el de tots els seus avantpassats (l’arrel sempre hi compta). Primer fes-la directament i digues-ne el cost; després fes-la en Θ(n)\Theta(n).

Mostra la solució

Directa: es porta el camí dels avantpassats en un vector, i a cada node es recorre sencer per veure si algun és més gran o igual.

// Pre: cami conté els valors dels avantpassats de a (de l'arrel al pare).
// Post: retorna quants nodes de a tenen un valor més gran que tots els seus avantpassats.
int records_arbre_lent(const BinTree<int>& a, vector<int>& cami) {
    if (a.empty()) return 0;
    bool record = true;
    for (int x : cami) if (x >= a.value()) record = false;   // Θ(profunditat)
    cami.push_back(a.value());
    int compte = records_arbre_lent(a.left(), cami) + records_arbre_lent(a.right(), cami);
    cami.pop_back();
    if (record) ++compte;
    return compte;
}

A cada node es fa una feina proporcional a la seva profunditat: Θ(n⋅h)\Theta(n \cdot h), on hh és l’alçada. En una línia, Θ(n2)\Theta(n^2).

Millorada, amb una immersió per dades: en lloc de recalcular el màxim del camí, es passa cap avall.

// Pre: m és el màxim dels valors del camí des de l'arrel original fins al pare de a
//      (o INT_MIN, si a és l'arrel original). Cap valor de l'arbre és INT_MIN.
// Post: retorna quants nodes de a tenen un valor més gran que tots els seus avantpassats.
int records_arbre(const BinTree<int>& a, int m) {
    if (a.empty()) return 0;
    int nou = max(m, a.value());
    int compte = records_arbre(a.left(), nou) + records_arbre(a.right(), nou);
    if (a.value() > m) ++compte;
    return compte;
}

Crida inicial: records_arbre(a, INT_MIN) (cal <climits>). Cada node fa una feina constant: Θ(n)\Theta(n). És el mateix patró que els rècords d’un vector: portar el màxim en lloc de recalcular-lo.

ExerciciTria l'estructura pel cost

Un programa rep nn operacions, cadascuna de les quals és «afegeix el número xx» o «quants números més petits que xx s’han afegit?». Compara el cost total amb aquestes representacions:

  1. Un vector sense ordenar.
  2. Un vector ordenat (inserint a la posició que toca).
  3. Un map<int, int> (número → quantes vegades).
Mostra la solució

Amb nn operacions, l’estructura arriba a tenir Θ(n)\Theta(n) elements.

  1. Vector sense ordenar: afegir és Θ(1)\Theta(1) (push_back); comptar els més petits és un recorregut, Θ(n)\Theta(n). En el cas pitjor (meitat i meitat), Θ(n2)\Theta(n^2) en total.
  2. Vector ordenat: comptar és una cerca dicotòmica (Θ(log⁡n)\Theta(\log n): la posició del primer ≥x\ge x és el nombre d’elements més petits). Però afegir és Θ(n)\Theta(n) (cal desplaçar). Total Θ(n2)\Theta(n^2).
  3. map: afegir és Θ(log⁡n)\Theta(\log n), però per comptar els més petits cal recórrer el map fins a xx: Θ(n)\Theta(n) en el cas pitjor. Total Θ(n2)\Theta(n^2).

Cap de les tres no baixa de Θ(n2)\Theta(n^2) en el cas pitjor. Cal una estructura més sofisticada (un arbre de cerca on cada node sap la mida del seu subarbre, que veuràs a EDA) per fer totes dues operacions en Θ(log⁡n)\Theta(\log n). De vegades la resposta a «quina estructura?» és «cap de les que tinc»: l’anàlisi de cost t’ho diu abans de programar res.