Corrigé bac NSI 2026 Métropole jour 1 — Exercice 2 : Jeu du Recto-Verso : configurations binaires, xor et graphe des configurations
Sujet officiel du baccalauréat, spécialité numérique et sciences informatiques, session 2026. Corrigé rédigé par Ibrahim Alame.
Travailler ce sujet sur Adloun Sujet officiel (PDF) Corrigé complet (PDF)
Énoncé
Cet exercice porte sur le binaire, les graphes, la récursivité.
Le jeu du Recto-Verso, de la famille des solitaires, se compose de neuf jetons disposés sur un plateau carré. Le plateau a 3 lignes et 3 colonnes. Chaque jeton possède une face Recto, dessinée en couleur noire sur les figures, et une face Verso, dessinée en blanc.
Partie A : représentation binaire des configurations
On appelle configuration l'état du plateau. La figure 1 illustre un exemple de configuration.
Figure 1. Illustration d'une configuration
1. Justifier que le nombre total de configurations possibles est .
Le but du jeu est, depuis une configuration quelconque, de retourner tous les jetons sur leur face Recto (noire). Pour ce faire, le seul type d'opération autorisé est de choisir une ligne ou une colonne ou une diagonale, et d'en retourner tous les jetons. On appelle une telle opération un coup. La figure 2 illustre les huit différents coups possibles à partir d'une configuration donnée.
Figure 2. Les coups possibles depuis la configuration 67
2. Indiquer en justifiant si la séquence de coups suivante, jouée depuis la configuration donnée en figure 1, permet de mener à la victoire : ligne 1, ligne 2, colonne 2.
Une configuration est représentée, ligne par ligne et de haut en bas, par une liste de bits en considérant les jetons Recto comme des 1 et les jetons Verso comme des 0. Ainsi on lit la configuration initiale de la figure 2 comme la liste [0, 0, 1, 0, 0, 0, 0, 1, 1].
On peut aussi lire la liste de bits d'une configuration comme l'écriture binaire d'un nombre entier. Pour l'exemple de la figure 2, la configuration initiale est donc aussi représentée par l'entier 67.
3. En détaillant le calcul, donner l'entier représentant la configuration illustrée en figure 1.
4. Implémenter une fonction representant prenant en paramètre une liste de 0 et de 1 et renvoyant l'entier correspondant.
Dans la suite on considère à disposition une fonction binaire prenant réciproquement en paramètre un entier et renvoyant son écriture en binaire sous forme d'une liste de neuf 0 ou 1.
L'opérateur ou exclusif, noté , a pour table de vérité :
| X | Y | XY |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
La fonction xor définie ci-dessous prend en paramètres deux bits b1, b2 (de type int) et renvoie le résultat de b1b2.
def xor(b1, b2):
if b1 == b2:
return 0
else:
return 1
L'opérateur peut être appliqué sur deux listes de même longueur l_x et l_y contenant plusieurs bits : pour chaque indice de ces listes, on applique le aux deux bits positionnés à cet indice. Par exemple, si l_x = [1, 1] et l_y = [0, 1], alors l_xl_y vaut [1, 0], puisque vaut 1, et vaut 0.
5. Implémenter une fonction xor_etendu qui prend en paramètre deux listes de bits de même longueur l_x et l_y et renvoie la liste l_xl_y.
On peut observer que pour un bit X donné, l'opération X1 a pour résultat l'inverse de X et l'opération X0 a pour résultat X.
Si X représente un jeton, alors X1 le retourne et X0 le laisse inchangé.
Appliquer un coup à une configuration revient à lister les jetons retournés et les jetons non-retournés. On peut donc représenter chaque coup par une liste de bits. Par exemple le coup « ligne 1 » est représenté par la liste [1, 1, 1, 0, 0, 0, 0, 0, 0] et l'appliquer à la configuration 67 revient à calculer [0, 0, 1, 0, 0, 0, 0, 1, 1][1, 1, 1, 0, 0, 0, 0, 0, 0].
La figure 3 illustre les 8 coups possibles de la même manière au moyen du (par souci de lisibilité la notation en liste y est simplifiée) :
| Coup | `67` | masque | résultat | entier |
|---|---|---|---|---|
| ligne 1 | `001000011` | `111000000` | `110000011` | 387 |
| ligne 2 | `001000011` | `000111000` | `001111011` | 123 |
| ligne 3 | `001000011` | `000000111` | `001000100` | 68 |
| colonne 1 | `001000011` | `100100100` | `101100111` | 359 |
| colonne 2 | `001000011` | `010010010` | `011010001` | 209 |
| colonne 3 | `001000011` | `001001001` | `000001010` | 10 |
| diagonale 1 | `001000011` | `100010001` | `101010010` | 338 |
| diagonale 2 | `001000011` | `001010100` | `000010111` | 23 |
Figure 3. Les coups possibles depuis la configuration 67 appliqués par un xor.
On suppose à disposition le dictionnaire coups_possibles qui énumère chaque coup représenté de la sorte :
coups_possibles = {
"ligne 1" : [1, 1, 1, 0, 0, 0, 0, 0, 0],
"ligne 2" : [0, 0, 0, 1, 1, 1, 0, 0, 0],
"ligne 3" : [0, 0, 0, 0, 0, 0, 1, 1, 1],
"colonne 1" : [1, 0, 0, 1, 0, 0, 1, 0, 0],
"colonne 2" : [0, 1, 0, 0, 1, 0, 0, 1, 0],
"colonne 3" : [0, 0, 1, 0, 0, 1, 0, 0, 1],
"diagonale 1" : [1, 0, 0, 0, 1, 0, 0, 0, 1],
"diagonale 2" : [0, 0, 1, 0, 1, 0, 1, 0, 0]
}
On souhaite implémenter une fonction qui prend en paramètre un entier config représentant une configuration et qui renvoie la liste des entiers correspondant à toutes les configurations accessibles en un seul coup.
def configurations_suivantes(config):
config_bin = ...
voisins = []
for ... in ...:
... # ligne facultative
... = xor_etendu(..., ...)
voisins.append(representant(...))
return voisins
6. Recopier et compléter la fonction configurations_suivantes.
Partie B : graphe des configurations
On constate qu'il semble impossible d'atteindre la victoire depuis la configuration représentée par l'entier 4. On appelle configuration perdante toute configuration pour laquelle il n'existe aucune suite de coups menant à la victoire (c'est-à-dire à tous les jetons côté Recto). Inversement, on appelle configuration gagnante toute configuration qui n'est pas perdante.
La configuration représentée par l'entier 4 est effectivement perdante. Pour s'en convaincre on construit le graphe des configurations du jeu Recto-Verso.
Les sommets de ce graphe sont les différentes configurations possibles et il existe une arête d'un sommet vers un sommet si et seulement s'il est possible de passer de la configuration à la configuration en jouant un coup autorisé.
7. Montrer que s'il existe une arête dans le graphe, alors il existe également une arête .
En Python, on choisit de représenter ce graphe par un dictionnaire construit grâce à la fonction configurations_suivantes :
graphe = {
0 : [...],
...
67 : [387, 123, 68, 359, 209, 10, 338, 23],
...
}
8. Une représentation par matrice d'adjacence aurait aussi pu être envisagée. Justifier le choix du dictionnaire en comparant pour les deux structures le nombre d'entiers nécessaires pour stocker en mémoire le graphe des configurations. On pourra laisser ce nombre écrit sous la forme d'une puissance de 2.
La fonction parcours_profondeur ci-dessous implémente un parcours en profondeur d'abord. Elle prend en paramètres la représentation d'un graphe, un entier configuration qui indique le sommet que l'on est en train de visiter, et une liste vus de sommets déjà visités par le parcours. Cette fonction effectue le parcours en rajoutant les sommets visités à la liste vus.
def parcours_profondeur(graphe, configuration, vus):
vus.append(...)
for s in ...:
if s not in vus:
parcours_profondeur(..., ..., ...)
9. Recopier et compléter la fonction parcours_profondeur.
10. En utilisant la fonction parcours_profondeur, écrire en Python une suite d'instructions permettant de vérifier que la configuration représentée par l'entier 4 est bien perdante.
11. Indiquer et justifier quel parcours de graphe (en largeur ou en profondeur) est le plus approprié pour trouver un chemin partant d'une configuration gagnante et menant à la victoire en un minimum de coups.
Corrigé
Partie A : représentation binaire des configurations
1. Le plateau compte jetons et chaque jeton a exactement deux états possibles (Recto ou Verso), indépendamment des huit autres. Une configuration est le choix d'un état pour chacun des neuf jetons : par le principe multiplicatif, il y a configurations. C'est aussi le nombre de mots de 9 bits, ce qui annonce la représentation binaire de la suite.
2. On note un plateau ligne par ligne, Recto et Verso . La configuration de la figure 1 est [0, 0, 1, 0, 0, 1, 1, 1, 0]. On joue les trois coups, chacun retournant les trois jetons de la ligne ou de la colonne choisie :
| Coup joué | ligne 1 | ligne 2 | ligne 3 |
|---|---|---|---|
| (départ, figure 1) | `0 0 1` | `0 0 1` | `1 1 0` |
| après « ligne 1 » | `1 1 0` | `0 0 1` | `1 1 0` |
| après « ligne 2 » | `1 1 0` | `1 1 0` | `1 1 0` |
| après « colonne 2 » | `1 0 0` | `1 0 0` | `1 0 0` |
La configuration finale [1, 0, 0, 1, 0, 0, 1, 0, 0] n'est pas la configuration victorieuse (les neuf jetons côté Recto) : la séquence ne mène pas à la victoire.
Ce que le correcteur attend : l'erreur porte sur le dernier coup. Après « ligne 1 » puis « ligne 2 », seule la colonne 3 est entièrement Verso ; le coup gagnant est « colonne 3 » et non « colonne 2 », qui retourne trois jetons déjà Recto. La séquence ligne 1, ligne 2, colonne 3 mène bien à la victoire (nous l'avons vérifié).
3. La liste de la figure 1, [0, 0, 1, 0, 0, 1, 1, 1, 0], lue comme l'écriture binaire d'un entier (le premier bit est celui de poids fort, ), donne :
La configuration de la figure 1 est représentée par l'entier 78. (Contrôle de la méthode sur l'exemple du sujet : [0, 0, 1, 0, 0, 0, 0, 1, 1] donne .)
4. On parcourt la liste de gauche à droite : à chaque bit, l'entier déjà construit est décalé d'un rang (multiplié par 2) et le bit courant s'y ajoute (schéma de Horner). Cela évite de calculer des puissances de 2 et fonctionne quelle que soit la longueur de la liste.
def representant(liste):
n = 0
for bit in liste:
n = 2 * n + bit
return n
Trace sur [0, 0, 1, 0, 0, 0, 0, 1, 1] : n prend successivement les valeurs ; la fonction renvoie 67. Sur la liste de la figure 1 elle renvoie 78, sur [1]*9 elle renvoie 511 et sur la liste vide elle renvoie 0.
Variante équivalente avec les puissances de 2, l'indice portant le poids :
def representant(liste):
n = 0
for i in range(len(liste)):
n = n + liste[i] * 2 ** (len(liste) - 1 - i)
return n
5. On applique xor indice par indice et l'on accumule les résultats dans une nouvelle liste, sans modifier les listes reçues :
def xor_etendu(l_x, l_y):
resultat = []
for i in range(len(l_x)):
resultat.append(xor(l_x[i], l_y[i]))
return resultat
Tests : xor_etendu([1, 1], [0, 1]) renvoie [1, 0] (l'exemple du sujet) ; xor_etendu([0, 0, 1, 0, 0, 0, 0, 1, 1], [1, 1, 1, 0, 0, 0, 0, 0, 0]) renvoie [1, 1, 0, 0, 0, 0, 0, 1, 1], dont le représentant est 387 (figure 3) ; sur deux listes vides elle renvoie []. La boucle fait len(l_x) tours : coût linéaire en la longueur des listes.
6. La configuration reçue est un entier : on la convertit d'abord en liste de neuf bits avec binaire. On parcourt ensuite les clés du dictionnaire (les noms des huit coups), on récupère pour chaque coup sa liste de bits (le « masque » des jetons à retourner), on l'applique par et l'on convertit le résultat en entier avec representant :
def configurations_suivantes(config):
config_bin = binaire(config)
voisins = []
for coup in coups_possibles:
masque = coups_possibles[coup] # ligne facultative
resultat = xor_etendu(config_bin, masque)
voisins.append(representant(resultat))
return voisins
La ligne facultative peut être supprimée en écrivant directement resultat = xor_etendu(config_bin, coups_possibles[coup]). On peut aussi parcourir coups_possibles.values() et se passer de la clé. Comme un dictionnaire Python est parcouru dans l'ordre d'insertion de ses clés, l'appel configurations_suivantes(67) renvoie exactement [387, 123, 68, 359, 209, 10, 338, 23], la liste de la figure 3 et de la ligne 67 du dictionnaire graphe (vérifié par exécution) ; configurations_suivantes(4) renvoie [452, 60, 3, 288, 150, 77, 277, 80].
Partie B : graphe des configurations
7. Soit une arête : il existe un coup, de masque , tel que (bit à bit). Jouons le même coup depuis :
car est associatif, qu'un bit « ou exclusif » lui-même vaut 0 ( et ) et que laisse chaque bit inchangé. Concrètement, retourner deux fois de suite les mêmes jetons les remet dans leur état initial : chaque coup est sa propre opération inverse. Le coup mène donc de à , et l'arête existe. Le graphe est en fait non orienté (nous l'avons vérifié sur ses 512 sommets) ; chaque sommet a exactement 8 voisins.
8. Le graphe possède sommets.
- Une matrice d'adjacence est un tableau carré à lignes et colonnes, soit entiers (des 0 et des 1), quel que soit le nombre d'arêtes.
- Le dictionnaire associe à chacun des sommets la liste de ses 8 voisins, donc entiers dans les listes, auxquels s'ajoutent les clés : entiers en tout.
La matrice occupe environ 64 fois plus de mémoire (), et elle est presque entièrement remplie de 0 : chaque sommet n'a que 8 voisins sur 512 possibles, le graphe est creux. Le dictionnaire (listes d'adjacence) ne stocke que les arêtes qui existent ; de plus, parcourir les voisins d'un sommet y coûte 8 opérations au lieu de 512 (une ligne entière de la matrice). C'est le choix adapté.
9. Le sommet en cours de visite est ajouté à vus, puis on visite récursivement chacun de ses voisins non encore vus ; les voisins sont lus dans le dictionnaire :
def parcours_profondeur(graphe, configuration, vus):
vus.append(configuration)
for s in graphe[configuration]:
if s not in vus:
parcours_profondeur(graphe, s, vus)
La récursion termine car chaque appel ajoute un nouveau sommet à vus et que le graphe est fini (au plus 512 appels, profondeur de pile bien inférieure à la limite de Python) ; le test s not in vus empêche de tourner en rond dans les cycles.
10. La configuration victorieuse a ses neuf jetons côté Recto : c'est la liste [1]*9, soit l'entier . Une configuration est perdante si aucune suite de coups ne mène à la victoire, c'est-à-dire si 511 n'est pas atteignable depuis elle dans le graphe : on lance le parcours depuis 4 et l'on regarde si 511 figure parmi les sommets vus.
victoire = representant([1, 1, 1, 1, 1, 1, 1, 1, 1]) # 511
vus = []
parcours_profondeur(graphe, 4, vus)
print(victoire not in vus) # affiche True : 4 est perdante
On peut aussi écrire assert 511 not in vus. Exécuté sur le graphe construit avec configurations_suivantes, le parcours depuis 4 atteint 128 configurations, et 511 n'en fait pas partie : la configuration 4 est bien perdante. (Depuis 67, au contraire, les 128 configurations atteintes contiennent 511.)
Ce que le correcteur attend : la liste vus doit être créée vide avant l'appel et passée en argument, puisque c'est la fonction qui la remplit ; l'argument est l'entier 4, pas la liste binaire(4).
11. Le parcours en largeur est le plus approprié. Il explore le graphe par « couches » à l'aide d'une file : d'abord la configuration de départ, puis toutes celles accessibles en un coup, puis celles accessibles en deux coups, etc. Comme toutes les arêtes ont le même poids (un coup), la première fois que la victoire est rencontrée, c'est nécessairement à la distance minimale : le chemin reconstitué a le minimum de coups.
Le parcours en profondeur, lui, s'enfonce le plus loin possible le long d'une branche avant de revenir en arrière : il atteint la victoire par le premier chemin qu'il trouve, en général très long. Par exemple, depuis la configuration 78 de la figure 1, le parcours en profondeur écrit à la question 9 atteint 511 après 23 coups, alors que le parcours en largeur donne le chemin minimal en 3 coups : ligne 1, ligne 2, colonne 3 (question 2).