Algoritmos Cualquiera en su vida ha encontrado e implementado, con distintos grados de éxito, “procedimientos”, ya sea para llenar una forma, para armar algo o para cocinar. Y cualquiera también sabe de algunas de las limitaciones con las que vienen acompañados: escrituras con términos o sintaxis incomprensibles, operaciones sugeridas imprecisas o irrealizables, obtención de un resultado diferente del esperado o el tiempo de elaboración es exageradamente largo. Cuidarse contra esos infortunios es esencial: por lo tanto es necesario fijar un marco para la expresión de tales procedimientos. Sin intentar desarrollar una definición precisa de la palabra algoritmo, que deriva del nombre del matemático Al Juarismi (Bagdad, siglo IX), nos conformaremos con ver cuál es su uso o función: dentro de este marco, la funcion de un algoritmo es guiar un procesador (un ejecutante) en la ejecución de un conjunto de acciones básicas. El objetivo de este capítulo es presentar el modo de escritura de los algoritmos que utilizaremos en lo que sigue, apoyándonos en un algunos ejemplos simples. Será también ocasión de introducir dos nociones básicas sobre algoritmos, la noción de invariante como auxiliar para la prueba de un algoritmo y la noción de algoritmo no determinista. 1 Ejemplo 1: el algoritmo Enigma Empecemos con un enigma: ¿qué produce el algoritmo siguiente? A partir de este primer algoritmo, precisaremos de abordar algunos elementos de la terminología. Primero echaremos un ojo a la traza, ésto es una descripción de la ejecución del algoritmo para un conjunto datos dados, para finalmente resolver el enigma. La notación ⌊x⌋ designa la parte entera del número x. Algorithm 1: Enigma Entrada: dos números naturales a y b Resultado: ? x ←− a y ←− b z ←− 0 mientras x 6= 0 hacer si x es impar entonces z ←− z + y x ←− ⌊x/2⌋ y ←− y × 2 regresa z fin 1 En el contexto que usaremos aquí, una acción básica es la asignación, ordenadas por la escritura: h variable i ←− h expresión i es decir que una asignación es de la forma del nombre de una variable seguida por el símbolo ←−, seguida de la escritura de una expresión, como por ejemplo: z ←− z + y. En el algoritmo Enigma, las asignaciones que intervienen son: • 3 variables: x, y y z; • 6 expresiones: a, b, 0, z + y ⌊x/2⌋ y y × 2. Las expresiones son expresiones en el sentido algebraico del término, esto es, arreglos con respecto a las reglas de formación aplicadas a expresiones básicas que son las constantes y las variables en el entorno de cómputo. La ejecución de una asignación es un trabajo que se ejecuta en dos tiempos: 1. evaluar la expresión (llamada miembro derecho de la asignación) en función del valor actual de las varaibles que han tenido una ocurrencia y aplicar las reglas de cálculo “usuales”; 2. asignar a la variable dada (llamada miembro izquierdo de la asignación) el valor resultante de la evaluación precedente. Por ejemplo, ejecutar la asignacion z ←− z + y si los valores de las variables y y z son respectivamente seis y once, respectivamente, nos lleva a: 1. evaluar la expresión z + y (miembro derecho de la asignación), lo que nos conduce al resultado de 17, 2. asignar este valor 17 a la variable z (miembro izquierdo de la asignación). Si la asignación resultante z ←− z + 2y, la variable z tednrá el valor 29 después de la ejecución. Otra acción básica es: regresar h expresión i dada por la palabra reservada regresar y donde la instrucción consiste en regresar el resultado final de la evaluación de la expresión indicada después de esta palabra reservada. En nuestro ejemplo, el resultado del cálculo efectuado por el algoritmo es el valor de la variable z cuando el programa termina (pensando por supuesto que termina). El papel de las otras palabras reservadas (las palabras en negritas en el algoritmo) utilizadas como en el ejemplo se comprende gracias a la noción de instrucción: • una acción básica es una instrucción. 2 • si uno llama condición a toda expresión booleana, es decir, toda expresión donde el resultado de la evaluación sea el valor verdadero, sea el valor falso entonces las cuatro formas sintácticas siguientes son instrucciones: 1. si condición entonces instrucción 2. si condición entonces instrucción 1 sino instrucción 2 3. mientras condición hacer instrucción 4. instrucción 1; instrucción 2; ..., instrucción k sobre una misma línea o 4. | instrucción 1 | instrucción 2 | ... | instrucción k Para fijar ideas, diremos que: 1. una instrucción de tipo 1 es una estructura condicional; 2. una instrucción de tipo 2 es una estructura alternativa; 3. una instrucción de tipo 3 es una estructura iterativa; 4. una instrucción de tipo 4 es una estructura secuencial La ejecución de estas instrucción puede ser descrita con la ayuda de los diagramas: Notemos que, por la definición de instrucción iterativa: “mientras C hacer I”, donde C representa una condicion e I una instrucción llamada el cuerpo de la iteración, si el valor regresado por la evaluación de C es falso, la ejecución de la iteración se termina, si el valor es verdadero, la instrucción I se ejecuta y sera repetida hasta que la condición C tome el valor de falso. A priori, no sabemos si la iteración terminará ni el número de veces que la iteración deberá ejecutarse. Así, si k es un entero positivo cualquiera, definiremos la sucesión de Syracusa (un ) por u0 = k y un+1 = un /2 si un es par, (3un + 1)/2 si un es impar. No sabemos para que valor de k la sucesión (un ) no tiene nunca un término igual a uno, pero tampoco podemos demostrar que para todo valor de k, la sucesión (un ) tiene un término igual a uno. En consecuencia, no sabemos si en las instrucciones u ←− k; mientras u 6= 1 hacer si u es un número par entonces u ←− u div 2 sino u ←− (3u + 1) div 2“ la repetición termina y, si es el caso, el número de veces que se repetirá la iteración. Se sigue que dos casos extremos particulares pueden presentarse: 1. La ejecución repite la iteración sin cambiar el valor de la condición, como por ejemplo con la instrucción: ”mientras x = x hacer x ←− x + 1“ y la ejecución de la iteración genera la ejecución de un número infinito de acciones básicas. Tal 3 situación no es admisible y será conveniente probar que para todo algoritmo que use iteraciones, que tal condición es imposible para el juego de datos declarados válidos para el algoritmo. 2. La condición para la ejecución de la iteración siempre es falsa, como por ejemplo con la instrucción ”mientras x 6= x hacer x ←− x + 1“. En otras palabras, la instrucción I debe ”repetirse cero veces“, en cuyo caso diremos que la iteración ha sido ejecutada y que esta ejecución se traduce en la forma de una acción nula (la acción nula se invoca de forma similar cuando se ejecuta la condicional ”si C entonces I“). La lista de los cuatro tipos de instrucciones definidas arriba no es exhaustiva y los lenguajes de programación usualmente los ofrecen con variantes y adiciones. Notemos que la iteración de tipo por usualmente es usada (donde I es una instrucción cualquiera): para i ←− 1 hasta n hacer I que podemos considerar como equivalente a la secuencia: y es conveniente usarlo i ←− 1; mientras i ≤ n hacer I; i ←− i + 1; fin siempre que conozcamos el número específico de iteraciones a realizar. Más aún, podemos mostrar que las estructuras secuencial e iterativa mientras serán por sí mismas suficientes para llevar a cabo todos nuestras cálculos, todos las otras instrucciones pueden reducirse a ellas 1 . Por ejemplo, la instrucción condicional ”si C entonces I“ puede ser definidia con un atajo equivalente a escribir la siguiente secuencia, donde b es una variable boleana. y aparece como un elemento de comodidad para expresar algoritmos y no como un tipo de instrucción que abra posibilidades nuevas de cálculo en el modelo que sólo posee las secuencias y la iteración mientras. Regresando al algoritmo Enigma, para dar una traza (figura ): se fijan valores numéricos para los datos del algoritmo, por ejemplo diez para a y trece para b, y se ejecutan acción básica tras acción básica, escrupulosamente siguiente las instrucciones dadas por el enunciaado del algoritmo. Notemos que el algoritmo requiere que el ejecutante sepa: 1 Sin embargo, se conocen algoritmos escritos según el modelo secuencia+iterativa mientras que no pueden ser escritos según el modelo secuencia+iterativa para 4 b ←− C; mientras b hacer I; i ←− falso; fin • sumar dos números; • diferenciar los números pares de los impares; • obtener la mitad de un número; • doblar un número; • comparar un número con cero. x 10 y ? 13 0 z ? ? 0 5 26 26 2 52 1 104 130 0 208 instrucción o expresion evaluación x ←− a y ←− a z ←− 0 x 6= 0 verdadero x es impar falso x ←− ⌊x/2⌋ y ←− y × 2 x 6= 0 verdadero x es impar verdadero z ←− z + y x ←− ⌊x/2⌋ y ←− x × 2 x 6= 0 verdadero x es impar falso 1 ←− |x/2| x ←− ytimes2 x 6= 0 verdadero x es impar verdadero z ←− z + y x ←− ⌊x/2⌋ y ←− y × 2 x 6= 0 falso regresa z 130 5 Se deja al lector probar otras trazas. Algunos experimentos o la simple lectura del algoritmo para lectores más sofisticados, permiten conjeturar que es para calcular el producto de los números dados en la entrada, lo interesante aquí es hacerlo desde la duplicación y la división por dos. Podría ser conveniente, especialmente para usar en otros algoritmos, ver el algoQ ritmo como formado por un solo enunciado, que podríamos llamar (a, b) (preferentemente después de haber demostrado que ese algoritmo es ciertamente el producto de a por b) y poder analizar la estructura. En efecto, estas instrucciones se descomponen en una secuencia de cinco instrucciones (figura 3): tres asignaciones, una iteración y una instrucción de regreso. Las tres primeras instrucciones y la última instrucción son instrucciones elementales, es decir, que su ejecucion se traduce directamente por la ejecución de una acción básica. En contraste, la cuarta es una iteración cuyo cuerpo se descompone en uuna secuencia de tres instrucciones donde la primera es una condicional que también se puede descomponer y donde las dos últimas son asinganciones, que no son descomponibles. Notemos para la memoria que este concepto de instrucciones que, a partir de los años 60, se empezaba a hablar de programación estructura por oposicón al modo de programación anterio llamada programacion con ’goto’2 Pero una sola traza o mil trazas, no muestran que tenemos una técnica universal de multiplicación (técnica sin utilización de tablas, será notado). Así, ¿cpomo podemos probar que dados dos enteros naturales a y b, el resutlado del algoritmo es siempre su producto ab? La prueba se hace en dos partes: primero mostraremos que la iteración del algoritmo converge y, dando dos enteros positivos a y b, el resultado regresado por el algoritmo es el producto ab. • El algoritmo converge Cuando el algoritmo incluye una o más iteraciones, debemos mostrar que cada una converge, es decir, que para todas las entradas permitidas de datos, su ejecución termina en algún momento. Lo que siempre mostraremos para cada iteración es una expresión que, tomando en cuenta la evolución de los valores de las variables que ocurren alguna vez, sólo pueden tomar un número finito de valores (una expresiones puede ser reducida a una sola variable). En nuestro ejemplo, en cada iteración, es decir, en cada ejecución del cuerpo de la iteración, el valor de la variable x es modificada por la única asignación x ←− ⌊x/2⌋. Uno deduce que la sucesión de valores tomadas sucesivamente por la variable x es un sucesión de enteros, estrictamente decrecientes mientras no alcance el 0 y la ejecución de la iteración termia en cero. Como sólo existe un número finito de valores entre cero y el valor inicial, la iteración converge. 2 Programación en la que el algoritmo se expresa como un secuencia de instrucciones ejecutadas en secuencia, algunas instrucciones están identificadas por una etiqueta; la instrucción ”goto etiq“ permite romper la secuencia de ejecución para hacer la instrucción marcada con la etiqueta etiq. 6 Nota. Analicemos qué pasa si, debido a un erro tipograico, la condición x 6= 0 se escribe como y 6= 0. En este caso, ninguan ejecución del algoritmo para la cual la entrada b es diferente de 0 terminará después de un número finito de acciones básicas: el algoritmo que tenga este error no convergerá. • Ahora mostraremos que el algoritmo Enigma calcula el producto ab de los números a y b provistos en la entrada. Propondremos una técnica, llamada demostración invariante, donde el objetivo es estables los valores de ciertas variables escogidas son ligadas de manera especial al final de la ejecución de la iterativa, habiendo probado que esta termina (de aquí el requiisto de mostrar la convergencia de la iteración). Siguiendo con el ejemplo del algoritmo Enigma, definamos la proposición: z + xy = ab (P ) 1. La propiedad (P) se preserva por la iteración. Lo que queremos decir es que si la propiedad (P) se satisface antes de la ejecución de una iteración, se satisface después de la ejecución de la iteración. Regresando a nuestro ejemplo y a la propiedad (P) definida arriba, supongamos que en la ejecución de la iteración las tres variables x, y y z tienen valores asignados de la última ejecución de la iteración y que la propiedad (P) se satisface, es decir, z +xy = ab. En otras palabras, llamemos, respectivamente, α, β y γ a los valores de esas tres variables obtenidas en la última ejecución de la iteración. Supongamos que tenemos: α 6= 0 y γ + αβ = ab. Ahroa debemos verificar que la propiedad (P) se sigue satisfaciendo por las variables x, y y z después de la ejecución de la iteración, con los nuevos valores que reciban: que es el cuerpo de la iteración. si x es impar entonces z ←− z + y x ←− ⌊x/2⌋ y ←− y × 2 La primera instrucción es una condicional cuya condición es ”x es impar“, analicemos qué casos son posibles: (a) Primer caso: el valor α asignado a x es par, sea α = 2k. 7 La ejecución de la condicional conduce a no hacer nada. Las instrucciones restantes asignan a x el valor k y la tercera y última instrucción de la iteración asigna a y el valor 2β. ¿La propiedad (P) se sigue satisfaciendo? La respuesta está dada por la evaluación de la expresión z + xy calculada con los nuevos valores de x, y, z: z + xy 7−→ γ + k(2β) = γ + (2k)β = γ + αβ = ab. (b) Segundo caso: el valor de α asignado a x es impar, sea α = 2k + 1. Ya que α es un entero impar, la ejecución de la condicional nos lleva a ejecutar la asignación de z del resultado de la evaluación de la expresión z + y, a saber el valor de γ + β. La instrucción siguiente asinga a x el valor k y la tercera y última instrucción de la iteración asigna a γ el valor 2β. La evaluación de la expresión z + xy entonces da: z + xy 7−→ (γ + β) + k(2β) = γ + (2k + 1)β = γ + αβ = ab. De los dos casos la propiedad (P) se sigue satisfaciendo después de la ejecución de la iteración, de ahí que sea preservada por la iteración. 2. La propiedad (P) es estable antes de la iteración. Queremos decir que las instrucciones que preceden la iterativa, que constituyen la secuencia ”x ←− a; y ←− b; z ←− 0“ y que sirven para inicializar las variables, aseguran que la propiedad (P) se satisface cuando empieza la ejecución de la iteración: se tiene: z + xy = ab. 3. Una aplicación del principio de inducción nos permite así concluir que en cada iteración la propiedad (P) se satisface y que en particular, si la iteración converge, su ejecución termina cuando satisface: non(x 6= 0) y z + xy = ab de donde concluimos que z = ab. Nota. Si un desafortunado error tipográfico (¿otra vez?) nos lleva a escribir ”z ←− 10“ en vez de ”z ←− 0“ que está en la última instrucción que precede a la interacion, el algoritmo convergerá, P será bien preservado por la iteración pero no nos será de mayor interés, naturalmente. Así, para establecer que una propiedad (Q) está establecida por la ejecución de una iteración ”mientras C hacer I“, el pirncipio de demostración por invarientes que hemos utilizado consiste en mostrar que: • la iteración converge; 8 • una propiedad (P) es estable antes de la iteración; • esta misma propiedad (P) es preservada por la iteración, es decir, que si uno ejecuta la instrucción I bajo la doble hipótesis C y (P), la propiedad (P) se sigue satisfaciendo después de esta ejecución; • finalmente, (no C y (P)) ⇒ (Q). Antes de terminar con este eejmplo, notemos que este algoritmo es similar al que escribió el egipcio Ahmes describió en el papirus de Rhind (1650 a.C.) para multiplicar dos números. Tal operación ha sido por largo tiempo preocupación de los adultos más hábiles en el campo numérico. Hoy en día, poemos dar una versión elemental de primaria del método implementado por el algoirimto Enigma (que convendrá renombrar como Algoritmo producto): 1. bajo el número a, construye una columna tal que cada número sea la mitad o la mitad del piso del número que le precede; hasta parar en el valor uno. 2. bajo el número b, construye una columna tal que cada número sea el doble del que le precede; uno construye esta columna tan larga como la primera; 3. construir una tercera columna de la forma siguiente: por cada línea, copiamos el valor de la segunda columna cada vez que el contenido de la primera columna es un número impar; 4. el resutlado de la multiplicación de a por b se obtiene al sumar los números de la tercera columna. 9