Ejemplo: Solución de un Laberinto


Descripción

Se presenta un programa que resuelve laberintos en forma recursiva. El laberinto a resolver se representa mediante una matriz de números enteros, y su configuración está dada por la inicialización que se haga a dicha matriz. Por esto, para cambiar el laberinto será necesario cambiar el código del programa.

Una vez que se haya introducido el concepto de archivo será posible ampliar este programa para que sea más útil, almacenando distintas configuraciones de laberinto en distintos archivos.


Diseño

A continuación se aplicará nuestra metodología para la solución de problemas para diseñar el programa mencionado.

1. Definición del problema

Conceptualización: El problema se ubica en un contexto en el que se debe buscar una solución a un laberinto con una entrada y una salida, y con movimientos válidos en cuatro direcciones (no diagonales). En el caso de este ejemplo, el contexto está limitado a una aplicación de computador, en la que el laberinto está representado mediante una matriz de números enteros.

Objetivo: Partiendo de la entrada del laberinto, señalar una ruta que nos guíe hasta la salida. Por ejemplo, dado el siguiente laberinto:

Una solución que nos permitiría alcanzar nuestro objetivo sería la ruta mostrada con asteriscos en la siguiente figura:

Descripción general de la solución: El problema se resolverá haciendo uso de la recursividad. La solución se basa en una tarea o función que se llamará recursivamente para intentar solucionar el laberinto a partir de una posición particular. Desde cualquier lugar dentro del laberinto se intentará resolverlo primero intentando hacia izquierda, luego a la derecha, luego arriba y finalmente abajo. Para hacer estos intentos se empleará la función recursiva. El procedimiento parte de la posición de entrada. Se utilizarán marcas que serán dejadas en el camino para no intentar por caminos por los que ya se pasó y, en particular, no volver atrás salvo si se determina que no hay solución por el camino que se sigue hasta ahora.

Elementos involucrados: No existen elementos externos involucrados en la búsqueda de la solución. El único elemento activo es el computador en sí llevando a cabo su trabajo. En el problema participan también los siguientes elementos pasivos:


2. Conceptualización de la solución

Variables: Como se mencionó, el laberinto estará representado mediante una matriz de números enteros: lab. Además, la posición de la entrada en el laberinto estará dada por las coordenadas dentro de la matriz, representadas por las variables x0, y0, para la fila y la columna, respectivamente. De manera similar, la posición de la salida del laberinto se guardará en xf, yf Todas estas variables serán globales.

Cada posición de la matriz puede estar en uno de tres estados:

En el programa en C, la declaración de las variables y constantes globales se hará de la siguiente manera:

/* Constantes que definen la dimension del laberinto */
#define FILAS 13
#define COLUMNAS 21

/* Caracteres que se almacenan en las posiciones del laberinto:    */
/*    ASCII 219 es un cuadro relleno que se usa para las paredes   */
/*    ' ' es un espacio en blanco, para los caminos libres         */
/*    '*' es la marca que senala las posiciones visitadas (camino) */
#define L 219
#define E ' '
#define MARCA '*'

/* Constantes logicas para valor de retorno de funciones */
#define TRUE 1
#define FALSE 0

/* Matriz global que representa el laberinto. En la inicializacion, */
/* L representa un ladrillo en la pared y E un espacio en blanco    */
int lab[FILAS][COLUMNAS] =
   { { L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L },
     { E, E, E, L, E, L, E, L, E, L, E, E, E, L, E, E, E, E, E, E, L },
     { L, L, E, L, E, E, E, L, E, E, E, L, E, L, E, L, E, E, L, E, L },
     { L, E, E, E, E, L, E, L, L, L, E, L, E, L, E, L, L, E, L, E, L },
     { L, E, L, L, L, L, E, L, E, L, E, L, E, L, L, L, E, E, L, E, L },
     { L, E, E, E, L, E, E, L, E, L, E, L, E, E, E, E, E, L, L, E, L },
     { L, E, L, L, L, L, L, L, E, L, E, L, L, E, L, E, E, E, E, E, L },
     { L, E, E, E, E, E, E, E, E, E, E, E, L, E, L, E, L, L, E, L, L },
     { L, L, L, L, L, L, L, L, L, L, L, E, L, E, L, E, E, L, E, L, L },
     { L, E, E, E, E, E, E, E, L, E, E, E, L, L, L, L, L, L, E, L, L },
     { L, E, L, L, L, L, L, E, E, E, L, L, L, E, L, E, E, E, E, E, L },
     { L, E, E, L, E, E, E, E, L, E, E, L, E, E, E, E, L, L, L, E, E },
     { L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L, L }
   };

La inicialización de la matriz que aquí se muestra corresponde al laberinto presentado en la figura del inicio. Los caminos están formados de espacios libres (E's). Observe el contorno del laberinto, formado por L's, y los puntos de entrada y salida.

En la implementación de las distintas subtareas se hace uso de otras variables locales, propias de la función de cada subtarea.

Descomposición en Tareas: El problema global puede descomponerse en las siguientes tareas. Como las variables más importantes se manejan en forma global, no es necesario pasarlas como parámetros. Este es el caso de la variable lab, la matriz que representa el laberinto.

Cada una de las tareas aquí definidas se implementará como una función en el programa en C. Para una mejor comprensión, se incluye el código en C que corresponde a cada una de estas funciones.

  • valida
         int valida(int f, int c) {
            int resultado = TRUE;   /* Se supone inicialmente valida */
    
            /* Controla si la posicion esta fuera del laberinto */
            if ((f<0) || (f>=FILAS) || (c<0) || (c>=COLUMNAS))
               resultado = FALSE;
    
            /* Controla si la posicion ya fue visitada o es muro */
            if (lab[f][c] == MARCA || lab[f][c] == L)
               resultado = FALSE;
    
            return(resultado);
         }
    

  • recorrer
         int recorrer(int fil, int col) {
            int listo = FALSE;   /* Indica si se ha encontrado la salida */
    
            /* Se marca la casilla como visitada */
            lab[fil][col] = MARCA;
    
            /* Se controla la condicion de termino de recursividad: */
            /*    " Llegamos a la salida? "                         */
            if (fil == xf && col == yf)
               return(TRUE);
    
            if (!listo && valida(fil,col-1))   /* Intento a la izquierda */
               listo = recorre(fil,col-1);
            if (!listo && valida(fil,col+1))   /* Intento a la derecha */
               listo = recorre(fil,col+1);
            if (!listo && valida(fil-1,col))   /* Intento hacia arriba */
               listo = recorre(fil-1,col);
            if (!listo && valida(fil+1,col))   /* Intento hacia abajo */
               listo = recorre(fil+1,col);
    
            /* Si no se logro resolver el laberinto desde esta posicion, se   */
            /* desmarca la casilla pues no sera parte de la solucion. En este */
            /* caso se retornara falso lo que provocara que se retroceda.     */
            if (!listo)
               lab[fil][col] = E;
    
            /* Se retorna TRUE/FALSE dependiendo de si se encontro solucion */
            return(listo);
         }
    

  • desplegar
         void desplegar() {
            int i, j;
    
            printf("\n");
            for (i=0; i<FILAS; i++) {
               for (j=0; j<COLUMNAS; j++) {
                  printf("%c", lab[i][j]);
               }
               printf("\n");
            }
            return;
         }
    


    3. Especificación del algoritmo

    El algoritmo principal debe llamar a la tarea que recorre el laberinto, especificando como posición la entrada: ( x0 , y0 ). Antes y después de la resolución se despliega el laberinto. La segunda vez incluirá el camino que forma la solución (las posiciones que fueron marcadas y no desmarcadas posteriormente).

    Algoritmo:


    Código en C:

         main() {
            int ok;   /* Para saber si se obtuvo solucion */
    
            desplegar();   /* Despliega el laberinto sin resolver */
    
            /* Resuelve el laberinto desde la entrada: (x0,y0) */
            ok = recorrer(x0,y0);
    
            if (!ok)   /* Si no se pudo resolver se envia un mensaje */
               printf("\nLaberinto sin solucion\n");
    
            desplegar();   /* Despliega el laberinto resuelto (con el camino) */
         }
    

    4. Validación del algoritmo

    La validación de un algoritmo recursivo requiere verificar que la condición de término se cumplirá en algún momento. En caso contrario, el algoritmo se llamará a sí mismo recursivamente hasta que el stack se agote y el programa colapse (error de ejecución).

    En este caso, la condición de término se alcanza cuando se logra llegar a la salida del laberinto o cuando se han probado todas las posibles direcciones (izquierda, derecha, arriba y abajo) y aún así no se ha encontrado solución. En este caso, el algoritmo recursivo se ha planteado de una forma bastante segura. El procedimiento siempre termina, ya sea positiva o negativamente, encontrando la solución o desistiendo de encontrarla, respectivamente.

    Por otra parte, es necesario definir dominios que caractericen situaciones distintas bajo las cuales se puede ejecutar el algoritmo.


    5. Limitaciones del algoritmo


    Posibles modificaciones (Ejercicios)

    Existen diversas variantes de este programa que pueden implementarse como ejercicio. Algunas requieren leves modificaciones, principalmente en la función recursiva, pero otras involucran mayores cambios.


    Implementación en C



    Volver al inicio Materia del curso