Portada arrow Algoritmos arrow Algoritmos para números romanos
Algoritmos para números romanos
martes, 13 de marzo de 2007

Obelix: 'Están locos estos romanos' "Están locos estos romanos"... es lo que decía Obelix.

Bueno... un poco locos sí debían estar para utilizar el sistema de numeración que usaban. Desde nuestra perspectiva, resulta un poco complicado, debido a la dificultad de realizar operaciones aritméticas con él. A ellos les costaba un poco menos, ya que normalmente operaban con un ábaco adaptado a sus necesidades: el ábaco romano.

Aún así, podemos hacernos una idea de lo complicado que podía ser... En fín, desde que utilizamos el sistema decimal, los números romanos han quedado relegados a unas pocas funciones meramente decorativas: los siglos, los capítulos de un libro... y poco más.

En éste artículo, proponemos un algoritmo voraz para escribir en números romanos, y otro para hacer lo contrario, dado un número romano, conocer su valor.

 

Si no tienes fresco lo de los números romanos, te recomiendo un breve resumen que podrás encontrar en Gaussianos.com External link (además de describir el sistema de numeración romana, también proponen formas de realizar operaciones aritméticas con él).

Pero vamos a meternos en harina. Sólo vamos a considerar la numeración romana clásica, en la que podemos contar sólo hasta números inferiores al 4000.

Si nos dan un número, como por ejemplo el 2345 para ponerlo en numeración romana, nosotros intentamos siempre hacer encajar primero el símbolo que nos proporcione la mayor cantidad posible. En el ejemplo del 2345, inmediatamente sabemos que el primer símbolo que tenemos que utiliza es la "M", ya que no hay otro símbolo que nos proporcione mayor cantidad.

Una vez hemos decidido poner la "M", todavía nos quedan 1345 unidades por poner. Nuevamente optamos por la "M", y nos quedan 345. Ya no nos vale la "M" (1000) ni la "D" (500)" así que sabemos que el símbolo siguiente es la "C"... En fín, seguro que sabes continuar... pero a donde quiero llegar es a que siempre probamos con el símbolo romano que nos proporciona la mayor cantidad... es decir, estamos aplicando un algoritmo voraz.

El sistema de numeración romano tiene una pequeña dificultad añadida. No se pueden colocar más de tres símbolos iguales seguidos. Es decir, 900 se representa como CM y no como DCCC. El problema se soluciona rápidamente si consideramos algunos símbolos formados por dos letras: "CM" (900), "CD" (400), "XC" (90), "XL" (40), "IX" (9), "IV" (4).

Es decir, además de los símbolos romanos M D C L X V e I, añadimos CM CD XC XL IX e IV:

Símbolo Valor
M 1000
CM 900
D 500
CD 400
C 100
XC 90
L 50
XL 40
X 10
IX 9
V 5
IV 4
I 1

Manteniendo una lista de símbolos con sus respectivos valores, ya podemos aplicar un algoritmo voraz sin dificultad.

La idea es que dado un número que queremos pasar a números romanos, probamos a ver si se puede descontar el valor más alto de la tabla. Si es así, descontamos ese valor y ya sabemos que el símbolo asociado va al resultado. Hacemos eso hasta que hayamos podido descontarlo todo.

En la implementación que proponemos (en C#, como de costumbre), utilizamos un par de arrays unidimensionales para almacenar la tabla de arriba. Un array de enteros para los valores y uno de cadenas para los símbolos. Nos moveremos por ellos con el mismo índice.

Utilizaremos como condición de terminación del bucle principal que al final hayamos podido descontarlo todo, es decir, que nos quede 0. Esto podría ser peligroso debido a que el índice que recorre los vectores podría salirse de rango, pero en este caso sabemos que no es así, ya que el último valor del array es 1, que siempre puede descontarse.

Ahí va el algoritmo con comentarios.

//devolvera una cadena vacía si
//i es menor que 1
String EnteroARomano(int i)
{
   //Un vector de valores de cada simbolo y otro
   //de símbolos. Nos moveremos por los dos con
   //el mismo índice "p".
   int[] valor ={ 1000, 900, 500, 400, 100, 90,
                  50, 40, 10, 9, 5, 4, 1 };
   String[] simbolo ={ "M", "CM", "D", "CD", "C",
                  "XC", "L", "XL", "X", 
                  "IX", "V", "IV", "I"};
   //El resultado
   String r = "";
   //inicializamos el índice a 0, es decir, al
   //símbolo de mayor valor M (1000)
   int p = 0;
   //Comprobamos que el número de entrada está en
   //un rango válido
   if (i>=1 && i < 4000)
   {
     int x=i; //no queremos perder i
     //mientras x tenga valor
     while (x > 0)
     {
       //probamos a descontar el valor
       //del símbolo actual
       while (x >= valor[p])
       {
         //añadir al resultado
         r += simbolo[p];
         //restar su valor
         x = x - valor[p];
       }
       //una vez agotado un símbolo, pasamos
       //al siguiente
       //ojo: esto es peligroso porque nos
       //podríamos salir del rango de los arrays,
       //pero sabemos que no va a ser así porque
       //cuando p esté fuera de rango x valdrá 0
       //debido a que el último valor es 1, que
       //siempre se puede descontar
       p++;
    } //while
  }//if
return r;
}

Puedes ver el resultado con esta minidemo: Si le das das un rango de valores entre 1 y 3999, generará esos números en notación romana. (Dependiendo del rango que introduzcas puede tardar unos segundos en presentar el resultado).

También podemos plantearnos la operación contraria, es decir, dada la representación de un número al estilo romano, obtener su valor.

La verdad es que no se me ocurre ninguna aplicación práctica para esto, pero bueno... Ya metidos en harina vamos a por ello.

En este caso la estrategia es muy sencilla. Vamos símbolo por símbolo sumando su valor, teniendo en cuenta sólo M D C L X V e I peeeeeero hay una excepción. Si hay un símbolo de menor valor se coloca delante de otro de mayor valor no se suma, sino que se resta. Así que a medida que recorremos el número romano, tenemos que saber cuál era el símbolo anterior.

En la implementación que proponemos no nos vamos a quedar con el símbolo anterior, sino con su valor, es decir, con el último valor que hemos sumado. Iremos comprobando que cada valor que sumamos es menor o igual que el último valor que sumamos. Si no es así, significa que el último valor no debíamos haberlo sumado, sino restado... así que en ese caso, descontaremos el último valor dos veces (una para deshacer la suma que hicimos, y otra para restarlo realmente) y continuaremos hasta haber recorrido todo el número romano.

Por ejemplo, vamos a ver qué pasaría con el número MMCMLX (2960)...

Primera M → sumo 1000
Segunda M → sumo 1000 (ya tengo 2000). Como esta M vale lo mismo que la anterior, sigo.
La C → sumo 100 (ya tengo 2100). Como esta C vale menos que la M anterior, sigo.
La M siguiente → sumo 1000 (ya tengo 3100). Como esta M vale más que la C anterior, descuento 2*100, y me quedo en 2900... hemos corregido el haber sumado la C en lugar de restarla.
La L suma 50 más (2950)
La X suma 10 más (2960)

Vamos con el algoritmo.

//no podemos comprobar exhaustivamente
//si el número romano está bien formado,
//ya que eso complicaría mucho el algoritmo,
//pero sí devolverá un -1 si alguno de los
//símbolos utilizados no es válido
int RomanoAEntero(string s)
{
    //pasar la entrada a mayúsculas
    //(por si acaso)
    s = s.ToUpper();
    //ultimovalor guardará el valor
    //del símbolo anterior al que
    //estemos examinando
    int ultimovalor = 0;
    //los símbolos los almacenamos en una cadena
    //la posición del símbolo coincide con su valor
    //en el vector valor
    String simbolos = "MDCLXVI";
    int[] valor ={ 1000, 500, 100, 50, 10, 5, 1 };
    //Guardaremos la posición del símbolo que
    //estamos examinando dentro de la cadena
    //simbolos, y por lo tanto, servirá de índice
    //para obtener el valor
    int v;
    //el resultado
    int r = 0;
    //si detectamos un símbolo no valido lo
    //ponemos a true y nos servirá para terminar
    bool valido = true;
    //índice para movernos por la entrada
    int indice = 0;
    //el símbolo de la entrada que estamos
    //examinando
    char c;
    while (valido && (indice < s.Length))
    {
        c = s[indice];
        v = simbolos.IndexOf(c);
        if (v >= 0)
        {
           //sumamos su valor
           r += valor[v];
           //si el valor es mayor que el de la
           //vuelta anterior del bucle, entonces
           //el anterior debíamos haberlo restado
           if (valor[v] > ultimovalor)
           {
              r -= 2 * ultimovalor;
           }
           //preparamos el ultimo valor para
           //la siguiente vuelta del bucle
           ultimovalor = valor[v];
        }
        else //el símbolo no se reconoce
        {
           valido = false;
           r = -1;
        }
        indice++; //pasamos al siguiente simbolo
    }//while
    return r;
}

Puedes ver el resultado del algoritmo en esta minidemo, pero presta atención: no se comprueba si la cifra romana introducida es correcta. Se asume que lo es, y se intenta conocer su valor.

En fin... "Están locos estos romanos"... ;-)

 
←Artículo anterior   Artículo siguiente→

Categorías

Artículos relacionados

No se encontraron artículos relacionados

Suscríbete

RSS feed Sindicación RSS

(¿Qué es la sindicación RSS?)


Suscribir por e-mail

¿Dónde estoy?

Estás en La tecla de ESCAPE, un sitio web personal en el que nos gusta hablar de algoritmos, informática, tecnología, ciencia, ingeniería, internet... y cualquier tontería que se nos ocurra. El punto de vista de nuestros artículos técnicos suele ser muy básico, así que a menudo adoptamos grandes simplificaciones. (Más...-Términos de uso)