Académique Documents
Professionnel Documents
Culture Documents
Este texto se encuentra dirigido a aquellos que ya se han estrellado contra la asignatura. Los
novatos pueden usarlo también, por supuesto.
Vamos a suponer que el examen consiste básicamente en el desarrollo, como en la práctica,
de un supuesto. Y que en dicho supuesto hay un vector.
RECURSIVIDAD
Especificación de la función:
Consiste en una terna QSR en la que:
Q es la precondición. Lo que deben cumplir los datos de entrada.
S es la definición (exclusivamente) de la función. Debe tener variables de entrada y de
salida.
R es la postcondición. Las condiciones que debe cumplir la (las) variables de salida.
En primer lugar comprueba que el tipo de datos coincide con el especificado en la salida. Si
es binario (CIERTO, FALSO), tienes que establecer unos criterios lógicos (algo es igual o
distinto a otra cosa, Y/O etc.). Si es natural comprueba que haces cálculos en el ámbito de
los naturales (div en lugar de /). También procura no mezclar enteros con naturales, y por
supuesto, cuidado con mezclar ambos con los reales.
Pero, como suponemos que hay un vector que recorrer, no es tan difícil:
a) Encontrar algo. Responder cierto si...
R(x,y)≡{b=(∃ α∈ [1..n]· α cumple la condición)} b es binario.
b) Demostrar que se cumple algo. Responder cierto si...
R(x,y)≡{b=(∀ α∈ [1..n]· α cumple la condición} b es binario.
c) Contar los elementos que cumplen la condición.
R(x,y)≡{s=Ν α (desde 1 hasta n) · α cumple la condición} s es natural.
d) Sumar elementos del vector.
R(x,y)≡{s=Σ α (desde 1 hasta n) } s depende del tipo de los datos del vector.
e) Multiplicar elementos del vector.
R(x,y)≡{s=Π α (desde 1 hasta n) } s depende del tipo de los datos del vector.
Q debe ser lo menos restrictivo posible. Y ahí entran las opiniones. Si el equipo docente de
la asignatura decide que has sido demasiado restrictivo, mal comienzas:
Si un número es natural, poner que es mayor o igual que cero es una tontería. Mejor no
ponerlo.
Si un número es natural, (CARDINAL) o entero (INTEGER), poner que tiene unos límites
(MAXINT, MAXCARD) es demasiado apegado a la práctica y aquí somos muy teóricos.
Prueba con “verdadero”. Y piensa en qué trampa vas a caer. ¿Realmente hay alguna
condición que deben cumplir los parámetros de entrada?.
La especificación no vale para nada, y nadie le pide que haga nada, porque no es posible. Así
que vamos a “sumergirla” (inmersión) para que aprenda a bucear. Es decir, vamos a
convertirla en recursiva.
Para ello necesitamos reforzar la poscondición (R) debilitándola. Oficialmente y para todos
los efectos la estamos debilitando. Que el libro [Peña97] parezca contradecirse a sí mismo es
sólo una circunstancia en la que no vamos a entrar (*). DEBILITAR.
Y debilitar significa coger ese (“existe, para todo, conteo, suma o producto” desde el primero
hasta el último) y convertirlo en otro desde el primero hasta una variable i.
¿Que usaste ya i precisamente en eso?. ¡Qué falta de previsión!. Vale. Usaremos c.
Por tanto:
R’≡(R debilitada) ∧ (c=n)
Aunque R’ ,R y Rdebilitada son incomparables por lógica (según [Peña97]**), la lógica aquí
vale poco. A todos los efectos R’ y R son equivalentes.
¿Qué pasa con S?. Debemos reescribir S incluyendo la variable c.
¿Qué pasa con Q?. Tenemos que reforzarla (esta vez sí) incluyendo una limitación a c.
Pongamos que Q’ ≡ Q ∧ (c<=n).
NO es la única solución. Pero al equipo docente de la asignatura le gusta esta. Tanto que
muy pocos se atreven a hacerlo de otra manera. Por si acaso, vamos a hacerlo así.
¿Qué tipo le has dado a c?. ¿Natural?. Es natural que lo hagas. Pero no te confíes. En su
momento puede que tengas que romperlo todo y empezar de nuevo.
Francamente, y ahora que no nos oye el equipo docente de la asignatura: esto tiene más
agujeros que un queso Emmental (que es el de los agujeros. El gruyére es como el queso de
barra, con unos agujeritos muy pequeños).
Es decir: R(x,triv(x)) es el resultado booleano de una igualdad. Y la igualdad debe ser cierta.
Si no lo es, echa mano del tipex. O tacha, que hay que arreglar poco.
Lo que hace:
R(s(x),y) b=(x[c]=r) {b=(∃ [1..c-1] x[i]=r)}
Lo que debería hacer:
R(x,c(y’,x)){b=( [1..c] x[i]=r)}
Y si no lo ves recuerda que el símbolo se puede sustituir por una concatenación de
disyunciones ( ) y el símbolo por una concatenación de conjunciones ( ).
Y se acabó.
Esto lo coge Charles Dogdson (alias Lewis Carroll) y se parte de risa. ¡Feliz no
cumpleaños!.
Comenzamos de nuevo:
1.- Buscamos una variable c. Esta variable va a recorrer el vector desde el extremo superior
(n) hasta el extremo inferior (1 ó 0).
Definimos automáticamente t = c. Ya tenemos el preorden bien fundado.
2.- Le hacemos un sitio a c dentro de la función inútil. Para ello...
a) “Debilitamos” R. R≡Rdébil (c=n). En Rdébil sustituimos cualquier referencia a n
por c.
b) Introducimos c dentro de las variables de entrada de la función.
c) Reforzamos Q incluyendo una referencia a c. Pero ojo, dale suficiente espacio: cuando
se salga del vector, y se tiene que salir, aún debe cumplir Q’
Q’(x)=Q(x) (c<=n)
fun iLoQueSea (x:variables de entrada, c:entero):dev y
R’(x,y)=Rdebil(x,y) (c=n)
3.- Llamada inicial. La tenemos encima: c=n. Pero si lo recorres de abajo a arriba en ambos
casos debes poner c=1 (ó c=0 dependiendo del rango del vector).
4.- Análisis por casos:
a) El caso Bt es cuando c se sale del vector [c<1, c<0 ó c>n]. Dependiendo que el vector
esté en [0..n] ó [1..n]. Y si lo recorremos hacia arriba o hacia abajo.
b) El caso Bnt es cuando c está en el vector. Todo lo demás. Es decir: [c>=1; c>=0;
c<=n]
a2) El caso Bt conduce a triv(x). Que es el elemento neutro de la operación que vamos a
poner debajo, dentro de un momento.
b2) El caso Bnt conduce al examen del momento “c” OP (operado con) una llamada
recursiva en la que c pasa a c-1 ó c+1.
Podríamos decir que es (Rdébil para c) OP (Rdébil desde 1 hasta c-1) [llamada
recursiva].
¿Alguien te ha informado ya que a los programadores se los paga o clasifica por los miles de
líneas de código al mes que pueden hacer?.
Problemas:
-Tu R(x,y) es un desastre. Ni se acerca a lo que te están pidiendo. Mala suerte.
-Q(x) es demasiado optimista o pesimista. Es decir más débil o más fuerte de la cuenta, en
opinión del equipo docente de la asignatura. Mala suerte.
-Te has empeñado en que 0 es el elemento neutro del producto. O, lo que es más habitual,
que cierto es el elemento neutro de la disyunción (∃).
-Por un olvido estúpido la llamada recursiva va a c. Lo que hace que el ordenador entre en
un ciclo infinito y tengas que apagarlo a lo bruto.
-Por enésima vez: ‘falso⇒CualquierCosa’ es formalmente correcto. Pero no sirve.
Inmersión final:
Según uno de los dos posibles libros de texto de la asignatura, [Peña 97], la inmersión final
crea código más compacto, eficiente y menos comprensible.
Si has tenido que hacer X prácticas obligatorias, con X >=2 sabes que eso no es cierto. No es
más eficiente. Menos comprensible, sí, por supuesto.
Entonces, ¿para qué la inmersión final?. Incumpliendo las reglas que me he impuesto para
hacer esta guía, te voy a contar una historia que puede que te aclare este concepto. Y voy a
hacerlo porque creo que nadie se ha molestado en explicártelo. Por supuesto no va a servir
para que apruebes, así que puedes saltártelo tranquilamente.
Supongamos que te encuentras en lo alto de una escalera. No sabes los pisos que hay hasta la
salida. Tampoco sabes si los pisos tienen todos la misma altura o no, si la escalera se
subdivide en varias, ni si hay varias salidas. En resumen, sabes poco. Pero quieres poder
salir del edificio en caso de necesidad, a oscuras, medio dormido. Así que coges un montón
de miguitas de pan y dejas caer una en el suelo. Después bajas un escalón (PUSH)
llamándote recursivamente a tí mismo. Es decir: (miguita, escalón), (miguita, escalón), etc.
Cuando se acaba la escalera y/o vislumbras la salida te dices a tí mismo: “Vale”. Esto es
triv(x).
Te das media vuelta y comienzas a subir los escalones (POP) recogiendo las miguitas que
has dejado caer. Cuando estás de vuelta en el origen, tienes un montón de miguitas que te
dicen el número de escalones que tienes que descender. Además puedes haber recopilado
otra mucha información: número de descansillos, escaleras alternativas, puertas, etc.
Después de hacer esto un par de cientos de veces te dices a tí mismo: ¿Y porqué no llevo
conmigo una bolsa donde guardar las miguitas?. Así cuando termine, puedo coger el
ascensor.
¿Coger el ascensor?.
Sí. En informática después de un número indeterminado de PUSH (llamadas a subrutinas) no
tiene porque haber igual número de POP (retornos). Un único POP puede devolverte al punto
de origen, a la llamada original.
Siempre que no hayas tocado la pila de datos. Es decir, siempre que no te hayas dejado
miguitas por ahí.
Una función recursiva final no se deja cálculos para después. Se los lleva dentro de la propia
función, dentro de la llamada recursiva. Así que tiene su bolsa, real o virtual en la que
guardar las miguitas.
En resumen: la posible mejor eficiencia de una función inmersora final depende del
compilador.
Según [Peña97] en su página 62 el término inglés para “recursiva final” es ‘tail recursion’, es
decir, recursividad de cola. Y las ‘funciones recursivas no finales” son en inglés ‘nontail
recursive function’. Es decir, sin cola.
VALE.
Inmersión final:
Vamos a crear una función en la que las operaciones que hasta ahora se hacían antes de la
llamada recursiva, se hagan en la propia llamada recursiva.
Una función inmersora final o recursiva final tiene una variable (una nueva variable) en la
que se van acumulando los cálculos.
Y el proceso de transformar una función recursiva final en otra final es automático y se
llama “Desplegado y Plegado”. Que es lo que te van a poner en el examen.
1.- Hacer el árbol sintáctico de la operación. Queda muy bonito así que hazlo. Si lo haces a
lápiz es más fácil arreglarlo.
2.- Defines ‘g’ como lo que ocurre en Bnt(x). Suele ser algo así: w OP iLoQueSea (x,c)
Por tanto: iiLoQueSea (x,c,w):=w OP iLoQueSea(x,c)
Esto empieza a parecerse a eso de s:=s+t.
3.-En lugar de iLoQueSea(x,c) pones su análisis por casos. Y usas la propiedad distributiva.
No te olvides, en algún momento, de poner que has usado la propiedad asociativa. Lo
esperan de tí, no los defraudes.
4.- Como tu objetivo final es convertir el caso no trivial en una llamada a iiLoQueSea, busca
la ocasión.
Notas:
No olvides que en el caso trivial nos llevamos la bolsita de migas. Eso es w. Si has utilizado
correctamente la propiedad distributiva, no hay problema.
La llamada inicial es triv(x). iiLoQueSea (x,c,triv(x))=iLoQueSea (x,c)
Así que:
Q’‘(x)
fun iiLoQueSea (x:variables de entrada, c: entero, w:datos):dev s datos.
caso Bt(x) → w
Bnt(x) → iiLoqueSea (x,c-1, (w OP Rdébil para c))
ffun
R(x,y)
Fin de la recursividad.
Ejercicio para meditar: ¿Porqué no se verifican las funciones recursivas finales?. Parece un
poco tonto hacer una cosa, comprobarla, modificarla y no comprobar la modificación.
O como se dice en Informática: “Si funciona, no lo toques”.
Así que, ¿porqué no se verifica después de obtener la función recursiva final?.
[Peña 97] da un argumento: “No tiene interés optimizar un programa incorrecto”.
Bien. ¿Qué opinas?.
ITERACIÓN
Una cosa buena que tienen los programas iterativos es que el proceso de verificación formal
es idéntico al proceso de creación del programa. Es decir, la verificación formal puede
utilizarse para crear un programa correcto.
Q(X)
fun LoQueSea-it (x variables de entrada):dev s
inicio
(P)Mientras B hacer
restablecer
avanzar
fmientras
dev s
ffun
R(x,y)
Suponemos, como ya hemos hecho con el caso recursivo que tenemos que recorrer un
vector.
Este paso 0 es, en su segunda parte, igual al paso 5 del proceso recursivo. Respecto a la
primera parte, también tuvimos que hacer algo así cuando generamos la función recursiva no
final. Y, realmente, también en ese momento se definía t.
Cae por su propio peso. Siempre que consideremos que el R original y el R descompuesto en
una conjunción son equivalentes, aunque sean incomparables. Como ya lo había dicho para
el caso recursivo, no insistiré en el tema. Escribes lo mismo en los dos lados de la ecuación
lógica y ya está.
Dado que ¬B es (c=n) entonces B ≡(c ≠ n)
2.- {Q}Inicio{P}
¿Esto qué es?. ¿Es una ecuación o inecuación lógica?. ¿Hay alguna manera de saber qué está
en qué lado de la fórmula, igualdad, o lo que sea?.
Después de una meditación profunda llegamos a que {Q}Inicio{P} puede ser interpretado
como Q≡ P(inicio). Y en unos momentos sustituiremos ≡ por ⇒ .
Vamos a ser más concretos.
Necesitamos dos variables. Una ya la conocemos, c, y vamos a usarla en el control del bucle.
Y la otra es una variable que al final será s. No podemos usar s porque no está definida (en
Módula2, por ejemplo se define el tipo pero no el nombre de la variable que devuelve la
función. Es la función llamante la que tiene un nombre (propio) para esa variable).
Para mantener la terminología dentro de un orden será w, nuestra querida bolsita de migas de
pan. Así pues:
inicio:
var
c:entero;
w:mismo tipo que s;
fvar;
c:=extremo inferior del rango del vector-1 (cero si el rango es [1..n]; -1 si el rango es [0..n])
w:=triv(x)
finicio
Te recuerdo que ¬B es (c=n) por decisión unilateral. Es decir que recorremos el vector
desde el extremo inferior al superior. Puedes hacerlo al revés, si quieres. Al equipo docente
le gusta así, lo que es un argumento importante.
¿Entonces porqué definimos c fuera de rango?.
Porque esto, en el fondo, es equivalente a lo que poníamos en el punto 3 de la verificación de
programas recursivos: base de inducción. P tiene que dar cierto. Y para eso, como hacíamos
allí,
[0=Σ ”desde 1 hasta 0” de algo]. Sustituimos 0(w=triv(x)) por 1, cierto o falso y Σ por Π ,∀
ó∃.
3.- {P ∧ B} S {P}
Ya que estamos buscando paralelismos que nos orienten, volvemos al caso recursivo. Esto
es, terminologías aparte, igual que el punto 2 de la verificación recursiva. Allí decíamos que:
Q(x) ∧ Bnt ⇒ Q(s(x)).
Como acabamos de decir, {P} es precondición del bucle. Luego:
{P ∧ B} S {P} ≡ P ∧ B ⇒ P(S). Que es lo mismo que en recursión.
Lo cual nos da una idea bastante clara de lo que tenemos que hacer.
Demostrar: Debemos calcular P(S). Dado que P es un invariante, es decir, una fórmula que
no varía a lo largo del bucle, si todo es correcto entonces P ⇒ P.
Construir: Para construir bien un algoritmo iterativo de entrada, necesitamos definir las dos
subfunciones restablecer y avanzar.
Avanzar, la última es fácil. Tenemos que asegurarnos que la función t recorre su camino
hasta el final. Y como estamos hablando de vectores, y como lo estamos recorriendo desde el
extremo inferior (1) al superior (n) y t=(n-c) entonces
avanzar:
c:=c+1
favanzar.
NOTA IMPORTANTE:
Cualquiera acostumbrado a programar escribiría:
“mientras B hacer
c:=c+1;
Rdébil para c;
fmientras.”
Lo cual parece correcto, bonito y fácilmente comprensible. Además es más lógico: avanzar
destruye primero la invariancia y después restablecer cumple con su función.
¿Quieres aprobar?.
Entonces escribe:
“mientras B hacer
R débil para c+1
c:=c+1
fmientras.”
4.- P ∧ B ⇒ t>=0
A t no se le pide que sea un preorden bien fundado. Se le pide que sea natural. Y los
naturales, con la relación < es un orden estricto. Que es más estricto, más fuerte que los
p.b.f.
{P ∧ B} es, para estos efectos, {B}
Es decir, en nuestro supuesto:
(c ≠n) ⇒ (n-c)>=0
Lo cual es cierto si consideramos que c<=n. Que es como lo hemos definido. En Inicio.
5.- {P ∧ B ∧ (t=T)}S{t<T}
Parece ser que hemos terminado. Y que el procedimiento era muy similar al de los
programas recursivos.
¿Donde está entonces el núcleo de la inducción noetheriana, el paso de inducción?.
En el punto 3. No te lo dije para no ponerte nervioso.
TRANSFORMACIÓN RECURSIVO-ITERATIVO.
Q(x)
fun LoQueSea (x, variables de entrada):dev s
inicioA:
var
c: entero (o natural);
w:mismo tipo que s;
inicioB:
c:=extremo inferior -1
w:=triv(x);
mientras c≠ n hacer
restablecer:
w:=w OP Rdébil para c+1;
avanzar:
c:=c+1;
fmientras
dev w;
ffun;
R(x,y) ≡ Rdébil(desde 1 hasta c) ∧ (c=n)
Si estamos repasando un vector para averiguar algo, las probabilidades de que el coste esté
en el orden de Θ(n) son muy altas.
El libro de [Peña97] no es mal libro en este sentido. Conviene memorizar las recurrencias
por resta y por división. Las soluciones, por supuesto.
En el análisis de recurrencias por división nos encontramos con las mismas letras k, b y a. La
diferencia está, como ya he dicho que en el primer caso se calcula (n-b) y en este (n/b).
Comparamos a con bk. Como habitualmente k=0 comparamos a con 1. (No con b. b0=1).
Si a < 1 (difícil. Muy difícil) el coste es Θ(n0) (1).
Si a=1 entonces el coste está en Θ(logn).
Si a>1 entonces el coste está en Θ(n logba). Considerando a=2 y b=2 entonces (n).
FIN
(*) ¿Realmente se contradice a sí mismo?. No. Lo que pasa es que juega al despiste.
Las páginas claves son 42-43, 86-87 y 134.
A continuación en la misma página nos enteramos que un predicado siempre puede ser
reforzado añadiendo una conjunción. Y eso es cierto incluso si el segundo predicado es una
tautología (cierto), es decir, no modificamos el primer predicado. Igualmente puede ser
debilitado añadiéndole una disyunción sea cual sea el segundo predicado.
Pasemos a las páginas 86 y 87. No presumiré de comprender lo que pone en el apartado 2.b
de la página 86. Sin embargo da la impresión que la implicación está equivocada. Si
pasamos al ejemplo 3.3, en la página 87, después de la palabra “obviamente” aparece algo
que sí parece comprensible. Y el símbolo ⇒ está al revés. El libro se contradice a sí mismo.
Despiste 2: Rdébil (pág 87) es más débil que R. Pero ambas son incomparables, según
hemos visto en la página 42.
Despiste 3: Rdébil (a,b,s,i) ∧ (i=n) es equivalente a R. Por tanto no sólo es más débil que R
sino también más fuerte que R. (en sentido no estricto, por supuesto).
La oportunidad más clara que tiene el libro de contradecirse a sí mismo está en la palabra
“obviamente”. Porque no es obvio que la implicación tenga que estar en ese sentido ni
pasaría nada por ponerla en el otro sentido. Y sólo es obvio si comprendemos que las dos
expresiones son equivalentes. Pero el libro no dice que no lo sean. Claro que tampoco dice
en ningún momento que lo sean.
Terminaré con la página 134. En principio es una repetición de las páginas 86-87, pero
enfocado en el invariante P, que es equivalente al Rdébil. Incluso lo reconoce al hablar de la
similitud del método con el presentado en la Sección 3.4. Pero cada vez que utiliza la
expresión “débil” (más débil, debilitar, debilitada, debilitamiento. Hasta 6 veces aparecen
términos que contienen “débil”) lo hace en sentido estricto. Lo que no ayuda nada en
absoluto.
Pero explica porqué en la página 87 se escribe la implicación al revés. Porque aquí se
necesita que P ∧¬B⇒R.
Lo que nos lleva al despiste fundamental que hace que uno se rompa la cabeza y no logre
entender nada:
Despiste 0: El término “más débil” tiene dos significados. Y el libro sólo lo utiliza en
sentido estricto, que es el sentido habitual. Es cuando no utiliza el término cuando lo utiliza
con sentido no estricto. Es decir, cuando nos encontramos la implicación pero el libro se
cuida muy mucho de escribir en palabras la relación que existe entre ambos predicados.
BIBLIOGRAFÍA:
He utilizado un único libro, el primero de los dos alternativos que se citan en la guía del
curso, y tal como es citado por el equipo docente del curso:
NOTA: El curso pasado (1999-2000) me enfrenté a esta asignatura con los mismos
prejuicios y reticencias que todos tenemos debido a los comentarios que hemos oído acerca
de su dificultad. Quizá la tomé como un desafío y por eso le dediqué muchas más horas que
a cualquier otra asignatura.
Como es lógico al principio no entendía nada. La primera luz apareció al consultar la página
de Jerónimo Quesada (http://www.bitabit.com). Es más que interesante que la visitéis sobre
todo a la hora de acometer la práctica.
Cuando conseguí entender de qué iba la asignatura volví sobre mis pasos y empecé a
estudiarla de nuevo.
Lo que tienes ahora en tus manos es el resultado de esas horas dedicadas a la asignatura. He
intentado plasmarlo más que como apuntes como guía para el estudio. Es decir, si yo tuviera
que volver a estudiar la asignatura lo haría siguiendo este orden. Al principio todo te
parecerá soporífero, pero si tienes paciencia, quién sabe, a lo mejor al final hasta te resulta
interesante. Por eso, es importante que al principio tengas un poco de paciencia y te empeñes
en llegar por lo menos hasta la página 17 de esta guía. Si al llegar a este punto has entendido
los ejemplos (páginas 10 a 17), quizá valga la pena que vuelvas a empezar desde el
principio, seguramente lo entenderás todo mucho mejor.
Que tengas mucha suerte.
PRELIMINARES
ESPECIFICACIÓN DE PROBLEMAS
• La especificación ha de ser “precisa” y “breve” por lo que se requiere una “especificación
formal”
• Una técnica de especificación formal, basada en la lógica de predicados, se conoce como
técnica “pre/post” :
Si S representa la función a especificar
Q es un predicado que tiene como variables libres los parámetros de entrada de S
R es un predicado que tiene como variable libres los parámetros de entrada y de salida de S
la especificación formal de S se expresa: {Q} S {R}
• Q se denomina PRECONDICIÓN y caracteriza el conjunto de estados iniciales para los
que está garantizado que S funciona (Se dice que la precondición describe las obligaciones
del usuario del programa).
• R se denomina POSTCONDICIÓN y caracteriza la relación entre cada estado inicial y el
estado final correspondiente. (Se dice que R describe las obligaciones del implementador).
• {Q} S {R} se lee como: “ Si S comienza su ejecución en un estado descrito por Q, S
termina, y lo hace en un estado descrito por R “.
(Si el usuario llama al algoritmo en un estado no definido por Q, no es posible afirmar qué
sucederá. La función puede no terminar, terminar con un resultado absurdo o hacerlo con un
resultado razonable).
• Emplearemos la siguiente notación para S:
a) Cuando las variables que aportan valores de entrada no pueden ser modificadas por el
algoritmo:
fun nombre (p1:t1;…;pn:tn) dev (q1:r1;…qm:rm)
Denominamos parámetros al conjunto de las variables que forman la interface del
algoritmo, distinguiendo entre parámetros de entrada (pi) y parámetros de salida (qj).
Las ti representan los tipos de datos asociados a los parámetros de entrada.
Las rj son los tipos de datos asociados a los parámetros de salida.