Apunts d’Estructures de Dades i Algorismes: Programació dinàmica

1. Nombres de Fibonacci

L'algorisme trivial per al càlcul dels nombres de Fibonacci repeteix molts càlculs. Podem evitar-ho desant tots els valors que calculem en una matriu i cercant-los allà quan ens calguin abans de calcular-los un altre cop (memoization). Si només ens cal obtenir un sol nombre de Fibonacci, podem construir una versió iterativa de l'algorisme que eviti els càlculs sense utilitzar espai de memòria addicional.

2. Producte encadenat de matrius

[FIXME: imatge multiplicació de matrius]. Hem vist que per multiplicar dues matrius podem utilitzar l'algorisme escolar o l'algorisme d'Strassen. Amb el primer d'ells, cal calcular \(\theta(pqr)\) productes escalars.

Si volem dur a terme una seqüència de multiplicacions, el nombre de productes escalars a fer canvia significativament segons com posem els parèntesis (el qual no té cap efecte sobre el resultat). Per exemple, amb tres matrius \(A_1 = 10×100\), \(A_2 = 100×5\) i \(A_3 = 5×50\), l'operació \((A_1 \cdot A_2) A_3\) resulta en \(10 \cdot 100 \cdot 50 + 10 \cdot 5 \cdot 50 = 7500\) productes escalars. En canvi, l'operació \(A_1 \cdot (A_2 \cdot A_3)\), amb les mateixes matrius, resultaria en \(100 \cdot 5 \cdot 50 + 10 \cdot 100 \cdot 50 = 75000\) productes escalars.

Heus aquí el problema la multiplicació encadenada de matrius: on «posar» els parèntesis per poder dur a terme les multiplicacions amb el mínim nombre de productes escalars?

Per força bruta, amb \(n\) matrius, les possibles formes de posar els parèntesis són igual al nombre d'arbres binaris amb \(n\) fulles, el qual correspon a \(C_n = \frac{1}{n + 1}{2n \choose n} \approx \frac{4^n}{3^{n/2}}\) (\(\small C\) és el nombre de Catalan). Podem definir la següent funció per trobar el nombre mínim de productes escalars per multiplicar les matrius \(A_i\), …, \(A_j\):

\(m(i, j) = \begin{cases}0 & i = j \\ min\left\{m(i, k) + m(k+1, j) + p_{i-1} \cdot p_k \cdot p_j\right\} & i < j\end{cases}\)

Aquesta operació duplica càlculs → memorització! En resulta el següent codi C++ que triga temps \(\theta(n^3)\):

vector<vector<int>> mem(n+1, vector<int>(n+1, -1));
vector<vector<int>> tall(n+1, vector<int>(n+1, -1));
 
int m(int i, int j) {
    if (mem[i][j] != -1) return mem[i][j];
    if (i == j) return 0;
    int s = MAX_INT; // infinit
    for (int k = i; k <= j; ++k) {
        int x = m(i, k) + m(k+1, j) + p[i-1] + p[k] + p[j];
        if (x < s) {
            s = x;
            tall[i][j] = k; // resposta al problema de com multiplicar-les
        }
    }
    return m[i][j] = s;
}

També podem construir-ne una versió iterativa:

for (int i = 1; i <= n; ++i) mem[i][i] = 0;
for (int i = n; i >= 1; --i) {
    for (int j = i+1; j <= n; ++j) {
        int s = MAX_INT; // infinit
        for (int k = i; k <= j; ++k) {
            int x = mem[i][k] + mem[k+1][j] + p[i-1] + p[k] + p[j];
            if (x <= s) s = x;
        }
        mem[i][j] = s;
    }
}

3. Problema de la subseqüència comuna més llarga

Tenim dues cadenes X="abcbdab" i Y="bdcaba" i volem trobar la subseqüència comuna (per exemple, "bca") més llarga (en aquest cas, la més llarga és "bdab"). Definim \(m=|X|\), \(n=|Y|\), \(X'_i\)=\(X_1\)…\(X_i\).

  • Si X i Y acaben igual: l'últim caràcter d'X (o Y) ha de ser l'últim caràcter d'LCS(X, Y).
    LCS(X, Y) = \(LCS(X'_{m-1}, Y'_{n-1}) \cdot X_m\).
  • Si X i Y no acaben igual, pensem què passa amb LCS(X, Y).
    \(LCS(X, Y) = \begin{cases}LCS(X_{n-1}, Y) \\ LCS(X, Y_{n-1})\end{cases}\) Quina de les dues? La més llarga.

Definim la funció \(l(i, j)\) = llargada de l'LCS(\(X'_i\), \(Y'_i\)).

\(l(i, j) = \begin{cases}0 & i = 0 \text{ o } j=0 \\ l(i-1, j-1)+1 & x_i = y_j \\ max\{l(i-1, j), l(i, j-1)\} & sin\acute{o}\end{cases}\)

Apunts d’Estructures de Dades i Algorismes: Les classes de problemes P i NP

Classes de problemes – P i NP

Entrades Problemes Classes de problemes (no excloents)
\(x\) \(\pi\) P Coneixem algorismes amb temps pitjor polinomial (o millor) per a decidir el problema.
NP Coneixem algorismes en temps polinòmic indeterminista per a verificar si una solució és correcta.
Exemples de problemes que estan en NP:
  • HAM – és difícil trobar un cicle Hamiltonià en un graf (backtracking), però donada una sentència de vèrtexs és fàcil comprovar si és una solució.
  • Element mínim.
  • TSP (problema del viatjant de comerç).
  • Colorabilitat.
  • Quadrat llatí.
  • Sudoku.
Teorema

\(P \subseteq NP\)

Queda una pregunta clau: \(P = NP\)?

Reducció

Siguin \(L_1 \le E_1\) i \(L_2 \le E_2\) dos problemes decisionals. Diem que \(L_1\) es redueix a \(L_2\) en temps polinòmic si podem trobar un algorisme polinòmic \(A: L_1 \rightarrow L_2\), és a dir, que converteixi tota solució a \(E_1\) en una solució d'\(E_2\).

En altres paraules, una reducció en temps polinòmic d'\(L_1\) a \(L_2\) és un algorise \(R: E_1 \rightarrow E2 / \forall x \in E_1: x \in L1 \Leftrightarrow R(x) \in L_2\).

Podem reduir, per exemple, el problema del graf hamiltonià (HAM) al problema del viatjant de comerç (TSP).

Teorema: Si \(L_1 \le_p L_2\) i \(L_2 \in P\), llavors \(L_1 \in P\).

Un problema decisional \(L\) és NP-hard si tot problema en NP es pot reduir a ell en temps polinòmic. \(L \in \text{NP-hard} \Leftrightarrow \forall L' \in NP, L' \le_p L\).

Un problema és NP-complet si és NP-hard i es troba en NP.

Teorema:
\(L \in NP-hard, L \in P \Leftrightarrow P = NP\).

Teorema:
\(\left\{\begin{array}{l}L_1 \in \text{NP-C} \\ L_2 \in NP \\ L_1 \hookrightarrow L_2\end{array}\right\} L_2 \in NP-C\)

Teorema de Cook (~1970)
\(\small\text{CIRCUIT-SAT} \normalsize\in \small\text{NP-C}\) (va ser el primer problema NP-complet).

El problema decisional CIRCUIT-SAT pren com a entrada un circuit amb \(n\) entrades booleanes, una sortida i portes AND, OR i NOT i el que pregunta és, «hi ha alguna entrada del circuit que faci que la sortida sigui 1?».

Nota: Una possible solució donada per justificar que un problema decisional té solució s'anomena testimoni.

Solucions pràctiques per a problemes NP

  • Heurístiques.
  • Aproximacions.
  • Paral·lelisme, ordinadors quàntics…

El problema de l'aturada

A part de les classes de problemes P, NP i NP-C n'hi ha moltes d'altres, i una d'elles que cal mencionar és la dels problemes irresolubles: aquells que l'ordinador (o millor dit, una màquina de Turing) no pot solucionar.

Un exemple de problema irresoluble és el de l'aturada: donada la descripció d'un programa i el seu estat inicial, determinar si el programa, si l'executem, arribarà a completar-se o bé funcionarà fins la fi de l'eternitat. Podem veure-ho amb el següent programa d'exemple:

typedef String Programa;
typedef String Entrada;
 
// Funci&oacute; que retorna cert si el programa p, donada l&#39;entrada
// x, arriba a aturar-se
bool acaba(Programa p, Entrada x);
 
void vinga(Programa p) {
    // Entra en un bucle infinit si el programa p, donant-li com a entrada
    // el mateix codi del programa, arriba a aturar-se
    if (acaba(p, p)) while (true);
}

Si analitzem aquest programa i executem vinga(vinga) veiem que pot haver-hi dos casos:

  1. Que acabi – si acaba, vinga entra en el bucle i no acaba → contradicció.
  2. Que no acabi – si no acaba, vinga acaba → contradicció.

Així doncs, no es pot solucionar el problema de l'aturada.

Apunts d’Estructures de Dades i Algorismes: Cerca exhaustiva

Cerca exhaustiva

La cerca exhaustiva és un mètode de disseny d'algorismes que consisteix en mirar-se sistemàtica i intel·ligentment totes les possibles solucions a un problema donat.

Generalment els problemes que es resolen mitjançant aquesta tècnica són problemes d'optimització o combinatoris.

La tècnica consisteix en generar implícitament un arbre representant totes les possibles solucions i explorar-lo en profunditat, deixant de recórrer aquelles branques que corresponguin a «candidats a solució» que no compleixen les restriccions del problema donat. Nosaltres la veurem per backtracking.

Exemple. El problema de les «n» reines

Donat un tauler d'\(n\)x\(n\) caselles i \(n\) reines, cal situar-les de manera que no s'amenacin entre elles (segons les regles dels escacs). Vés aquí algunes opcions:

  1. Podem mirar totes les possibles configuracions i veure quines compleixen les restriccions del problema. Això són \(n^2 \choose n\) combinacions; amb \(n=8\) això són \({64 \choose 8} = 4426165368\) possibilitats.
  2. Podem limitar-nos a posar una única reina en cada línia. Això són \(n^n\) possibilitats; amb \(n=8\), \(8^8 = 16777216\).
  3. Podem posar una única reina en cada fila i columna. Això són \(n!\) possibilitats; amb \(n=8\), \(8! = 40320\).
  4. Podem posar una única reina en cada fila, columna i diagonal.

El problema amb aquestes propostes és que fins que no hem acabat de construir una configuració no es mira si les condicions es compleixen o no. Ja en podríem desestimar moltes mentre s'estan construint: utilitzem la tècnica del backtracking.

Una part important d'aquesta tècnica consisteix en determinar una manera per a representar les possibles configuracions (solucions) del problema. En el cas de les reines, ho farem amb una taula d'\(n\) posicions, tal que \(v[i]\) és la columna on va la reina i que al tauler anirà en \((i, v[i])\).

Definim un vector \(k\)-completable de la següent manera: sigui \((k-1)\)-completable, i \(\forall i\ tal\ que\ 1 \le i \le k-1, v[i] \ne v[k], i + v[i] \ne k + v[k], n – v[i] + i \ne n – v[k] + k\). Les solucions al problema de les \(n\) reines són els vector \(n\)-completables.

Observacions:

  1. \(i \ne j, \forall 1 \le i, j \le k, v[i] \ne v[j], v[i] + i \ne v[j] + j,\) \(n – 1 – v[i] + i \ne n – 1 – v[j] + j\) (les dues darrers condicions cobreixen les diagonals).
  2. El vector ha de ser \((k – 1)\)-completable i, a més, \(\forall 1 \le i \le k – 1, v[i] \ne v[k], v[i] + i \ne v[k] + k, n – 1 – v[i] + i \ne n -1 – v[k] + k\).
  3. Tots els vectors són \(1\)-completables.
  4. Les solucions al problema són els vectors n-completables.
Exemple. El problema de la motxilla

«Una motxilla té lloc per a C unitats de pes. Tria d'entre \(n\) objectes amb pesos \(p[i], …, p[n]\) i valor \(v[i], …, v[n]\) la combinació d'objectes amb pes total inferior a C amb el valor màxim».

Apunts d’Estructures de Dades i Algorismes: Grafs

Definició i propietats

Graf:
\(\begin{array}{lll}G: & \small V\grave{e}rtexs/Nodes & V = \{1, 2, 3, 4\} \\ & \small Arestes & E = \left\{\{1, 2\}, \{1, 3\}, \{2, 3\}, \{3, 4\}\right\}\end{array}\)

Graf dirigit (digraf):
\(\begin{array}{lll}G': & \small V\grave{e}rtexs/Nodes & V = \{1, 2, 3, 4\} \\ & \small Arcs & E \subset VxV\ \small (un\ arc\ \acute{e}s\ un\ parell\ ordenat\ de\ v\grave{e}rtexs)\\ & & E = \left\{(1, 2), (1, 3), (2, 3), (3, 4)\right\}\end{array}\)

Un camí de longitud \(l\) és una seqüència de vèrtexs connectats (per arrestes/arcs) entre ells. Si tots els vèrtexs són diferents, el camí és simple. Un cicle és un camí simple amb la peculiaritat que comença i acaba al mateix vèrtex.

El diàmetre d'un graf és el màxim de tots els camins mínims.

Diem que un graf \(G = (V, E)\) és connex si per tot parell de vèrtexs \(u, v \in V\) existeix un camí que comença en \(u\) i acaba en \(v\).

Un graf no dirigit, connex i acíclic (sense cicles) és un arbre (arbre lliure); és un graf no dirigit on hi ha exactament un camí entre cada parell de vèrtexs. Els arbres tenen les següents propietats:

  • un arbre amb \(n\) vèrtex té \(n – 1\) arestes;
  • si s'afegeix una aresta a un arbre llavors tindrà un (únic) cicle;
  • si s'elimina una aresta d'un arbre llavors deixa de ser connex.

Continue reading →

Apunts d’Estructures de Dades i Algorismes: Diccionaris

Introducció

Són un tipus abstracte de dades (TAD), és a dir, un conjunt d'elements i un conjunt d'operacions sobre ells.

Un diccionari és un conjunt d'elements amb una clau associada (treta d'un conjunt amb un ordre total definit) amb les operacions de cercar, inserir i esborrar un element.

Una estructura de dades és una estructura que ens permet implementar TAD.

Implementacions de diccionaris

1) Taules (no ordenades)
  • Inserir un element nou: \(\theta(1)\).
  • Esborrar un element: cost de cercar l'element (\(O(n)\)) + cost de treure'l de la taula (varia segons el mecanisme d'eliminació, possiblement \(O(n)\)).
2) Llistes (enllaçades, encadenades)

Per recórrer una llista s'ha de fer de forma seqüencial, mitjançant iteradors (punters).

  • Inserir un element nou:
      - al principi: \(\theta(1)\);
      - al final de la llista, recorrent-la sencera: \(\theta(n)\).
  • Cercar un element: \(O(n)\)
  • Esborrar un element: \(O(n)\)
3) Taules de dispersió (hash tables)

El disseny de la funció de hash és molt important per obtenir taules ben equilibrades.

En cas pitjor, el cost serà de \(\theta(n)\), inserint al final amb una funció de dispersió que resulti en llistes llargues. Però, si la funció de dispersió està ben dissenyada, repartirà els elements equitativament, resultant llistes de mitjana \(\alpha = \frac{n}{M}\) amb un cost \(\theta(\alpha)\).

Continue reading →

 
web development