Tema 2: Diseño de algoritmos recursivos

Recursividad

La recursividad y la iteración son los dos mecanismos de los lenguajes para repetir cálculos. Una función es recursiva si se llama a sí misma en su definición.

  • Tiene la misma potencia expresiva que la iteración: se pueden describir los mismos cómputos.
  • Da algoritmos más compactos.
  • Cuesta menos razonar en recursivo que en iterativo, y luego se puede pasar a iterativo sin perder corrección ni eficiencia.

La idea viene de las matemáticas, donde muchas definiciones son recursivas:

n!={1n=0(caso base)n⋅(n−1)!n>0(caso general)n! = \begin{cases} 1 & n = 0 \quad \text{(caso base)} \\ n \cdot (n-1)! & n > 0 \quad \text{(caso general)} \end{cases}

Igual que en una demostración por inducción, al diseñar una función recursiva se puede suponer que el problema ya está resuelto para un tamaño menor.

Iterativo frente a recursivo

Problema: resolver un problema PP sobre unos datos DD.

  • Iterativo: resolver nn veces un problema más simple pp sobre datos dd. En el factorial, pp es multiplicar dos números, repetido nn veces.
  • Recursivo: resolver PP sobre DD a partir de PP sobre unos datos más pequeños D′D'. A su vez D′D' se resuelve con D′′D'', y así se forma una sucesión D>D′>D′′>⋯>DtD > D' > D'' > \dots > D_t que acaba en un caso que se resuelve directamente.
Q ≡ { n ≥ 0 }
Función FACTORIAL (n: entero) retorna (f: entero)
  caso
    n = 0 → 1
    n > 0 → FACTORIAL(n-1) * n
  fcaso
ffunción

La precondición QQ caracteriza los estados iniciales para los que el algoritmo funciona.

Para llegar a esto: si se conoce (n−1)!(n-1)!, entonces n!=(n−1)!⋅nn! = (n-1)! \cdot n. Las llamadas forman la cadena (n),(n−1),…,(1),(0)(n), (n-1), \dots, (1), (0): una sucesión estrictamente decreciente y finita que termina en el caso base n=0n = 0.


Principio de inducción

Es la base teórica que garantiza que el planteamiento recursivo es correcto. Permite demostrar que una propiedad PP se cumple para todos los naturales a partir de uno dado, en tres fases: base, recurrencia y conclusión.

Inducción débil (1.ª forma)

  • Base: PP es cierta para un natural bb.
  • Recurrencia (la propiedad es hereditaria):
    • Hipótesis: PP es cierta para un natural cualquiera n≥bn \ge b.
    • Tesis: PP es cierta para n+1n + 1.
  • Conclusión: PP es cierta para todo natural ≥b\ge b.

El truco es escribir el caso n+1n+1 en función del caso nn para poder usar la hipótesis.

Inducción fuerte (2.ª forma)

Cambia solo la hipótesis: se supone que PP es cierta para todos los naturales del intervalo [b,n][b, n], no solo para nn.

Inducción noetheriana

Para demostrar propiedades de conjuntos que no son N\mathbb{N}: se asocia un natural a cada elemento y se hace inducción sobre ese natural.

Conjuntos definidos por recurrencia e inducción estructural

Un conjunto se puede definir:

  • En extensión: enumerando sus elementos, como {1,3,5,7,9}\{1, 3, 5, 7, 9\}. Solo si son pocos.
  • En comprensión: con una propiedad, como {x∈N:10≤x<100∧∃y∈N, x=2y}\{x \in \mathbb{N} : 10 \le x < 100 \land \exists y \in \mathbb{N},\ x = 2y\}.
  • Por recurrencia (constructiva):
    • Base: los elementos de partida.
    • Recurrencia: reglas para crear elementos nuevos a partir de otros ya creados.

Cuando el conjunto está definido por recurrencia se puede razonar directamente sobre esa definición (inducción estructural):

  • Base: PP se cumple para los elementos base.
  • Hipótesis: los elementos con los que se construye ee cumplen PP.
  • Tesis: ee cumple PP.

Cómo diseñar una función recursiva

  1. Especificar formalmente la función: nombre, dominio y tipo, imagen y tipo, precondición y postcondición.
  2. Elegir el principio de inducción: ver cómo descomponer los datos para obtener la solución a partir de soluciones del mismo problema con datos más pequeños.
  3. Análisis por casos: decidir cuándo el problema es trivial (y qué se devuelve) y cuándo no trivial (y cómo se resuelve).
  4. Escribir el algoritmo en pseudocódigo.
  5. Verificar formalmente que es correcto.

Esquema general

{ Q(x̄) }
función f(x̄: T1) retorna (ȳ: T2)
  caso
    Bt(x̄)  → triv(x̄)
    Bnt(x̄) → c(f(s(x̄)), x̄)
  fcaso
ffunción
{ R(x̄, ȳ) }

xˉ\bar{x} e yˉ\bar{y} son tuplas de parámetros y resultados.

ElementoQué es
QQPrecondición: estados válidos para llamar a ff
RRPostcondición: relación entre entrada xˉ\bar{x} y resultado yˉ\bar{y}
BtB_t, BntB_{nt}Condiciones de caso trivial y no trivial. Deben excluirse: Bt(xˉ)∧Bnt(xˉ)≡falsoB_t(\bar{x}) \land B_{nt}(\bar{x}) \equiv \text{falso}
triv:T1→T2\text{triv} : T_1 \to T_2Solución del caso trivial
s:T1→T1s : T_1 \to T_1Función sucesor: descompone xˉ\bar{x} en el subproblema xˉ′=s(xˉ)\bar{x}' = s(\bar{x})
c:T2×T1→T2c : T_2 \times T_1 \to T_2Función de combinación: junta yˉ′=f(s(xˉ))\bar{y}' = f(s(\bar{x})) con los datos xˉ\bar{x}

Ejercicios propuestos en las diapositivas: número de cifras y suma de cifras de nn, Fibonacci, rayuela (de cuántas formas se llega a la casilla nn con saltos de 1 o 2), plano (caminos de (0,0)(0,0) a (x,y)(x,y) moviéndose solo a la derecha o hacia arriba) y tableros (formas de colocar un tablero m×mm \times m sobre uno n×nn \times n).


Especificación formal

Se usa la especificación pre/post, basada en lógica de predicados.

  • Precondición QQ: tiene como variables libres los parámetros de entrada y describe en qué estados se puede ejecutar el algoritmo.
  • Postcondición RR: tiene como variables libres la entrada y los resultados, y describe el estado final (la relación entre entrada y salida).

{Q} A {R}\{Q\}\ A\ \{R\} se lee: si AA empieza en un estado que cumple QQ, entonces termina, y lo hace en un estado que cumple RR.

Cuantificadores

Formato: (Cuantificador i)(E(i):r(i))(\text{Cuantificador } i)(E(i) : r(i)), donde r(i)r(i) es el rango y E(i)E(i) la expresión.

(∀i)(A[i]=3:1≤i≤n)≡(A[1]=3)∧(A[2]=3)∧⋯∧(A[n]=3)(\forall i)(A[i] = 3 : 1 \le i \le n) \equiv (A[1] = 3) \land (A[2] = 3) \land \dots \land (A[n] = 3)
(∃i)(A[i]=3:1≤i≤n)≡(A[1]=3)∨(A[2]=3)∨⋯∨(A[n]=3)(\exists i)(A[i] = 3 : 1 \le i \le n) \equiv (A[1] = 3) \lor (A[2] = 3) \lor \dots \lor (A[n] = 3)
CuantificadorEjemploRango vacío
Universal ∀\forall(∀i)(A[i]=3:1≤i≤n)(\forall i)(A[i] = 3 : 1 \le i \le n)cierto
Existencial ∃\exists(∃i)(A[i]=3:1≤i≤n)(\exists i)(A[i] = 3 : 1 \le i \le n)falso
Sumatorio Σ\Sigma(Σi)(A[i]:1≤i≤n)(\Sigma i)(A[i] : 1 \le i \le n)00
Producto Π\Pi(Πi)(A[i]:1≤i≤n)(\Pi i)(A[i] : 1 \le i \le n)11
Máximo MAX\text{MAX}(MAX i)(A[i]:1≤i≤n)(\text{MAX } i)(A[i] : 1 \le i \le n)—
Mínimo MIN\text{MIN}(MIN i)(A[i]:1≤i≤n)(\text{MIN } i)(A[i] : 1 \le i \le n)—
Conteo NN(N i)(A[i]=3:1≤i≤n)(N\, i)(A[i] = 3 : 1 \le i \le n)00

Inmersión

A menudo no se puede diseñar ff recursivamente porque no hay forma de descomponer los datos. Por ejemplo, para sumar un vector A[1..n]A[1..n] habría que llamar a la función con un vector "más pequeño", que ya sería de otro tipo.

La solución es definir una función gg más general, con más parámetros (o más resultados), que para ciertos valores de esos parámetros calcule lo mismo que ff. Es una inmersión de ff en gg: gg es la función inmersora y ff la sumergida.

{Q(xˉ)} f(xˉ) {R(xˉ,yˉ)}⟹{Q′(xˉ,wˉ)} g(xˉ,wˉ) {R′(xˉ,wˉ,yˉ)}\{Q(\bar{x})\}\ f(\bar{x})\ \{R(\bar{x}, \bar{y})\} \quad\Longrightarrow\quad \{Q'(\bar{x}, \bar{w})\}\ g(\bar{x}, \bar{w})\ \{R'(\bar{x}, \bar{w}, \bar{y})\}

Para obtener ff basta con llamar a gg con los valores iniciales adecuados de los parámetros nuevos.

Inmersión no final: pasos

Con el ejemplo de la suma, Q≡{n>0}Q \equiv \{n > 0\} y R≡{e=(Σi)(A[i]:1≤i≤n)}R \equiv \{e = (\Sigma i)(A[i] : 1 \le i \le n)\}:

  1. Obtener R′R' por sustitución simbólica: cambiar una constante o expresión de RR por una variable nueva. Cambiando nn por jj:
R′≡{e=(Σi)(A[i]:1≤i≤j)},R′∧(j=n)⇒RR' \equiv \{e = (\Sigma i)(A[i] : 1 \le i \le j)\}, \qquad R' \land (j = n) \Rightarrow R
  1. Obtener Q′Q': Q′≡Q∧dominio(j)Q' \equiv Q \land \text{dominio}(j). Aquí Q′≡{1≤j≤n}Q' \equiv \{1 \le j \le n\}.
  2. Llamada inicial: el valor de jj con el que gg calcula lo mismo que ff. Aquí jini=nj_{ini} = n: SUMA(A)=iSUMA(A,n)\text{SUMA}(A) = \text{iSUMA}(A, n).
  3. Análisis por casos de la función inmersora.

Para el análisis por casos de iSUMA(A, j): se supone conocida la suma de A[1..j−1]A[1..j-1], que es iSUMA(A, j-1), y entonces iSUMA(A, j) = iSUMA(A, j-1) + A[j]. El caso trivial es j=1j = 1, cuya suma es A[1]A[1].

Q' ≡ { 1 ≤ j ≤ n }
Función iSUMA (A[1..n]: vector de enteros, j: entero) retorna (e: entero)
  caso
    j = 1 → A[1]
    j > 1 → iSUMA(A, j-1) + A[j]
  fcaso
ffunción
R' ≡ { e = (Σi)(A[i] : 1 ≤ i ≤ j) }

Llamada inicial: iSUMA(A, n)

Qué constante sustituir

Según qué se sustituya, se recorre el vector de una forma u otra:

SustituciónSecciónLlamada inicial
nn por jj[1..j][1..j]j=nj = n
11 por jj[j..n][j..n]j=1j = 1
11 por ii y nn por jj[i..j][i..j]i=1i = 1, j=nj = n

Sustituyendo 11 por jj, la suma queda así: se supone conocida la suma de A[j+1..n]A[j+1..n] y el caso trivial es j=nj = n.

Q' ≡ { 1 ≤ j ≤ n }
Función iSUMA (A[1..n]: vector de enteros, j: entero) retorna (e: entero)
  caso
    j = n → A[n]
    j < n → iSUMA(A, j+1) + A[j]
  fcaso
ffunción
R' ≡ { e = (Σi)(A[i] : j ≤ i ≤ n) }

Llamada inicial: iSUMA(A, 1)

Rango vacío

Si se permite n≥0n \ge 0 (vector vacío), el caso trivial pasa a ser la sección vacía, cuya suma es 00 (el neutro), y el dominio del parámetro nuevo se amplía en uno:

VersiónQ′Q'Caso trivialCaso no trivialLlamada inicial
[1..j][1..j]0≤j≤n0 \le j \le nj=0→0j = 0 \to 0j>0→j > 0 \to iSUMA(A, j-1) + A[j]iSUMA(A, n)
[j..n][j..n]1≤j≤n+11 \le j \le n+1j=n+1→0j = n+1 \to 0j<n+1→j < n+1 \to iSUMA(A, j+1) + A[j]iSUMA(A, 1)

Verificación formal

Para garantizar que una función recursiva es correcta hay que demostrar 6 propiedades. Todos los símbolos son los del esquema general.

PropiedadQué garantiza
1Q(xˉ)⇒Bt(xˉ)∨Bnt(xˉ)Q(\bar{x}) \Rightarrow B_t(\bar{x}) \lor B_{nt}(\bar{x})Bien definida: en todo estado válido se entra en algún caso
2Q(xˉ)∧Bnt(xˉ)⇒Q(s(xˉ))Q(\bar{x}) \land B_{nt}(\bar{x}) \Rightarrow Q(s(\bar{x}))Bien definida: la llamada recursiva cumple la precondición
3Q(xˉ)∧Bt(xˉ)⇒R(xˉ,triv(xˉ))Q(\bar{x}) \land B_t(\bar{x}) \Rightarrow R(\bar{x}, \text{triv}(\bar{x}))Base de la inducción: el caso trivial es correcto
4Q(xˉ)∧Bnt(xˉ)∧R(s(xˉ),yˉ′)⇒R(xˉ,c(yˉ′,xˉ))Q(\bar{x}) \land B_{nt}(\bar{x}) \land R(s(\bar{x}), \bar{y}') \Rightarrow R(\bar{x}, c(\bar{y}', \bar{x}))Paso de inducción: R(s(xˉ),yˉ′)R(s(\bar{x}), \bar{y}') es la hipótesis de inducción
5Existe t:D→Zt : D \to \mathbb{Z} con Q(xˉ)⇒t(xˉ)≥0Q(\bar{x}) \Rightarrow t(\bar{x}) \ge 0Terminación: la función limitadora está acotada inferiormente
6Q(xˉ)∧Bnt(xˉ)⇒t(s(xˉ))<t(xˉ)Q(\bar{x}) \land B_{nt}(\bar{x}) \Rightarrow t(s(\bar{x})) < t(\bar{x})Terminación: cada llamada hace decrecer tt

Las 3 y 4 juntas demuestran por inducción que Q(xˉ)⇒R(xˉ,f(xˉ))Q(\bar{x}) \Rightarrow R(\bar{x}, f(\bar{x})) para todo xˉ\bar{x}. Las 5 y 6 aseguran que las llamadas forman una sucesión estrictamente decreciente y finita, así que la recursión termina.

Cómo elegir la función tt

tt mide cuánto queda por hacer: debe ser ≥0\ge 0 y bajar en cada llamada. Suele ser el tamaño de la sección que falta por procesar.

  • iSUMA sobre [1..j][1..j] (baja con j−1j - 1): t(A,j)=jt(A, j) = j.
  • iSUMA sobre [j..n][j..n] (sube con j+1j + 1): t(A,j)=n−j+1t(A, j) = n - j + 1, el número de elementos de A[j..n]A[j..n]. Con j=1j = 1 vale nn, y baja en uno en cada llamada.