Millores d'eficiència
Quan una funció recalcula el mateix moltes vegades, li pots fer retornar més dades (immersió per resultats) o passar-li dades ja calculades (immersió per dades). Als bucles, la idea és la mateixa: no tornar a calcular el que ja saps.
Conceptes clau
- Detectar càlculs repetits
- Immersió d'eficiència per resultats
- Immersió d'eficiència per dades
- Exemple: arbre equilibrat en
- Millores en programes iteratius: sumes acumulades i rècords
Per què?
Hi ha dues maneres d’arribar a un algorisme eficient: tenir una idea brillant des del principi, o bé escriure primer una solució correcta i senzilla i després millorar-la traient-ne els càlculs innecessaris. La segona és la més fiable, i és la que veuràs aquí.
Les eines són les mateixes immersions del disseny recursiu, però amb un objectiu diferent: no per poder fer la recursió, sinó per fer-la més ràpida.
Com es detecta un càlcul repetit
Mira la teva solució i pregunta’t:
- Es crida dues vegades la mateixa funció amb els mateixos paràmetres? (
fib(n-1)ifib(n-2), que a dins torna a cridarfib(n-2)…) - Dins d’una funció recursiva, es crida una altra funció que recorre el mateix que la recursió? (
alcadadins d’equilibrat.) - Dins d’un bucle, hi ha un altre bucle que recalcula una cosa que es podria anar actualitzant? (Sumar tots els elements de la dreta per a cada posició.)
Si la resposta és sí, hi ha marge per millorar.
Immersió per resultats
La idea: que la funció recursiva retorni més coses de les que demana el problema, perquè qui la crida les aprofiti i no les hagi de calcular.
Arbre equilibrat
Un arbre és equilibrat si, a cada node, les alçades dels dos fills difereixen com a molt en 1. La solució directa (la del capítol anterior) crida alcada a cada node i costa en el cas pitjor.
Però per saber si un subarbre és equilibrat, la recursió ja recorre tot el subarbre. Podria calcular-ne l’alçada al mateix temps i retornar-la:
// Pre: cert. Post: retorna si a és equilibrat; h és l'alçada de a.
bool equilibrat(const BinTree<int>& a, int& h) {
if (a.empty()) {
h = 0;
return true;
}
int he, hd;
bool ee = equilibrat(a.left(), he);
bool ed = equilibrat(a.right(), hd);
h = 1 + max(he, hd);
return ee and ed and he - hd >= -1 and he - hd <= 1;
}
Ara cada node fa una feina constant a més de les seves dues crides: .
La postcondició de la immersió és més forta que la de la funció original: diu més coses (també l’alçada). La funció original s’obté ignorant el resultat extra:
bool equilibrat(const BinTree<int>& a) {
int h;
return equilibrat(a, h);
}
Fibonacci
La funció de Fibonacci directa fa dues crides, fib(n-1) i fib(n-2), i és exponencial. Però si fib(n-1) retornés també fib(n-2), n’hi hauria prou amb una crida:
// Pre: n >= 1. Post: f = fib(n), g = fib(n-1).
void fib2(int n, long long& f, long long& g) {
if (n == 1) {
f = 1;
g = 0;
}
else {
long long f1, g1;
fib2(n - 1, f1, g1); // HI: f1 = fib(n-1), g1 = fib(n-2)
f = f1 + g1;
g = f1;
}
}
Una crida per cada valor de : , és a dir, . De a .
La funció escala del tema anterior és la mateixa: escala(n) = fib(n+1). Per tant, fib2(n + 1, f, g) et dona la versió .
Immersió per dades
L’altra direcció: passar a la crida recursiva informació que ja s’ha calculat, perquè no l’hagi de tornar a calcular. És el reforçament de la precondició del disseny recursiu.
Amb Fibonacci, en lloc de baixar fins a 1 i calcular en tornar, es pot anar pujant des del principi, portant els dos últims valors:
// Pre: 1 <= i <= n, a = fib(i-1), b = fib(i). Post: retorna fib(n).
long long fib_dades(int n, int i, long long a, long long b) {
if (i == n) return b;
return fib_dades(n, i + 1, b, a + b);
}
// fib(n) = fib_dades(n, 1, 0, 1), per a n >= 1
També és , i és recursiva final: es pot passar a un bucle de manera mecànica (amb i com a variables).
Una immersió per dades també serveix quan els nodes d’un arbre necessiten informació dels seus avantpassats: la suma del camí des de l’arrel, la profunditat, el màxim que hi ha per sobre… En lloc de recalcular-la per a cada node, es passa cap avall com a paràmetre.
Millores en programes iteratius
Als bucles la idea és la mateixa: no recalculis el que pots anar actualitzant.
Sumes acumulades
Vols comptar quantes posicions d’un vector tenen un valor igual a la suma de tots els elements de la seva dreta. Directament:
for (int i = 0; i < n; ++i) {
int s = 0;
for (int j = i + 1; j < n; ++j) s += v[j]; // suma de la dreta, de nou cada vegada
if (v[i] == s) ++compte;
}
. Però la suma de la dreta de és la de més : es pot anar actualitzant si es recorre el vector des del final.
// Pre: cert. Post: retorna quantes posicions i compleixen v[i] = suma de v[i+1..].
int iguals_suma_dreta(const vector<int>& v) {
int compte = 0;
int s = 0; // suma de v[i+1..]
int i = int(v.size()) - 1;
// Inv: -1 <= i < v.size(), s = suma de v[i+1..], compte = posicions de v[i+1..] que compleixen
while (i >= 0) {
if (v[i] == s) ++compte;
s += v[i];
--i;
}
return compte;
}
. Fixa’t que la millora apareix a l’invariant: hi hem afegit « = suma de », una dada que el bucle manté i que estalvia el bucle de dins.
Rècords
Vols comptar quantes posicions d’un vector són un rècord: tenen un valor més gran que tots els de la seva esquerra. Directament, per a cada calcularies el màxim de amb un bucle: . Però el màxim de l’esquerra de és el de comparat amb : es pot anar actualitzant.
// Pre: cert. Post: retorna quantes posicions i tenen v[i] més gran que tots els de v[0..i-1].
int records(const vector<int>& v) {
int n = int(v.size());
if (n == 0) return 0;
int compte = 1; // v[0] sempre és un rècord
int maxim = v[0];
int i = 1;
// Inv: 1 <= i <= n, maxim = màxim de v[0..i-1], compte = rècords de v[0..i-1]
while (i < n) {
if (v[i] > maxim) {
++compte;
maxim = v[i];
}
++i;
}
return compte;
}
. Amb en dona 4 (el 3, el 4, el 5 i el 9).
Errors habituals
Exercicis
ExerciciNodes iguals a la mida
Escriu una funció que compti quants nodes d’un arbre binari tenen un valor igual a la mida del seu subarbre (el nombre de nodes que té a sota, ell inclòs). Ha de costar .
Mostra la solució
Comprovar-ho a cada node cridant mida seria en el cas pitjor. Fem una immersió per resultats que retorni també la mida:
// Pre: cert. Post: retorna quants nodes de a tenen un valor igual a la mida del seu subarbre;
// m és la mida de a.
int iguals_mida(const BinTree<int>& a, int& m) {
if (a.empty()) {
m = 0;
return 0;
}
int me, md;
int n = iguals_mida(a.left(), me) + iguals_mida(a.right(), md);
m = 1 + me + md;
if (a.value() == m) ++n;
return n;
}Feina constant per node: .
ExerciciEl diàmetre
El diàmetre d’un arbre binari és el nombre de nodes del camí més llarg entre dos nodes qualssevol (no cal que passi per l’arrel). Escriu una funció que el calculi en .
Mostra la solució
El camí més llarg d’un arbre o bé passa per l’arrel (i llavors té nodes, on i són les alçades dels fills), o bé és tot dins d’un dels fills (i és el diàmetre d’aquell fill). Necessitem, per a cada subarbre, el diàmetre i l’alçada:
// Pre: cert. Post: retorna el nombre de nodes del camí més llarg entre dos nodes de a;
// h és l'alçada de a.
int diametre(const BinTree<int>& a, int& h) {
if (a.empty()) {
h = 0;
return 0;
}
int he, hd;
int de = diametre(a.left(), he);
int dd = diametre(a.right(), hd);
h = 1 + max(he, hd);
return max(he + hd + 1, max(de, dd));
}Sense la immersió (calculant les alçades amb alcada a cada node) seria . Amb ella, .
ExerciciIgual a la suma dels avantpassats
Escriu una funció que compti quants nodes d’un arbre binari tenen un valor igual a la suma de tots els seus avantpassats (els nodes del camí des de l’arrel fins al pare; l’arrel no en té cap, i la suma és 0). Ha de costar .
Mostra la solució
La informació ve de dalt: immersió per dades amb la suma del camí fins al pare.
// Pre: s és la suma dels valors del camí des de l'arrel original fins al pare de a.
// Post: retorna quants nodes de a tenen un valor igual a la suma dels seus avantpassats.
int iguals_avantpassats(const BinTree<int>& a, int s) {
if (a.empty()) return 0;
int n = iguals_avantpassats(a.left(), s + a.value()) + iguals_avantpassats(a.right(), s + a.value());
if (a.value() == s) ++n;
return n;
}Crida inicial: iguals_avantpassats(a, 0). Sense el paràmetre, cada node hauria de recalcular la suma del seu camí (que no pot, perquè un BinTree no sap qui és el seu pare), o caldria passar-hi el camí sencer.
ExerciciEl punt d'equilibri
Escriu una funció que retorni la primera posició d’un vector tal que la suma de és igual a la suma de , o si no n’hi ha cap. Amb és la 2. Ha de costar .
Mostra la solució
Si calculem el total primer, la suma de la dreta és el total menys la de l’esquerra, que es va acumulant. Dos recorreguts lineals.
// Pre: cert. Post: retorna la primera posició i tal que suma de v[0..i] = suma de v[i+1..], o -1.
int punt_equilibri(const vector<int>& v) {
int total = 0;
for (int x : v) total += x;
int esq = 0;
int i = 0;
int r = -1;
// Inv: 0 <= i <= v.size(), esq = suma de v[0..i-1], cap j < i és un punt d'equilibri;
// si r != -1, r = i i i és un punt d'equilibri
while (i < int(v.size()) and r == -1) {
if (esq + v[i] == total - esq - v[i]) r = i;
else {
esq += v[i];
++i;
}
}
return r;
}Sense el total, per a cada caldria sumar els dos costats: .
ExerciciFibonacci iteratiu
Passa fib_dades a un bucle. Quin és l’invariant?
Mostra la solució
Aplicant la recepta de recursiu a iteratiu, amb la crida inicial fib_dades(n, 1, 0, 1):
// Pre: n >= 0. Post: retorna fib(n).
long long fib_iter(int n) {
if (n == 0) return 0;
long long a = 0, b = 1;
int i = 1;
// Inv: 1 <= i <= n, a = fib(i-1), b = fib(i)
while (i != n) {
long long c = a + b;
a = b;
b = c;
++i;
}
return b;
}L’invariant és la Pre de la immersió. Compte amb l’ordre: la crida feia (b, a + b) amb els valors vells, i per això cal la variable c.