Detective Pikaptcha
Table of Contents
Ce document est disponible sur ma page personnelle
Figure 1: Détective Pikachu - Legendary Pictures & The Pokémon Company International
1 Détective Pikaptcha, épisode 1
1.1 Problèmatique
Le principe de ce puzzle est de déterminer pour chaque case vide d'un espace donné, le nombre de passages directement adjacents et de l'écrire à l'emplacement de cette dite case. On reçoit par entrée clavier, les informations suivantes:
- La largeur de la zone de labyrinthe noté width
- La hauteur de la zone de labyrinthe noté height
- Le labyrinthe donné ligne par ligne sous forme de chaîne de caractères avec # pour représenter les murs et 0 pour représenter les espaces vides
Les contraintes pour réaliser ce puzzle sont les suivantes:
- 1 \(\leq\) width & height \(\leq\) 100
- temps de réponse \(\leq\) 2 secondes
1.2 Résolution
1.2.1 Stockage des entrées
Pour résoudre ce puzzle, il m'a d'abord semblé indispensable de disposer de l'intégralité du labyrinthe sous forme de tableau à deux dimensions. En effet, puisque le labyrinthe est renseigné ligne par ligne, afin d'étudier si une case dispose d'un espace libre à sa verticale il faut nécessairement garder l'information des lignes suivantes et précédentes.
Pour ce faire, j'ai créé un tableau de façon dynamique afin de ne pas utiliser plus d'espace que nécessaire que j'ai ensuite rempli ligne après ligne avec les lignes du labyrinthe donné en entrée.
1.2.2 Parcours du tableau
Une fois le labyrinthe stocké sous forme de tableau à deux dimensions, il est assez simple de déterminer le nombre de passages juxtaposés à chaque case vide.
Pour ce faire, je parcours chaque case 0 du tableau grâce à une double boucle for. Puis, pour chaque case parcourue, je compte le nombre de cases voisines dont la valeur est différente de # et j'écrase la valeur de la case actuellement analysée par ce nombre.
1.2.3 Affichage du résultat
Une fois le tableau modifié, il ne reste plus qu'à l'afficher ligne par ligne.
Enfin, je termine mon programme en libérant la mémoire allouée à mon tableau en début de programme.
1.2.4 Code source
1: #include <stdlib.h> 2: #include <stdio.h> 3: 4: int main() 5: { 6: int width; 7: int height; 8: scanf("%d%d", &width, &height); 9: char ** map = (char **) malloc(height*sizeof(char*)); 10: for (int i = 0; i < height; i++) { 11: map[i] = (char *) malloc(width*sizeof(char)); 12: scanf("%s", map[i]); 13: } 14: for (int i = 0; i < height; i++) { 15: for (int j = 0; j < width; j++) { 16: if(map[i][j]!='#'){ 17: if(i>0 && map[i-1][j]!='#'){ 18: map[i][j]++; 19: } 20: if(i<height-1 && map[i+1][j]!='#'){ 21: map[i][j]++; 22: } 23: if(j>0 && map[i][j-1]!='#'){ 24: map[i][j]++; 25: } 26: if(j<width-1 && map[i][j+1]!='#'){ 27: map[i][j]++; 28: } 29: } 30: } 31: } 32: for (int i = 0; i < height; i++) { 33: for (int j = 0; j < width; j++){ 34: printf("| %c ", map[i][j]); 35: } 36: printf("|\n"); 37: free(map[i]); 38: map[i]=NULL; 39: } 40: free(map); 41: map=NULL; 42: return 0; 43: }
1.3 Solution proposée par mzbear
J'ai choisi de présenter la solution de mzbear car bien qu'elle soit assez compacte, elle reste néanmoins très compréhensible et très semblable à ma solution dans le raisonnement.
Néanmoins, l'utilisation de certaines astuces de programmation que je ne connaissais pas ou que je n'ai pas l'habitude d'utiliser permettent de rendre le code plus compact sans en perdre en lisibilité.
1.3.1 Stockage des entrées
Pour garder en mémoire les données du labyrinthe, mzbear utilise lui aussi un tableau de deux dimensions. Cependant puisque les contraintes du puzzle n'imposent pas de devoir stocker un tableau de plus de 100 cases, il se contente de déclarer un tableau de 101 cases. Puis, il initialise son tableau ligne par ligne en lisant les lignes entrées.
Notons qu'il évite le fait que son programme essaie de stocker une ligne plus longue que le largeur de son tableau en typant sont entrée à lire par 100s. Ainsi, dans le cas où l'utilisateur entre une ligne plus longue que la largeur du tableau qu'il a déclaré, le programme ne lira que les 100 premiers caractères.
1.3.2 Parcours du tableau
Pour effectuer des tests sur chaque case de son tableau, mzbear utilise une double boucle for.
Pour effectuer ses tests sur les cases du tableau, il utilise les mêmes conditions et opérations que moi, néanmois je trouve que l'utilisation de l'instruction continue et l'alignement des tests suivants rendent le code bien plus lisible que ne peut l'être le mien.
1.3.3 Affichage du résultat
1.3.4 Code source
1: #include <stdlib.io> 2: #include <stdio.io> 3: 4: int main() 5: { 6: char map[101][101]; 7: int w,h; 8: scanf("%d%d", &w, &h); 9: for(int i=0;i<h;i++) 10: scanf("%100s", map[i]); 11: for(int y=0; y<h; y++) 12: for(int x=0; x<w;x++){ 13: if (map[y][x]=='#') continue; 14: if (x>0 && map[y][x-1]!='#') map[y][x]++; 15: if (x<w-1 && map[y][x+1]!='#') map[y][x]++; 16: if (y>0 && map[y-1][x]!='#') map[y][x]++; 17: if (y>h-1 && map[y+1][x]!='#') map[y][x]++; 18: } 19: for(int i=0; i<h; i++) 20: printf("%s\n",map[i]); 21: }
2 Détective Pikaptcha, épisode 2
2.1 Problèmatique
Le principe de ce puzzle est assez semblable à l'épisode 1. Cependant, la méthode d'exploration du labyrinthe est différente puisque l'on doit explorer le labyrinthe à l'aide d'un personnage représenté par les caractères {>,v,<,^}, dont l'orientation représente la direction dans laquelle est orienté le personnage.
Les consignes du puzzle imposent de déplacer le personnage uniquement en suivant soit le mur à sa droite, soit le mur à sa gauche et de ne l'arrêter que lorsqu'il se retrouve sur sa case de départ. Aussi, le personnage doit incrémenter le numéro de chaque case par laquelle il passe, ce qui permettra de déterminer le nombre de passage adjacents à chaque case. On reçoit par entrée clavier, les informations suivantes:
- La largeur de la zone de labyrinthe noté width
- La hauteur de la zone de labyrinthe noté height
- Le labyrinthe donné ligne par ligne sous forme de chaîne de caractères avec # pour représenter les murs, 0 pour représenter les espaces vides et un des caractère {>,v,<,^} représentant la position et l'orientation de départ du personnage.
- Le caractère L si le personnage doit suivre le mur à sa gauche ou le caractère R si le personnage doit suivre le mur à sa droite.
Les contraintes pour réaliser ce puzzle sont les suivantes:
- 1 \(\leq\) width & height \(\leq\) 100
- temps de réponse \(\leq\) 2 secondes
2.2 Résolution
2.2.1 Stockage des entrées
Pour travailler sur ce puzzle, il m'a d'abord fallu comme pour l'épisode 1, stocker l'intégralité du labyrinthe sous forme d'un tableau à deux dimensions.
Pour ce faire j'ai de la même façon créé un tableau alloué dynamiquement grâce à des malloc que j'ai directement rempli ligne après ligne par les lignes données en entrée et lues par un scanf de l'intégralité de la chaine de caractère.
Ainsi je pouvais disposer d'une représentation sous forme de tableau du labyrinthe auquel il fallait encore effectuer quelques opérations avant de pouvoir travailler sur les déplacements du personnage.
2.2.2 Localisation du personnage et orientation
Avant de travailler sur les déplacements du personnage, il me fallait d'abord déterminer sa position ainsi que son orientation de départ dans le labyrinthe puisque ces informations ne sont données qu'implicitement par la position d'un des caractères {>,v,<,^} dans le labyrinthe.
Pour détecter l'emplacement de l'un de ces caractère j'ai créé une double boucle for permettant de parcourir l'intégralité du tableau tant que l'on ne rencontre pas l'un des caractères {>,v,<,^}
En revanche si l'un de ces caractères est rencontré, il est stocké dans une variable de même que sa position dans le tableau, puis ce caractère est remplacé par un 0 dans le tableau.
2.2.3 Parcours du tableau
Afin de parcourir le tableau en faisant longer un mur au personnage, il m'a semblé qu'un protocole récursif était la solution qui se prêtait le mieux à la situation.
Ainsi j'ai créé un protocole explore que l'on peut décomposer en trois parties:
- D'abord une condition qui teste si le protocole a été appelé 4 fois sans que le personnage n'eut eu la possibilité de se déplacer d'une case, auquel cas la variable first passe à 0, ce qui aura pour effet de sortir de la récursive.
- Ensuite le protocole comporte un deuxième test qui vérifie si la position du personnage est la même que sa position de départ l'ayant déjà quitté ou étant bloqué. Si ce test est vrai, alors on quitte la récursive.
- Enfin, le coeur de la récursive vérifie si pour une certaine orientation, position et mur à suivre, le personnage peut se déplacer du coté du mur qu'il doit suivre. S'il le peut on incrémente la case où il était puis on rappelle la récursive avec une nouvelle position et orientation du personnage. Dans le cas contraire, on rappel la récursive en réorientant le personnage dans le sens inverse du mur à suivre.
2.2.4 Affichage du résultat
Enfin, une fois que les modifications sur le tableau ont été effectuées, il ne reste plus qu'à afficher le dît tableau.
Ainsi, de la même manière que pour l'épisode 1, une simple boucle permet d'afficher le tableau modifié ligne après ligne tout en libérant l'espace alloué à chaque ligne juste après leur affichage.
2.2.5 Code source
1: #include <stdlib.h> 2: #include <stdio.h> 3: 4: int first = 1; 5: int count = 0; 6: void explore(char ** map, int width, int height, char facing, char side, int xPos, int yPos, int xStart, int yStart) 7: { 8: if(first){ 9: if(count>4){ 10: return; 11: } 12: count++; 13: } 14: 15: if(yPos==yStart && xPos==xStart && !first){ 16: return; 17: } 18: 19: if(side == 'L'){ 20: if(facing=='>' && yPos>0 && map[yPos-1][xPos]!='#'){ 21: map[yPos][xPos] = map[yPos][xPos]+1; 22: first=0; 23: explore(map, width, height, '^', side, xPos, yPos-1, xStart, yStart); 24: } 25: else if(facing=='>'){ 26: explore(map, width, height, 'v', side, xPos, yPos, xStart, yStart); 27: } 28: else if(facing=='v' && xPos<width-1 && map[yPos][xPos+1]!='#'){ 29: map[yPos][xPos] = map[yPos][xPos]+1; 30: first=0; 31: explore(map, width, height, '>', side, xPos+1, yPos, xStart, yStart); 32: } 33: else if(facing=='v'){ 34: explore(map, width, height, '<', side, xPos, yPos, xStart, yStart); 35: } 36: else if(facing=='<' && yPos<height-1 && map[yPos+1][xPos]!='#'){ 37: map[yPos][xPos] = map[yPos][xPos]+1; 38: first=0; 39: explore(map, width, height, 'v', side, xPos, yPos+1, xStart, yStart); 40: } 41: else if(facing=='<'){ 42: explore(map, width, height, '^', side, xPos, yPos, xStart, yStart); 43: } 44: else if(facing=='^' && xPos>0 && map[yPos][xPos-1]!='#'){ 45: map[yPos][xPos] = map[yPos][xPos]+1; 46: first=0; 47: explore(map, width, height, '<', side, xPos-1, yPos, xStart, yStart); 48: } 49: else if(facing=='^'){ 50: explore(map, width, height, '>', side, xPos, yPos, xStart, yStart); 51: } 52: } 53: else{ 54: if(facing=='<' && yPos>0 && map[yPos-1][xPos]!='#'){ 55: map[yPos][xPos] = map[yPos][xPos]+1; 56: first=0; 57: explore(map, width, height, '^', side, xPos, yPos-1, xStart, yStart); 58: } 59: else if(facing=='<'){ 60: explore(map, width, height, 'v', side, xPos, yPos, xStart, yStart); 61: } 62: else if(facing=='^' && xPos<width-1 && map[yPos][xPos+1]!='#'){ 63: map[yPos][xPos] = map[yPos][xPos]+1; 64: first=0; 65: explore(map, width, height, '>', side, xPos+1, yPos, xStart, yStart); 66: } 67: else if(facing=='^'){ 68: explore(map, width, height, '<', side, xPos, yPos, xStart, yStart); 69: } 70: else if(facing=='>' && yPos<height-1 && map[yPos+1][xPos]!='#'){ 71: map[yPos][xPos] = map[yPos][xPos]+1; 72: first=0; 73: explore(map, width, height, 'v', side, xPos, yPos+1, xStart, yStart); 74: } 75: else if(facing=='>'){ 76: explore(map, width, height, '^', side, xPos, yPos, xStart, yStart); 77: } 78: else if(facing=='v' && xPos>0 && map[yPos][xPos-1]!='#'){ 79: map[yPos][xPos] = map[yPos][xPos]+1; 80: first=0; 81: explore(map, width, height, '<', side, xPos-1, yPos, xStart, yStart); 82: } 83: else if(facing=='v'){ 84: explore(map, width, height, '>', side, xPos, yPos, xStart, yStart); 85: } 86: } 87: } 88: 89: int main() 90: { 91: int width; 92: int height; 93: scanf("%d%d", &width, &height); 94: char ** map = (char**) malloc(height*sizeof(char*)); 95: for (int i = 0; i < height; i++) { 96: map[i] = (char*) malloc(width*sizeof(char)); 97: scanf("%s", map[i]); 98: } 99: char side[2]; 100: scanf("%s", side); 101: 102: int yStart, yPos = 0; 103: int xStart, xPos = 0; 104: char facing; 105: int found = 0; 106: for(int i = 0; found==0 && i<height; i++){ 107: for(int j = 0; found==0 && j<width; j++){ 108: if(map[i][j]!='0' && map[i][j]!='#'){ 109: found = 1; 110: yStart = i; 111: yPos = i; 112: xStart = j; 113: xPos = j; 114: facing = map[yStart][xStart]; 115: map[yStart][xStart] = '0'; 116: } 117: } 118: } 119: 120: explore(map, width, height, facing, side[0], xPos, yPos, xStart, yStart); 121: for (int i = 0; i < height; i++) { 122: for (int j = 0; j < width; j++) { 123: printf("| %c ", map[i][j]); 124: } 125: printf("|\n"); 126: free(map[i]); 127: map[i]=NULL; 128: } 129: free(map); 130: map=NULL; 131: return 0; 132: }
2.3 Solution proposée par mzbear
Eh oui, j'ai encore choisi d'étudier le code de mzbear. En effet, je trouve encore une fois qu'il s'agit du code le plus court tout en était assez simple à comprendre dans son fonctionnement.
2.3.1 Stockage des entrées
Pour ce qui est du stockage des entrées, mzbear utilise le même procédé que pour l'épisode 1, à savoir de stocker l'intégralité du labyrinthe dans un tableau de deux dimensions.
En revanche, pour déterminer la position ainsi que l'orientation de départ du personnage, il utilise une solution semblable à la mienne mais mise en place différemment. En effet, grâce à la fonction strchr(const char \*str, int c), qui renvoie NULL si le caractère c n'est pas trouvé dans str il peut déterminer à quel moment un des caractères est trouvé dans le tableau et quitter la double boucle for grâce à un goto.
Puis il sauvegarde la position de départ ainsi que l'orientation dans des variables afin de plus tard déterminer si son personnage est de retour à sa case départ.
2.3.2 Parcours du tableau
Si jusqu'ici mzbear a proposé des solutions semblables aux miennes si ce n'est dans l'utilisation d'astuces de programmation différentes, la méthode qu'il utilise pour parcourir son tableau est particulièrement remarquable puisqu'elle ne se compose que d'une dizaine de lignes là où beaucoup, ont comme moi utilisé des méthodes bien plus longues.
Tout d'abord, il définit un entier p égal à la position du caractère de départ du personnage dans la chaine ">v<^", ainsi qu'un tableau de \(2\times 4\) contenant les 4 déplacements d'ne case possible.
Puis, il détermine si en effectuant un déplacement correspondant à la ligne p du tableau précédemment définie, le personnage se situe sur une case vide du tableau. Si c'est le cas, il effectue ce déplacement en modifiant la valeur de p, sinon il modifie seulement la valeur de p. Aussi, ces opérations sont répétées tant que la position actuelle ne correspond pas à la position de départ du personnage.
Nous pouvons finalement remarquer que cette solution effectue les mêmes actions que mon protocole récursif sur le personnage. En revanche cette solution est bien meilleure puisqu'en plus d'être plus courte, elle est plus générale que la mienne, mzbear a codé un seul protocole qui s'adapte à tous les tests qui nous sont soumis tandis que j'ai divisé le problème en sous-cas.
2.3.3 Code Source
1: #include <stdlib.h> 2: #include <stdio.h> 3: #include <string.h> 4: 5: int main() 6: { 7: char map[101][101]; 8: int w,h; 9: char side; 10: 11: scanf("%d%d", &w, &h); 12: for(int i=0;i<h;i++) 13: scanf("%100s\n", map[i]); 14: scanf("%c", &side); 15: 16: int x,y,p; 17: const char *dirs = ">v<^"; 18: for(y=0;y<h;y++) 19: for(x=0;x<w;x++) 20: if (strchr(dirs,map[y][x])) 21: goto found; 22: found: 23: p = strchr(dirs,map[y][x])-dirs; 24: map[y][x] = '0'; 25: int px=x,py=y; 26: int delta[4][2] = {{1,0},{0,1},{-1,0},{0,-1}}; 27: 28: do { 29: p=(4+p+(side=='L'?-1:1))%4; 30: for(int i=0;i<4;i++) { 31: int dx=delta[p][0], dy=delta[p][1]; 32: if (px+dx>=0 && px+dx<w && py+dy>=0 && py+dy<h && map[py+dy][px+dx]!='#') { 33: px+=dx; 34: py+=dy; 35: map[py][px]++; 36: break; 37: } 38: p=(4+p+(side=='L'?1:-1))%4; 39: } 40: } while(!(px==x&&py==y)); 41: 42: for(int i=0;i<h;i++) 43: printf("%s\n",map[i]); 44: }
3 Présentation de quelques tests intéressants
3.1 A small chamber
3.1.1 Labyrinthe donné en entrée
Le premier test est un petit labyrinthe assez simple de dimension \(6\times\ 3\), il permet de vérifier que les deux programmes réalisent bien la tâche voulue:
0 | 0 | 0 | 0 | # |
# | 0 | # | 0 | 0 |
0 | 0 | # | 0 | # |
3.1.2 Résultat par le premier programme
Le résultat par le premier programme est bien celui auquel on pouvait s'attendre, chaque case qui n'est pas un mur comporte bien le nombre de passages adjacents.
1 | 3 | 2 | 2 | # |
# | 2 | # | 3 | 1 |
1 | 2 | # | 1 | # |
3.1.3 Résultat par le second programme
Le résultat par le second programme est le même que celui renvoyé par le premier programme, ce qui est logique puisque chaque case vide du labyrinthe est toujours collée à un mur. Ainsi, le personnage passe bien par toutes les cases du labyrinthe.
1 | 3 | 2 | 2 | # |
# | 2 | # | 3 | 1 |
1 | 2 | # | 1 | # |
3.2 Two chamber
3.2.1 Labyrinthe donné en entrée
Le deuxième test que j'ai sélectionné est intéressant car il permet de mettre en évidence la différence de fonctionnement entre les deux programmes.
En effet, ce labyrinthe de \(9\times\ 3\) cases est composé de deux pièces liées par un couloir de 1 case de largeur:
# | 0 | 0 | # | # | # | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | # | # | 0 | 0 | 0 | 0 |
3.2.2 Résultat par le premier programme
Par le premier programme, on constate que toutes les cases du labyrinthe comportent bien le nombre de passages adjacents.
# | 2 | 2 | # | # | # | 2 | 3 | 2 |
2 | 4 | 4 | 2 | 2 | 3 | 4 | 4 | 3 |
2 | 3 | 2 | # | # | 2 | 3 | 3 | 2 |
3.2.3 Résultat par le second programme
En revanche, pour ce qui est du résultat renvoyé par le deuxième programme, on constate que la "pièce" de droite du labyrinthe n'a pas du tout été parcourue par le personnage car en longeant le mur, il retombe sur sa case de départ avant d'arriver au niveau de la "pièce" de droite.
Ainsi, contrairement au résultat renvoyé par le premier programme, toutes les cases du labyrinthe ne contiennent pas le nombre de cases adjacentes.
# | 1 | 1 | # | # | # | 0 | 0 | 0 |
1 | 1 | 2 | 2 | 1 | 0 | 0 | 0 | 0 |
1 | 1 | 1 | # | # | 0 | 0 | 0 | 0 |
3.3 Cross road
3.3.1 Labyrinthe donné en entrée
Enfin, le dernier test que j'ai sélectionné est un labyrinthe de \(10\times\ 6\) cases, ayant la particularité d'avoir certaines de ses cases vides qui ne sont pas collées à un mur.
# | 0 | # | # | # | # | # | 0 | 0 | 0 |
# | 0 | # | 0 | 0 | 0 | # | 0 | 0 | 0 |
# | 0 | # | # | 0 | # | # | 0 | # | 0 |
# | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
# | # | # | # | 0 | # | # | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | # | 0 | 0 | 0 | 0 |
3.3.2 Résultat par le premier programme
Ainsi, comme l'on pouvait s'y attendre, le résultat renvoyé par le premier programme est bien un labyrinthe dans lequel toutes les cases du labyrinthe comportent bien le nombre de passages adjacents.
# | 1 | # | # | # | # | # | 2 | 3 | 2 |
# | 2 | # | 1 | 3 | 1 | # | 3 | 3 | 3 |
# | 2 | # | # | 2 | # | # | 2 | # | 2 |
# | 2 | 2 | 2 | 4 | 2 | 2 | 4 | 3 | 3 |
# | # | # | # | 2 | # | # | 3 | 4 | 3 |
1 | 2 | 2 | 2 | 2 | # | 1 | 3 | 3 | 2 |
3.3.3 Résultat par le second programme
En revanche, le résultat renvoyé par le deuxième programme contient des cases dont la valeur est toujours égale à 0 même si contrairement au deuxième test, elles ne sont pas dans une "pièce" dans laquelle le personnage n'est pas passé.
Ceci s'explique par le fait que ces cases ne sont pas collées à un mur que le personnage a parcouru, et que de ce fait n'ont pas été affectés par son passage.
# | 1 | # | # | # | # | # | 1 | 1 | 1 |
# | 2 | # | 1 | 3 | 1 | # | 1 | 0 | 1 |
# | 2 | # | # | 2 | # | # | 1 | # | 1 |
# | 2 | 2 | 2 | 4 | 2 | 2 | 2 | 0 | 1 |
# | # | # | # | 2 | # | # | 1 | 0 | 1 |
1 | 2 | 2 | 2 | 2 | # | 1 | 2 | 1 | 1 |