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: . 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 . Substracció () o divisió (), 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. |
Les recurrències de PRO2, en una taula
| Recurrència | Cost | On surt |
|---|---|---|
| recursió lineal sobre un vector o un número | ||
| recursió que a cada pas recorre el que queda | ||
| dues crides amb (repetir una crida) | ||
| dicotòmica, potència ràpida | ||
| «dicotòmica» sobre una llista | ||
| recorregut d’un arbre equilibrat, potència amb la crida repetida | ||
| mergesort |
Errors habituals de tot el tema
- Passar contenidors per valor sense voler: cada crida copia, .
- 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…): en el cas pitjor (un arbre en línia), si és equilibrat. - Confondre cas pitjor i cas concret, o donar una fita 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 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) : accessos a un
map, cadascun (la mida delmapcreix fins a ). - b) : inserir al principi d’un vector desplaça tots els elements que hi ha. Com que
vja en té , cada inserció en desplaça entre i : en total, . - c) :
eraseen una llista és . - d) : voltes amb un
counti potser un[], tots dos .
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) : divisió, , , : .
- b) : divisió, , , : . (Escriu asteriscs.)
- c) . Com a mínim és , que ja és exponencial: per a una certa (de fet, ).
ExerciciParelles que sumen k
Tens un vector ordenat d’enters diferents i un enter . Vols comptar quantes parelles de posicions tenen . Amb i , n’hi ha 3 (, , ).
- Escriu la solució directa i digues-ne el cost.
- Fes-ne una de 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;
}.
2. Un índex i al principi i un j al final. Mira :
- si , és massa petit fins i tot amb el més gran que queda (): cap parella amb funciona. Avança
i. - si , és massa gran fins i tot amb el més petit que queda: retrocedeix
j. - si , n’hem trobat una. Com que els elements són diferents, ni ni 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: ): com a molt voltes, .
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: 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;
}. É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 .
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: , on és l’alçada. En una línia, .
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: . É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 operacions, cadascuna de les quals és «afegeix el número » o «quants números més petits que s’han afegit?». Compara el cost total amb aquestes representacions:
- Un
vectorsense ordenar. - Un
vectorordenat (inserint a la posició que toca). - Un
map<int, int>(número → quantes vegades).
Mostra la solució
Amb operacions, l’estructura arriba a tenir elements.
- Vector sense ordenar: afegir és (
push_back); comptar els més petits és un recorregut, . En el cas pitjor (meitat i meitat), en total. - Vector ordenat: comptar és una cerca dicotòmica (: la posició del primer és el nombre d’elements més petits). Però afegir és (cal desplaçar). Total .
map: afegir és , però per comptar els més petits cal recórrer elmapfins a : en el cas pitjor. Total .
Cap de les tres no baixa de 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 . 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.