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:
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 sobre unos datos .
- Iterativo: resolver veces un problema más simple sobre datos . En el factorial, es multiplicar dos números, repetido veces.
- Recursivo: resolver sobre a partir de sobre unos datos más pequeños . A su vez se resuelve con , y así se forma una sucesión 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 caracteriza los estados iniciales para los que el algoritmo funciona.
Para llegar a esto: si se conoce , entonces . Las llamadas forman la cadena : una sucesión estrictamente decreciente y finita que termina en el caso base .
Traza de FACTORIAL(3)
Las llamadas bajan hasta el caso base y los resultados suben combinándose: ; luego , , , .
Principio de inducción
Es la base teórica que garantiza que el planteamiento recursivo es correcto. Permite demostrar que una propiedad 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: es cierta para un natural .
- Recurrencia (la propiedad es hereditaria):
- Hipótesis: es cierta para un natural cualquiera .
- Tesis: es cierta para .
- Conclusión: es cierta para todo natural .
es divisible por 9 para todo
- Base (): .
- Hipótesis: para algún , es decir, .
- Tesis: , múltiplo de 9.
El truco es escribir el caso en función del caso para poder usar la hipótesis.
Inducción fuerte (2.ª forma)
Cambia solo la hipótesis: se supone que es cierta para todos los naturales del intervalo , no solo para .
Todo natural se descompone en producto de primos
- Base: 2 es primo ().
- Hipótesis: la propiedad se cumple para todos los naturales de .
- Tesis: para hay dos casos:
- Si es primo, ya está.
- Si no, con . Por hipótesis y se descomponen en primos, así que también.
Aquí hace falta la forma fuerte porque y no son , sino números menores cualesquiera.
Inducción noetheriana
Para demostrar propiedades de conjuntos que no son : se asocia un natural a cada elemento y se hace inducción sobre ese natural.
Un conjunto de cardinal tiene partes
Las partes de son sus subconjuntos: . Por ejemplo, .
Inducción sobre el cardinal:
-
Base: el único conjunto de cardinal 0 es , que tiene una parte (él mismo), y .
-
Hipótesis: todo conjunto de cardinal tiene partes.
-
Tesis: sea de cardinal , y , que tiene elementos. Cada parte de contiene o no contiene a :
- Las que no lo contienen son las partes de : por hipótesis.
- Las que sí lo contienen se obtienen añadiendo a cada parte de : otras .
En total, .
Con : sin están , y con están .
Conjuntos definidos por recurrencia e inducción estructural
Un conjunto se puede definir:
- En extensión: enumerando sus elementos, como . Solo si son pocos.
- En comprensión: con una propiedad, como .
- 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: se cumple para los elementos base.
- Hipótesis: los elementos con los que se construye cumplen .
- Tesis: cumple .
Los impares cumplen
Definición recurrente de : , y si entonces .
- Base: .
- Hipótesis: .
- Tesis: .
Cómo diseñar una función recursiva
- Especificar formalmente la función: nombre, dominio y tipo, imagen y tipo, precondición y postcondición.
- 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.
- 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).
- Escribir el algoritmo en pseudocódigo.
- 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̄, ȳ) }
e son tuplas de parámetros y resultados.
| Elemento | Qué es |
|---|---|
| Precondición: estados válidos para llamar a | |
| Postcondición: relación entre entrada y resultado | |
| , | Condiciones de caso trivial y no trivial. Deben excluirse: |
| Solución del caso trivial | |
| Función sucesor: descompone en el subproblema | |
| Función de combinación: junta con los datos |
Potencia
Q ≡ { a ≥ 0 ∧ n ≥ 0 }
Funcion POTENCIA (a, n: entero) retorna (p: entero)
caso
n = 0 → 1
n > 0 → POTENCIA(a, n-1) * a
fcaso
ffunción
R ≡ { p = aⁿ }
Ejercicios propuestos en las diapositivas: número de cifras y suma de cifras de , Fibonacci, rayuela (de cuántas formas se llega a la casilla con saltos de 1 o 2), plano (caminos de a moviéndose solo a la derecha o hacia arriba) y tableros (formas de colocar un tablero sobre uno ).
Especificación formal
Se usa la especificación pre/post, basada en lógica de predicados.
- Precondición : tiene como variables libres los parámetros de entrada y describe en qué estados se puede ejecutar el algoritmo.
- Postcondición : tiene como variables libres la entrada y los resultados, y describe el estado final (la relación entre entrada y salida).
se lee: si empieza en un estado que cumple , entonces termina, y lo hace en un estado que cumple .
Cuantificadores
Formato: , donde es el rango y la expresión.
| Cuantificador | Ejemplo | Rango vacío |
|---|---|---|
| Universal | cierto | |
| Existencial | falso | |
| Sumatorio | ||
| Producto | ||
| Máximo | — | |
| Mínimo | — | |
| Conteo |
Rango vacío = elemento neutro
Si ningún cumple el rango, el resultado es el elemento neutro de la operación: cierto para , falso para , para la suma, para el producto.
Inmersión
A menudo no se puede diseñar recursivamente porque no hay forma de descomponer los datos. Por ejemplo, para sumar un vector 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 más general, con más parámetros (o más resultados), que para ciertos valores de esos parámetros calcule lo mismo que . Es una inmersión de en : es la función inmersora y la sumergida.
Para obtener basta con llamar a con los valores iniciales adecuados de los parámetros nuevos.
Inmersión no final: pasos
Con el ejemplo de la suma, y :
- Obtener por sustitución simbólica: cambiar una constante o expresión de por una variable nueva. Cambiando por :
- Obtener : . Aquí .
- Llamada inicial: el valor de con el que calcula lo mismo que . Aquí : .
- 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 , que es iSUMA(A, j-1), y entonces iSUMA(A, j) = iSUMA(A, j-1) + A[j]. El caso trivial es , cuya suma es .
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ón | Sección | Llamada inicial |
|---|---|---|
| por | ||
| por | ||
| por y por | , |
Sustituyendo por , la suma queda así: se supone conocida la suma de y el caso trivial es .
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 (vector vacío), el caso trivial pasa a ser la sección vacía, cuya suma es (el neutro), y el dominio del parámetro nuevo se amplía en uno:
| Versión | Caso trivial | Caso no trivial | Llamada inicial | |
|---|---|---|---|---|
iSUMA(A, j-1) + A[j] | iSUMA(A, n) | |||
iSUMA(A, j+1) + A[j] | iSUMA(A, 1) |
Vector capicúa
, .
Sustituyendo por y por : , .
Función iCAPICUA (A[1..n]: vector de enteros, i, j: entero) retorna (s: booleano)
caso
i = j → VERDADERO
i = j-1 → (A[i] = A[j])
i < j-1 → iCAPICUA(A, i+1, j-1) AND (A[i] = A[j])
fcaso
ffuncion
Llamada inicial: iCAPICUA(A, 1, n). Hacen falta dos casos triviales porque la sección puede quedar con un elemento (longitud impar) o con dos (longitud par).
Con rango vacío (), y basta con:
i > j → VERDADERO
i ≤ j → iCAPICUA(A, i+1, j-1) AND (A[i] = A[j])
Media aritmética
. Sustituyendo por : y .
Si se conoce la media de los primeros, su suma es esa media por :
Función iMEDIA (A[1..n]: vector de reales, k: entero) retorna (e: real)
caso
k = 1 → A[1]
k > 1 → (iMEDIA(A, k-1) * (k-1) + A[k]) / k
fcaso
ffuncion
Llamada inicial: iMEDIA(A, n).
Matriz simétrica
. Sustituyendo por se comprueba la submatriz de las filas y columnas , con .
Si la submatriz es simétrica, solo falta comprobar la nueva "banda" de la fila y columna :
Función iSIMETRICA (A[1..n][1..n]: matriz de enteros; k: entero) retorna (b: booleano)
si k = 1 entonces retorna VERDADERO
sino retorna iSIMETRICA(A, k-1) ∧ Banda_Simetrica(A, k)
fsi
ffunción
{ 2 ≤ k ≤ n }
Función Banda_Simetrica (A[1..n][1..n]: matriz de enteros; k: entero) retorna (b: booleano)
i = 0
iguales = VERDADERO
mientras (i < k-1 ∧ iguales) hacer
i = i + 1
si (A[k][i] ≠ A[i][k]) entonces iguales = FALSO fsi
fmientras
retorna iguales
ffunción
{ b = (∀i)(A[k][i] = A[i][k] : 1 ≤ i ≤ k-1) }
Llamada inicial: iSIMETRICA(A, n).
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.
| Propiedad | Qué garantiza | |
|---|---|---|
| 1 | Bien definida: en todo estado válido se entra en algún caso | |
| 2 | Bien definida: la llamada recursiva cumple la precondición | |
| 3 | Base de la inducción: el caso trivial es correcto | |
| 4 | Paso de inducción: es la hipótesis de inducción | |
| 5 | Existe con | Terminación: la función limitadora está acotada inferiormente |
| 6 | Terminación: cada llamada hace decrecer |
Las 3 y 4 juntas demuestran por inducción que para todo . Las 5 y 6 aseguran que las llamadas forman una sucesión estrictamente decreciente y finita, así que la recursión termina.
Ejemplo 1: POTENCIA
, , , , , , , , .
- ✓
- ✓
- : ✓
- : ✓
- : ✓
- ✓
Ejemplo 2: FACTORIAL
, .
- 3: con el rango del producto es vacío, así que vale , que es lo que devuelve. ✓
- 4: si , entonces . ✓
- 5 y 6: , igual que en la potencia.
Ejemplo 3: dos llamadas recursivas
{ n ≥ 0 }
Funcion F (n: entero) retorna (p: entero)
caso
n = 0 → 1
n = 1 → 3
n > 1 → 2 * F(n-1) + 3 * F(n-2)
fcaso
ffuncion
{ p = 3ⁿ }
Con dos llamadas, las propiedades 2, 4 y 6 se comprueban para cada una, y la 3 para cada caso trivial.
- ✓
- y ✓
- y ✓
- Con y : ✓
- ✓
- y ✓
Cómo elegir la función
mide cuánto queda por hacer: debe ser y bajar en cada llamada. Suele ser el tamaño de la sección que falta por procesar.
iSUMAsobre (baja con ): .iSUMAsobre (sube con ): , el número de elementos de . Con vale , y baja en uno en cada llamada.