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