INTRODUCCIÓN En este tema vamos a volver al tipo de algoritmo secuencial sin recursividad, al que estábamos acostumbrados en Programación I (en este tema los llamaremos algoritmos iterativos, porque contienen bucles). Hasta ahora hemos aprendido varias cosas sobre este tipo de programas: - - cómo se calcula su eficiencia (tema 1). - - cómo obtener un algoritmo iterativo a partir de uno recursivo. En este tema vamos a centrarnos en tres ideas: cómo se verifica que un algoritmo secuencial/iterativo es correcto. - - - cuáles son los puntos esenciales para la comprobación de que un algoritmo iterativo es correcto (basándonos en el concepto de INVARIANTE) y en lo aprendido en la idea anterior. - cómo se obtiene (deriva) un algoritmo iterativo a partir de una especificación formal. DISEÑO ITERATIVO Lenguaje imperativo: secuencias de órdenes. Los estados por los que pasa la ejecución de un programa, dado un estado inicial, está determinada. La especificación de un algoritmo, como sabemos, viene dada por dos predicados: PRECONDICIÓN: conjunto de estados válidos al comienzo del algoritmo. POSTCONDICIÓN: conjunto de estados alcanzables a su terminación. Para verificar un algoritmo iterativo, vamos a fijarnos en los estados intermedios (entre instrucciones). ASERCIÓN o ASERTO: conjunto de estados alcanzados por un algoritmo en un punto intermedio de su ejecución. Cada uno de ellos representa una afirmación sobre una propiedad que se satisface siempre que el control del programa pasa por el punto donde está situado el mismo. Escribiendo un aserto entre cada dos instrucciones elementales del algoritmo obtenemos una forma de razonar sobre la corrección de un programa imperativo. S = s1;s2;s3;...sn { Q ≡ P0 } s1 { P1 };...;{ Pn-1 } sn { Pn ≡ R } Si el aserto inicial Q se satisface, y cada “programa” elemental si , consistente en una sola instrucción satisface { Pi-1 } si { Pi }, entonces se satisface finalmente la postcondición R y el programa es correcto. 1 Reglas para decidir si una instrucción elemental s satisface una especificación dada {Q} s {R}. Formato a emplear: E1,....,En ------------E1’....,Em’ que significa: de la veracidad de las expresiones E1,....,En se infiere la veracidad de las expresiones E1’....,Em’ Cuando a partir de una instrucción y el aserto que aparece después de ella, nos piden calcular el aserto que debe ir antes de ella, a este predicado se le llama “pmd” o precondición más débil. Vamos a ir calculando, a partir de la postcondición, la precondición más débil de cada uno de las instrucciones del programa, desde la última instrucción hasta la primera. Cada pmd calculada la tomaremos como postcondición de la instrucción anterior, y así sucesivamente hasta el comienzo del programa. La pmd que obtengamos de este proceso será la pmd del programa. Si la precondición que teníamos (Q) cumple Q => pmd (S, R), siendo S el algoritmo y R su postcondición, el algoritmo realizado es correcto. Todas las definiciones que vamos a ver respetan unas normas generales: 1.- Una precondición siempre puede ser reforzada {Q} S {R}, Q’ Q ----------------------{Q’} S {R} 2.- Una postcondición siempre puede ser debilitada {Q} S {R}, R R’ ----------------------{Q} S {R’} 3.- Las precondiciones admisibles para un algoritmo pueden componerse mediante las conectivas ∧ y ∨, siempre que se haga lo mismo con las respectivas postcondiciones. {Q1} S {R1}, {Q2} S {R2} -------------------------------------------------------{Q1 ∧ Q2} S {R1 ∧ R2}, {Q1 ∨ Q2} S {R1 ∨ R2} INSTRUCCIÓN “nada” La instrucción nada tiene como efecto el no hacer ninguna acción 2 ----------------------------{Q} nada {Q} INSTRUCCIÓN “abortar” Esta instrucción impide que se alcance cualquier estado útil -------------------------------{falso} abortar {Q} No existe ningún estado de partida que garantice que la instrucción abortar termina en un estado definido. INSTRUCCIÓN de asignación Esta instrucción x := E tiene como efecto que la expresión E sea evaluada en el estado en curso y su valor sea asignado a la variable x, que pierde su antiguo valor ----------------------------------{ Dom (E) ∧ REx} x::= E {R} Esta regla permite obtener una precondición admisible a partir de la postcondición deseada para la instrucción. E El predicado R x se construye sustituyendo en R todas las apariciones libres de x por la expresión E. Por su parte, Dom(E) es el conjunto de todos los estados en los que la expresión E está definida. Cuando trabajamos con asignaciones múltiples ( la asignación de una lista de expresiones a una lista de variables), utilizamos la notación <x1,...,xn> := <E1,...,En> siendo cada expresión Ei del mismo tipo que la variable xi y todas las xi variables distintas. El significado es que primero se evalúan en el estado en curso todas las expresiones Ei. A continuación, se asignan los valores obtenidos a sus correspondientes variables, que pierden sus valores anteriores. ------------------------------------------------------------{ RE1,E2,...,Enx1,x2,...xn} <x1,...,xn> := <E1,...,En> {R} COMPOSICIÓN SECUENCIAL DE INSTRUCCIONES { Q } S1 { P } , { P } S2 { R} -------------------------------- 3 { Q } S1 ; S2 { R } COMPOSICIÓN CONDICIONAL DE INSTRUCCIONES (simple) Si B entonces S1 si no S2 fsi { Q ∧ B } S1 { R } , { Q ∧ B } S2 { R} -------------------------------------------------------------------{ Q ∧ Dom(B) } Si B entonces S1 si no S2 fsi { R } (múltiple) Caso B1 → S1 ... Bn → Sn fcaso { Q ∧ B1 } S1 { R } ,... ,{ Q ∧ Bn} Sn { R} ----------------------------------------------------------------------------------{ Q ∧ UNA (B1,...,Bn)} caso B1 → S1 ... Bn → Sn fcaso { R } donde UNA (B1,...,Bn) ≡ (∧ ∧ i ∈ {i..n} Dom (bi)) ∧ (N i ∈ {i..n}. Bi) = 1 INSTRUCCIONES ITERATIVAS. INVARIANTES mientras B hacer S fmientras INVARIANTE predicado que describe todos los estados por los que atraviesa el cómputo realizado por el bucle, observados justo antes de evaluar la condición B de terminación. Se denomina así porque se satisface antes y después de cada iteración E. Si el bucle termina, lo hace satisfaciendo el invariante P, y, además, la negación de la condición B. La postcondición del bucle será pues, P ∧ B El invariante P es a la vez precondición y postcondición del cuerpo S de la iteración, ya que ésta modifica el estado pero no las relaciones entre las variables. Además, Dom (B) ha de cumplirse cada vez que la condición B va a ser evaluada, es decir, ha de satisfacerse P Dom(B). También debemos encontrar una función (llamada función limitadora) t: estado→ Z que se mantenga no negativa cada vez que se va a ejecutar el cuerpo S y que decrezca cada vez que se ejecuta éste. Para obtener esta función debemos observar las variables que son modificadas por el cuerpo S del bucle y construir con (algunas de) ellas una expresión entera t que decrezca. Después se ajusta t, si es necesario, para garantizar que se mantiene no negativa siempre que se cumple P ∧ B. Se debe satisfacer: 1.- P ∧ B t≥0 2.- { P ∧ B ∧ t = T} S { t < T}, para cualquier constante entera T. P Dom (B), {P ∧ B} S{P}, P ∧ B t ≥ 0, { P ∧ B ∧ t = T} S { t < T} ---------------------------------------------------------------------------------------- 4 {P} mientras B hacer S fmientras {P ∧ B} El invariante debe ser lo suficientemente fuerte para poder deducir la postcondición después. El invariante debe ser lo suficientemente simple para no restringir el dominio. Los bucles “repetir” y “para” se verificarán a partir de la forma equivalente “mientras” correspondiente. VERIFICACIÓN A POSTERIORI de UN PROGRAMA IMPERATIVO Vamos a trabajar con pequeños programas ya construidos. El esquema de programa con el que vamos a trabajar es: {Q} Inic; Mientras B hacer {P} S; Fmientras {R} Utilizamos este tipo de programas por su generalidad y porque cualquier programa imperativo puede descomponerse en pequeños algoritmos que respondan a este esquema (si hay varios bucles en secuencia, se inventan predicados intermedios, y si hay varios bucles anidados, se comienza verificando el más interior) Reglas a aplicar: 0.- “Inventar” un invariante P y una función limitadora t. Comprobar que P => Dom (B). Pueden existir muchos predicados invariantes de un bucle. El que se necesita ha de ser: lo suficientemente fuerte para que, junto con B, conduzca a la postcondición R. - - - lo suficientemente débil para que las instrucciones de inicialización hagan que se satisfaga antes de la primera iteración y el cuerpo S del bucle lo mantenga invariante. Para hallar el invariante, se puede comenzar tabulando para unas cuantas iteraciones los valores de las variables modificadas por el bucle. Comprobar: 1.- P ∧ B => R 2.- { Q } Inic { P } 3.- { P ∧ B } S { P } 4.- P ∧ B => t ≥ 0 5.- { P ∧ B ∧ t = T } S { t < T } {Q≡ n ≥ 0} 5 fun iisumaCit (v: vector) dev (s: entero) <s,i> := <0,1> mientras i ≤ n hacer {P} s := s + v[i]; i := i + 1; fmientras ffun { R ≡ s = α ∈ {1 ..n}. v [α] } 0.- Inventamos un invariante para el problema y una función limitadora Realizamos una tabulación con los valores de las variables en el bucle para ver cuál es la relación que guardan i 1 2 3 4 5 s 0 v[1] v[1] + v[2] v[1] + v[2] + v[3] v[1] + v[2] + v[3] + v[4] Como se debe cumplir, además P ∧ B => R, escogemos como invariante (por el momento) P ≡ 1 ≤ i ≤ n+1 ∧ s = Σ α ∈{1..i-1}v[α]. Hay que comprobar que P => Dom (B) 1 ≤ i ≤ n+1 ∧ s = Σ α ∈{1..i-1}v[α] => Dom (i ≤ n) Como función limitadora elegimos t = n – i, ya que es una expresión en función de s y de i que se mantiene no negativa y que decrece en cada vuelta del bucle. Vamos a comprobar los 5 puntos de la verificación. 1.- P ∧ B => R (1 ≤ i ≤ n+1 ∧ s = Σ α ∈{1..i-1}v[α] ) ∧ i > n => s = Σ α ∈{1..n}v[α] cierto 2.- { Q } Inic { P } Verificaremos el siguiente trozo de programa: {Q≡ n ≥ 0} <s,i> := <0,1> {P≡ 1 ≤ i ≤ n+1 ∧ s = Σ α ∈{1..i-1}v[α]} pmd (P, ins) = precondición más débil de la ejecución de ins con postcondición R. Q => pmd (P, <s,i> := <0,1>) 0,1 Q => P s,i Q => (1 ≤ 1 ≤ n+1 ∧ 0 = Σ α ∈{1..0}.v[α] ) Q => 0 ≤ n n ≥ 0 => 0 ≤ n cierto 3.- { P ∧ B } S { P } Verificaremos el siguiente trozo de programa: 6 { 1 ≤ i ≤ n+1 ∧ s = Σ α ∈{1..i-1}v[α] ∧ i ≤ n} s := s + v[i]; i := i + 1; {1 ≤ i ≤ n+1 ∧ s = Σ α ∈{1..i-1}v[α] } P ∧ B => pmd (P, S) pmd (P, S) ≡ (Pii+1 )ss+a[i] ≡ (1 ≤ i+1 ≤ n+1 ∧ s = Σ α ∈{1..i}v[α] ) ss+a[i] ≡ (1 ≤ i+1 ≤ n+1 ∧ s + a[i] = Σ α ∈{1..i}v[α] ) (1 ≤ i ≤ n+1 ∧ s = Σ α ∈{1..i-1}v[α] ) ∧ (i ≤ n) => (1 ≤ i+1 ≤ n+1 ∧ s + a[i] = Σ α ∈{1..i}v[α] ) cierto 4.- P ∧ B => t ≥ 0 (1 ≤ i ≤ n+1 ∧ s = Σ α ∈{1..i-1}v[α] ) ∧ (i ≤ n) => (n – i ≥ 0) cierto i 5.- { P ∧ B ∧ t = T } S { t < T } Verificaremos el siguiente trozo de programa { 1 ≤ i ≤ n+1 ∧ s = Σ α ∈{1..i-1}v[α] ∧ i ≤ n ∧ T = n-i} s := s + v[i]; i := i + 1; { n-i < T } (P ∧ B ∧ t = T) => pmd (t<T, S) pmd (t<T, S) ≡ ((n-i < T) ii+1 )ss+a[i] ≡ ( n – i – 1 < T ) ss+a[i] ≡ ( n – i – 1 < T ) (1 ≤ i ≤ n+1 ∧ s = Σ α ∈{1..i-1}v[α] ∧ i ≤ n ∧ T = n - i) => ( n – i – 1 < T ) cierto 7 DERIVACIÓN FORMAL DE PROGRAMAS IMPERATIVOS Programación “dirigida por objetivos”: tenemos la postcondición y vamos a hacer nuestro programa en función de ella. La técnica incluye: - - Derivación de instrucciones simples - - Derivación de bucles: 1.- Precisar la especificación (Sobre todo, la postcondición) 2.- Derivar el invariante a partir de la postcondición. 3.- Derivar cada parte del programa (condición de terminación, inicializaciones y cuerpo del bucle), a partir del invariante. El tipo de programas con el que vamos a trabajar es: {Q} Inic; Mientras B hacer {P} S; Fmientras {R} DERIVACIÓN DE INSTRUCCIONES SIMPLES Cuando no necesitamos bucles, podemos deducir instrucciones simples de un modo semiformal, partiendo siempre de la postcondición R que deseamos para cada instrucción. - Si la postcondición R incluye igualdades de la forma x = E, siendo E una expresion, se ensayarán en primer lugar instrucciones de asignación de la forma x := E, que traten de establecer dichas igualdades. - - Para cada instrucción de asignación se calculará la precondición Dom(E) ∧ R Ex suficiente para que dicha asignación establezca R. E - Si la precondición Q de la instrucción garantiza Dom(E) ∧ R es todo lo que necesitamos. - Si no es así, se analiza qué condición adicional Bx se necesita para que Q ∧ Bx R. Si la disyunción de todas las condiciones Bx para las distintas instrucciones de asignación ensayadas es implicada por Q, entonces una instrucción condicional de la forma caso B1 -> x1 := E1 ... Bn -> xn := En fcaso es lo que necesitamos para establecer R. 8 x , la asignación x := E DERIVACIÓN DEL INVARIANTE A PARTIR DE LA POSTCONDICIÓN Son técnicas aproximadas, no hay un método “fijo”. Una de las técnicas consiste en ver el invariante como un predicado cuya componente principal es un predicado más débil que la postcondición. Técnicas para debilitar un predicado: - Eliminar una conjunción. Si R es de la forma R1 ∧ R2, una de las dos conjunciones ( la que sea más fácil de establecer al comienzo del bucle, se tomará como P. La otra será directamente B, de forma que P ∧ B conducen obviamente a R. - Incorporar una variable. Si en R sustituimos cierta expresión φ(x), donde x representa el conjunto de variables que intervienen en el bucle, por una variable nueva w, obtenemos un nuevo predicado P(x,w), que satisface la siguiente implicación P(x,w) ∧ (w = φ(x)) R Tomaremos P como invariante y w = φ(x) como condición de terminación B. DERIVACIÓN DE BUCLES A PARTIR DE INVARIANTES - Conociendo el invariante P y la postcondición R del bucle, determinar cuál ha de ser B de forma que se satisfaga P ∧ B R. A partir de B, obtener B. - Conociendo P, determinar unas instrucciones de inicialización Inic, lo más simples posibles ( normalmente asignaciones), que establezcan P al comienzo del bucle y que satisfagan {Q} Inic {P}. En este proceso pueden ser necesarios algunos ajustes a la precondición Q. - - Hasta el momento, tenemos {Q} Inic; Mientras B hacer {P} ¿? Fmientras {R} Ahora debemos derivar el cuerpo S del bucle, para lo cual seguiremos los siguientes pasos: a) a) Incluimos, al final de S, una instrucción avanzar, que haga progresar el bucle hacia su terminación. (conocemos el valor inicial de las variables y la condición B) que hace terminar el bucle. También se conoce la estrategia general de diseño del bucle, con lo que no es difícil conjeturar una función limitadora t. La instrucción avanzar ha de hacer decrecer t. b) b) Para restablecer la invariancia que incluye la instrucción avanzar, debemos incluir otras instrucciones que restablezcan dicha invariancia, ya que ha de cumplirse { P ∧ B } S { P }. Las llamaremos restablecer. El cuerpo del bucle va a quedar: 9 Mientras B hacer {P} {P ∧ B} restablecer; avanzar; {R} Fmientras Supongamos que tenemos que derivar el cuerpo del algoritmo cuya postcondición es fun iisumaCit2 (v: vector) dev (s: entero) { R ≡ s = α ∈ {1 ..n}. v [α] } El primer paso es conseguir un invariante adecuado para el bucle. Para ello debilitaremos la postcondición. R≡s= R’ ≡ s = α ∈ {1 ..n}. v [α]} α ∈ {1 ..i}. v [α]} ∧ i = n) Elegimos como invariante P ≡ (s = α ∈ {1 ..i}. v [α]) ∧ (0 ≤ i ≤ n) (añadimos condiciones de dominio sobre los valores de la variable i Como condición de terminación tendremos i = n (por lo tanto B ≡ i < n) Ahora añadiremos las condiciones de inicialización que hagan que se cumpla el invariante al comienzo de la ejecución del mismo y que permitan el posterior avance del mismo. Estas instrucciones serán i := 0; s := 0; Instrucción avanzar i := i + 1; Instrucción restablecer s := s + v[i +1] Función completa n ≥ 0} fun iisumaCit2 (v: vector) dev (s: entero) <s,i> := <0,0> mientras i < n hacer {P(s = s := s + v[i+1]; i := i + 1; fmientras ffun { R ≡ s = α ∈ {1 ..n}. v [α] } {Q≡ α ∈ {1 ..i}. v [α]) ∧ (0 ≤ i ≤ n) } 10