Cómo Resolver Problemas de Programación

Vamos a ver un método para resolver problemas de programación. No se trata de un método para programar, para lo que existen diversas «metodologías de desarrollo de software», sino un proceso definido para resolver problemas de programación (como los que nos ponen en los exámenes), especialmente algoritmos.

Este proceso consta de 6 pasos:

  1. Visualizar.
  2. Crear el Andamio.
  3. Preparar Ejemplos.
  4. Crear la Estructura de Datos
  5. Programar el Recorrido.
  6. Programar el Caso General.
  7. Programar los Casos Especiales.
  8. Probar, probar y probar.
  9. Optimizar.

Veamos cada uno de ellos.

1.- Visualizar

Cuando hacemos un programa empezamos por el interfaz gráfico. Igualmente aquí empezamos por «visualizar» la salida que generará el programa. Habitualmente será una salida por consola, pero también podría ser por red o simplemente una variación de la memoria.

Por ejemplo, si nos piden «Hacer un programa que muestre por pantalla los números primos entre dos números dados», visualizaremos la salida como:

Dime dos números: 6 40
Los números primos entre ellos son 7, 11, 13, 17, 19, 23, 29, 31, 37.

Sólo con este paso ya hemos elegido los textos que se verán y decidido que los números se verán en línea, separados por un espacio y una coma. Estamos un paso más cerca de resolver el problema.

2.- Crear el Andamio

En programación, tanto como saber qué programar es importante saber dónde hay que programarlo. En este paso empezamos a codificar, creando el entorno (andamio) en el que se incluirá nuestra solución.

Por ejemplo, podrían habernos pedido programar una función, o un método de una clase. Para nuestro caso, vamos a suponer que nos han pedido desarrollar la solución como un programa de Java.

public class ProblemaDePrimos{
  public static void main(String[] args){
    // Aquí irá el código
  }
}

Ya tenemos el andamio creado.

3.- Preparar Ejemplos

Es imposible que programemos un algoritmo si nosotros no sabemos hacer el proceso. Al mismo tiempo, los ejemplos que hagamos nos servirán como pruebas para comprobar que nuestro programa funciona correctamente.

En nuestro ejemplo, si el usuario indica 6 y 40, el resultado debe ser 7, 11, 13, 17, 19, 23, 29, 31, 37, Y si indica 1 y 10 debe ser 2, 3, 5, 7.

Conviene que estudiemos también los casos más complicados. ¿Qué va a hacer nuestro programa si el usuario indica 50 y 2 (al revés)? ¿Y si indica dos números iguales? ¿Y si alguno es negativo? ¿Y si pone un texto?

Cuantos más ejemplos diferentes resolvamos, más completo será nuestro programa.

5.- Crear la Estructura de Datos

Los programas manejan datos. Así que antes de pensar en cómo obtener el resultado, tendremos que elegir en que estructura de datos vamos a guardar toda la información del problema. Esto incluye no solo los datos que nos proporciona el usuario, sino también los datos propios del problema y las variables intermedias.

Tendremos que elegir si lo guardamos en variables de tipo entero o real, si formaremos listas o matrices, si queremos estructurarlo como objetos…

En nuestro ejemplo vamos a utilizar:

  • 3 enteros para guardar los dos valores que nos da el usuario (menor y mayor) y el número de números primos encontrados (numPrimos). Esta última variable la incializamos a cero.
  • Un texto para mostrar la lista de números primos encontrados.
  • Un Scanner (sc) para leer el teclado.
public class ProblemaDePrimos{
  public static void main(String[] args){
        int menor, mayor, numPrimos = 0;
        String solucion = "";
        Scanner sc = new Scanner(System.in);

       //Aquí irá el código
 } 
}

Seguro que durante el desarrollo veremos que necesitamos alguna más.

Pero recuerda, si no tienes claro como se guardan los datos, no podrás programar.

5.- Programar el Recorrido

Sé por experiencia que cuando se propone un problema de programación a un alumno lo primero que hace es divagar con expresiones como «esto se resuelve con dos for anidados» o «mejor hago una cadena de ifs» o «pongo un while y dentro …». Bien, todo eso es inútil (y habitualmente desacertado) sin los pasos anteriores. Pero tras haber visualizado la salida, creado el andamio y preparado los ejemplos, ahora es el momento de ver qué recorrido requiere nuestro programa.

Habitualmente nuestro programa tendrá que recorrer algunas estructuras de datos. Pueden ser matrices, listas, vectores… En nuestro ejemplo, será necesario crear un bucle desde el valor menor indicado por el usuario hasta el valor mayor.

public class ProblemaDePrimos{
  public static void main(String[] args){

    for (int i = menor; i <= mayor; i++)

  }
}

Fíjate que he creado el bucle sin hacer nada dentro de él. De eso nos encargaremos luego. Si intentas resolverlo todo a la vez, es más fácil que metas la pata.

También me he visto obligado a tomar algunas decisiones. Por ejemplo he decidido que este programa evaluará los valores que indique el usuario. Es decir, que el valor menor y el mayor se mostrarán si son números primos. Otra alternativa habría sido

    for (int i = menor+1; i < mayor; i++)

Y así, si el usuario indica 3 y 11 el resultado habría sido 5, 7 y no 3, 5, 7, 11. Debería haber puesto este caso en mis ejemplos.

6.- Programar el Caso General

Con el recorrido listo, es el momento de programar el caso general. Para ello nos imaginamos en una posición cualquiera del recorrido, mejor si no es ni al principio ni al final y vemos qué lógica tenemos que programar en el bucle.

En nuestro ejemplo para los primos entre 6 y 40 supongamos que estamos en el número 25, ¿qué tenemos que hacer?

Para responder esta pregunta tendrás que analizar qué haces tú al resolver el ejemplo. Y la respuesta es pruebo a ver si es divisible por alguno de los números menores que él. Es decir calculamos el resto de la división entera por los números menores que él y si alguno da cero, ya sabemos que no es primo. Y si al final ninguno ha dado cero, es que es primo y lo mostramos por pantalla.

Eso tendremos que programarlo con una bandera y un bucle while, pues no conocemos de antemano el número de iteraciones:

    for (int i = menor; i <= mayor; i++){
        boolean encontradoDivisor = false;
        int aux = 2;
        do{
            if (i % aux == 0)
                encontradoDivisor = true;
            aux++;
        }while (!encontradoDivisor && aux < i);
        if (!encontradoDivisor)
            System.out.print(i + ",");
    }

Ahora es el momento de probar si nuestro algoritmo funciona. (No te olvides de declarar e inicializar la bandera)

7.- Programar los Casos Especiales

Tras resolver el caso general del algoritmo, tenemos que resolver los casos especiales. Siempre debemos revisar el caso inicial y el caso final, por si hubiera que tratarlo de forma distinta. Y, en ocasiones, tendremos también valores especiales a lo largo del recorrido (por ejemplo valores cero para una división).

En nuestro ejemplo, tras realizar las pruebas, habremos detectado varios problemas:

Dime dos números: 6 40
7, 11, 13, 17, 19, 23, 29, 31, 37,

Vemos que no aparece el texto «Los números primos entre ellos son: » y que aparece una coma extra al final de la solución en lugar de un punto. Vamos a resolverlo creando un String con la solución.

(Si alguno está bien avanzado en Java, la solución ideal sería utilizar un StringBuilder, pero no lo vamos a complicar ahora). El código también muestra el Scanner que necesitamos para leer los números.

public class ProblemaDePrimos {
    public static void main(String[] args) {
        int menor, mayor;
        boolean encontradoDivisor = false;
        String solucion = "Los números primos entre ellos son: ";
        Scanner sc = new Scanner(System.in);

        System.out.print("Dime dos números: ");
        menor = sc.nextInt();
        mayor = sc.nextInt();
        for (int i = menor; i <= mayor; i++) {
            boolean encontradoDivisor = false;
            int aux = 2;
            do {
                if (i % aux == 0)
                    encontradoDivisor = true;
                aux++;
            } while (!encontradoDivisor && aux < i);
            if (!encontradoDivisor)
                solucion += i + ", ";
        }
        solucion = solucion.substring(0, solucion.length() - 2) + ".";
        System.out.println(solucion);
    }
}

Esto ya va por muy buen camino.

8.- Probar, probar y probar

Pero todavía quedan cosas que pulir. Si probamos los ejemplos que preparamos en el punto 3 y estos son suficientemente exhaustivos, podríamos detectar varios fallos en nuestro programa.

Por ejemplo, si los datos de entrada son 10 y 5 la solución que muestra es errónea. Lo mismo que si ponemos 10 y 4 ó 10 y 10. El texto de la solución tampoco es correcto si ponemos 15 y 16, entre los que no hay ningún número primo o 5 y 6 entre los que sólo hay un número primo.

Pues todas estas situaciones las detectamos a base de pruebas. Y descubrirlas es el primer paso para poder solucionarlas.

Nuestro programa podría mejorarse así:

public class ProblemaDePrimos {
    public static void main(String[] args) {
        int menor, mayor, numPrimos = 0;
        boolean encontradoDivisor = false;
        String solucion = "Los números primos que hay entre ellos son: ";
        Scanner sc = new Scanner(System.in);

        System.out.print("Dime dos números: ");
        menor = sc.nextInt();
        mayor = sc.nextInt();

        if (mayor < menor) {
            int aux = menor;
            menor = mayor;
            mayor = aux;
        }

        for (int i = menor; i <= mayor; i++) {
            boolean encontradoDivisor = false;
            int aux = 2;
            do {
                if (i % aux == 0)
                    encontradoDivisor = true;
                aux++;
            } while (!encontradoDivisor && aux < i);
            if (!encontradoDivisor){
                solucion += i + ", ";
                numPrimos++;
            }
        }
        if (numPrimos == 0)
            solucion = "No ha ningún número primo.";
        else{
            if (numPrimos == 1)
                solucion = "El número primo entre ambos es " + solucion;
            else 
                solucion = "Los números primos entre ambos son " + solucion;    
            solucion = solucion.substring(0, solucion.length() - 2) + ".";
        }
        System.out.println(solucion);
    }
}

Y ya tenemos un programa bastante completo. (Falla si en lugar de un número entero el usuario pone un texto, pero para solucionarlo necesitaríamos excepciones).

9.- Optimizar

Nuestro programa ya hace lo que queríamos, pero nos queda un último paso: optimizarlo. ¿Puede hacerlo mejor?

La respuesta es siempre sí. Todo programa puede mejorarse.

En nuestro ejemplo, podríamos darnos cuenta de que no es necesario comprobar todos los números en el bucle while para ver si dividen al que estamos considerando en la iteración (i). Nos basta con comprobar hasta la mitad (no existen divisores de un número mayores que su mitad). Y podemos acelerar el programa sustituyendo la condición del while así:

while (!encontradoDivisor && aux < i/2);

Pero incluso esto es mejorable… Podríamos ir creando una lista de números primos y comprobar simplemente con ellos… O podríamos…

Siempre se puede mejorar, así que no te obsesiones con ello. Sino no terminarás nunca.

Espero que este proceso te sirva de guía para tu próximo problema de programación.