Diseño y verificación de algoritmos recursivos

Anuncio
INTRODUCCIÓN
Los algoritmos recursivos son aquellos que “se invocan a sí mismos”. En este tema de la
asignatura (que es el más amplio, y que es el verdadero “alma” de la misma) vamos a conocer
este tipo de algoritmos, y veremos a qué tipo de problemas pueden aplicarse.
Contenido:
- Concepto de recursividad. Veremos en qué consisten este tipo de algoritmos.
- Ejemplos. Ejemplos sencillos de algoritmos recursivos.
- Principio de inducción. Base teórica que nos permite utilizar este tipo de
algoritmos para los números enteros.
- Inducción noetheriana. Aplicaremos el principio de inducción a problemas que
tengan como parámetros no sólo números enteros.
- Lenguaje LR. Es la notación que vamos a utilizar para mostrar este tipo de
algoritmos.
- Elementos de un algoritmo recursivo.
- Clasificación de los algoritmos recursivos.(Lineal, no lineal, final, no final).
- Diseño de algoritmos recursivos. Método para, a partir de un problema,
conseguir un algoritmo recursivo correctamente formulado que lo resuelva.
o o
Especificación
o o
Análisis por casos
o o
Composición
o o
Verificación
o o
Estudio de la eficiencia
- Demostración de la corrección de un algoritmo recursivo.
- Cálculo de la eficiencia de un algoritmo recursivo.
- Ejemplos
- Técnicas de inmersión.
o o
No final
o o
Final
o o
Inmersión por razones de eficiencia
- Técnica de desplegado y plegado. Sirve para hacer un algoritmo recursivo
final a partir de uno no final
o o
Generalización
o o
Plegado
o o
Desplegado
- Transformación de un algoritmo recursivo en uno iterativo.
1
CONTENIDO
CONCEPTO DE RECURSIVIDAD
En muchas ocasiones, para la definición de un conjunto o de una propiedad, denotamos unos
pocos elementos básicos, denotados “explícitamente”, y también unas reglas de generación de
nuevos objetos a partir de objetos previamente generados (definición de un conjunto o de una
propiedad por recurrencia).
Ejemplos : fbf, números impares, factorial, potencia...
Recursividad: Característica de la mayoría de los lenguajes de programación que permite que
un procedimiento o función hagan referencia a sí mismos dentro de su definición.
Una invocación al subprograma recursivo genera una o más invocaciones al propio
subprograma, cada una de las cuales genera nuevas invocaciones y así sucesivamente.
Si la definición está bien hecha, y los parámetros de entrada son adecuados, las cadenas de
invocaciones así formadas, terminan en alguna llamada que no genera nuevas
invocaciones.
Estas llamadas terminales acaban su ejecución y devuelven el control a la llamada anterior
que, a su vez, terminará en algún momento devolviendo el control a la anterior, y así hasta que
todas las cadenas y la propia llamada inicial terminan.
Un algoritmo recursivo, en función de los parámetros de entrada, tiene dos comportamientos
-Uno o varios comportamientos “sencillos”, en el cual su solución es fácil/trivial. Para
cuando el tamaño de los parámetros de entrada es pequeño/simple. Caso (s) base.
-Uno o varios comportamientos más “elaborados”. Este comportamiento, se resuelve
invocando al mismo algoritmo con un parámetro más pequeño/sencillo que el que
se ha recibido, y aplicando al resultado de su invocación alguna operación sencilla.
Caso(s) recursivos.
EJEMPLOS
- Ejemplo: FACTORIAL DE UN NÚMERO
n! = n * (n-1) * (n-2) * ... * 2 * 1
factorial:=1;
WHILE n>1 DO
factorial := factorial * n;
n:= n-1;
END;
- Otra forma de verlo:
-
Si n = 1 entonces factorial (n) = 1
(Caso sencillo)
Si n > 1 entonces factorial (n) = n * factorial (n -1) (Caso elaborado, en el que
se utiliza la misma función factorial pero con un tamaño de parámetro más pequeño)
2
- Resultado: algoritmo recursivo
PROCEDURE Factorial ( n: INTEGER) : INTEGER;
VAR
aux: INTEGER;
BEGIN
IF (n = 1) THEN aux := 1;
ELSE
aux := n * Factorial (n-1);
END;
RETURN (aux);
END Factorial;
an = a * a * a * ...* a (n veces)
- Ejemplo: POTENCIA
resultado:=1;
WHILE n>1 DO
resultado := resultado * a;
n:= n-1;
END;
- Otra forma de verlo:
-
Si n = 1 entonces potencia (n) = a
(Caso sencillo).
Si n > 1 entonces potencia (n) = a * potencia(n -1) (Caso elaborado, en el que
se utiliza la misma función factorial pero con un tamaño de parámetro más pequeño).
- Resultado: algoritmo recursivo
PROCEDURE Potencia (a, n: INTEGER) : INTEGER;
VAR
aux : INTEGER;
BEGIN
IF (n = 1) THEN
aux := a;
ELSE
aux := Potencia (a, n-1) * a;
END;
RETURN (aux);
END Potencia;
Ejemplo de “ejecución”
3
4
EL PRINCIPIO DE INDUCCIÓN
Es muy útil para probar propiedades de los números naturales. Es la base teórica que nos
“asegura” que podemos utilizar algoritmos recursivos para resolver problemas cuyos
parámetros sean los números enteros.
Primer principio de inducción
Sea P(n) un predicado (propiedad) que depende de n ∈ N. Si se verifican las condiciones:
1.- P(0) o P(1)
2.- ∀n ∈N. P(n) → P (n + 1)
entonces ∀n ∈ N. P(n)
INDUCCIÓN NOETHERIANA
Principios en los que se basa la demostración de la corrección de una función recursiva, pero
para “cualquier” tipo de parámetros de entrada ( el principio de inducción “simple” sólo nos
permitía demostrarlo para los enteros. Dada una especificación formal de la forma,
{Q(x)}
fun f (x:T1) dev (y:T2)
{R(x, y)}
verificar la corrección de f equivale a demostrar la validez del siguiente predicado:
∀ x ∈ DT1. Q(x) → R(x, f(x))
La inducción noetheriana sirve para demostrar propiedades por inducción en dominios que
no sean los números enteros.
Lo que queremos demostrar es que los conjuntos con los que trabajamos siguen “cierto orden”,
que son preórdenes bien fundados o pbf’s.
Dado un conjunto D, una relación binaria en D, “≤“ ⊆ D x D se dice que es un preorden sobre
D, si es reflexiva y transitiva.
- reflexiva: ∀ x ∈ D . x ≤ x
- transitiva: ∀ x, y, z ∈ D. x ≤ y ∧ y ≤ z → x ≤ z
Si ≤ es un preorden en D, también llamaremos preorden al par (D, ≤)
Dado un preorden (D, ≤), se define una relación “<“, preorden estricto, como
x < y ≡ x ≤ y ∧ ¬ (y ≤ x)
Dado un preorden (D, ≤), se dice que un elemento m ∈ D es minimal ó mínimo en D si no tiene
5
predecesores estrictos, es decir
m es minimal ó mínimo en D ≡ ¬ (∃ x ∈ D. x < m)
Un preorden (D, ≤) se dice que es bien fundado (pbf) si no existen sucesiones infinitas
estrictamente decrecientes
(D, ≤) es un PBF ssi todo subconjunto A no vacío de D tiene al menos un mínimo.
Si (D2, ≤2) es un PBF , D1 un conjunto f :D1→D2 una aplicación. Sean a, b ∈ D1 y se define
a ≤1 b ≡ f(a) ≤2 f(b)
Entonces, (D1, ≤1) es un PBF
PRINCIPIO DE INDUCCIÓN NOETHERIANA O PRINCIPIO DE INDUCCIÓN COMPLETA
SOBRE PBF
Sea (D, ≤) un PBF y sea P un predicado definico sobre los elementos de D. Entonces
∀ a ∈ D. (∀ b ∈ D. b<a → P(b)) → P(a)
________________________________
∀ a ∈ D. P(a)
Este principio es útil para probar propiedades (enunciadas como predicados ) de PBF.
- Base de la inducción: probar la propiedad para el elemento mínimo
- Hipótesis de la inducción: asumir que, para un e cualquiera, la propiedad es cierta para todo
predecesor suyo.
- Paso de inducción: probar que la propiedad se verifica para el propio elemento e.
LENGUAJE LR
LR es el lenguaje utilizado para el diseño recursivo de programas.
Es un lenguaje en el que en lugar de instrucciones en el sentido de órdenes que han de ser
seguidas por un computador, tenemos expresiones.
(Ejemplo) hasta ahora:
fun potencia( entrada a:entero; n:natural) dev (p: entero);
principio
caso
n = 0 : p := 1;
n > 0 : p := a * potencia (a, n - 1);
6
fin
fcaso
devuelve (p);
En lenguaje LR,
fun potencia (a: entero; n : natural) dev (p: entero) =
caso n = 0 → 1
n > 0 → a * potencia (a, n - 1)
fcaso
ffun
CLASIFICACIÓN /TERMINOLOGÍA
LINEAL/ NO LINEAL
-
LINEAL. Cuando una función recursiva genera a lo sumo una llamada interna por
cada llamada externa, diremos que es una función recursiva lineal o recursiva simple.
(ejemplos: potencia, factorial)
-
NO LINEAL. Cuando genera dos o más llamadas internas por cada llamada
externa diremos que es recursiva no lineal o recursiva múltiple.
Ejemplo de recursividad múltiple: suma de los elementos de un vector
Si el vector tiene un elemento = la suma es sólo el valor de ese elemento.
Si el vector tiene más de un elemento = suma de la primera mitad + suma de la segunda mitad.
fun suma (entrada
var
m : entero;
principio
Si
i > j
sino si
i
sino si
i
fin
fcaso
a:vect; i,j : entero) dev (s: entero);
entonces s := 0;;
= j entonces s := a [ i ];
< j entonces
m := ( i + j ) div 2;
s := suma ( a, i, m) + suma (a, m+1, j);
fun suma (a:vect; i,j : entero) dev (s: entero) =
caso i > j → 0
i=j→a[i]
i < j → sea m = ( i + j ) div 2 en
suma ( a, i, m) + suma (a, m+1, j)
fcaso
ffun
7
FINAL/NO FINAL
-
NO FINAL: El caso recursivo se resuelve haciendo alguna operación sobre la
invocación al algoritmo recursivo.
Ejemplo: Al resultado de la llamada recursiva a potencia, se le realiza la multiplicación
por a.
fun potencia (a: entero; n : natural) dev (p: entero) =
caso n = 0 → 1
n > 0 → a * potencia (a, n - 1)
fcaso
ffun
-
FINAL: El caso recursivo se resuelve simplemente devolviendo el resultado de la
invocación al algoritmo recursivo. En este caso, el caso base devuelve ya el resultado
del problema completo. Necesito un parámetro “acumulador” que me vaya guardando
el resultado de mis cálculos.
Ejemplo: El caso recursivo devuelve directamente el resultado de la invocación
recursiva, sin hacer ninguna operación sobre el mismo.
fun potencia (a: entero; aux: entero; n : natural) dev (p: entero) =
caso n = 0 → aux
n > 0 → potencia (a, a*aux , n - 1)
fcaso
ffun
8
ELEMENTOS DE UNA FUNCIÓN RECURSIVA GENÉRICA
Vamos a razonar sobre la corrección de una función recursiva lineal, y para ello vamos a
utilizar el esquema global de programa que aparece a continuación.
{ Q( x ) }
fun f ( x : T1) dev ( y : T2) =
caso Bt(x) → triv (x)
Bnt(x) → c(f(s(x)), x)
fcaso
ffun
{ R (x, y) }
Los parámetros formales x e y han de entenderse como tuplas x1, ..., xn
respectivamente.
e y1, ...yn
Las expresiones booleanas Bt(x) y Bnt(x) distinguen, respectivamente, si el problema x es
trivial o no.
La función triv calcula la solución de f cuando x es trivial
La función s es la función sucesor que realiza la descomposición recursiva de los datos x,
calculando el subproblema x’ de x al cual se aplica la invocación recursiva de f.
La función c (combinar) se encarga de combinar el resultado devuelto por f con (parte de) los
propios parámetros de entrada x, que devuelve la solución de f en el caso no trivial.
R(x,y) es la postcondición de f que establece la relación que ha de cumplirse entre los
parámetros de entrada x y los resultados y.
Cuando la función c no es necesaria, es decir, cuando f(x) = f(s(x)) en el caso no trivial, diremos
que f es recursiva final. En caso contrario, diremos que f es recursiva no final.
DISEÑO DE ALGORITMOS RECURSIVOS
Se compone de cinco etapas (Son los pasos que vamos a dar para resolver todos los
problemas de la asignatura):
1.- Especificación formal del algoritmo
2.- Análisis por casos
3.- Composición
4.- Verificación formal de la corrección
5.- Estudio de la eficiencia
1.- Especificación formal del algoritmo. (Precondición involucra a parámetros y
Postcondición involucra a parámetros y a resultados).
2.- Análisis por casos.
- Analizar los casos que se pueden presentar a la función, identificando bajo qué condiciones
el problema ha de considerarse no trivial y cuál ha de ser la solución a aplicar en ese caso, y
bajo qué condiciones el problema ha de considerarse trivial y cómo ha de resolverse.
9
Condición de caso trivial
Condición de caso no trivial
Solución al caso trivial
Solución al caso no trivial
Dos comprobaciones importantes:
- Que la reducción aplicada al problema en el caso no trivial conduce a problemas cada vez
más pequeños que necesariamente han de terminar en el caso trivial.
- Que entre los casos triviales y los no triviales se han cubierto todos los estados
previstos por la precondición.
3.- Composición
Expresar, en forma de lenguaje LR, el resultado de análisis por casos. Debemos asegurarnos
de que las condiciones de las instrucciones condicionales son mutuamente excluyentes.
4.- Verificación formal de cada caso
- Verificación formal del algoritmo, cuando se han realizado con cuidado el resto de las etapas,
esta resulta sencilla.
5.- Estudio de la eficiencia
-
Se trata de encontrar una expresión del orden de la eficiencia.
Plantear una ecuación recurrente y resolverla / aplicar la fórmula general para el
cálculo de la eficiencia en algoritmos recursivos.
DEMOSTRACIÓN DE LA CORRECCIÓN DE UN ALGORITMO RECURSIVO
Debemos comprobar los siguientes puntos:
La función f ha de estar definida dentro del dominio de los parámetros
Q(x)
Bt(x) ∨ Bnt(x)
La función f se invoca siempre en estados que satisfacen su precondición
Q(x) ∧ Bnt(x)
Q(s(x))
La llamada interna a f se realiza con parámetros estrictamente más pequeños que x, con
respecto a la relación de preorden, bajo la que D es un preorden. (en cada llamada generada
recursivamente, los parámetros son más pequeños).
Q(x) ∧ Bnt(x)
t(s(x)) < t(x)
Los elementos minimales se encuentran incluidos en el caso no trivial y verifican la
precondición.
(Si el punto anterior es correcto, no hará falta demostrar este)
Consiste en demostrar seis puntos
1. Q(x)
Bt(x) ∨ Bnt(x)
2. Q(x) ∧ Bnt(x)
Q(s(x))
3. Q(x) ∧ Bt(x)
R(x, triv(x))(Base de la inducción)
4. Q(x) ∧ Bnt(x) ∧ R(s(x), y)
R(x, c(y’,x)) . (Paso de inducción, donde R(s(x), y’)
representa la hipótesis de inducción)
10
5. Encontrar t: DT1 → Z tal que Q(x)
t(x) ≥ 0 ( definición de la estructura de pbf)
6. Q(x) ∧ Bnt(x)
t(s(x)) < t(x). (El tamaño de los subproblemas decrece estrictamente)
VERIFICACIÓN DE ALGORITMOS RECURSIVOS
CORRECCIÓN PARCIAL
1. Q(x)
Bt(x) ∨ Bnt(x) (Completitud de la alternativa entre los casos triviales y no triviales)
2. Q(x) ∧ Bnt(x)
interna)
Q(s(x)) (Satisfacción de la precondición para los parámetros de la llamada
3. Q(x) ∧ Bt(x)
casos triviales)
R(x, triv(x))(Base de la inducción) (satisfacción de la postcondición para los
4. Q(x) ∧ Bnt(x) ∧ R(s(x), y)
R(x, c(y’,x)) . (Paso de inducción, donde R(s(x), y’) representa
la hipótesis de inducción) (satisfacción de la postcondición para los casos recursivos, a partir
de la hipótesis de inducción, esto es, la suposición de la satisfacción de dicha postcondición
para los datos de la llamada interna).
TERMINACIÓN DE LA FUNCIÓN
5. Encontrar t: DT1 → Z tal que Q(x)
6. Q(x) ∧ Bnt(x)
t(x) ≥ 0 ( definición de la estructura de pbf)
t(s(x)) < t(x). (El tamaño de los subproblemas decrece estrictamente)
CÁLCULO DE LA EFICIENCIA EN PROGRAMAS RECURSIVOS
Pasos a seguir:
1.- Elección del tamaño del problema
2.- Hallar una expresión para T(n), que mide el tiempo de ejecución en función del tamaño del
problema
3.- Resolver la recurrencia.
ECUACIÓN
SOLUCIÓN
T(n) ∈
θ (nk), si a < 1
θ (nk+1), si a = 1
θ (a n div b), si a > 1
T(n) ∈
θ (n ), si a < b
k
k
θ (n log n), si a = b
log b a
k
θ (n
), si a > b
k
c n , si 0 ≤ n < b
T(n) =
a T(n-b) + cnk, si n ≥ b
k
k
c n , si 0 ≤ n < b
T(n)
k
a T(n/b) + cn , si n ≥ b
11
k
EJEMPLO: POTENCIA
{Q ≡ n ≥ 0 }
fun potencia (a: entero; n : natural) dev (p: entero) =
caso n = 0 → 1
n > 0 → a * potencia (a, n - 1)
fcaso
ffun
{R ≡ p = an }
x = (a,n)
y=p
Q(a,n) ≡ n ≥ 0
R((a,n),p) ≡ p = an
c(p’,(a, n)) ≡ a * p’
Bt(a,n) ≡ n = 0
Bnt(a, n) ≡ n > 0
s (a,n) ≡ (a, n-1)
triv(a,n) ≡ 1
t(a, n) (función limitadora) ≡ n
1. Q(x)
n≥0
Bt(x) ∨ Bnt(x)
n=0∨n>0
2. Q(x) ∧ Bnt(x)
Q(s(x))
n≥0 ∧ n>0
(n-1) ≥ 0
3. Q(x) ∧ Bt(x)
R(x, triv(x))(Base de la inducción)
n≥0 ∧ n=0
1=a
n
4. Q(x) ∧ Bnt(x) ∧ R(s(x), y’)
hipótesis de inducción)
n ≥ 0 ∧ n > 0 ∧ p’ = an-1
R(x, c(y’,x)) . (Paso de inducción, donde R(s(x), y’) representa la
a * p’ = an
5. Encontrar t: DT1 → Z tal que Q(x)
n≥0
t(x) ≥ 0 ( definición de la estructura de pbf)
n≥0
6. Q(x) ∧ Bnt(x)
n≥0∧n>0
t(s(x)) < t(x). (El tamaño de los subproblemas decrece estrictamente)
n -1 < n
EFICIENCIA
Tamaño del problema : n
T(n) = k si n = 0
T(n) = T(n-1) +k si n >0
T(n) ∈ Θ (n)
EJEMPLO: sumaComponentes
12
{ Q ≡ 1 ≤ pI ≤ pD ≤ N }
fun sumaC (v: vector; pI, pD : entero) dev (s: entero) =
caso pI = pD → v[pI]
pI < pD → v[pI] + sumaC (v, pI + 1, pD)
fcaso
ffun
{ R ≡ s = α ∈ {pI .. pD}. v[α] }
x = (v, pI, pD)
y=s
Q(v, pI, pD) ≡ 1 ≤ pI ≤ pD ≤ N
R((v, pI, pD),s) ≡ s = α ∈ {pI .. pD}. v[α]
c(s’,( v, pI, pD)) ≡ v[pI] + s’
Bt(v, pI, pD) ≡ pI = pD
Bnt(v, pI, pD) ≡ pI < pD
s (v, pI, pD) ≡ (n, pI +1, pD)
triv(v, pI, pD) ≡ v[pI]
t(v, pI, pD) (función limitadora) ≡ pD - pI
1. Q(x)
Bt(x) ∨ Bnt(x)
(1 ≤ pI ≤ pD ≤ N)
2. Q(x) ∧ Bnt(x)
((pI = pD) ∨ (pI < pD))
Q(s(x))
(1 ≤ pI ≤ pD ≤ N) ∧ (pI < pD))
3. Q(x) ∧ Bt(x)
(1 ≤ pI +1 ≤ pD ≤ N)
R(x, triv(x))(Base de la inducción)
((1 ≤ pI ≤ pD ≤ N) ∧ (pI = pD))
4. Q(x) ∧ Bnt(x) ∧ R(s(x), y’)
hipótesis de inducción)
v[pI] =
α ∈ {pI .. pD}. v[α] = v[pI]
R(x, c(y’,x)) . (Paso de inducción, donde R(s(x), y’) representa la
((1 ≤ pI ≤ pD ≤ N) ∧ (pI < pD) ∧ s = α ∈ {pI+1 .. pD}. v[α] ))
v[pI] + s = α ∈ {pI .. pD}. v[α] )
5. Encontrar t: DT1 → Z tal que Q(x)
t(x) ≥ 0 ( definición de la estructura de pbf)
t(v, pI, pD) = pD -pI
(1 ≤ pI ≤ pD ≤ N) → (pD - pI) ≥ 0
6. Q(x) ∧ Bnt(x)
t(s(x)) < t(x). (El tamaño de los subproblemas decrece estrictamente)
(1 ≤ pI ≤ pD ≤ N) ∧ (pI < pD)
pD - pI > pD -(pI +1)
EFICIENCIA
Tamaño del problema : n
T(n) = k si n = 0
T(n) = T(n-1) +k si n >0
T(n) ∈ Θ (n)
EJEMPLO: División entera mediante restas
13
{ Q ≡ (a ≥0) ∧ (b > 0)}
fun divide (a, b: entero) dev (q, r: entero) =
caso a<b → <0, a>
a ≥ b → sea <q’, r’> = divide(a-b, b) en <q’+ 1, r’>
fcaso
ffun
{ R ≡ (a = b * q + r) ∧ ( 0 ≤ r < b)}
x = (a, b)
y = (q,r)
Q(a, b) ≡ (a ≥0) ∧ (b > 0)
R((a,b),(q,r)) ≡ (a = b * q + r) ∧ ( 0 ≤ r < b)
c((q’,r’),(a, b)) ≡ (q+1, r)
Bt(a, b) ≡ a < b
Bnt(a, b) ≡ a ≥ b
s (a, b) ≡ (a -b, b)
triv(a, b) ≡ (0, a)
t(a, b) (función limitadora) ≡ a div b!!
1. Q(x)
Bt(x) ∨ Bnt(x)
(a ≥0) ∧ (b > 0)
2. Q(x) ∧ Bnt(x)
(a < b) ∨ (a ≥ b)
Q(s(x))
(a ≥0) ∧ (b > 0) ∧ (a ≥ b)
3. Q(x) ∧ Bt(x)
(a-b ≥ 0) ∧ (b > 0)
R(x, triv(x)) (Base de la inducción)
(a ≥0) ∧ (b > 0) ∧ a < b
R
0,a
q,r
≡ ((a = a) ∧ (0 ≤ a < b))
4. Q(x) ∧ Bnt(x) ∧ R(s(x), y’)
R(x, c(y’,x)) . (Paso de inducción, donde R(s(x), y’) representa la
hipótesis de inducción)
(a ≥0) ∧ (b > 0) ∧ (a ≥ b) ∧ Ra-b,b, q’, r’ a,b, q, r R a,b, q’+1, r’ a,b, q, r
(a ≥0) ∧ (b > 0) ∧ (a ≥ b) ∧ (a - b = b * q’ + r’ ) ∧ ( 0 ≤ r’ < b)
(a = b * (q’ + 1) + r’) ∧ ( 0 ≤ r’ < b)
5. Encontrar t: DT1 → Z tal que Q(x)
t(x) ≥ 0 ( definición de la estructura de pbf)
t(a, b) = a div b
(a ≥0) ∧ (b > 0) → a div b ≥ 0
6. Q(x) ∧ Bnt(x)
t(s(x)) < t(x). (El tamaño de los subproblemas decrece estrictamente)
(a ≥0) ∧ (b > 0) ∧ (a ≥ b)
(a - b div b) < a div b
EFICIENCIA
Tamaño del problema : n
n = a div b
n-1 = (a-b) div b
14
T(n) = k si n = 0
T(n) = T(n-1) +k si n >0
T(n) ∈ Θ (n)
TÉCNICAS DE INMERSIÓN
Se aplican cuando no es posible abordar directamente el diseño recursivo de una función
f porque no se encuentra una descomposición adecuada de sus datos.
Una técnica que puede solucionar el diseño consiste en definir una función g, más general
que f, con más parámetros o/y más resultados que, para ciertos valores de los nuevos
parámetros, (alguno de sus resultados) calcula lo mismo que f.
Diremos que hemos aplicado una inmersión de f en g.
La función más general, g, se denomina función inmersora, y la función original, f, se
denomina función sumergida.
La ventaja de definir una inmersión es que la adición de nuevos parámetros o resultados
hace posible el diseño recursivo de la función inmersora. Para calcular la función original
basta con establecer el valor inicial de los nuevos parámetros que hacen que la función
inmersora se comporte como la sumergida.
Se parte de una función f cuya especificación conocemos, y debemos especificar y diseñar una
función más general g, es decir,
{Q(x)}
fun f(x) dev (y)
{R(x, y)}
{Q’(x,w)}
fun g(x, w) dev (y, z)
{R’ (x, w, y , z)}
Donde w y z representan respectivamente los parámetros y resultados adicionales que g
tiene con respecto a f. Bajo ciertas condiciones P, los resultados y de g son los mismos que
calcularía f.
Q’(x, w) ∧ P(x, w) ∧ R’ (x,w,y,z)
R(x, y)
Técnicas para encontrar la generalización adecuada. Intentamos obtener la especificación
de g a partir de la especificación de f, encontrar una nueva precondición y postcondición
para g a partir de las de f. Esto implica decidir cuáles han de ser los parámetros (w) y/o
los resultados (z) inmersores .
Buscamos una función
{Q’(x,w)}
fun g(x,w) dev (y)
{R’(x,w,y)}
Técnicas:
- Cuando NO tenemos una función recursiva, y queremos una función que
resuelva el problema y lo sea.
- Inmersión no final: conduce a una función g recursiva no final. Se realiza
mediante un debilitamiento de la postcondición.
- Inmersión final: conduce a una función g recursiva final. Se realiza
mediante un reforzamiento de la precondición.
- Cuando tenemos una solución recursiva, pero aún así queremos hacerla más
eficiente
- Inmersión de parámetros.
15
-
-
Inmersión de resultados.
INMERSIÓN NO FINAL
{ Q ≡ cierto }
fun sumaComponentes (E v: vect) dev (s: entero);
{ R ≡ s = Σα∈ {1..n}.v[α]}
Sólo a partir de x no es posible el diseño recursivo, por esta razón se añaden los parámetros
w. Es de esperar que los parámetros w sean modificados por g durante la cadena de llamadas
recursivas, desde el valor inicial wini, hasta un cierto valor final wfin, que corresponde al caso
trivial de g.
A esta técnica también se la conoce como debilitamiento de la postcondición.
w = i (componente del vector hasta la que vamos a tener calculada la suma)
iini = n
ifin = 1
Es de esperar que los resultados yfin de la llamada recursiva sean progresivamente modificados
hasta obtener los resultados yini de la llamada inicial.
sfin va a tener la suma calculada hasta una determinada posición, cuando esa posición
sea la última, tendremos ya los resultados de la llamada inicial.
Esta técnica consiste en obtener R’ a partir de R. Para ello, se toma R(x,y) y se sustituyen
constantes o expresiones que sólo dependan de x, por nuevas variables w (variables
inmersoras). El nuevo predicado es R’(x,w,y). Llamando Φ(X) a las expresiones sustituidas en
R, tenemos (P(x, w) ≡ w = Φ(x))
R ≡ s = Σα∈ {1..n}.v[α]
R’ ≡ s = Σα∈ {1..i}.v[α]
P≡i=n
R’(x,w,y)wΦ(x) = R(x,y)
R’(x,w,y) ∧P(x, w)
R(x, y)
Σα∈ {1..i}.v[α] ∧ i = n
s = Σα∈ {1..n}.v[α]
La precondición Q’(x,w) se obtiene mediante la conjunción de Q(x) y las constantes adicionales
para w (D(w, x) condiciones de dominio).
El dominio de w excluirá aquellos valores de w que hacen que R’ se evalúe a falso o esté
indefinido.
Q’(x,w) ≡ Q(x) ∧ D(x, w)
Q’ ≡ (cierto ∧ 1 ≤ i ≤ n)
Queda por establecer wini que hace cierta la implicación anterior y que garantiza que g se
comporta como f.
(wini = Φ(x)) ∧ Q(x)
D(wini, x)
iini = n ∧ cierto
1≤n≤n
{ Q ≡ 1≤ i ≤ n }
16
fun isumaComponentes (E v: vect, i: entero) dev (s: entero);
caso i = 1 → v[1]
i > 1 → v[i] + isumaComponentes(v, i-1)
fcaso
ffun
{ R ≡ s = Σα∈ {1..i}.v[α]}
INMERSIÓN FINAL
Esta técnica conduce a una función inmersora g recursiva final.
Por lo tanto, los resultados devueltos por g en el caso trivial son ya los resultados que
emulan los resultados de la función sumergida f.
A la técnica usada se le llama “reforzamiento de la precondición”
La postcondición de g es directamente R (no cambia), y no depende de los parámetros
adicionales w.
Para conseguir esto, algunos parámetros de w han de acumular parte del resultado. Los
parámetros se llaman en este caso parámetros acumuladores.
Parte de la postcondición R se satisface ya en la precondición Q’.
Impondremos que g devuelva como resultado una parte de w.
w = ( w1, w2 )
La función g será de la forma:
Caso
Bt : w1
Bnt : g(...)
Fcaso
Se satisface la siguiente implicación:
Q’(x,w) ∧ Bt(x,w)
R(x, w1)
Los pasos a seguir son:
1.- Renombrar en R, y por w1.
2.- Tratar de expresar R como una conjunción: A(x, w 1) ∧ C(x, w 1)
a) Si es posible, entonces w = w1 (como parámetro de inmersión) y escoger una
expresión como Q y otra como Bt. (La nueva pre lleva parte de la antigua post)
En nuestro ejemplo no es posible, la postcondición no es una conjunción
b) Si no es posible, o no obtenemos nada útil por la utilización del caso anterior,
construimos un predicado Rdebil (x,w1, w2) sustituyendo en R una expresión Φ que dependa de x
y de w1, por w2, de modo que,
W 1 es el nuevo parámetro introducido para acumular el resultado
W 2 es el nuevo parámetro introducido para poder realizar la recursividad
17
Rdebil(x, w1, w2) ∧ (w2 = Φ (x, w1))
R(x, w1))
Ss va a ser nuestro nuevo parámetro acumulador y i el parámetro que vamos a utilizar
para realizar la recursividad.
Rdebil(v, i, ss) ≡ R ≡ ss = Σα∈ {1..i}.v[α]
Rdebil ∧ i = n ss = Σα∈ {1..n}.v[α]
3.- Diseñar el resto del algoritmo
{ Q’ ≡ ss = Σα∈ {1..i}.v[α] ∧ 0 ≤ i ≤ n }
fun iisumaComponentes (E v: vect, i, ss: entero) dev (s: entero);
caso i = 1 → ss
i > 1 → i isumaComponentes(v, i-1, ss + v[i-1])
fcaso
ffun
{ R ≡ s = Σα∈ {1..n}.v[α]}
INMERSIÓN POR RAZONES DE EFICIENCIA
Para los casos en los que recalculemos algunas expresiones varias veces sin aprovecharnos
de las hechas en llamadas precedentes.
Dos situaciones.
La expresión compleja a ser evaluada sólo depende de los parámetros x. Inmersión de
parámetros, añadimos a la función parámetros acumuladores, que llevan precauculada la
expresión deseada.
Pasos a seguir.
- Añadimos a Q(x), una conjunción de la forma w = Φ (x), siendo w el parámetro acumulador y
Φ (x) la expresión precalculada)
- A continuación , se sustituye en el texto de f toda aparición de Φ (x) por el nuevo parámetro
w.
- Calcular en la función sucesor de g el nuevo parámetro acumulador w’, de forma que se
mantenga invariante la precondición.
- El valor inicial wini se obtiene por la propia precondición.
La expresión compleja se evalúa después de la llamada recursiva e involucra los resultados y
devueltos por esta.
Inmersión de resultados, añadimos a la función resultados acumuladores que llevarán
recalculada la expresión deseada.
Pasos a seguir:
- Añadir la conjunción z = Φ (x, y ) a la postcondición R(x, y) (x el resultado acumulador ) y (Φ
(x’, y’) la expresión que la llamada interna devuelve precalculada para su uso en la llamada en
curso.
En f
- sustituir toda aparición de la expresión Φ (x,’ y’) por el nuevo resultado precalculado z’.
18
- restablecer la poscondición en el caso no trivial, calculando z a partir de z’.
- restablecer la poscondición en el cso trivial, calculando un valor de z que
satisfaga la poscondición.
- TÉCNICA DE DESPLEGADO Y PLEGADO
- Se utiliza para hacer transformaciones de programas recursivos no finales a finales ( Ya
tenemos una solución recursiva).
- Los programas recursivos finales son más eficientes.
- Buscamos encontrar otra función más general, que se comporte como la que ya tenemos.
Debemos realizar tres pasos:
-generalización. Se define una función g(x,w) como una expresión que depende de f y
representa una generalización de la misma. Se establece un valor wini para el cual g se
comporta como f. (w es un nuevo parámetro)
- desplegado. Se despliega la definición de f dentro de la definición de g y se realizan ciertas
manipulaciones algebraicas.
- plegado. Se sustituye la expresión del caso no trivial de g en que aparece f, por una
expresión equivalente en la que sólo aparece g. Se obtiene así una definción recursiva de g
que resulta ser final.
{ Q( x ) }
fun f ( x : T1) dev ( y : T2) =
caso Bt(x) → triv (x)
Bnt(x) → c(f(s(x)), x)
fcaso
ffun
{ R (x, y) }
{Q≡1≤i≤ N}
fun isumaC (v: vector; i : entero) dev (sC: entero) ≡
caso i = 1 → v[1]
i > 1 → v[i] + sumaC (v, i-1)
fcaso
ffun
{ R ≡ s = α ∈ {1 ..i}. v[α] }
GENERALIZACIÓN
La definición de la función g se basa en la expresión del caso no trivial de f.
g(x, w) = c(f(x), w)
Si la operación c es compleja, se contruye el árbol sintáctico de la operación c.
A continuación, se conserva el camino que va desde la raíz del árbol hasta la invocación a f.Por
último, cada subárbol lateral a este camino se sustituye por un parámetro de inmersión
diferente.
Si la función c tiene elemento neutro w0, entonces
f(x) = c(f(x), w0) = g(x, w0)
19
es decir, g es una generalización de f que se comporta igual que ella para el valor inicial w =
w0.
iisumaC (v, i, w) = sumaC(v, i) + w
isumaC(v, i) = isumaC(v, i) + 0 = iisumaC (v, i, 0)
DESPLEGADO
Usando la definición que hemos hecho anteriormente,
g(x, w) = c(f(x), w)
sustituimos en ella f(x) por su definición, obteniendo:
g(x,w) = c(f(x),w)
= c(caso Bt(x) → triv (x)
Bnt(x) → c(f(s(x)), x)
fcaso, w)
= caso Bt(x) → c(triv (x),w)
Bnt(x) → c(c(f(s(x)), x), w)
fcaso
Si la operación c es asociativa, la expresión del caso no trivial puede ser reordenada, dando
lugar a
g(x,w) = caso Bt(x) → c(triv (x),w)
Bnt(x) → g(s(x), c(x,w))
fcaso
iiSumaC (v, i, w)
= isumaC(v, i) + w
= (caso i = 1 → v[1]
i > 1 → v[i] + sumaC (v, i-1)
fcaso) + w
Como la operación de suma, que es la que realiza c, es asociativa, podemos reordenar la
expresión del caso trivial.
iiSumaC (v, i, w)
= caso i = 1 → v[1] + w
i > 1 → sumaC (v, i-1) + (v[i] + w)
fcaso
PLEGADO
La expresión del caso no trivial de g tiene el mismo aspecto que la definición de la función, por
lo que se puede plegar este caso no trivial, dando lugar a:
g(x,w) = caso Bt(x) → c(triv(x),w)
Bnt(x) → g(f(s(x)), c(x,w))
fcaso
20
que define g como función recursiva final.
Para aplicar esta técnica, la función c (combinar) de f ha de poseer elemento neutro y ser
asociativa.
iiSumaC (v, i, w)
= caso i = 1 → v[1] + w
i > 1 → ii sumaC (v, i-1, v[i] + w)
fcaso
TRANSFORMACIÓN DE RECURSIVO A ITERATIVO
(problemas del punto 10 de la colección de problemas)
En este apartado se van a diseñar versiones iterativas de los esquemas genéricos recursivos
final y no final usados a lo largo del tema.
Razones para desear transformar a iterativo una función recursiva:
El lenguaje disponible no soporta la recursividad.
-
-
-
La eficiencia es mayor en tiempo en los algoritmos iterativos(el tiempo es menor)
. Esto es debido a los mecanismos de llamada a procedimientos y paso de parámetros.
-
La eficiencia es mayor en memoria en los algoritmos iterativos(el tamaño de la
memoria utilizada es menor). Esto es debido a que se hace mucho uso de la pila.
No obstante, la versión iterativa será siempre menos legible y modificable.
Para ver si la transformación realizada es correcta, utilizaremos el concepto de INVARIANTE.
P(x, xini) es un predicado llamado INVARIANTE y que es satisfecho por todos los estados
en los que el contador de programa se halla justo antes de preguntar por la condición
Bnt. Se denomina así porque se satisface antes y después de cada iteración E.
DESDE FUNCIÓN RECURSIVA FINAL
{ Q( x ) }
fun f ( x : T1) dev ( y : T2) =
caso Bt(x) → triv (x)
Bnt(x) → f(s(x))
fcaso
ffun
{ R (x, y) }
{Q(xini)}
fun f (xini: T1) dev (y:T2)
var x: T1 fvar
x := xini;
{P(x, xini}
mientras Bnt(x) hacer
x := s(x)
fmientras
dev triv(x)
ffun
{R(xini,y)}
{ Q ≡ w = Σα∈ { i+1..n}.v[α] ∧ 0 ≤ i ≤ N }
{Q≡ wini = Σα∈ { iini+1..n}.v[α] ∧ 0 ≤ iini ≤ N}
fun iisumaC (v: vector; i : entero; w:entero) dev fun iisumaCit (vini: vector, iini: entero; wini: entero)
dev (sC: entero)
(sC: entero) ≡
var v: vector; i: entero; w: entero fvar
caso i = 1 → v[1] + w
i := iini v :=vini; w := wiini;
i > 1 → ii sumaC (v, i-1, v[i] + w)
{P(x, xini}
fcaso
mientras i >1 hacer
i: =1-1;
ffun
w := v[i] + w;
{ R ≡ sC = α ∈ {1 ..n}. v[α] }
21
fmientras
dev (v[1] + w)
ffun
{ R ≡ sC = α ∈ {1 ..n}. vini [α] }
En particular, se satisface antes de la primera y después de la última iteración (en este caso,
junto con la condición de terminación del bucle Bnt). El invariante del bucle del ejemplo anterior
es:
P(x, xini) ≡ Q(x) ∧ f(xini) = f(x)
Con este invariante, podemos razonar sobre la corrección de la transformación del modo
siguiente:
1.- El invariante se satisface antes de la primera iteración. Ello viene garantizado
trivialmente por la precondición Q(xini) y por la asignación previa al bucle.
2.- Si el invariante se satisface antes de una iteración cualquiera, también se satisface
después de la misma, es decir
Q(x) ∧ (f(xini) = f(x)) ∧ Bnt(x)
Q(s(x)) ∧ f(xini) = f(s(x))
Que viene garantizado por el punto dos de la corrección de una función recursiva, y por la
propia estructura de f, que en el caso no trivial, satisface f(x) = f(s(x)).
3.- Si el bucle termina, a su terminación se satisface la postcondición R(xini, y). La
implicación que necesitamos en este caso es:
Q(x) ∧ f(xini) = f(x) ∧ Bnt(x)
R(xini, triv(x))
Por el punto primero de la demostración de la corrección de algoritmos recursivos, podemos
deducir que
Q(x) ∧ Bnt(x)
Bt(x)
y, por el punto 3 de la misma tabla,
Q(x) ∧ Bt(x)
R(x, triv(x)), es decir, la postcondición se satisface para el valor de x
correspondiente al caso trivial. La segunda condición garantiza que el valor de y para ese caso
es válido para todas las x de la cadena, en particular para xini.
4.- El bucle termina. Es obvio que cada iteración del bucle corresponde a una llamada
recursiva. La prueba de terminación de la función recursiva sirve como prueba de terminación
del bucle. Otra forma de demostrarlo es basarse en la estructura de pbf de los valores de x: la
cadena de valores calculados por el bucle es estrictamente decreciente (garantizado por el
punto 4 de la demostración de corrección de algoritmos recursivos) y no puede ser infinita.
DESDE FUNCIÓN RECURSIVA NO FINAL
{ Q( x ) }
fun f ( x : T1) dev ( y : T2) =
caso Bt(x) → triv (x)
Bnt(x) → c(f(s(x)), x)
fcaso
ffun
{Q(xini)}
fun f (xini: T1) dev (y:T2)
var x: T1 fvar
x := xini;
{P1(x, xini}
mientras Bnt(X) hacer
22
{ R (x, y) }
x := s(x)
fmientras
y := triv(x)
{P2(x, xini, y)}
mientras x ≠ xini hacer
x := s -1(x);
y := c(y,x)
fmientras
dev y
ffun
{R(xini,y)}
Primer bucle : “descenso” en la cadena de llamadas recursivas, transformando los parámetros
x de la llamada en curso en los parámetros s(x) de la llamada sucesora, hasta encontrar un
valor x correspondiente al caso trivial.
La siguiente asignación permite calcular el primer resultado y, correspondiente al caso trivial.
Segundo bucle: “ascenso” en la cadena de llamadas, aplicando reiteradamente la función c de
combinación para calcular los resultados de la llamada en curso en función de los de la llamada
sucesora.
{Q≡1≤i≤ N}
{Q ≡ 1 ≤ i ini ≤ N)}
fun isumaC (v: vector; i : entero) dev (sC: fun f (vini: vector,iini: entero) dev (sC: entero)
var v: vector; i : entero fvar
entero) ≡
v := vini;
caso i = 1 → v[1]
i :=iini;
i > 1 → v[i] + sumaC (v, i-1)
{P1(x, xini}
fcaso
mientras i > 1 hacer
ffun
i := i-1
{ R ≡ s = α ∈ {1 ..i}. v[α] }
fmientras
sC :=v[1]
{P2(x, xini, y)}
mientras i ≠ iini hacer
i := i+1
sC := sC + v[i]
fmientras
dev sC
ffun
{ R ≡ s = α ∈ {1 ..iini}. v[α] }
-1
Es necesario, para este esquema, que la función s
( inversa de la función sucesor) sea
calculable. Si no es calculable, se conservarán los parámetros x de todas las llamadas en una
pila.
Los invariantes de los bucles del caso general son:
P1 (x, xini) ≡ Q(x) ∧ SUC(x, xini)
P2 (x,xini,y) ≡ P1(x, xini) ∧ R(x,y)
siendo
k
SUC (x, xini) ≡ ∃ k ∈ N.(x= s (xini)
k’
k’
∧ ∀ k’ ∈ {0..k-1}. Bnt (s (xini) ) ∧ Q(s (xini)))
23
sk (xini) significa la aplicación reiterada, cero o más veces, de la función s.
Demostración de la corrección de la transformación:
1.- El invariante P1(x, xini) se cumple trivialmente antes del primer bucle. El predicado SUC
se satisface para k = 0.
2.- P1(x, xini) es realmente invariante:
Q(x) ∧ SUC(x,xini) ∧ Bnt(x)
Q(s(x)) ∧ SUC(s(x), xini)
3.- A la terminación del primer bucle se satisface por primera vez R(x,y) para el valor del caso
trivial de x. Por lo tanto, el invariante P2(x, xini, y) es inicialmente cierto.
4.- P2 es invariante. Equivale a demostrar la implicación:
P2(x, xini, y) ∧ x ≠ xini
P2
s ^(-1),c(y,s^(-1)(x))
x,y
Como x ≠ xini se garantiza que x tiene prececesor. La parte relacionada con R corresponde al
paso de inducción de la demostración de corrección de la función recursiva, es decir, al punto 4
de la tabla 3.2.
5.- A la terminación del segundo bucle se satisface R(xini, y) , consecuencia directa del
invariante P2 y de la condición de terminación del bucle x = xini.
6.- Ambos bucles terminan. La argumentación es idéntica a la dada en el caso de la
recursividad final.
24
Documentos relacionados
Descargar