Autor Tema: (C) Máximo Común Divisor - Algoritmo de Euclides

0 Usuarios y 1 Visitante están viendo este tema.

17 Agosto, 2013, 12:44 am
Leído 16389 veces

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,738
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Por el mero gusto de programar, voy aquí a escribir un programa en C del Algoritmo de Euclides, que permite calcular el máximo cómun divisor de dos números dados.

Dados dos enteros \( m, n \), el máximo común divisor de \( m \) y \( n \) es el máximo número entero \( g \) tal que \( g \) es divisor tanto de \( m \) como de \( n \).
Con esa definición, el máximo común divisor siempre resulta no negativo: \( g\geq 0 \).

¿Cuánto es \( g \) si alguno de los números es 0?
Todo número entero es divisor de 0, así que si \( m = 0 \) y \( n > 0 \), entonces \( g = n \), porque \( n \) divide a \( 0 \) y \( n \) simultáneamente, y es el máximo entero que puede hacer eso.

Finalmente, si \( m = n = 0 \), ocurre que todo entero \( g \) divide a ambos valores... por lo tanto no hay un divisor común que sea el "máximo", y de ahí que podamos escribir algo como \( g = \infty \).
Sin embargo, conviene decir que en este caso el valor de \( g \) no está definido.

Cuando existe, el máximo común divisor será denotado como \( gcd(m, n) \).



Euclides observó que si \( m > n \), entonces el máximo común divisor de \( m \) y \( n \) coincide con el máximo común divisor de \( n \) y el \( m \% n \) (esto denota al resto de la división \( m / n \)):

\( gcd(m, n) = gcd(n, m \% n). \)

Demostración.

Supongamos, para simplificar, que \( m\geq n\geq 0 \).
Si \( d \) divide a \( m \) y \( d \) divide a \( n \), entonces d es divisor de \( m - n \).
Recíprocamente, si \( d \) divide a \( n \) y \( d \) divide a \( m - n \), entonces \( d \) divide a \( m \).
Entonces el máximo común divisor de \( m \) y \( n \) tiene que coincide con el máximo común divisor de \( n \) y \( m-n \).

Si repetimos esto un número \( k \) de veces, obtenemos que \( gcd(m, n) = gcd(m, m - kn) \).
Si ahora hacemos \( k = q \), donde \( m = q n + r \), con \( 0 \leq r < n \) (el algoritmo de división), resulta que:

\( gcd(m, n) = gcd(m, m - qn)  = gcd(m, r)=gcd(m, m\% n) \)

[cerrar]

Por definición, cuando \( n \) es positivo, el resto de la división \( m / n \), es el menor número \( r \) que satisface \( 0 \leq r < n \), \( m = nq + r \), para algún entero \( q \) (que es el cociente).

Lo que nos interesa de esto es que \( r < n \).

Si repetimos el procedimiento, vemos que:

\( gcd(m, n) = gcd(n, m \% n) = gcd(n , r) = gcd(n, n  \% r). \)

O sea que podemos calcular el máximo común divisor calculando sucesivos restos de divisiones, que van decreciendo estrictamente, hasta llegar a algún paso en el que el resto obtenido sea 0. Ahí el máximo común divisor se obtiene trivialmente, aprovechando que:

\( gcd(x, 0) = x. \)

Sabiendo que esto funciona así de bien, podemos ponerlo en un programa.



Supongamos que tenemos dos números enteros positivos tales que \( m  > n>0 \).
El algoritmo que usaríamos sería esto:

* Calcular el resto \( r = m\% n \).
* Reemplazar el viejo valor de \( m \) por \( n \), y el viejo valor de \( n \) por \( r \).
* Tras hacer eso nos queda que \( m>n \), y podemos volver desde el principio, porque el máximo común divisor entre estos dos nuevos valores coincide con el máximo común divisor de los valores \( m, n \) originales.
* En cada paso se obtiene siempre que \( 0< r  < n \), y entonces los valores de \( m, n \), van decreciendo. Como se trata de números enteros positivos, hay una cantidad finita de pasos tras la cual esto termina. Esto quiere decir que habrá un momento en el que \( r \) no podrá seguir siendo positivo, o sea, se obtendrá \( r = 0 \).
* Cuando esto ocurre, no conviene dividir por 0 porque genera un error en el programa...
* Sin embargo observamos que la búsqueda ha terminado, porque cuando \( m > n = 0 \), lo que significa es que \( gcd(m, n) = gcd(m, 0) = m \). Esto quiere decir que nos hemos "topado" con el máximo común divisor que buscábamos, y podemos informar que ese valor es el resultado buscado.



Podemos ahora definir una función gcd() en C, que resuelva este problema, así:


int gcd(int m, int n) {
    /* Suponemos m > n > 0 */
    do {
        int temp = n; n = m % n; m = temp;
        /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) >= 0 */
    } while(n > 0);

    /* n == 0, m == gcd(original m, original n) */

    return m;   
}


La función gcd() toma dos parámetros enteros \( m, n \), y retorna como resultado otro número entero, el máximo común divisor de \( m \) y \( n \).
Por ahora suponemos que \( m > n > 0 \), pero después tendremos que considerar todos los casos posibles.

A continuación iniciamos un bucle del tipo do {} while();
El primer paso calcula el resto de dividir m y n, y luego definiremos nuevos valores para m, n, así:

* El más pequeño de los "viejos" valores \( m, n \), será el "nuevo" valor asignado a \( m \).
* El valor del resto de la división de los "viejos" valores \( m, n \) será el "nuevo" valor de \( n \).
* Reordenar los valores, si fuera necesario, de manera que \( m > n \).

Cada vez que hay que reordenar dos valores, hace falta usar una variable auxiliar, que llamaremos \( temp \).
Observando que ya habíamos tomado de entrada que m > n, el más pequeño de los dos será n.
Y como el resto \( r = m \% n \) satisface siempre que \( n > r \), no hay nada que reordenar:
Guardamos en \( temp \) el "viejo" valor de \( n \), porque lo vamos a necesitar en la siguiente iteración, el cual será el "nuevo" \( m \).
Calculamos el resto \( m\% n \), y lo guardamos en el "nuevo" \( n \), sabiendo ya de antemano que este valor será más que pequeño que el "nuevo" \( m \).

Ahora bien, como estamos usando las mismas variables \( m, n \), para denotar los valores "viejos" y "nuevos", hay que tener cuidado en el orden en que efectuamos las asignaciones, para no perder información.
Lo que conviene es guardar en \( temp \) el "viejo" \( n \), luego calcular el "nuevo" \( n \), y por último definir el "nuevo" \( m \) igual al valor previamente guardado en \( temp \).



Para garantizar que el ciclo no continúa eternamente, tenemos que aprovechar la propiedad que hemos mencionado de que, al calcular los sucesivos restos de las divisiones, los valores de \( m, n \), van decreciendo en cada iteración.
Esto lo hemos escrito en el programa mismo como un comentario debajo de la línea que efectúa los cálculos.
Esa es la condición que implica que el ciclo terminará en una cantidad finita de pasos.

En particular, siempre tenemos la condición \( m > n \) en cada iteración (un invariante del algoritmo), que en cierto modo es una de las claves que permite que el algoritmo avance sin problemas.

Además, como ya hemos mencionado, el algoritmo termina cuando el resto es 0.
Así que, para que el ciclo se repita, hemos puesto la condición \( (n > 0) \) en el do {} while();

Más aún, tras terminar el ciclo podemos estar seguros que \( n == 0 \) y que \( m > 0 \).
En ese caso, el máximo común divisor es exactamente \( m \), sin lugar a dudas, y el proceso ha terminado.
Así, este valor es el que debe devolver la función gcd() como resultado.



En los posts que siguen voy a agregar los detalles que faltan.

17 Agosto, 2013, 03:02 am
Respuesta #1

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,738
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Lo que tenemos que hacer ahora es arreglar la función gcd() para que funcione en todos los casos posibles.
La función tiene que ser capaz de lidiar con todo tipo de valores \( m, n \), y retornar un valor correcto, o con sentido.

Una de las propiedades del máximo común divisor es que no depende del signo:

\( gcd(m,n)= gcd(|m|,|n|). \)

Así, cuando alguno de los parámetros \( m,n \), es negativo, esto no tiene que importar.

Lo que haremos será sustituir los parámetros originales \( m, n \), por sus valores absolutos,
mediante la sentencia siguiente:


m = abs(m);
n = abs(n);


Aquí es necesario invocar la función abs(), que retorna el valor absoluto de un número.
Podemos usar las matemáticas predefinidas de C, o bien realizar nuestra propia versión.

Suelo preferir no sobrecargar los programas invocando librerías.
Así que voy a poner mi propia versión de "función valor absoluto".
He aquí el prototipo:


int abs(int);


La función abs() tomará como parámetro un número entero (tipo int) y retornará como resultado su valor absoluto, como un dato de tipo int.



Es conveniente documentar correctamente el programa indicando estas dos propiedades matemáticas:

* La propiedad lógico-matemática que le da sentido a una determinada sentencia del programa.
* La condición lógica que siempre se cumple tras ejecutar dicha sentencia.



Así, mejor escribimos las sentencias anteriores rodeadas de comentarios:


    m = abs(m);
    n = abs(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */


La 3er línea contiene un comentario que expresa que el máximo común divisor será invariante respecto la aplicación de valor absoluto a cada parámetro. Esto es consecuente con la matemática subyacente.

En la 4ta línea se expresan las condiciones \( m\geq 0, n\geq 0 \), que son invariantes del algoritmo.
Lo que significa es que: siempre que el programa llegue a ese punto de ejecución, las condiciones \( m\geq 0, n\geq 0 \), son verdaderas.



Resuelto el problema de los signos, tenemos ahora que intentar poner las cosas en el formato cómodo que ya habíamos estudiado, a saber: \( m>n>0 \).
Hay que manejar los casos en que algún parámetro es 0, o en que ambos parámetros son iguales, etc.

Lo que conviene hacer, digo yo, es reordenar los parámetros de manera que \( m\geq n \).
Y como ya hemos logrado que ambos sean no-negativos, tendremos más precisamente: \( m\geq n\geq 0 \).
Si ya fuera cierto que \( m\geq n \), entonces no haríamos nada.
En cambio, si \( m<n \), intercambiaríamos los papeles de \( m, n \).

Esto se puede hacer sin problemas porque la operación de máximo común divisor es "conmutativa", es decir, no varía si invertimos los parámetros:

\( gcd(m, n) = gcd(n,m). \)

Esta propiedad es la que nos autoriza a intercambiar los valores de las variables para que nos quede del modo que a nosotros nos resulte cómodo.

El intercambio de valores entre \( m, n \), requiere, como es usual, el uso de una variable temporal.
Por ahora vamos a hacerlo de forma sencilla, así:


 /* swap m and n ... */
{
   int temp = m; m = n; n = m;
}


La sentencia completa luce así:


    if (m < n)
       /* swap m and n ... */
      {
            int temp = m; m = n; n = m;
      }
    /* gcd(m, n) == gcd(n, m) */
    /* m >= n >= 0*/


Como se puede apreciar, tras realizar el "swap" (intercambio),
se cumplirá siempre la condición invariante deseada: \( m \geq n\geq 0 \).

Hemos agregado también una línea de comentario que explica el resultado matemático que se obtendrá como consecuencia del "swap": la función \( gcd(m, n) \) dará el mismo resultado que \( gcd(n, m) \).



El siguiente caso que quisiéramos descartar de inmediato es cuando \( m = n = 0 \).
Este caso es el único que no da un número entero válido como resultado.

Pero la función gcd() necesita retornar un número entero, porque está declarada como int.
¿Cómo lo arreglamos?

Lo primero que recordamos aquí es que, cuando el valor gcd(m, n) está bien definido, es un número entero \( \geq 0 \).
Entonces podemos usar valores negativos a modo de "valores señalizadores" para indicar que algo anduvo mal.

Esto deberá estar documentado en la cabecera de la función gcd(), pero lo haremos luego.

Ahora simplemente manejamos el caso \( m = n = 0 \) retornando un valor negativo sencillo, como \( -1 \), que para nosotros va a significar: "señal de valor infinito" o "señal de resultado no definido".


    if ( (m == 0) && (n == 0) )
       return -1;
    /* m > 0, m >= n >= 0 */


Una vez que ese "if" ha pasado al siguiente punto de ejecución del programa,
obtenemos que \( m\geq n\geq 0 \), como antes, pero además sabemos que \( m>0 \).
Estas dos condiciones las ponemos como comentario en ese punto de ejecución.



Una vez que hemos descartado el caso insalubre de \( m = n = 0 \),
todavía nos gustaría sacarnos de encima los casos triviales en que \( m = n \neq 0 \).
En esta situación es fácil calcular el máximo común divisor:

\( gcd(m, m) = m. \)

Sin embargo, lo que realmente nos importa es sacarnos de encima el caso \( m = n \),
porque nuestro algoritmo original funcionaba de maravillas sólo para \( m > n \),
y queremos poder aprovechar eso.

Supongamos que \( m = n \). Como ya hemos descartado el caso \( m = n = 0 \), entonces tenemos la certeza ahora que \( m = n \neq 0 \), y por lo tanto el valor el máximo común divisor es \( m \), y no \( \infty \).

Aquí vemos, pues, un ejemplo de la utilidad de los invariantes: nos ahorra tener que evaluar condiciones que previamente ya hemos tomado en cuenta.

El código sería éste:


    if (m == n)
       return m;
    /* m == n > 0 ---> gcd(m, n) == m */
    /* m > n >= 0 */


En la 3er línea ponemos en un comentario la conclusión lógica de las operaciones que hemos efectuado: "en caso de que \( m = n > 0 \), la función \( gcd(m, n) \) retornará \( m \)".

En la 4ta línea tomamos nota del estado en que se encuentran las variables m, n, en el presente punto de ejecución. Al llegar allí podemos estar seguros de que \( m > n  \geq  0 \).



Sin embargo, vemos que todavía no nos hemos sacado de encima el posible caso en que n = 0.
Y puede que tampoco sea necesario...
Pero por ahora lo vamos a descartar sin más, y más tarde mejoraremos el algoritmo.

Si \( n = 0 \), como ahora \( m > n \), obtenemos \( m > 0 \), y entonces \( gcd(m, 0) = m \), con lo cual tranquilamente podemos retornar el valor \( m \).



Habiendo ya descartado todos los casos "molestos", llegamos finalmente a la situación principal deseada, en que \( m > n > 0 \).
Aquí basta insertar lo que ya teníamos antes.

Además voy a agregar la línea de comentario que justifica el algoritmo:


        /* gcd(m, n) == gcd(n, m % n) */




La función gcd() completa quedaría así:


int gcd(int m, int n) {
   
    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna -1, que significa: "valor infinito/indefinido" */
   
    m = abs(m); n = abs(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    if (m < n)
       /* swap m and n ... */
       {
                int temp = m; m = n; n = m;
       }
    /* gcd(m, n) == gcd(n, m) */
    /* m >= n */
   
    if ((m == 0) && (n == 0))
       /* ---> m == n == 0 */
       return -1;
    /* m > 0, m >= n >= 0 */
   
    if (m == n)
       return m;
    /* m > 0 ---> gcd(m, m) = m */
    /* m > n >= 0 */
   
    if (n == 0)
      return m;
    /* gcd(m, 0) == m */
    /* m > n > 0 */
   
    do {
        /* m > n > 0 */
        /* gcd(m, n) == gcd(n, m % n) */
        int temp = n; n = m % n; m = temp;
        /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
    } while(n > 0);
    /* n == 0, m == gcd(original m, original n) */
   
    return m;   
}




En el siguiente post haremos retoques totalmente inútiles,
pero que ilustran algunas técnicas de programación.

17 Agosto, 2013, 03:51 am
Respuesta #2

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,738
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Hemos completado la definición de la función gcd(), que analiza todos los casos posibles, sin dejar cabos sueltos.
Vamos a hacer algunas pequeñas modificaciones a dicha función.

Notemos que la línea que evalúa el caso: (m == n) && (n == 0)
tiene algo de redundancia.
En efecto, el invariante previo indica que, en ese punto de ejecución, es cierta la condición \( m\geq n\geq 0 \).
Por lo tanto, si verificamos que \( m = 0 \), esto implica inmediatamente que también \( n = 0 \), y por lo tanto no hay que obigar al programa a que efectivamente haga la comprobación.

Luego, esa sentencia "if" puede cambiarse por esta otra:


    if (m == 0)
       /* ---> m == n == 0 */
       return -1;
    /* m > 0, m >= n >= 0 */


En vez de comprobar que "n == 0" en la instrucción "if",
mejor hemos "trasladado" esa condición a un comentario en la línea siguiente.

Estos comentarios pueden llamarse también "aserciones lógicas" del algoritmo.
En el punto de ejecución correspondiente a la línea inmediata debajo del "if" podemos estar seguros que \( m = n = 0 \), y entonces es válido retornar \( -1 \), que usábamos para indicar \( \infty \).



El número \( -1 \) es lo que en programación se denomina, a veces, número "mágico".
Esto se refiere a números que no tienen significado alguno cuando una persona lee el programa en cuestión.
Si bien hemos documentado dentro de la función gcd() el significado de ese número,
conviene enmascararlo de algún modo, poniendo en evidencia el verdadero significado que tiene el numerito en cuestión.

Vamos a definir, pues, una constante llamada INFTY_SIGNAL (que significa: "señal de infinito"),
y cuyo valor pondremos igual a \( -1 \).


#define INFTY_SIGNAL -1  /* Debe ser un valor int negativo */


La directiva #define permite sustituir el símbolo INFTY_SIGNAL automáticamente por el valor \( -1 \).
Nosotros escribimos INFTY_SIGNAL y el compilador entiende \( -1 \).

Si bien para el compilador no hay diferencia alguna, en cambio para nosotros sí la hay:

* El idenfiticador INFTY_SIGNAL tiene un significado o sentido intuitivo concreto para nosotros, mientras que el número -1 es sólo un número.

* Si nos equivocamos poniendo un número distinto a -1, el compilador no notará que es un error. En cambio, una vez que INFTY_SIGNAL tiene asignado el valor -1, este valor nunca cambia. Se reduce así la posibilidad de asignar valores erróneos, difíciles de detectar.

* Si por alguna razón de diseño necesitamos cambiar el valor -1 por otro valor, no tenemos necesidad de rastrear todas las partes del programa en que figura el dichoso -1, sino que basta cambiar una sola vez, en la línea #define, el valor -1 por otro, digamos -3. La lógica del programa no se verá afectada.

* Si usamos el mismo valor -1 con otro significado en otras partes del programa, sería confuso que se usara para indicar varios sentidos distintos. Peor aún sería que se nos ocurra cambiar ese valor por otro para uno de los significados, y para el otro no. Pasaríamos toda una tarde cambiando pacientemente numeritos, con grandes riesgos de errores. En cambio, con #define, todo se puede arreglar con mayor claridad y rapidez.


¿Y qué es ese comentario que pusimos al costado, diciendo que el valor de INFTY_SIGNAL tiene que ser un número negativo?
Bueno, esto tiene que ver con lo que hemos dicho arriba, de que se nos puede ocurrir cambiar el valor de la constante -1 por algún otro que nos parezca mejor.
En ese caso, el comentario indica que podemos hacer esto, pero que no puede ponerse cualquier número ahí, sino que ha de ser un entero negativo, y de tipo int.



La función abs() que calcula el valor absoluto la implementamos así:



int abs(int x) {
    return (x < 0)? (-x): (x);
}


Hemos usado el operador ?: el cual permite abreviar engorrosas sentencias "if", transformándolas en sencillas operaciones aritméticas...

Como se ve, la función tiene una definición simple y breve:

* Toma como parámetro un entero \( x \).
* Si \( x \) es negativo, le cambia el signo.
* Si no, lo deja tal cual está.

El resultado es un valor de tipo int, que da el valor absoluto de \( x \).



La operación de "swap" es muy común en la programación,
así que conviene tenerla definida en forma genérica,
para no tener que estar repitiéndola todo el tiempo cada vez que nos haga falta.

Debido a que el "swap" (o intercambio de valores entre dos variables)
es una operación que suele hacerse para cualquier tipo de datos,
no conviene restringirse a un tipo específico, sino declararla en un modo genérico.

De modo que no definiremos una función swap(), sino una macro llamada SWAP().

Dentro del "swap" hace falta una variable temporal en donde guardar por un rato el valor de una de las dos variables a intercambiar.
Aquí hace falta declarar dicha variable temporal, y esto quiere decir que tenemos que darle un tipo específico.

El tipo de la variable temporal tiene que ser el mismo que el de las dos variables que habrán de intercambiarse.
¿Cómo hace la macro SWAP para saber el tipo de datos a utilizar?

Bueno, como las computadoras no son adivinas, tendremos que decírselo.
Así, la macro SWAP aceptará 3 parámetros: el primero será un "tipo de datos", y los otros dos serán las variables a intercambiar. La definición es ésta:


/* La macro SWAP(TYPE, X, Y) toma como parámetros:
     * TYPE: un tipo de datos válido de C
     * X, Y: dos lvalues de tipo TYPE
   La macro intercambia entre sí los valores de X é Y
*/
 
#define SWAP(TYPE, X, Y) { TYPE __Temp = X; X = Y; Y = __Temp; }


Un ejemplo de uso sería éste:


SWAP(int, m, n)


Esa invocación equivale a hacer esto:


{ int __Temp = m; m = n; n = __Temp; }


Como las sentencias están dentro de un bloque encerrado por llaves { }
el efecto es que la variable __Temp tiene duración sólo durante el breve lapso de tiempo en que se ejecuta el bloque en cuestión.

Unas líneas más arriba hemos puesto 4 líneas de comentarios que explican el modo correcto de usar la macro SWAP, y cuál es el resultado esperado.

Esto hay que hacerlo por varias razones:

  • Es más importante lo que "se supone" que hace una macro o una función, que el "cómo lo hace".
    En efecto, si sabemos lo que una macro o una función tiene que hacer,
    y si resulta que la hemos programado mal,
    entonces será fácil corregirla.

    En cambio, si definimos la macro o función con todas sus operaciones,
    pero en ninguna parte describimos lo que se supone que tiene que hacer,
    es imposible darse cuenta si tiene o no un error de diseño, porque no hay ningún criterio que la valide o no.

  • En general sucede que las macros no son código "seguro", es decir,
    pueden dar lugar a comportamientos anómalos inesperados.

    Por ejemplo, en el caso de la macro SWAP(), es fácil "crackearla",
    ya que si no se respetan las condiciones exigidas en los comentarios,
    los resultados no serán los esperados.

    Ejemplo 1:

    float x = 1.0; char *s = "Hola";
    SWAP(void, x, s);

    Ejemplo 2:

    SWAP(int, 3, 4);

    Ejemplo 3:

    int x = 0, y = 1;
    SWAP(3.14, x, y);

    ___

    El Ejemplo 1 falla porque intercambia variables de distinto tipo, y encima el tipo indicado es todavía otro: void.
    El Ejemplo 2 falla porque los parámetros X, Y, no son lvalues.
    El Ejemplo 3 falla porque el parámetro TYPE es 3.14, que no es un tipo válido.




Con estas pequeñas modificaciones, la función gcd() queda así:


#define INFTY_SIGNAL -1  /* Debe ser un valor int negativo */

/* La macro SWAP(TYPE, X, Y) toma como parámetros:
     * TYPE: un tipo de datos válido de C
     * X, Y: dos lvalues de tipo TYPE
   La macro intercambia entre sí los valores de X é Y
*/
 
#define SWAP(TYPE, X, Y) { TYPE __Temp = X; X = Y; Y = __Temp; }

int abs(int x) {
    return (x < 0)? (-x): (x);
}

int gcd(int m, int n) {
   
    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna INFTY_SIGNAL */
   
    m = abs(m); n = abs(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    if (m < n)
       SWAP(int, m, n);
    /* gcd(m, n) == gcd(n, m) */
    /* m >= n */
   
    if (m == 0)
       /* ---> m == n == 0 */
       return INFTY_SIGNAL;
    /* m > 0, m >= n >= 0 */
   
    if (m == n)
       return m;
    /* m > 0 ---> gcd(m, m) = m */
    /* m > n >= 0 */
   
    if (n == 0)
      return m;
    /* gcd(m, 0) == m */
    /* m > n > 0 */
   
    do {
        /* m > n > 0 */
        /* gcd(m, n) == gcd(n, m % n) */
        int temp = n; n = m % n; m = temp;
        /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
    } while(n > 0);
    /* n == 0, m == gcd(original m, original n) */
   
    return m;   
}





Nuestra función gcd() quedó maravillosa, pero todavía no tenemos un programa que haga algo.

En el siguiente post vamos a implementar un programa que efectivamente corra y muestre resultados de una vez por todas.



17 Agosto, 2013, 04:24 am
Respuesta #3

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,738
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Para interactuar con el usuario usaremos la librería estándar <stdio.h>.

Dado que quisiéramos divertirnos jugando con el máximo común divisor hasta cansarnos,
vamos a hacer que nuestro programa sea en realidad un bucle repetitivo tal que,
en cada iteración, solicite al usuario el ingreso de dos números enteros.
Tras el ingreso de datos, el programa llama a la función gcd() con esos dos números como parámetros, y muestra en pantalla el resultado.

También es cierto que el programa tiene que terminar alguna vez,
y para eso necesitamos alguna condición que indique finalización.
Una posibilidad sería la de preguntarle al usuario si desea continuar usando el programa.
Pero en esta primera versión no vamos a hacer eso.

Más bien vamos a poner una condición sencilla de terminación.
Vamos a hacer que el programa termine cuando el usuario ingrese a propósito el caso infinito,
es decir, el caso en que \( m = n = 0 \).

Para esto, guardaremos el resultado de la llamada a función gcd() en una variable de nombre ans (abreviatura de "answer" = "respuesta").
Cuando el resultado de gcd() es igual a INFTY_SIGNAL, esto quiere decir que el usuario ha ingresado los valores 0 0.
En ese caso daremos por terminado el programa.

La rutina sería así:


#include <stdio.h>

int main(void) {
   int m, n, ans;   
   
   do {
     scanf("%d %d", &m, &n);
     printf("gcd(%d, %d) = %d\n\n", m, n, ans = gcd(m, n));     
   } while (ans >= 0) ;

   return 0;
}


La primer línea invoca la librería <stdio.h> para funciones de entrada/salida de datos.
Allí están definidas las funciones clásicas scanf() (para entrada de datos por teclado) y printf() (para salida estándar de datos por pantalla).

Los parámetros %d indican a scanf() que se espera que el usuario ingreso números enteros como datos.
Se deben ingresar dos números separados por un espacio en blanco.

Dentro de la función main()
se declaran las variables \( m, n, ans \).

Finalmente viene un bucle do {} while()
que se ejecuta al menos una vez,
y de ahí en más se repetirá hasta que la respuesta obtenida en ans sea un valor negativo.



Observemos que las variables \( m, n, ans \), no necesitan inicializarse ya que sus valores son iniciados en el momento apropiado por scanf(), o por la asignación siguiente: ans = gcd(m, n).



(Finalmente viene la sentencie return 0;, que devuelve el control al sistema operativo).



Para mayor prolijidad, vamos a agregar los prototipos de las funciones utilizadas:


int abs(int);
int gcd(int, int);


Esto deja más a la vista las funciones que se usan en el programa,
sin tener que estar mirando todo el engorro de cómo están implementadas dichas funciones por dentro.
Dicha implementación se posterga para la parte inferior del archivo de programa.



También vamos a agregar información para que el usuario del programa sepa qué diablos hace el programa, qué datos tiene que ingresar, qué significa el resultado devuelto, y cómo se hace para terminar el programa.

Lo hacemos encadenando strings en un único printf():


   printf(
     "Este programa calcula el M.C.D. de dos nros. enteros\n"
     "Ignora los signos negativos.\n"
     "Si ambos valores son nulos, "
     "el resultado es infinito y el programa termina.\n"
     "\n"
     "Modo de uso:\n"
     "   Escriba un par de nros. enteros separados por un espacio en blanco.\n"
     "\n"
   );




El programa completo quedar ahora así:

(Programa completo GCD:)

/* ========================================================================= */
/* GCD 1.00
/*
/* Este programa calcula el máximo común divisor de dos números enteros.
/*
/* 2013/ago/16
/* rinconmatematico.com
/* Argentinator
/*
/* ========================================================================= */
 
#define INFTY_SIGNAL -1  /* Debe ser un valor int negativo */

/* La macro SWAP(TYPE, X, Y) toma como parámetros:
     * TYPE: un tipo de datos válido de C
     * X, Y: dos lvalues de tipo TYPE
   La macro intercambia entre sí los valores de X é Y
*/
 
#define SWAP(TYPE, X, Y) { TYPE __Temp = X; X = Y; Y = __Temp; }

int abs(int);
int gcd(int, int);

#include <stdio.h>

int main(void) {
   printf(
     "Este programa calcula el M.C.D. de dos nros. enteros\n"
     "Ignora los signos negativos.\n"
     "Si ambos valores son nulos, "
     "el resultado es infinito y el programa termina.\n"
     "\n"
     "Modo de uso:\n"
     "   Escriba un par de nros. enteros separados por un espacio en blanco.\n"
     "\n"
   );
   
   int m, n, ans;   
   
   do {
     scanf("%d %d", &m, &n);
     printf("gcd(%d, %d) = %d\n\n", m, n, ans = gcd(m, n));     
   } while (ans >= 0) ;
   
   return 0;
}

int abs(int x) {
    return (x < 0)? (-x): (x);
}

int gcd(int m, int n) {
   
    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna INFTY_SIGNAL */
   
    m = abs(m); n = abs(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    if (m < n)
       SWAP(int, m, n);
    /* gcd(m, n) == gcd(n, m) */
    /* m >= n */
   
    if (m == 0)
       /* ---> m == n == 0 */
       return INFTY_SIGNAL;
    /* m > 0, m >= n >= 0 */
   
    if (m == n)
       return m;
    /* m > 0 ---> gcd(m, m) = m */
    /* m > n >= 0 */
   
    if (n == 0)
      return m;
    /* gcd(m, 0) == m */
    /* m > n > 0 */
   
    do {
        /* m > n > 0 */
        /* gcd(m, n) == gcd(n, m % n) */
        int temp = n; n = m % n; m = temp;
        /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
    } while(n > 0);
    /* n == 0, m == gcd(original m, original n) */
   
    return m;   
}



[cerrar]

17 Agosto, 2013, 05:32 am
Respuesta #4

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,738
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
En resumen...

Hasta ahora hemos hecho más o menos esto:

  • (1) Análisis matemático del problema.
  • (2) Reflexión sobre el algoritmo para el caso típico del problema.
  • (3) Estudio de los casos excepcionales o atípicos, y su cuidadosa implementación.
  • (4) Comentarios del tipo "aserción lógica".
  • (5) Comentarios del tipo "implicación lógica".
  • (6) Comentarios adicionales.
  • (7) Funciones y macros de uso frecuente (SWAP, abs).
  • (8) Puesta en marcha de un programa que permite mostrar resultados concretos.
  • (9) Información previa que permite al usuario ayudarle en el uso e interpretación del programa.

Comentemos los ítems (1) a (9):

  • (1) Primero enunciamos el problema matemático, y estudiamos teoremas que permiten arribar a una solución práctica, implementable en un algoritmo sistemático eficiente de computación.

    Esto se basó en el estudio de las propiedades del máximo común divisor y
    cómo se comportan los restos de las divisiones,
    o el hecho de que toda función estrictamente decreciente de enteros positivos tiene que hacerse 0 en una cantidad finita de pasos.

    Son todos ellos hechos matemáticos que luego se tienen en cuenta al diseñar el algoritmo.
  • (2) Se busca luego la manera de llevar las observaciones matemáticas hacia un procedimiento iterativo, o automático de alguna índole, para poder ser programable en una computadora.

    Ahí surgió la idea de mantener siempre la condición \( m > n \) para facilitar el cálculo de las sucesivas iteraciones del máximo común divisor, reduciéndolo a un problema de calcular restos sucesivos de una secuencia decreciente de números enteros positivos.

    Con este método se evita estar comprobando todo el tiempo cuál de los dos números es el más grande, para después intercambiar sus valores, etc.

    Se simplifica también el diseño del algoritmo, reduciéndolo a pocas sentencias sencillas y claras.
    De lo contrario, podría producirse un engorro al considerar múltiples casos intermedios, y tomar acciones según cada caso...
  • (3) Luego de haber resuelto el problema para el caso más común o más importante, es menester tranquilizarse un poco y analizar con paciencia todos los casos posibles.

    Desde el punto de vista informático, una función que acepta ciertos parámetros, tiene que reaccionar de determinada manera, y de forma correcta, para todos los valores posibles en el rango soportado por el tipo de dichos parámetros.

    Si un caso no es tenido en cuenta, la función está mal programada, porque dejará sin resolver algunos casos, o peor todavía,
    puede dejar cabos sueltos en el diseño, que conduzcan a errores por imprevisión de situaciones límite o patológicas.

    En nuestro ejemplo, hemos tenido que analizar con cuidado los casos en que los parámetros son negativos, o cero, y dar una respuesta adecuada en cada caso.

    Los resultados atípicos deben estar bien documentados, en lo posible en el cuerpo mismo de la función, y no en el prototipo. (Un prototipo puede improvisarse en cualquier parte, y resulta "volátil" como fuente de documentación para el programador).

  • (4) Las aserciones lógicas permiten llevar un control del estado de programa, sabiendo todo el tiempo qué condiciones se cumplen en cada punto de ejecución.

    A menudo esto facilita el diseño de algunas porciones de código, porque uno puede estar tranquilo sabiendo que varias condiciones ideales se cumplen en determinado momento, y sin necesidad de estar comprobándolas con un "if".

    Esto no sólo da programas más simples, sino también da un método de hacer programas correctos.

    También, al tener certezas lógico-matemáticas, se evitan comprobaciones y cálculos innecesarios en el programa. Esto redunda en un ahorro de esfuerzo y tiempo de computación.

    No obstante, las "comparaciones" que ahora pueden quitarse de los "if" (por ser redundantes), han de aparecer escritas de todos modos, pero en forma de "comentarios".
    Los comentarios no se "ejecutan", aunque dan información valiosa al programador.

  • (5) Al tomar ciertas decisiones en un algoritmo (como el de hacer m = abs(m)),
    se obliga al programa a actuar de determinada manera.
    Esto trae "implicaciones" en el comportamiento del programa,
    y dichas implicaciones pueden documentarse.

    El resultado de esto han sido comentarios que muestran condiciones matemáticas que el programa cumple tras efectuar determinadas operaciones.

    Si nos acostumbramos a tomar conciencia de las consecuencias de las operaciones que realizamos en un algoritmo, nos será más fácil depurar nuestros programas, porque tendremos a la vista, dentro del mismo programa fuente, un resumen de las condiciones que el programa cumple en determinadas circunstancias.

    Es decir, tendremos una visión más clara del comportamiento esperado de nuestros programas.

    En nuestro ejemplo hemos indicado las "implicaciones" con una especie de "flecha" en un comentario:

    /*     ---> m == n == 0      */

  • (6) Los comentarios adicionales incluyen el nombre del programa, la versión, la fecha, el autor,
    y lo más importante de todo: para qué sirve el programa.

    También se pueden incluir comentarios que explican cómo se usa una determinada función o una macro, y qué comportamientos cabe esperar bajo determinadas circunstancias, o cuál es el uso recomendado, precauciones a tener en cuenta, y otras informaciones.
  • (7) Las operaciones de uso frecuente, como "swap" o "calcular valor absoluto", conviene definirlas como macros o como funciones.

    Más aún, existen en C las funciones inline, que tienen la interface de una función, pero funcionan casi como una macro, pues el compilador reemplaza cada invocación de la función por el cuerpo mismo de la función.

    El uso de funciones o macros da más agilidad al proceso de diseño del programa,
    permitiendo tratar por separado pequeñas partes de un problema,
    y hasta reusarlas en otros programas.

    La definición de funciones o macros de uso frecuente tiene la misma utilidad que la definición de identificadores de constantes: permite repetir en forma consistente y sólida un mismo procedimiento a lo largo de todo el programa.
    O sea, si repetimos una operación, no hace falta reescribir todo el código, sino simplemente escribir la macro o función una sola vez, y reutilizarla tantas veces como haga falta.

    Y si tuviéramos un error en dicha función o macro, bastaría arreglarla una sola vez, para que la corrección repercuta automáticamente en todo lugar en que se la invoque.
  • (8) Dentro de la función main() se ponen las instrucciones principales que controlan el flujo principal del programa.

    Es en este lugar donde conviene realizar las operaciones de entrada/salida de datos.

    Resulta más prolijo no utilizar funciones de entrada/salida en otras partes del programa que no están dentro main().

    Es aquí donde nos preocupamos por el problema de cómo interactuar con el usuario.
    Este aspecto debe ser independiente, en lo posible, de los demás trozos del programa.
    De esta manera, una misma solución a un determinado problema puede ser presentada al usuario con distintos formatos o interfaces, según convenga.

  • (9) El programa tiene que ser fácil de entender por el usuario. Tiene que tener una interface clara y concreta.
    Por sobretodas las cosas, debe ser informativo.

    No se puede hacer un programa "pelado" que sólo calcule cosas extrañas y escupa números sin razón aparente.

    Esta "documentación" beneficia a un hipotético usuario del programa ya compilado,
    y por eso difiere de la "documentación" interna del programa, puesta en forma de comentarios con /* */,
    cuya única utilidad es hacerle la vida más fácil al programador.


Hay muchísimos tips de diseño, y en realidad más que reglas para esto, lo que hay es una discusión que está "viva".
A medida que vamos puliendo nuestros programas, aparecen nuevas ideas para hacerlos mejores en uno u otro sentido, hasta que al final queda algo irreconocible.



Debemos anotar también que el uso de scanf() no es seguro.
Da lugar a que el programa falle.
Hagamos el ejercicio de compilar y correr el programa, y de introducir números que no son enteros, o bien cualquier dato basura que no sean números.
En ese caso veremos que el comportamiento del programa se vuelve impredecible.
La razón es que, al ingresar otros tipos de datos, ocupan distintos tamaños en memoria, produciendo invasiones en zonas de memoria inadecuadas, o alguna que otra patología.



Una de las mejoras que tendremos que hacerle a nuestro programa, pues,
será la de evitar el uso de funciones inseguras como scanf().

También debemos preocuparnos en repensar el algoritmo gcd() para ver si hay modos más convenientes de escribirlo.

Por último, nos está faltando un análisis de eficiencia: ¿cuántos recursos de memoria y tiempo gasta el programa? ¿Cuántos cálculos realiza?
Hay que hacer un análisis asintótica de comportamiento en el peor caso posible.

En caso de obtener un rendimiento pobre, habrá que replantear el algoritmo para ver si hay versiones más eficientes.

Finalmente, debemos poner los algoritmos en archivos separados del programa principal,
para que se vuelvan independientes y reutilizables,
y entonces serían invocados cargándolos como librerías de usuario. (Aquí entendemos "usuario" como el "programador").


17 Agosto, 2013, 07:06 am
Respuesta #5

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,738
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Reformulemos el Algoritmo de Euclides...

La función abs() innecesariamente se restringe al tipo de datos int.
Cualquier tipo de datos numérico debiera funcionar con el mismo procedimiento.
Parece más bien que abs() es candidata a ser una macro antes que una función.

Algo como esto funcionaría:


#define ABS(X) ((X < 0)? (-(X)): X)


Luego la invocaríamos con:

m = ABS(m);

Observemos que lo más probable es que el usuario ingrese números positivos.
En ese caso, la operación de asignación (=) es innecesaria.
Estaríamos "gastando" una operación de asignación, cuando en la mayoría de los casos no hará falta.

Entonces cambiamos un poco el enfoque, evitando hacer esa asignación en el caso que el valor de la variable \( m \) ya sea previamente positivo.
Hacemos pues una macro que haga todo junto: cambia el signo y hace la asignación a la variable, si el valor es negativo, y sólo retorna el valor que tenía la variable previamente, en otro caso:


#define TO_ABS(X) ((X < 0.0)? (X = -(X)): X)


Así, TO_ABS(X) tiene el significado de: "convertir a valor absoluto de X".
La sentencia TO_ABS(m) equivale ahora a:

((m < 0.0)? (m = -(m)): m)

En caso de que m sea un número (entero o real) negativo, se llevará a cabo la asignación m = -m.
Si no, sólo se devuelve m, ahorrando una asignación.

¿Por qué devolvemos m?
Bueno, ocurre que la sintaxis del operador ?: nos obliga a poner algo detrás del signo :
Y ya que en la 1er parte, la asignación m = -m hace que el valor de la expresión sea igual a m,
ponemos, consecuentemente, también m en la 2da parte.
Así, en cualquier caso, la expresión total tiene valor igual a m.

Si el parámetro X no fuese un lvalue, la macro no funcionará.

Comparando con 0.0 en vez de 0 otorga mayor generalidad, y no afecta al funcionamiento de la macro, y no tiene nada que ver con el tipo de número que es X.
Sólo está 0.0 como parte de una operación de comparación.

Se evitan además comparaciones con punteros.

Sin embargo, vamos a comparar mejor contra el entero 0.
Esto lo hacemos así porque el caso típico en nuestro programa será el ingreso de números enteros.
Se evitan entonces costosas operaciones de conversión innecesario de X a un tipo float.

Aunque ahora puede que X sea un puntero que se compara con 0,
el compilador típicamente ha de dar en este caso algún tipo de aviso de esto.



Una vez arreglado el signo de los parámetros,
observamos que los casos en que el usuario ingresa valores iguales son los más raros.
Así que preferimos evitar todas las comprobaciones innecesarias,
y quedarnos primero con el caso típico en que \( m\neq n \).

Allí, como siempre, si \( m < n \), hacemos el "swap".
Si \( m > n \), pasamos al algortimo principal.
Y si \( m == n \), entonces sólo restan dos casos: o bien \( m = n > 0 \) o bien \( m = n = 0. \)
Reunimos ambos casos en una sola sentencia, mediante el operador condicional ?:
y más aún, el resultado directamente lo pasamos como valor de retorno de la función mediante return.

Hay que tener especial cuidado al cambiar el orden de todas estas sentencias, porque en tal caso cambian también las aserciones lógicas que habíamos puesto en un principio.

En particular, se pueden juntar los casos en que \( m > n > 0 \) y \( m > n \geq 0 \) en uno solo,
notando que si \( n == 0 \), el algoritmo puede darse directamente por terminado,
siendo \( m \) el máximo común divisor.
Esto nos obliga a evaluar desde el principio la condición n > 0,
y entonces tenemos que reemplazar el bucle do {} while () con uno del tipo while(){}.



La función gcd() quedar ahora así:


#define INFTY_SIGNAL -1  /* Debe ser un valor int negativo */

/* La macro SWAP(TYPE, X, Y) toma como parámetros:
     * TYPE: un tipo de datos válido de C
     * X, Y: dos lvalues de tipo TYPE
   La macro intercambia entre sí los valores de X é Y
*/
#define SWAP(TYPE, X, Y) { TYPE __Temp = X; X = Y; Y = __Temp; }


/* Macro: TO_ABS(X)
/* ======
/* Convertir un número X a su valor absoluto.     */
/* X debe ser un lvalue.                          */
/* Funciona con todo tipo de dato numérico.       */
/* La conversión es eficiente, inline.            */
/* No hace nada si el número es no-negativo.      */
/* El resultado de la operación es |X|.           */
#define TO_ABS(X) ((X < 0)? (X = -(X)): X)

int gcd(int m, int n) {

    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna INFTY_SIGNAL */
   
    TO_ABS(m); TO_ABS(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    if (m < n)
       SWAP(int, m, n);
    /* gcd(m, n) == gcd(n, m) */
    /* m >= n >= 0 */
   
    /* Se analiza primero el caso típico,  */
    /* para evitar todas las comparaciones que vienen después. */
    if (m > n) {
        /* m > n >= 0 */ 
        while(n > 0) {
            /* m > n > 0 */
            /* gcd(m, n) == gcd(n, m % n) */
            int temp = n; n = m % n; m = temp;
            /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
        };
        /* n == 0, m == gcd(original m, original n) */
       
        return m;   
    }
    /* m == n >= 0 */
   
    return ((m > 0)? m: INFTY_SIGNAL);
    /* m > 0 ---> gcd(m, m) == m; m == 0 ---> gcd(0, 0) == INFTY_SIGNAL */
   
}




Podemos apreciar cómo se ha abreviado el algoritmo.
Se han quitado un par de comparaciones en el camino, y luce todo más conciso.



17 Agosto, 2013, 07:38 pm
Respuesta #6

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,738
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Otra reducción importante del algoritmo puede darse ahora que tenemos todo el panorama más despejado.

Observemos que tras la aserción lógica:

/* m >= n >= 0 */

podemos evitarnos analizar el caso en que (m == n), ya que el algoritmo funcionará de todos modos.
Quitando el condicional "if (m > n)" nos ahorramos una comparación.

En el caso que m == n, esto obliga al algoritmo a realizar una operación % y 3 asignaciones (dentro de las sentencias del while).
Pero esto es aceptable dado que m == n es un caso atípico, y en promedio no pesará demasiado sobre el total de casos posibles.

La ganancia real está en la manera simplificada en que nos queda escrito el algoritmo, así:


int gcd(int m, int n) {

    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna INFTY_SIGNAL */

    TO_ABS(m); TO_ABS(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    if (m < n)
       SWAP(int, m, n);
    /* gcd(m, n) == gcd(n, m) */
    /* m >= n >= 0 */
   
    while(n > 0) {
       /* m >= n > 0 */
       /* gcd(m, n) == gcd(n, m % n) */
       int temp = n; n = m % n; m = temp;
        /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
    };
    /* n == 0 */
       
    return ((m > 0)? m: INFTY_SIGNAL);
        /* m > 0  ---> m == gcd(original m, original n) */
        /* m == 0 ---> return INFTY_SIGNAL */
}




Observamos que al finalizar el while()
nos queda la aserción lógica n == 0 (¿por qué?).

Si m > 0, entonces la variable m contiene el máximo común divisor buscado.
Si m == 0, entonces estamos en el caso \( m = n = 0 \), que corresponde al caso infinito.

La pregunta de si \( m > 0 \) la hacemos directamente en la sentencia return, mediante el operador ?:
abreviando aún más el algoritmo.

17 Agosto, 2013, 08:02 pm
Respuesta #7

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,738
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
¿Realmente es necesario verificar que \( m > n \)?

Podemos quitar esta comprobación, reduciendo así aún más el algoritmo.

Supongamos que quitamos la línea del SWAP(int, m, n).

Si ocurriese que \( m < n \), el algoritmo mismo lo corregiría, ya que m % n == m en este caso,
y para la siguiente iteración queda n igual m % n, mientras que m queda igual al valor que tenía n.
Esto no es otra cosa que un swap implícito entre \( m, n \), que se realiza automáticamente.

Ni siquiera el caso m == 0 trae problemas, ya que tras el swap implícito dentro del bucle while(),
implica que en la siguiente iteración se obtiene m > 0, n == 0, que hace terminar el bucle.

Dado que de todos modos se realiza un swap cuando \( m < n \), es redundante hacer la comparación \( m < n \) antes de entrar al while().

La condición \( n > 0 \) que controla el bucle while() es suficiente para prevenir todo tipo de inconvenientes que surgirían de una hipotética división por 0.

Nuestro algoritmo queda entonces aún más breve:


int gcd(int m, int n) {
    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna INFTY_SIGNAL */

    TO_ABS(m); TO_ABS(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    while(n > 0) {
       /* n > 0 */
       int temp = n; n = m % n; m = temp;
       /* gcd(m, n) == gcd(n, m % n) */
       /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
       /* m >= n >= 0 */
    };
    /* n == 0 */
       
    return ((m > 0)? m: INFTY_SIGNAL);
        /* m > 0  ---> m == gcd(original m, original n) */
        /* m == 0 ---> return INFTY_SIGNAL */
}


Hemos tenido que hacer un cambio importante a nivel lógico:

Ya no es cierta al principio del while() la siguiente aserción lógica:

       /* m >= n > 0 */

Sin embargo se vuelve cierta hacia el final del while().
(Aunque también ahora puede ser n == 0, pero esto no es cierto al iniciar la iteración del while(), en donde obligadamente es n > 0).



Tenemos ahora un algoritmo totalmente simplificado, resumido y optimizado.

El programa ahora también se reduce, porque ya no necesitamos usar la macro SWAP(), y la borraremos.

El programa completo, en spoiler:

Spoiler

/* ========================================================================= */
/* GCD 1.04
/*
/* Este programa calcula el máximo común divisor de dos números enteros.
/*
/* 2013/ago/17
/* rinconmatematico.com
/* Argentinator
/*
/* ========================================================================= */
 
#define INFTY_SIGNAL -1  /* Debe ser un valor int negativo */

int gcd(int, int);

#include <stdio.h>

int main(void) {
   printf(
     "Este programa calcula el M.C.D. de dos nros. enteros\n"
     "Ignora los signos negativos.\n"
     "Si ambos valores son nulos, "
     "el resultado es infinito y el programa termina.\n"
     "\n"
     "Modo de uso:\n"
     "   Escriba un par de nros. enteros separados por un espacio en blanco.\n"
     "\n"
   );
   
   int m, n, ans;   
   
   do {
     scanf("%d %d", &m, &n);
     printf("gcd(%d, %d) = %d\n\n", m, n, ans = gcd(m, n));     
   } while (ans >= 0) ;
   
   return 0;
}

/* Macro: TO_ABS(X)
/* ======
/* Convertir un número X a su valor absoluto.     */
/* X debe ser un lvalue.                          */
/* Funciona con todo tipo de dato numérico.       */
/* La conversión es eficiente, inline.            */
/* No hace nada si el número es no-negativo.      */
/* El resultado de la operación es |X|.           */
#define TO_ABS(X) ((X < 0)? (X = -(X)): X)

int gcd(int m, int n) {
    /* gcd(m, n) devuelve el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna INFTY_SIGNAL */

    TO_ABS(m); TO_ABS(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    while(n > 0) {
       int temp = n; n = m % n; m = temp;
       /* gcd(m, n) == gcd(n, m % n) */
       /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
       /* m >= n > 0 */
    };
    /* n == 0 */
       
    return ((m > 0)? m: INFTY_SIGNAL);
        /* m > 0  ---> m == gcd(original m, original n) */
        /* m == 0 ---> return INFTY_SIGNAL */
}


[cerrar]


Si quitáramos todos los comentarios y aserciones, quedaría esto:


#define INFTY_SIGNAL  -1

#define TO_ABS(X) ((X < 0)? (X = -(X)): X)

int gcd(int m, int n) {
    TO_ABS(m); TO_ABS(n);

    while(n > 0) {
       int temp = n; n = m % n; m = temp;
    };

    return ((m > 0)? m: INFTY_SIGNAL);
}


17 Agosto, 2013, 08:38 pm
Respuesta #8

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,738
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Es hora de separar la interface del usuario del algoritmo, creando una librería para el máximo común divisor.


Nuestra primera versión de la librería (que he numerado como 1.10, para respetar las mofidicaciones que hemos venido haciendo hasta ahora).

Lo que haremos será "recortar y pegar" aquellas partes del programa que son inherentes exclusivamente al cálculo de la función gcd().
Esto abarca la constante INFTY_SIGNAL, la macro TO_ABS(), y la función gcd().

Al principio del archivo escribiremos largos comentarios a modo de documentación.
Explicaremos cuál es el contenido de la librería, qué hace, cómo se usa, cómo funciona,
cuáles son los resultados esperados, qué tipo de parámetros son válidos, etc.
Se informa también la versión, fecha, autor, etc.

Lo importante aquí es que la librería quede claramente dividida en 3 partes:

(1ra parte): Información genérica y manual de uso de la librería.
(2da parte): Interface que permite a los programadores ver de un golpe de vista cuáles son todas las definiciones, macros y funciones que ofrece la librería.
(3ra parte): Implementación de las macros y funciones, y también de rutinas ocultas a los usuarios de la librería.

La 1era parte es una extensa porción de texto puesto como comentarios.
Ahora mostraremos cómo quedó, y así me ahorro explicaciones.
Se enumeran ahí todos los usos de los objetos definidos en la librería.

La 2da parte contiene los prototipos de las funciones y todas las otras declaraciones que hagan falta.
No hay prototipos para macros, pero podemos poner en esta parte comentarios que contengan el prototipo de uso de la macro, y postergar su definición real hacia la 3ra parte.

En la 3ra parte ponemos todo el trabajo duro de programación, con todos los detalles de algoritmos, sentencias, análisis, y demás cosas, que ni el mismo tipo que las programó podría entenderlas.



Ahora tenemos dos archivos: la librería gcd, y el programa gcd que utiliza la librería:

(Librería gcd versión 1.10)
gcd0110.h (librería)


/* ========================================================================= */
/* GCD.h v. 01.10
/*
/* Esta librería define constantes, macros y funciones
/* para calcular el máximo común divisor de dos números enteros.
/*
/* 2013/ago/17
/* rinconmatematico.com
/* Argentinator
/*
/* ========================================================================= */
/* Interface:
/* ==========
/*
/* int gcd(int, int);
/*
/* gcd es una función que toma dos datos de tipo int,
/* calcula su máximo común divisor como un número de tipo int,
/* y devuelve ese valor como resultado.
/* Utiliza el algoritmo de Euclides.
/* Los valores retornados siempre son >= 0,
/* excepto cuando m == n == 0, que retorna el valor negativo INFTY_SIGNAL
/*
/* INFTY_SIGNAL
/*
/* Es una constante negativa de tipo int.
/*
/* TO_ABS(X)
/*
/* Es una macro que acepta un parámetro aritmético X,
/* y lo transforma en su valor absoluto |X|.
/* Si X ya era cero o positivo, no se hace nada.
/* Por lo tanto es eficiente.
/* TO_ABS(X) es una expresión que puede usarse dentro de un cálculo,
/* y el valor que tiene es el X (ya "transformado" a su valor absoluto).
/* Si X no tiene un tipo aritmético, la macro no compilará.
/*
/* ========================================================================= */

#define INFTY_SIGNAL -1  /* Debe ser un valor int negativo */


/* Macro: TO_ABS(X) */

int gcd(int, int);

/* ========================================================================= */
/* Implementación: */

#define TO_ABS(X) ((X < 0)? (X = -(X)): X)

int gcd(int m, int n) {
    /* gcd(m, n) retorna el máximo común divisor de los enteros m y n */
    /* gcd(m, n) siempre es >= 0 */
    /* Excepción: gcd(0, 0) retorna INFTY_SIGNAL, que es < 0 */

    TO_ABS(m); TO_ABS(n);
    /* gcd(m, n) == gcd(abs(m), abs(n)) */
    /* m >= 0, n >= 0 */
   
    while(n > 0) {
       /* n > 0 */
       int temp = n; n = m % n; m = temp;
       /* gcd(m, n) == gcd(n, m % n) */
       /* viejo m > viejo n == nuevo m > nuevo n == (viejo m) % (viejo n) */
       /* m >= n >= 0 */
    };
    /* n == 0 */
       
    return ((m > 0)? m: INFTY_SIGNAL);
        /* m > 0  ---> m == gcd(original m, original n) */
        /* m == 0 ---> return INFTY_SIGNAL */
}

[cerrar]

(Programa gcd versión 1.10)
gcd0110.h (librería)


/* ========================================================================= */
/* GCD 1.10
/*
/* Este programa calcula el máximo común divisor de dos números enteros.
/*
/* 2013/ago/17
/* rinconmatematico.com
/* Argentinator
/*
/* ========================================================================= */
 
#include "gcd0110.h"
#include <stdio.h>

int main(void) {
   printf(
     "Este programa calcula el M.C.D. de dos nros. enteros\n"
     "Ignora los signos negativos.\n"
     "Si ambos valores son nulos, "
     "el resultado es infinito y el programa termina.\n"
     "\n"
     "Modo de uso:\n"
     "   Escriba un par de nros. enteros separados por un espacio en blanco.\n"
     "\n"
   );
   
   int m, n, ans;   
   
   do {
     scanf("%d %d", &m, &n);
     printf("gcd(%d, %d) = %d\n\n", m, n, ans = gcd(m, n));     
   } while (ans >= 0) ;
   
   return 0;
}

[cerrar]


18 Agosto, 2013, 02:49 am
Respuesta #9

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,738
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
Cambios en la interface de usuario.

La función scanf() de la librería estándar stdio.h
sirve sólo para salir del paso, y así poder probar rápidamente nuestros algoritmos,
a ver si funcionan bien, o si hacen lo que esperamos.

Según el parámetro de conversión (por ejemplo %i para enteros)
es posible interpretar correctamente el tipo de datos que el usuario está ingresando a través del teclado.

Pero recordemos que lo que realmente ingresa el usuario por teclado es una string ó cadena de caracteres.

Aunque scanf() esté lista para recibir un entero,
el usuario del programa puede ingresar cualquier otra cadena de caracteres que no sea numérica,
y hacer añicos nuestro programa, dando resultados anómalos o inclusive errores en tiempo de ejecución.



Esto ocurre porque no tenemos el control absoluto de lo que ocurre con la entrada de datos.
Hay varias opciones:

(a) Controlar manualmente los caracteres de entrada, creando rutinas que permitan decidir si los datos son correctos o no.

(b) Estudiar más profundamente la función scanf() y ver si hay algún modo seguro de utilizarla.

(c) Alguna solución intermedia, usando funciones como gets() y sscanf().



La opción (a) a menudo termina siendo la preferible.
Involucra el uso de funciones de conversión de string a entero, y viceversa.

La opción (b) es un paso que podemos dar para profundizar nuestro conocimiento de las funciones de ingreso de datos de la librería stdio.h, pero no auguro buen final si nos quedamos sólo con esto.

Nosotros vamos a avanzar con la opción (c).



La función scanf() tiene la siguiente interface:

int scanf(char * fmt, ...);

Los puntos suspensivos indican un número variable de argumentos.
Allí debemos indicar las direcciones de memoria de las variables que deseamos ir cargando con los datos ingresados por el usuario.

La cadena de formato fmt indica lo que el usuario debe tipear, y el tipo de datos que será interpretado.

El valor de retorno de la función es un entero (int) que indica cuántas variables efectivamente se han cargado con éxito.
Si este valor de retorno es menor que el número de variables que hemos apuntado, entonces quiere decir que ha habido un error en los datos ingresados: el usuario ha escrito algo que es incompatible con el tipo de datos indicado en la cadena de formato.

Las directivas que indican el tipo de datos más típicas son éstas:

%i: Se ingresa un número entero.
%f: Se ingresa un número de punto flotante.
%c: Se ingresa un caracter.
%s: Se ingresa una string sin espacios en blanco.

Por lo general, los espacios en blanco se usan para separar datos distintos en la cadena ingresada por el usuario, y eso explica por qué %s no acepta blancos.

Más aún, los espacios en blanco son ignorados.
En general, todos los caracteres de tipo "blanco" son ignorados: "\b\n\r\t\a\f ".

También son ignorados los datos "sobrantes" que el usuario pudiera ingresar, hasta alcanzar el fin de línea.

Cuando se produce un error, la función scanf() interrumpe lo que está haciendo,
y retorna al programa principal, indicando el número de variables correctamente asignadas.
Aún en el caso de que se hayan asignado correctamente todas las variables, puede que el usuario haya ingresado datos adicionales, con formato erróneo o indeseado.
En teoría estos datos extra debieran ser ignorado, pero a veces el programa tiene comportamientos extraños.
Por ejemplo, puede que quede "colgado" algún caracter fin de línea que se lee siempre, aún cuando el usuario no presiona ENTER.
Las razones por las que puede ocurrir algo así no son muy claras para mí.  ???


NOTA importante: En general scanf() no funciona bien cuando el usuario presiona ENTER directamente, sin haber introducido dato alguno. El comportamiento puede ser errático, o simplemente puede que scanf() lo ignore, esperando a que el usuario ingrese al menos "algo" (una cadena no vacía).

Cuando scanf() no lee apropiadamente el fin de línea, produce errores de lectura en subsecuentes intentos de usar scanf(),
o incluso de otras funciones de lectura de datos, como getchar().




Para evitar desajustes en la lectura de datos del teclado,
primero leemos directamente una línea de la entrada mediante gets(),
y la guardamos en una variable string llamada str.



A continuación intentamos leer datos desde str, ahora usando sscanf().
La función sscanf() funciona igual que scanf(), sólo que en vez de tomar como datos de entrada al teclado, toma una string. La interface es esta:

int sscanf(char *str, char *fmt, ...);

Vayamos a la situación en la cual nos interesa ingresar un par de números enteros.

Dado que la entrada de datos por consola suele tener menos de 128 caracteres,
podemos asumir que 130 es un tamaño adecuado para una cadena de entrada (hay un caracter nulo adicional que gets() agrega, y descarta el caracter fin de línea).

Así, escribimos:

char str[130], sstr[130];
int m, n;
gets(str);
int err = sscanf(str, "%i %i", &m, &n);
if (err < 2)
  printf("Datos no validos\n");


La cadena str recibe una línea de texto que el usuario ingresa por teclado,
por medio de la función gets().
La función sscanf() lee los datos que están ahora en str,
y de allí intenta asignar dos valores enteros consecutivos a las variables m, n.
La cantidad de valores correctamente asignados quedan guardados en la variable err.
Si hay algún tipo de error, se obtiene un valor EOF para err (que es negativo).

Si err == 2, obviamente significa que la asignación ha sido exitosa,
ya que las 2 variables m, n, han sido correctamente asignadas, y podemos proseguir.

Si no, obtendremos err < 2.
En este caso, informaremos al usuario que ha ingresado datos erróneos,
y lo instaremos a que reintente.



Ahora vayamos al programa mismo, y supongamos que hacemos uso de nuestra función gcd(), que calcula el máximo común divisor.

Vemos que el programa contiene sólo dos líneas relativas al máximo común divisor:
la que incluye la librería gcd0110.h, y la que invoca la función gcd().
El resto del programa consiste sólo en interacción con el usuario y manejo de los datos ingresados por teclado, información e instrucciones para el usuario, etc.

Definiremos una función blank() que se encarga de determinar si una string contiene sólo caracteres blancos, o no. Retorna 1 (true, verdadero) en caso afirmativo, y 0 si no.
Para ello tendremos que hacer uso de la librería ctype.h, que contiene la función estándar isspace().
Es más sano, en general, hacer uso de las funciones de ctype.h para interpretar caracteres, antes que definir a mano funciones similares.

También usaremos el tipo de datos bool que viene con el estándar C99,
junto con los valores true (1) y false (0),
lo cual nos impele a usar la librería stdbool.h.

En todas partes agregamos comentarios que explican lo que harán las sentencias,
y detrás de cada sentencia ponemos aserciones lógicas que indican el estado del programa,
o del estado en que quedó el buffer de lectura de scanf() y sscanf().

El resultado es un programa "seguro", es decir, incrackeable por el usuario. (Que yo sepa  ::) ).
Veamos cómo queda el programa completo (en spoiler):

(Programa GCD, versión 1.11, manejo seguro de scanf():)

/* ========================================================================= */
/* GCD 1.10
/*
/* Este programa calcula el máximo común divisor de dos números enteros.
/*
/* 2013/ago/17
/* rinconmatematico.com
/* Argentinator
/*
/* ========================================================================= */
 
#include "gcd0110.h"
#include <stdio.h>
#include <ctype.h>
#include <stdbool.h>

#define STR_LEN 130  /* Longitud string de entrada por teclado. */

bool blank(char *s) {
    for ( ; *s; ++s)
       if (!isspace(*s))
          /* Se ha encontrado un caracter que no es blanco. */
          return false;
     
    /* s es una string que sólo contiene blancos */
    return true;
}

int main(void) {
   
   printf(
     "GCD 1.10\n\n"
     "rinconmatematico.com\n"
     "Argentinator\n\n"
     "Este programa calcula el M.C.D. de dos nros. enteros\n"
     "Si ambos valores son nulos, el resultado es INFINITO.\n"
     "\n\n"
   );
   
   do {
       /* Mensaje de error que se mostrará en caso de datos inválidos. */
       char *err_msg = "\nDatos no validos. "
                       "Se requieren dos numeros enteros. "
                       "Intente nuevamente.\n\n";

       /* Mensaje para instruir al usuario qué datos debe ingresar. */
       printf(
            "Escriba dos enteros separados por un caracter blanco\n"     
            "(o digite un ESPACIO seguido de ENTER para terminar).\n\n"
       );

       /* Primero se toman datos de la entrada estándar (teclado). */
       /* Con gets() se lee correctamente hasta el primer fin de línea. */

       char str[STR_LEN];       
       gets(str);

       int err, m, n;

       /* Ahora se relee la entrada mediante: sscanf()       */
       /* analizando la entrada previamente guardada en str. */
       /* Si hay datos de más, son ignorados. */
       err = sscanf(str, "%i %i", &m, &n);
       if (err < 2)
         /* La entrada del usuario es errónea: */
         /* sscanf() no ha podido asignar correctamente valores a m, n. */
         if (blank(str))
            /* blank(str): todos los caracteres de str son blancos.        */
            /* Esta condición es la que elegimos para terminar el programa */
            break;
/* ================== */           
/* FIN DEL DO() WHILE */           
/* ================== */           

         else {
            /* (err < 2) && !blank(str) */
            /*     ---> El usuario no ha ingresado números      */
            /*          y además ingresó caracteres no blancos. */
            /* err < 2     */
            /*     ---> No se han ingresado bastantes números correctos */
            /*          antes del primer dato incorrecto.               */
            printf("\nDatos no validos. "
                       "Se requieren dos numeros enteros. "
                       "Intente nuevamente.\n\n"
            );
            continue; /* Salta directo a la siguiente iteración */               
         }
       else /* if(err < 2) */
         ;
         
      /* err == 2 */
      /* sscanf() ha podido asignar correctamente valores a m, n */
         
     int ans = gcd(m, n);
     printf(((ans >= 0)? "gcd(%i,%i) = %i\n\n": "gcd(0, 0) = INFINITO\n\n"), m, n, ans);
       
   } while (1); 
   /* El bucle continúa en apariencia "para siempre".                       */
   /* Sin embargo, termina cuando el usuario ingresas una cadena en blanco, */
   /* y se sale en la línea que dice: break;.                               */
   
   return 0;
}

[cerrar]



La moraleja sería que es más seguro usar la combinación de gets() y sscanf() antes que usar directamente scanf().

Si miramos el programa, vamos a ver que es una maraña de comentarios y aserciones lógicas,
que explican el sentido de todo lo que ocurre.
Sin embargo, las sentencias "de verdad" son muy pocas.
El programa, sin comentarios, queda así:

Spoiler

/* ========================================================================= */
/* GCD 1.10
/*
/* Este programa calcula el máximo común divisor de dos números enteros.
/*
/* 2013/ago/17
/* rinconmatematico.com
/* Argentinator
/*
/* ========================================================================= */
 
#include "gcd0110.h"
#include <stdio.h>
#include <ctype.h>
#include <stdbool.h>

#define STR_LEN 130  /* Longitud string de entrada por teclado. */

bool blank(char *s) {
    for ( ; *s; ++s)
       if (!isspace(*s))
          return false;
    return true;
}

int main(void) {
   
   printf(
     "GCD 1.10\n\n"
     "rinconmatematico.com\n"
     "Argentinator\n\n"
     "Este programa calcula el M.C.D. de dos nros. enteros\n"
     "Si ambos valores son nulos, el resultado es INFINITO.\n"
     "\n\n"
   );
   
   do {
       printf(
            "Escriba dos enteros separados por un caracter blanco\n"     
            "(o digite un ESPACIO seguido de ENTER para terminar).\n\n"
       );

       char str[STR_LEN];       
       gets(str);

       int err, m, n;

       err = sscanf(str, "%i %i", &m, &n);
       if (err < 2)
         if (blank(str))
            break;
         else {
            printf("\nDatos no validos. "
                       "Se requieren dos numeros enteros. "
                       "Intente nuevamente.\n\n"
            );
            continue;
         }
       else /* if(err < 2) */
         ;
         
     int ans = gcd(m, n);
     printf(((ans >= 0)? "gcd(%i,%i) = %i\n\n": "gcd(0, 0) = INFINITO\n\n"), m, n, ans);
       
   } while (1); 

   return 0;
}
[cerrar]