Versos Algorítmicos iniciales, 1. ecabrera. Comunidad Algorítmica

Anuncio
Versos Algorítmicos iniciales, 1. ecabrera. Comunidad Algorítmica SDQDR. Febrero 2014
mod
Se dice que el número 6 es perfecto, porque es igual a la suma de sus factores propios, esto es, 6 = 1 + 2 + 3.
Un factor propio de un entero positivo n es un número entero positivo d menor que n y que lo divide exactamente.
Si d divide a n exactamente el residuo de la división de n por d es cero. En nuestro caso, los residuos de 6/1, 6/2, 6/3, son todos cero.
Adoptemos el operador mod de forma tal que al colocarlo entre dos valores enteros nos proporcione el residuo de su división.
Obtener la suma de los factores propios de n > 0
Declara s, n, d
Asigna s, 1; d, 2
Lee n
Mientras d < n
Si n mod d = 0
Asigna s, s + d
Asigna d, d + 1
Escribe s
Notemos que la suma inicia con 1 y no con cero porque 1 siempre es factor de n. El propósito deja claro que n ha de ser positivo.
Notemos la sentencia Asigna dentro del Si se ejecuta sólo cuando n mod d es cero, pero Asigna d, d +1, se ejecuta siempre que se ejecute el ciclo.
Esto se evidencia por los sangrados, que sitúan la primera asignación subordinada al Mientras y al Si.
Pero la segunda sólo al Mientras. Y notemos que la sentencia Escribe no está subordinada al ciclo, por lo que se ejecuta una sola vez.
Es fundamental observar el significado e importancia de los sangrados, pues nos dan la estructura lógica del algoritmo.
verdadero
Ocurre que como 28 = 1 + 2 + 4 + 7 + 14, también 28 es perfecto, pues iguala a la suma de sus factores propios.
Adoptemos la convención de que, en un contexto lógico, el valor 1 significa verdadero, y 0 significa falso.
Los números perfectos son relativamente escasos. Asumir, que un natural no es perfecto, es pues, razonable.
Determinar si n > 0 es perfecto, como 6 = 1 + 2 + 3
Declara p, n, d, s
Asigna p, 0; d, 2; s, 1
Lee n
Mientras d < n
Si n mod d = 0
Asigna s, s + d
Asigna d, d + 1
Si s = n
Asigna p, 1
Escribe p
Notemos que se asigna a p el valor lógico falso, esto es 0. Este valor es el que escribe el algoritmo por omisión.
A menos que al salir del ciclo resulte que el valor acumulado en s igual al que se lee para n al principio, entonces p pasa a ser 1, verdadero.
Este algoritmo, por tanto, siempre escribe un 0, significando falso (que n no es perfecto) a menos que s sea igual a, y p se haga 1, verdadero.
Este algoritmo, pues, aplica la filosofía de que un número NO es perfecto hasta que se demuestre lo contrario.
abundante
Decimos que un número natural es abundante cuando es menor que la suma de sus factores propios, como 12 < 1 + 2 + 3 + 4 + 6 = 16.
Determinar si t > 0 es abundante, como 12 > 1 + 2 + 3 + 4 + 6 = 16
Declara a, t, d, s
Asigna a, 0; d, 2; s, 1
Lee t
Mientras d < t
Si t mod d = 0
Asigna s, s + d
Asigna d, d + 1
Si s > n
Asigna a, 1
Escribe a
Notemos que hemos adaptado, reciclado, el algoritmo anterior para determinar si un número era perfecto.
En ambos casos se calcula la suma de los factores propios del número leído, sea n, o sea t.
Cuando la suma resulta ser igual al número, es perfecto; cuando sea mayor, es abundante.
En ambos casos, iniciando p en 0, se asume que n sea perfecto o t sea abundante. Se cambia si resultar serlo.
En ambos casos es claro que d toma todos los de 2 hasta n (o t), pero que el ciclo se ejecuta hasta n-1 (o t-1).
coc
Si dividimos un número natural, m, por otro, n, y desechamos la parte decimal, obtenemos el cociente entero de m/n.
Adoptemos, pues, el operador coc para incorporar el cálculo de cocientes enteros en nuestros algoritmos.
Así, 123 coc da 10; 12 coc 10 da 1; 1 coc da 0. En cada caso se obtiene el mismo número, pero sin el último dígito.
Salvo el caso de que el número tenga un solo dígito, pues entonces n coc 10 da 0, hecho que podemos utilizamos a conveniencia.
Obtener la cantidad de dígitos de n > 0
Declara n, c
Asigna c, 1
Lee n
Mientras n > 9
Asigna n, n coc 10
Asigna c, c + 1
Escribe c
Notemos que si n tiene un solo dígito la condición del ciclo es falsa, se salta el ciclo y se escribe 1, lo cual es correcto.
Pero si n tiene más de un dígito (asumiendo que es positivo) el ciclo se ejecuta una vez por cada dígito adicional.
Cada ejecución del ciclo “corta” un dígito a n, y como c se incrementa en 1 cada vez que se ejecuta el ciclo, registra el total de dígitos.
Notemos, además, que n puede ser arbitrariamente grande en teoría, pero Spike trabaja sólo con enteros con a lo sumo 18 dígitos.
dígitos
Vimos que el operador mod nos da el residuo el entero que sobra del cociente entre dos enteros y coc la parte entera de ese cociente.
Más aún, n mod 10 siempre nos el último dígito de n, y n coc 10 siempre da n sin su último dígito actual.
Combinando, pues, los operadores mod y coc podemos “separar”, uno a uno, los dígitos de un entero positivo,
Sumar dígitos del natural n
Declara n, d, s
Asigna s, 0
Lee n
Mientras n > 0
Asigna d, n mod 10
Asigna s, s + d
Asigna n, n coc 10
Escribe s
Notemos que se asume que n es entero positivo, por lo que el ciclo ha de ejecutarse al menos una vez, de hecho, una vez por cada dígito.
Por cada ejecución del ciclo se aparta en d el último dígito actual de n, se acumula en s y se corta un dígito a n.
Al salir del ciclo se escribe la suma de todos los dígitos de n, incluso si tiene un solo dígito, ya que la condición se cumple una vez en tal caso.
Si n vale 123, los valores de d son 3, 2, 1; los de s son 0, 0+3 = 3, 3+2 = 5,5+1 = 6; y los de n son 123, 12, 1, 0.
Como el último valor de s es 6, la suma de los dígitos de 123, es correcta. Pero funciona para un valor arbitrario de n.
invariante
3
3
3
Hay números, como 370 = 3 + 7 + 0 , que son iguales a la suma de los cubos de sus dígitos. Se llaman cubos narcisistas.
El nombre viene de la leyenda griega en la que Narciso se enamoró de sí mismo al ver su reflejo sobre agua tranquila.
Los cubos narcisistas son un caso especial de los llamados invariantes digitales, de la forma d1d2Hdn = d1m + d2mHdnm.
Notemos que no necesariamente n = m. Cuando n = m = 3, tenemos los cubos narcisistas. Pero n y m pueden ser naturales arbitrarios.
Determinar si t > 0 es un cubo narcisista
Declara n, t, s, d, c
Asigna n, 0; s, 0
Lee t
Asigna c, t
Mientras t > 0
Asigna d, t mod 10
Asigna s, s + d*d*d
Asigna t, t coc 10
Si s = c
Asigna n, 1
Escribe n
Se asigna 0 a n para asumir que t NO es un cubo narcisista, premisa que se cambia en el caso de que la suma s resulte igual a t.
La suma, s, se inicia en 0, y se le van acumulando los cubos de los dígitos de t, d*d*d, los cuales se obtienen con t mod 10.
Notemos que antes de entrar al ciclo el valor de t se copia a c. Esto así porque el valor de t siempre es cero al salir del ciclo.
La variable c es una copia del valor original de t. Por eso al salir del ciclo se compara la suma, s, con c, y no con t.
Si s resulta ser igual a t es porque t es un cubo narcisista. Luego del ciclo se escribe 1 ó 0, según t sea narcisista, o no.
menor
Para obtener el menor de varios números dados, podemos empezar leyendo el primero y asumirlo como el menor, hasta prueba en contrario.
Luego, en un ciclo vamos leyendo el resto de los números y comparamos cada uno con el asumido como menor.
Si el valor recién leído es menor que el hasta ahora considerado el menor, se cambia el menor por este nuevo valor.
Por tanto, al salir del ciclo tendremos en la variable original el menor de todos los valores comparados.
Claro, lo primero es leer la cantidad de valores entre los cuales se va a obtener el menor,
Obtener el menor de t números
Declara m, t, n, c
Asigna c, 1
Lee t, m
Mientras c < t
Lee n
Si n < m
Asigna m, n
Asigna c, c + 1
Escribe m
Notemos que se lee la cantidad de valores, t, y el primer valor, que es el menor por omisión.
El contador, c, se inicia en 1, no en 0, porque el primer valor se lee antes del ciclo, para contarlo antes.
En el ciclo, cada vez, se lee un valor para n, y se compara con el valor actual de m, el menor hasta el momento.
Si n es menor que m, se cambia el valor de m por el de n, pues se ha encontrado uno menor aún. Al final, se escribe a m, el menor de todos.
rango
Podemos adaptar los versos del algoritmo anterior para obtener la diferencia entre el mayor y el menor de t valores dados.
Obtener el rango de t números: mayor - menor
Declara me, ma, t, n, c
Asigna c, 1
Lee t, me
Asigna ma, me
Mientras c < t
Lee n
Si n < me
Asigna me, n
Si n > me
Asigna ma, n
Asigna c, c + 1
Escribe ma – me
Empezamos leyendo a t, la cantidad de valores, y a me, asumido como menor. Luego se inicia ma, el mayor, como el menor también.
Notemos que si t = 1 (sólo se lee t y me) o todos los otros valores leídos son iguales, el mayor y el menor coinciden, y el rango es cero.
En el ciclo se compara cada valor leído, con me y si es menor se actualiza me. También actualiza ma y n es mayor que ma.
Notemos que al salir del ciclo pedimos a Spike que escriba la diferencia entre ma y me, ma – me.
Spike puede, pues, calcular el valor de una expresión aritmética en una sentencia Escribe y desplegarlo.
Es claro que la diferencia entre ma y me es el rango que busca el algoritmo.
intervalo
Podríamos necesitar asegurarnos de que el valor que el usuario ingresa está dentro de los valores posibles en un intervalo.
Supongamos que los extremos del intervalo son 1 y 100, esto es que el valor deba estar entre 1 y 100, ambos inclusive.
Aceptar un valor sólo en el intervalo [1, 100]
Declara n
Asigna n, 0
Mientras n < 1 O n > 100
Lee n
Escribe n
Notemos que n se inicia con 0, un valor que hace verdadera la condición del ciclo. Se pudo usar cualquier valor negativo, o uno mayor que 100.
En el ciclo se lee un valor para n, el cual se verifica si es menor que 1 o mayor que 100, en cuyo caso se vuelve a ejecutar el ciclo.
El ciclo se mantiene, pues, ejecutándose hasta que el usuario introduzca un valor para n que haga falsa la condición, un número de 1 a 100.
Si el valor ingresado para n es menor que 1 la proposición es verdadera, se vuelve a ejecutar el ciclo, con lo que lee un nuevo valor.
Lo mismo ocurre con un valor mayor que 100, y se vuelve a ejecutar el ciclo y se lee otro valor.
Solamente la condición es falsa cuando el valor de n hace falsa las dos condiciones conectadas con el operador lógico O.
Esto es, n es entero de 1 a 100, ambos inclusive. Queda implícito que el usuario ingresa números enteros y no otra cosa.
cuadrado
Suponga que se desea que Spike dibuje un cuadrado asteriscos de 3x3 en pantalla.
Escribe un cuadrado de asteriscos de 3 x 3
Escribe "***\n"
Escribe "***\n "
Escribe "***\n "
Digamos que esta es una forma salvaje de lograr tal propósito. Consideremos, pues una forma “refinada”.
Escribe un cuadrado de asteriscos de n x n, n > 0
Declara n, f, c
Lee n
Asigna f, 0
Mientras f < n
Asigna c, 0
Mientras c < n
Escribe "*"
Asigna c, c +1
Escribe "\n"
Asigna f, f + 1
Notemos ahora se pide al usuario que decida de cuántos asteriscos por línea será el cuadrado, si 1 x 1, 2 x 2, H, n x n
Ahora tenemos un ciclo Mientras dentro de otro ciclo Mientras. El ciclo externo, el primero, se mueve más lentamente.
A f se inicia con 0, que como es menor que n (n > 0) se entra en el ciclo exterior (el primero) y allí permanece mientras f < n.
Cada vez que se ejecuta el ciclo externo se inicia c con 0, y entonces se ejecuta el ciclo interno n veces, escribiendo n asteriscos seguidos.
Al salir del ciclo interno se escribe un salto de línea y se incrementa a f, se ejecuta nuevamente el ciclo interno y esto ocurre n veces.
triángulo
Ahora queremos que Spike dibuje un triángulo de n líneas asteriscos en pantalla, con 1 asterisco en la primera línea, dos en la segunda.
Tres en la segunda, y así, hasta n en la n-ésima línea. Podemos adaptar el algoritmo anterior,
Escribe un triángulo de asteriscos con 1, 2, ... n, asteriscos por fila, n > 0
Declara n, f, c
Lee n
Asigna f, 1
Mientras f <= n
Asigna c, 0
Mientras c < f
Escribe "*"
Asigna c, c +1
Escribe "\n"
Asigna f, f + 1
Ahora el ciclo externo se ejecuta no n, pero el interno f veces, un número variable de veces cada vez, pues f se incrementa cada vez.
La primera vez f vale 1 y c empieza en 0. Se ejecuta una vez. La segunda vez se ejecuta dos veces, para c igual 0 y 1. Y así.
Como antes, cada vez que se abandona el ciclo interno se incrementa a f y se hace un salto de línea en pantalla.
Se puede imaginar que cuando un ciclo está insertado en otro, el movimiento de los contadores es semejante a los de un reloj electrónico.
Se empieza con las 0:0 y se pasa a las 0:1, 0:2, H, 0:59. La hora se queda fija durante 60 segundos.
Pero al terminar la primera hora, se incrementa la hora y se repite el proceso, ahora con 1:0, 1:1, 1:2, ..., 1:59.
Esto ejemplifica que es lo que significamos con que el ciclo exterior se mueve más lento que el exterior.
tema 3, funciones
Un problema relativamente complejo, como algunos que abordaremos aquí, se puede atacar por partes.
Descartes propuso, en su Discurso del Método, que un problema se puede dividir en problemas más pequeños.
Resolver los problemas pequeños y reunir las soluciones, da una solución al problema complejo.
Este es el llamado principio “Divide y vencerás.”, que se aplica tanto en la política dominicana, en mal sentido, claro.
Podemos redactar algoritmos reutilizables en el sentido de que son redactados para ser usados por otros algoritmos.
Veremos que a estos algoritmos aquí se les llama funciones y son una herramienta poderosa para resolver problemas.
Resolverlos descomponiéndolos en sub-problemas que luego son resueltos uno por uno, e integrados en la gran solución.
Y como cada una de estas soluciones “pequeñas” se hace de manera general, de manera que se puede reutilizar, sin cambios.
Esto significa que a medida que vamos resolviendo problemas vamos atesorando un repertorio de soluciones.
De suerte que ante un nuevo problema, puede que ya tengamos parte de él resuelto en forma de funciones previas.
Pero también podemos asumir que una función existe y redactar nuestro algoritmo como si así fuera.
Y redactar la función en detalle más adelante. Esto nos permite resolver problemas complejos en la modalidad “friendo y comiendo”.
función
Spike puede invocar y ejecutar un algoritmo redactado para ser usado por otros algoritmos.
A estos algoritmos auxiliares también se les llama sub-algoritmos, subrutinas, funciones. Aquí les llamamos funciones.
Una función pretende resolver un problema particular de una forma general, y generalmente se limita a resolver un solo problema.
Mediante las funciones descomponemos la solución de un problema relativamente complejo, en soluciones otros más sencillos.
Se aplica el principio de ‘Divide y vencerás.’, propuesto por Descartes hace más de doscientos años.
La siguiente función calcula y retorna el área de un triángulo de base b y altura a,
areaTriangulo(b, a)
Declara s
Asigna s, (b * a) / 2
Retorna s
Notemos la sentencia Retorna, cuya forma es
Retorna expresión
Por ahora, invocaremos las funciones mediante la sentencia Asigna, si bien hay otra forma que discutiremos más adelante.
A seguidas vamos a ilustrar la redacción y uso de funciones con numerosos ejemplos, tal y como hicimos con los ciclos.
invocar
Podemos descomponer un algoritmo que calcula el área de un triángulo en dos partes, el principal y una función.
Calcular área de triángulo de base b y altura a
Declara a, b, s
Lee b, a
Asigna s, areaTriangulo(b, a)
Escribe s
areaTriangulo(b, a)
Declara s
Asigna s, (b * a) / 2
Retorna s
El algoritmo principal invoca a la función areaTriangulo(), pasándole los valores de b y a. La función calcula el área y la retorna y asigna a s.
Esta división del trabajo aligera de los detalles de calcular el área al algoritmo principal, dejándolo a la función.
Notemos que hay una variable s en el algoritmo y otra en la función. Resulta que el valor de la de la función se retorna al algoritmo.
En este caso, el algoritmo compuesto resulta con mayor cantidad de texto que sin la función, pero en general no es así.
Además, una función se puede reutilizar, sin cambios, en algoritmos futuros, lo cual abre el camino a la reutilización de versos algorítmicos.
No estamos obligados a incluir el texto de cada función invocada por un algoritmo u una función cuando quede claro en el contexto.
Gauss
Recordemos el algoritmo de la suma de los primeros 100 naturales.
Sumar los primeros 100 naturales
Declara n, s
Asigna n, 1; s, 0
Mientras n <= 100
Asigna s, s + n
Asigna n, n + 1
Escribe s
Podemos adaptarlo para que sea una función a invocar por otro algoritmo, con la ventaja de que no se limite fijo de 100.
sumaGauss(n)
Declara s, c
Asigna s, 0; c, 1
Mientras c <= n
Asigna s, s + c
Asigna c, c + 1
Retorna s
Ahora el valor de n (cualquier número entero positivo) es recibido por la función sumaGauss() al ser invocada.
Ella obtiene la suma deseada y la retorna al algoritmo que la invoque.
Sumar los primeros 100 naturales
Escribe sumaGauss(100)
Este algoritmo asume que existe la función sumaGauss() a la cual pasa el valor 100. Entonces Spike invoca la función y escribe el valor que retorna.
Podemos hacer que el valor de n lo ingrese el usuario.
Sumar los primeros n naturales
Declara n
Lee n
Escribe sumaGauss(n)
Esta metodología permite al programador concentrarse en un problema a la vez, lo que lo hace más productivo.
factoriales
Si asumimos la existencia de la función factorial,
factorial(n)
Declara f, c
Asigna f, 1; c, 1
Mientras c <= n
Asigna f, f * c
Asigna c, c + 1
Retorna f
Consideremos ahora el algoritmo siguiente.
Obtener suma de los factoriales de los dígitos de n > 0
Declara s, n, d
Asigna s, 0
Lee n
Mientras n > 0
Asigna d, n mod 10
Asigna s, s + factorial(d)
Asigna n, n coc 10
Escribe s
En el ciclo, como vimos, se van separando los dígitos de n, pero ahora para cada dígito se calcula su factorial y se acumula en s.
Como dijimos, se pueden redactar juntos los dos algoritmos, o simplemente el principal, dejando implícita la función factorial.
factores
Consideremos una función que obtiene la suma de los factores propios de un entero n > 0.
sumaFactoresPropios(n)
Declara s, d
Asigna s, 1; d, 2
Mientras d < n
Si n mod d = 0
Asigna s, s + d
Asigna d, d + 1
Retorna s
Se dice que un número natural n es deficiente si la suma de sus factores es menor que n, perfecto cuando es igual a n, abundante si es mayor que n.
Determinar si un natural es deficiente (-1), perfecto (0), o abundante (1)
Declara n, t, s
Asigna t, -1
Lee n
Asigna s, sumaFactoresPropios(n)
Si s = n
Asigna t, 0
Si s > n
Asigna t, 1
Escribe t
Este algoritmo asume la existencia de la función sumaFactoresPropios(), la cual calcula y retorna la suma de los factores de n.
El valor por omisión de la variable t es -1, significando que n es deficiente, pero si s =n, t se hace 0, significando que n es perfecto.
Si s > n, entonces t se hace 1, significando que n es abundante. Este esquema de valores -1, 0, 1, se usa mucho en otros contextos.
El uso y asunción de funciones mejora la legibilidad de los versos algorítmicos y permite atacar problemas complejos con facilidad.
perfectos
Un algoritmo que asuma la función sumaFactoresPropios() y que obtenga los números perfectos menores que t < 0, podría ser.
Obtener números perfectos menores que t > 0
Declara t, n
Asigna n, 1
Lee t
Mientras n < t
Si n = sumaFactoresPropios(n)
Escribe n " "
Asigna n, n + 1
Y uno que obtenga los primeros t perfectos podría ser.
Obtener los primeros t > 0 números perfectos
Declara t, n, c
Asigna n, 1; c, 0
Lee t
Mientras c < t
Si n = sumaFactoresPropios(n)
Escribe n " "
Asigna c, c + 1
Asigna n, n + 1
Notemos que ahora el ciclo está controlado por el contador c, el cual sólo se incrementa se cumple la condición del Si, esto es, si n es perfecto.
Cuando esto ocurre se hacen dos cosas. Se escribe el valor de n y un espacio (pues es n perfecto) y se incrementa a c.
Notemos que n se incrementa cada vez que se ejecute el ciclo, por lo que no se sabe de antemano su valor final, como el de c, que termina con el de t.
Dicho sea de paso, Spike se “congela” cuando se ejecuta este algoritmo con valores de t mayores a 5.
productos
Se llama persistencia de un número entero positivo a la cantidad de pasos que toma convertirlo en un número de un dígito.
Y esto mediante sustituciones sucesivas del número por el producto de sus dígitos. La persistencia de 64 es 2 porque 64 -> 24 -> 8.
Se toma dos pasos en llegar desde 64 hasta 8, un número de un solo dígito, porque 6x4 = 24, 2x4 = 8.
Una función que obtiene el producto de los dígitos de un entero positivo podría ser.
productoDigitos(n)
Declara p, d
Asigna p, 1
Mientras n > 0
Asigna d, n mod 10
Asigna f, f * d
Asigna n, n coc 10
Retorna f
Para calcular la persistencia de n > 0 podemos usar.
persistencia(n)
Declara p
Asigna p, 0
Mientras n > 9
Asigna n, productoDigitos(n)
Asigna p, p + 1
Retorna p
Es claro que el ciclo se salta si el número tiene un dígito, pero se reitera mientras el producto de sus dígitos tenga más de un dígito.
Y cada vez que se ejecuta se incrementa a p, que al empezar en cero, al salir del ciclo registra las veces que se ejecutó: La persistencia.
persistencia
Para obtener el único número de dos dígitos con persistencia mayor que 3 podemos redactar el algoritmo principal.
Obtener número de dos dígitos con persistencia mayor que 3
Declara n, d, p
Asigna n, 10; s, 1
Mientras s = 1 Y n < 100
Asigna p, persistencia(n)
Si p > 3
Escribe n
Asigna p, 0
Asigna n, n + 1
Notemos que la variable n puede tomar los valores desde 10 hasta 99 sin salir del ciclo, números de dos dígitos.
La variable s se inicia en 1 para indicar que el ciclo se debe seguir ejecutando mientras s sea 1.
Pero tan pronto aparece una valor de n con persistencia mayor que 3, p se hace 0, y el ciclo se abandona, aunque n todavía sea menor que 100.
Notemos, además, que este algoritmo asume la función persistencia(), la cual a su vez asume a la función productoDigitos().
Esto ilustra que una función puede invocar a otra que a su vez invoque a otra, creando una cascada de invocaciones.
Este ejemplo ilustra se puede simplificar grandemente la solución de un problema al dividirlo en funciones.
Estas soluciones parciales se pueden reutilizar en la solución de problemas futuros, pues son genéricas, aunque nazcan de un problema específico.
Descargar