Détective Pikaptcha, ou s'échapper d'un labyrinthe

Valentin PORTAIL - Prép'ISIMA 1 - 2021/2022

logo_uca.png logo_isima.png

Lien vers la page web

pikaptcha.png

Image montant un personnage essayant de sortir d'un labyrinthe grâce à un "fil d'Ariane"


Table des matières

1 Premier exercice : Cartographier le labyrinthe

1.1 Enoncé du problème

On dispose d'une grille représentant un labyrinthe et contenant deux types de valeurs :

  • Un 0 s'il y a un passage.
  • Un # s'il y a un mur, c'est-à-dire, une case infranchissable.

Chaque case de la grille possède au maximum 4 voisins (en haut, en bas, à gauche ou à droite). Les cases en diagonale ne sont donc pas adjacentes.

L'objectif est d'analyser la grille donnée et de la renvoyer en remplaçant les 0 par le nombre de passages adjacents, c'est-à-dire, le nombre de cases 0 autour de chaque case. Les murs (#) ne seront pas remplacés.

Le programme prendra en argument :

  • deux entiers width et height représentant respectivement la largeur et la hauteur de la grille
  • un nombre height de chaînes de caractères nommées line et de longueur width contenant chacune une ligne de la grille avec les caractères 0 et #.

La grille est entourée de murs infranchissables. Il faudra donc vérifier les cases valides.

Le programme devra retourner les height chaînes de caractères de width caractères qui contiendront chacune une ligne de la grille transformée.

1.2 Solution proposée

Puisqu'on aura besoin de toutes les lignes pour pouvoir analyser les passages adjacents, on les stocke dans un tableau à deux dimensions appelé maze qui contient les cases du labyrinthe.

Chaque case sera identifiée par ses coordonnées i et j indiquant respectivement les coordonnées verticale et horizontale de la case.

On commence par demander à l'utilisateur la largeur et la hauteur du labyrinthe, puis les lignes une à une.

int width; // Largeur du labyrinthe
    int height; // Hauteur du labyrinthe
    scanf("%d%d", &width, &height);

    int i; // L'index vertical d'une case
    int j; // L'index horizontal d'une case

    char maze[101][101]; // Un tableau à 2 dimensions qui contiendra les cases du labyrinthe

    for (i = 0; i < height; ++i) {
        char line[101];
        scanf("%s", line);

        for (j = 0; j < width; ++j) {
            maze[i][j] = line[j]; // On place chaque case dans le tableau
        }
    }

On parcourt ensuite le labyrinthe ligne par ligne et colonne par colonne. Pour chaque case (i, j) qui n'est pas un mur, on compte le nombre de passages autour de la case, puis on remplace le 0 par ce nombre dans le tableau maze.

On affiche enfin les résultats ligne par ligne.

for (i = 0; i < height; ++i) {
    for (j = 0; j < width; ++j) { // On parcourt chaque case du tableau
        if (maze[i][j] != '#') { // Si la case n'est pas un mur
            int passages = 0; // On va compter le nombre de passages adjacents

            if (i > 0 && maze[i-1][j] != '#') { // S'il y a un passage valide en haut
                passages += 1;
            }
            if (i < height-1 && maze[i+1][j] != '#') { // S'il y a un passage valide en bas
                passages += 1;
            }
            if (j > 0 && maze[i][j-1] != '#') { // S'il y a un passage valide à gauche
                passages += 1;
            }
            if (j < width-1 && maze[i][j+1] != '#') { // S'il y a un passage valide à droite
                passages += 1;
            }

            maze[i][j] = '0' + passages; // Permet de convertir le nombre en un caractère ASCII
        }
    }
    maze[i][width] = '\0'; // Permet d'indiquer que la ligne est bien finie (pour éviter les erreurs)
    printf("%s\n", maze[i]);
}

Voici le plan du programme final :

#include <stdio.h>

int main() {
    <<pika1_demander_variables>>

    <<pika1_compter_passages>>
    return 0;
}

1.3 Tests

1.3.1 Test 1

Entrée :

5 3 0000# #0#00 00#0#

Sortie attendue :

1322# #2#31 12#1#

Test réussi

1.3.2 Test 2

Entrée :

9 3 #00###000 000000000 000##0000

Sortie attendue :

#22###232 244223443 232##2332

Test réussi

1.3.3 Test 3

Entrée :

3 3 0#0 #0# 0#0

Sortie attendue :

0#0 #0# 0#0

Test réussi

1.3.4 Test 4

Entrée :

7 6 00000#0 0#0#000 00#00## 000#000 #0#00#0 0#00#00

Sortie attendue :

22322#1 2#1#322 32#13## 241#322 #1#22#2 0#12#12

Test réussi

1.4 Description d'une autre solution

Cette solution a été proposée sur Codingame par l'utilisateur "xerneas02" :

#include <stdlib.h>
#include <stdio.h>

char tab[101][101];

int main()
{
    int width, height, i, j, count;

    scanf("%d%d", &width, &height);

    for (i = 0; i < height; i++) scanf("%s", tab[i]);
    for (i = 0; i < height; i++) {
        for(j = 0; j < width; j++){
            if(tab[i][j] == '#')continue;
            if(i < height-1 && tab[i+1][j] != '#') tab[i][j]++;
            if(j < width-1 && tab[i][j+1] != '#') tab[i][j]++;
            if(i > 0 && tab[i-1][j] != '#') tab[i][j]++;
            if(j > 0 && tab[i][j-1] != '#') tab[i][j]++;
        }
    }
    for(i=0 ; i < height ; i++) printf("%s\n", tab[i]);
    return 0;
}

Cette solution a un principe similaire à la mienne. Elle compte le nombre de passages adjacents et le met dans un tableau.

Cependant, elle possède plusieurs avantages :

  • Elle est plus compacte que la mienne (24 lignes contre 48 pour la mienne).
  • Elle modifie directement les variables du tableau au lieu de passer par un entier supplémentaire que l'on reconvertit en caractère.

En revanche, on peut aussi noter plusieurs défauts :

  • L'usage de la commande continue qui est déconseillé.
  • Certaines instructions sont sur la même ligne que les boucles qui leur sont associées et ne sont pas entourées d'accolades.
  • Le tableau tab est défini avant le main.

2 Deuxième exercice : Explorer le labyrinthe

2.1 Enoncé du problème

On dispose d'une grille similaire au premier exercice représentant un labyrinthe et contenant deux types de valeurs :

  • Un 0 s'il y a un passage.
  • Un # s'il y a un mur, c'est-à-dire, une case infranchissable.

La position et la direction de départ sont données par un caractère spécial :

  • > : vers la droite
  • v : vers le bas
  • < : vers la gauche
  • ^ : vers le haut

Un caractère permet d'indiquer le mur que l'on doit suivre :

  • R : Il faut suivre le mur de droite
  • L : Il faut suivre le mur de gauche

L'objectif est de parcourir le labyrinthe et de le renvoyer en remplaçant les 0 par le nombre de fois que l'on est passé par chaque case. Les murs # ne seront pas remplacés.

Le programme prendra en argument :

  • deux entiers width et height représentant respectivement la largeur et la hauteur de la grille
  • un nombre height de chaînes de caractères nommées line et de longueur width contenant chacune une ligne de la grille avec les caractères 0 et #, ainsi qu'un unique caractère >, v, < ou ^ représentant la situation de départ.
  • un caractère side de valeur R ou L représentant le mur à suivre.

La grille est entourée de murs infranchissables. Il faudra donc vérifier les cases valides.

Le programme devra retourner les height chaînes de caractères de width caractères qui contiendront chacune une ligne de la grille transformée.

2.2 Solution proposée

Dans ce programme, on notera la direction avec un entier compris entre 0 et 3 de la manière suivante :

  • 0 : droite
  • 1 : bas
  • 2 : gauche
  • 3 : haut

Chaque case sera identifiée par ses coordonnées i et j indiquant respectivement les coordonnées verticale et horizontale de la case.

On notera la direction de départ, les coordonnées de la case de départ et de la case suivante.

On commencera dans le programme principal par demander à l'utilisateur la largeur et la hauteur du labyrinthe, les lignes du labyrinthe une à une et le mur qu'il faut suivre.

int width; // Largeur du labyrinthe
    int height; // Hauteur du labyrinthe
    scanf("%d%d", &width, &height);

    char maze[101][101]; // Un tableau à 2 dimensions qui contiendra les cases du labyrinthe

    int i; // Coordonnée verticale d'une case
    int j; // Coordonnée horizontale d'une case

    int direction; // 0 : droite / 1 : bas / 2 : gauche / 3 : haut
    int direction_depart;

    int depart_i; // Coordonnée verticale de la case de départ
    int depart_j; // Coordonnée horizontale de la case de départ

    int next_i = -1; // Coordonnée verticale de la case suivante
    int next_j = -1; // Coordonnée horizontale de la case suivante
    // Initialisation à des valeurs impossibles pour le while

    for (i = 0; i < height; ++i) {
        char line[256];
        scanf("%s", line);

        for (j = 0; j < width; ++j) {
            maze[i][j] = line[j]; // On place chaque case dans le tableau
        }
    }

    char side[2]; // Mur qu'il faut suivre
    scanf("%s", side);

On définit ensuite deux fonctions nouveau_i et nouveau_j qui prennent en argument la coordonnée initiale i ou j, la direction direction et la largeur width pour i ou la hauteur height pour j. Elles renvoient la nouvelle coordonnée en allant dans la direction indiquée.

int nouveau_i (int i, int direction, int height) {
    int nv_i;

    if (((direction == 3) && (i == 0)) || ((direction == 1) && (i == height - 1))) {
        nv_i = -1; // Coordonnée impossible
    } else {
        switch (direction) {
            case 1: // Si on va en bas
                nv_i = i + 1;
                break;
            case 3: // Si on va en haut
                nv_i = i - 1;
                break;
            default: // Sinon
                nv_i = i;
        }
    }

    return nv_i;
}

int nouveau_j (int j, int direction, int width) {
    int nv_j;

    if ((direction == 2 && j == 0) || (direction == 0 && j == width - 1)) {
        nv_j = -1; // Coordonnée impossible
    } else {
        switch (direction) {
            case 0: // Si on va à droite
                nv_j = j + 1;
                break;
            case 2: // Si on va à gauche
                nv_j = j - 1;
                break;
            default: // Sinon
                nv_j = j;
        }
    }

    return nv_j;
}

On commence par trouver la case de départ, on récupère la direction initiale et on donne à la case la valeur 0.

int case_depart_trouvee = 0; // Booléen indiquant si la case de départ a été trouvée

    i = 0;
    while ((case_depart_trouvee == 0) && (i < height)) {

        j = 0;
        while (case_depart_trouvee == 0 && j < width) {

            if (maze[i][j] != '0' && maze[i][j] != '#') {
                case_depart_trouvee = 1;
                depart_i = i;
                depart_j = j;

                switch (maze[i][j]) {
                    case '>': // Si on va à droite
                        direction = 0;
                        break;
                    case 'v': // Si on va en bas
                        direction = 1;
                        break;
                    case '<': // Si on va à gauche
                        direction = 2;
                        break;
                    case '^': // Si on va en haut
                        direction = 3;
                        break;
                    default: // Sinon (cas exceptionnel)
                        direction = -1;
                }
            }

            ++j;
        }

        ++i;
    }

Une fois cette case trouvée, on parcourt le labyrinthe. Plusieurs cas sont possibles :

  • S'il n'y a pas de mur à gauche (ou à droite), on tourne à gauche (ou à droite) et on avance d'une case.
  • S'il y a un mur à gauche (ou à droite) et s'il y a un mur en face, on tourne à droite (ou à gauche).
  • Sinon, on avance d'une case sans tourner.

On ajoute 1 à chaque case sur laquelle on va.

i = depart_i;
j = depart_j;
direction_depart = direction;

maze[i][j] = '0';

while (maze[next_i][next_j] != maze[depart_i][depart_j] || (maze[depart_i][depart_j] == '0' && (maze[depart_i][depart_j] != '0' || direction != direction_depart))) {
    // On s'arrête quand :
    // - On est sur la case de départ et :
    //   - La case de départ n'est plus à 0 ou
    //   - La case de départ est toujours à 0 et la direction est celle de départ (on a fait un tour complet sans bouger)

    int devant_i = nouveau_i(i, direction, height);
    int devant_j = nouveau_j(j, direction, width); // Coordonnées de la case tout droit

    if (side[0] == 'L') { // Si on suit le mur de gauche

        int gauche_i = nouveau_i(i, (direction+3) % 4, height);
        int gauche_j = nouveau_j(j, (direction+3) % 4, width); // Coordonnées de la case à gauche

        if ((gauche_i != -1 && gauche_j != -1) && maze[gauche_i][gauche_j] != '#') { // S'il n'y a pas de mur à gauche
            next_i = gauche_i;
            next_j = gauche_j; // On va à gauche
            direction = (direction + 3) % 4; // On tourne à gauche
            maze[next_i][next_j] += 1;
        } else if ((devant_i == -1 || devant_j == -1) || maze[devant_i][devant_j] == '#') { // S'il y a un mur devant
            next_i = i;
            next_j = j; // On reste au même endroit
            direction = (direction + 1) % 4; // On tourne à droite
        } else { // Sinon
            next_i = devant_i;
            next_j = devant_j; // On va tout droit sans changer de direction
            maze[next_i][next_j] += 1;
        }

    } else if (side[0] == 'R') { // Si on suit le mur de droite

        int droite_i = nouveau_i(i, (direction+1) % 4, height);
        int droite_j = nouveau_j(j, (direction+1) % 4, width); // Coordonnées de la case à droite

        if ((droite_i != -1 && droite_j != -1) && maze[droite_i][droite_j] != '#') { // S'il n'y a pas de mur à droite
            next_i = droite_i;
            next_j = droite_j; // On va à droite
            direction = (direction + 1) % 4; // On tourne à droite
            maze[next_i][next_j] += 1;
        } else if (devant_i == -1 || devant_j == -1 || maze[devant_i][devant_j] == '#') { // S'il y a un mur devant
            next_i = i;
            next_j = j; // On reste au même endroit
            direction = (direction + 3) % 4; // On tourne à gauche
        } else {
            next_i = devant_i;
            next_j = devant_j; // On va tout droit sans changer de direction
            maze[next_i][next_j] += 1;
        }

    }

    i = next_i;
    j = next_j;

}

Enfin, on affiche les lignes une par une :

for (i = 0; i < height; ++i) {
    maze[i][width] = '\0'; // Pour éviter les erreurs lors de la lecture de la ligne
    printf("%s\n", maze[i]);
}

Voici le plan du programme final :

#include <stdio.h>

<<pika2_fonctions>>

int main() {
    <<pika2_demander_variables>>

    <<pika2_trouver_depart>>

    <<pika2_parcourir_labyrinthe>>

    <<pika2_afficher_lignes>>

    return 0;
}

2.3 Tests

2.3.1 Test 1

Entrée :

5 3 >000# #0#00 00#0# L

Sortie attendue :

1322# #2#31 12#1#

Test réussi

2.3.2 Test 2

Entrée :

9 3 #00###000 0000<0000 000##0000 R

Sortie attendue :

#11###000 112210000 111##0000

Test réussi

2.3.3 Test 3

Entrée :

3 3 0#0 #># 0#0 L

Sortie attendue :

0#0 #0# 0#0

Test réussi

2.3.4 Test 4

Entrée :

7 6 00000#0 0#0#000 00#0^## 000#000 #0#00#0 0#00#00 R

Sortie attendue :

22322#1 2#1#322 21#01## 131#000 #1#00#0 0#00#00

Test réussi

2.4 Description d'une autre solution

Cette solution a été proposée sur Codingame par l'utilisateur "Reynalde" :

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

/////////////////////////////////////
char dL[4]  = {'<', '^', '>', 'v'}; char dR[4]  = {'>', '^', '<', 'v'};
char dL1[4] = {'^', '>', 'v', '<'}; char dR1[4] = {'v', '>','^','<'};
char dL2[4] = {'>', 'v', '<', '^'}; char dR2[4] = {'<', 'v', '>', '^'};
char dL3[4] = {'v', '<', '^', '>'}; char dR3[4] = {'^', '<', 'v', '>'};
/////////////////////////////////////

struct position{
    int x,y;
    char dir;
    bool move;
};
typedef struct position pos;

 ////////////////// Deplacement ////////////////// 

pos moving(char grid[][256], pos p, char side, int h, int w, char directions[4]){
    int x = p.x; int y = p.y;
    int j=0; bool trouve=false;

 while(!trouve && j < 4){
         if ((directions[j] == '>' && y < w - 1 && grid[x][y+1] !='#') || !grid[x][y+1]==directions[j]){
            p.x=x; p.y=y+1; p.dir= '>'; p.move=true; trouve=true;
        }else if ((directions[j] == '^' && x > 0 && grid[x-1][y] != '#') || !grid[x-1][y]==directions[j]){
            p.x=x-1; p.y=y; p.dir= '^'; p.move=true; trouve=true; 
        }else if ((directions[j] == '<' && y > 0 && grid[x][y-1] != '#') || !grid[x][y-1]==directions[j]){
            p.x=x; p.y=y-1; p.dir= '<'; p.move=true; trouve=true; 
        }else if ((directions[j] == 'v' && x < h - 1  && grid[x+1][y] != '#') || !grid[x+1][y]==directions[j]){
            p.x=x+1; p.y=y; p.dir= 'v'; p.move=true; trouve=true;
        } j++;
    }   return p;
}
 ////////////////////////////////////////////////

int main(){
    int w,h,x,y; scanf("%d%d", &w, &h);
    char grid[256][256];
    pos p; bool end=false;

    for (int i = 0; i < h; i++) { char line[256]; scanf("%s", line);
        for(int j = 0; j < w; j++){
            grid[i][j]=line[j];
            line[j] != '0' && line[j] != '#' ? p.x = i, p.y = j, p.dir = line[j], p.move=false : 0;
        }
    }

    int pos=0; int index;
    int x_d = p.x; int y_d = p.y;
    char side[2]; scanf("%s", side); char s=side[0];

    while(end!=true){
        if (s=='L'){
            if (p.dir == '^') p=moving(grid,p,s,h,w,dL); 
            else if (p.dir == '>') p=moving(grid,p,s,h,w,dL1);
            else if (p.dir == 'v') p=moving(grid,p,s,h,w,dL2); 
            else if (p.dir == '<') p=moving(grid,p,s,h,w,dL3); 
        }else{
             if (p.dir == '^') p=moving(grid,p,s,h,w,dR);
             else if (p.dir == '>') p=moving(grid,p,s,h,w,dR1); 
             else if (p.dir == 'v') p=moving(grid,p,s,h,w,dR2); 
             else if (p.dir == '<') p=moving(grid,p,s,h,w,dR3);
       }
        x = p.x; y = p.y;
        if(grid[x][y] == '>' || grid[x][y] =='<' || grid[x][y] == '^' || grid[x][y] == 'v'){ 
            grid[x][y]='0';
            end=true;
        }else if(x == x_d && y == y_d){
            int pos=(int) grid[x][y]; pos++;
            grid[x][y]=(char) pos; end=true; 
        }
        if(p.move==true){ int pos= (int) grid[x][y]; pos++; grid[x][y]=(char) pos; }
    }

  ////////// Affichage  //////////
    for (int i = 0; i < h; i++) {
     for(int j = 0; j < w; j++) printf("%c", grid[i][j]);
      printf("\n");
    }
  ////////////////////////////////
return 0;
}

Dans cette solution, on crée une structure pos qui contient les coordonnées x et y, la direction dir et un booléen move qui indique si le déplacement a été effectué.

On peut trouver plusieurs avantages :

  • Il n'y a plus qu'une seule fonction qui s'occupe de faire les déplacements.
  • Le code est plus court et plus compact.

En revanche, il y a aussi de nombreux inconvénients :

  • Le code est peu lisible et moins évident à comprendre.
  • Il y a peu de commentaires qui pourraient faciliter la lecture du code.
  • Certaines instructions sont sur la même ligne que les boucles qui leur sont associées et ne sont pas entourées d'accolades.

3 Partie supplémentaire : Graphe réalisé avec GraphViz

La commande ci-dessous… :

strict digraph {
    Clermont -> Aurillac
    Clermont -> Le_Puy
    Clermont -> Montluçon
    Montluçon -> Moulins
    Moulins -> Vichy
    Vichy -> Clermont
    Montluçon -> Clermont
    Saint_Etienne -> Clermont
}

…permet de réaliser le graphe suivant :

graphviz.png

Date: 15/02/2022

Auteur: Valentin PORTAIL

Created: 2022-03-06 dim. 16:05

Validate