Un corrigé pédagogique pour comprendre et justifier vos réponses. Les conseils de rédaction ne constituent pas un barème officiel détaillé.
Exercice 1 · 6 points
Taquin : déplacements légaux et résolution par historique
Le taquin possède neuf cases : huit numérotées de 1 à 8 et une case vide, codée 0. La position gagnante de la figure 1 est :
| Ligne | Colonne 1 | Colonne 2 | Colonne 3 |
|---|---|---|---|
| 1 | Vide | 1 | 2 |
| 2 | 3 | 4 | 5 |
| 3 | 6 | 7 | 8 |
Seules les cases directement adjacentes au vide, horizontalement ou verticalement, peuvent permuter avec lui. Figure 2, grille mélangée :
| Ligne | Colonne 1 | Colonne 2 | Colonne 3 |
|---|---|---|---|
| 1 | 5 | 3 | 8 |
| 2 | Vide | 1 | 2 |
| 3 | 7 | 6 | 4 |
Les numéros déplaçables sont 5, 1 et 7. La représentation parcourt la grille ligne par ligne : tab = [5,3,8,0,1,2,7,6,4].
Question 1
#Donner l’indice dans tab de la case portant le numéro 2.
Indice
Python compte les indices à partir de zéro.
Comprendre la correction
5 : le numéro 2 est le sixième élément de la liste. Il faut distinguer la valeur de la tuile, 2, de l’indice où elle se trouve, 5.
tab[3] = tab[4]
tab[4] = 0Question 2
#Dessiner la grille après tab[3] = tab[4], puis tab[4] = 0.
Indice
La première affectation copie la tuile 1 avant de vider sa case.
Comprendre la correction
| Ligne | Colonne 1 | Colonne 2 | Colonne 3 |
|---|---|---|---|
| 1 | 5 | 3 | 8 |
| 2 | 1 | Vide | 2 |
| 3 | 7 | 6 | 4 |
La tuile 1 passe dans l’ancien vide d’indice 3 et le vide se retrouve à l’indice 4. Le tableau est [5,3,8,1,0,2,7,6,4]. Le numéro de cette question est absent de l’impression du PDF, mais cette consigne correspond au deuxième item entre les questions 1 et 3.
Question 3
#Donner une expression booléenne testant la position gagnante.
Indice
La victoire dépend de la place de chaque valeur.
Comprendre la correction
tab == [0,1,2,3,4,5,6,7,8]L’égalité des listes compare longueur et valeurs dans l’ordre. Tester seulement si tous les numéros sont présents ne suffit pas, car toute grille mélangée valide possède les mêmes numéros.
class Taquin:
def __init__(self):
self.tab = [0,1,2,3,4,5,6,7,8]Question 4
#Écrire Taquin.est_gagnant.
Indice
Réutilisez l’expression précédente sur l’attribut de l’objet.
Comprendre la correction
def est_gagnant(self):
return self.tab == [0,1,2,3,4,5,6,7,8]La méthode teste la grille de l’instance courante via self.tab. Elle ne modifie aucun état et renvoie directement le booléen de comparaison.
def indice(self, numero):
assert type(numero) == int, "numero doit être entier"
... , "numero de case non valide"
i = 0
while ...:
i += 1
return iQuestion 5
#Compléter les lignes 3 et 5 de indice(self,numero).
Indice
La méthode doit retrouver les tuiles et la case vide.
Comprendre la correction
assert 0 <= numero <= 8, "numero de case non valide"
# Condition de la boucle :
while self.tab[i] != numero:0 doit être autorisé car jouer cherche aussi l’indice du vide. Le tableau reste une permutation de 0 à 8 : un numéro valide est donc forcément trouvé avant de sortir de la liste. Cette garantie vient de l’invariant du jeu, pas du while seul. Si le tableau pouvait être corrompu, il faudrait borner la recherche et signaler l’absence.
est_possible(numero) est disponible et renvoie si la tuile de numéro 1 à 8 peut bouger.
def jouer(self, numero):
if ...:
i = self.indice(numero)
j = self.indice(0)
... = numero
... = ...Question 6
#Compléter jouer(numero) pour échanger un numéro déplaçable avec le vide.
Indice
Le numéro doit être placé à l’indice du vide, pas à son propre indice.
Comprendre la correction
def jouer(self, numero):
if self.est_possible(numero):
i = self.indice(numero)
j = self.indice(0)
self.tab[j] = numero
self.tab[i] = 0i est la position de la tuile et j celle du vide. La tuile va à j ; sa case initiale i devient vide. Les deux indices sont calculés avant les modifications. Un coup illégal ne change rien. Chaque coup est sa propre opération inverse : rejouer immédiatement le même numéro remet la grille dans l’état précédent.
coups_possibles() renvoie les numéros déplaçables, par exemple [5,1,7] pour la figure 2. random.choice(liste) choisit un élément de la liste.
def melanger(self, n):
precedent = None
i = 0
while ...:
possibilites = ...
choix = ...
if choix != precedent:
self.jouer(choix)
precedent = ...
i += 1Question 7
#Compléter melanger(n), qui effectue n coups aléatoires sans rejouer immédiatement le même numéro.
Indice
Un essai refusé ne doit pas être compté comme un mélange effectué.
Comprendre la correction
def melanger(self, n):
precedent = None
i = 0
while i < n:
possibilites = self.coups_possibles()
choix = random.choice(possibilites)
if choix != precedent:
self.jouer(choix)
precedent = choix
i += 1La liste des coups possibles doit être recalculée après chaque déplacement. Le compteur n’augmente qu’après un coup effectivement joué, pas après un tirage rejeté. Rejouer la même tuile annulerait simplement le dernier coup ; l’exclusion évite ces retours immédiats. Le tirage peut théoriquement répéter plusieurs fois le même numéro avant d’en choisir un autre ; filtrer la liste avant random.choice serait une variante plus directe.
On part d’une grille gagnante et d’une pile vide. Hors résolution automatique, tous les coups légaux de mélange et de jeu sont empilés. En résolution, on dépile et rejoue jusqu’à retrouver une grille gagnante. Exemple : 1,4,5 pour mélanger, puis 2 joué par le joueur.
Question 8
#Après mélange par 1,4,5 puis coup du joueur 2, donner l’ordre des coups de résolution et l’état de la pile à chaque étape.
Indice
Le sommet de la pile contient le dernier coup exécuté.
Comprendre la correction
| Étape | Coup annulé/joué | Pile restante, bas → sommet |
|---|---|---|
| Avant résolution | - | [1,4,5,2] |
| 1 | 2 | [1,4,5] |
| 2 | 5 | [1,4] |
| 3 | 4 | [1] |
| 4 | 1 | [] |
L’ordre est 2,5,4,1. Une pile LIFO retrouve d’abord le dernier changement. Chaque numéro rejoué annule exactement l’échange qu’il avait produit. L’ordre inverse est essentiel : rejouer les numéros dans l’ordre initial ne garantit pas de revenir à la solution.
class Taquin:
def __init__(self):
self.tab = [0,1,2,3,4,5,6,7,8]
self.pile = Pile()
self.mode_resolution = FalsePile fournit est_vide(), empiler(valeur) et depiler(). À la fin de la branche d’un coup légal, jouer contient :
if not self.mode_resolution:
self.pile.empiler(numero)Question 9
#Écrire resoudre : activer le mode automatique puis afficher chaque coup effectué jusqu’à la victoire.
Indice
L’historique ne doit pas enregistrer les opérations qui sont précisément en train de l’annuler.
Comprendre la correction
def resoudre(self):
self.mode_resolution = True
while not self.est_gagnant():
numero = self.pile.depiler()
self.jouer(numero)
print(numero)Le mode est activé avant le premier appel à jouer pour éviter de réempiler l’annulation. Sous l’invariant annoncé (tous les coups enregistrés depuis une grille gagnante), une grille non gagnante possède un historique suffisant pour revenir à la solution. Une application acceptant des états chargés de l’extérieur devrait aussi détecter une pile vide incohérente. On peut rétablir mode_resolution=False après la boucle si l’on souhaite reprendre une session de jeu avec enregistrement des nouveaux coups.
Question 10
#Pourquoi oublier le mode résolution peut-il entraîner la non-terminaison ?
Indice
Suivez le même numéro au sommet sur deux tours consécutifs.
Comprendre la correction
Le coup dépilé est rejoué puis immédiatement réempilé par jouer. Le sommet ne disparaît donc pas de l’historique. À l’itération suivante, on joue de nouveau ce même numéro, ce qui annule l’annulation : la grille oscille entre deux états. Dans l’exemple à quatre coups, aucun de ces deux états n’est la solution, donc la boucle ne termine pas.
La formulation ne doit pas être prise comme une impossibilité absolue de terminaison : si un seul coup sépare la grille de la solution, la première annulation peut déjà gagner et la boucle s’arrête. Le bug est que le mécanisme de progression n’est plus garanti, pas que toute entrée boucle forcément.
Question 11
#Modifier la gestion de pile dans jouer pour annuler deux numéros consécutifs identiques.
Indice
Lorsque les coups diffèrent, le nouveau doit rester au sommet.
Comprendre la correction
if not self.mode_resolution:
if not self.pile.est_vide():
precedent = self.pile.depiler()
if precedent != numero:
self.pile.empiler(precedent)
self.pile.empiler(numero)
else:
self.pile.empiler(numero)On retire temporairement le sommet. S’il est égal au nouveau coup, les deux mouvements se compensent : on ne remet rien. Sinon on restaure l’ancien sommet avant d’empiler le nouveau, pour conserver l’ordre chronologique. Cette logique reste à l’intérieur de la branche d’un coup légal ; un clic impossible ne doit jamais polluer l’historique.
Revenir en arrière dans un vrai historique de taquinUn atelier pour expérimenter
Les coups 1,4,5,2 ont été joués depuis la solution. Augmentez le nombre d’annulations et observez ensemble la pile et la grille ; chaque déplacement rejoue la même tuile en sens inverse.
Lire le résultat de l’expérience initiale
Encore 4 coups dans la pile
Les coups annulés sont aucun. Le sommet est le dernier élément de la pile et chaque annulation laisse l’historique se raccourcir.
| Ligne | Colonne 1 | Colonne 2 | Colonne 3 |
|---|---|---|---|
| 1 | 1 | 4 | Vide |
| 2 | 3 | 5 | 2 |
| 3 | 6 | 7 | 8 |
La pile convient à l’annulation parce que l’opération inverse doit être exécutée dans l’ordre opposé à l’historique.
Exercice 2 · 6 points
Jeu de mots : déduire un alphabet par tri topologique
Un jeu de société permet aux joueurs inscrits sous pseudonyme de participer à plusieurs parties et d’y gagner des points. Une partie peut réunir plusieurs joueurs. La base possède personne(id_pers,pseudo_pers,date_pers) et participation(id_partie,id_pers,nb_point). La date est un texte au format AAAA-MM-JJ. Les opérations SQL autorisées sont SELECT, FROM, WHERE, AND, OR, JOIN ... ON, INSERT, UPDATE, DELETE, DISTINCT et ORDER BY.
Question 1
#Dans quelle table id_pers est-elle une clé étrangère ?
Indice
La table qui enregistre une participation doit référencer un joueur existant.
Comprendre la correction
participation, où elle référence personne.id_pers. La même personne peut apparaître sur plusieurs participations, mais son identifiant est unique dans la table personne.
Question 2
#Pourquoi id_partie ne peut-elle pas être la clé primaire de participation ?
Indice
Un même identifiant de partie doit pouvoir apparaître une fois par joueur.
Comprendre la correction
Plusieurs joueurs participent à la même partie, donc plusieurs lignes partagent id_partie. Une clé primaire doit être unique. Le couple (id_partie,id_pers) conviendrait si un joueur possède une unique ligne de score par partie, convention naturelle de cette modélisation.
Question 3
#Ajouter la personne 42, pseudonyme theorie, inscrite le 14 décembre 2022.
Indice
Conservez l’ordre année-mois-jour exigé par le schéma.
Comprendre la correction
INSERT INTO personne (id_pers, pseudo_pers, date_pers)
VALUES (42, 'theorie', '2022-12-14');Le format ISO place l’année avant le mois et le jour. L’identifiant 42 est fourni par la consigne ; il doit être libre pour cette insertion.
Question 4
#Obtenir les identifiants de toutes les parties jouées par l’unique personne de pseudo test.
SQLRetrouver les parties du compte testÉcrivez votre solution et mettez-la à l’épreuve
Renvoyez les identifiants des parties jouées par l’unique personne dont le pseudonyme est test. Le pseudonyme identifie la personne à rechercher, sans donner directement son identifiant.
SELECT ...
FROM ...
WHERE ...;Les cas de test proposés :
- Jeu pédagogique sur les deux relations : La personne 42, theorie, et sa date sont les données de la question 3. Les autres comptes et parties sont des compléments pédagogiques pour exercer la requête de la question 4.
- Cas complémentaire : pseudonyme exact et identifiant différent : Jeu complémentaire pédagogique, distinct des données officielles. Le compte testeur ne doit pas être sélectionné. Le numéro d’une partie peut être égal à un identifiant de personne sans les désigner comme liés.
Indice
Reliez les participations à l’identité du joueur avant de filtrer son pseudonyme.
Comprendre la correction
SELECT DISTINCT pa.id_partie
FROM participation AS pa
JOIN personne AS p ON pa.id_pers = p.id_pers
WHERE p.pseudo_pers = 'test';Le pseudonyme est dans personne ; les identifiants de parties sont dans participation. La jointure retrouve les participations de ce compte. DISTINCT protège contre des répétitions éventuelles de projection ; si le couple partie/personne est effectivement unique, il est ici redondant mais correct.
Question 5
#Donner les requêtes supprimant la personne d’identifiant 8.
Indice
Supprimez la référence avant sa cible, en filtrant sur le joueur et non sur ses parties.
Comprendre la correction
DELETE FROM participation WHERE id_pers = 8;
DELETE FROM personne WHERE id_pers = 8;On supprime d’abord les lignes qui référencent le compte, puis la personne elle-même. L’ordre inverse serait refusé par l’intégrité référentielle en l’absence de cascade. Les autres joueurs des mêmes parties doivent rester dans participation.
Partie B : retrouver l’ordre secret
L’alphabet contient a,e,i,o,u,y. Le jeu choisit une permutation secrète, par exemple ouyeai, puis donne des couples de mots distincts déjà ordonnés selon cet alphabet. La comparaison lexicographique utilise la première lettre différente ; si un mot est un préfixe de l’autre, le plus court précède. Avec ouyeai : ouu précède aei ; ouy précède oue ; ou précède oue.
def indice(lettre, ordre):
for i in range(len(ordre)):
if ...:
return ...Question 6
#Compléter indice(lettre,ordre), où ordre mélange les six voyelles.
Indice
Le rang est la position de la lettre dans ordre, pas dans l’alphabet habituel.
Comprendre la correction
if ordre[i] == lettre:
return iL’indice exprime la priorité de la lettre dans l’alphabet secret. La précondition garantit que lettre est une des six voyelles et apparaît dans ordre, donc un return est rencontré. Sans cette garantie, la fonction renverrait implicitement None.
def comparer(mot1, mot2, ordre):
i = 0
while i < len(mot1) and i < len(mot2):
i1 = indice(mot1[i], ordre)
i2 = indice(mot2[i], ordre)
if i1 < i2:
return ...
elif i1 > i2:
return ...
i += 1
return ...Question 7
#Compléter comparer(mot1,mot2,ordre), vrai si le premier mot distinct précède le second.
Indice
La première différence décide ; si elle n’existe pas, comparez les longueurs.
Comprendre la correction
def comparer(mot1, mot2, ordre):
i = 0
while i < len(mot1) and i < len(mot2):
i1 = indice(mot1[i], ordre)
i2 = indice(mot2[i], ordre)
if i1 < i2:
return True
elif i1 > i2:
return False
i += 1
return len(mot1) < len(mot2)Les lettres communes sont ignorées jusqu’à la première différence. Les rangs dans ordre décident immédiatement : les lettres suivantes ne comptent plus. Si la boucle finit sans différence, un mot est préfixe de l’autre, donc sa longueur décide. Comparer directement les chaînes Python utiliserait l’ordre Unicode, pas l’ordre secret.
Chaque première différence impose une contrainte de lettre. Exemple oou avant ooai implique u avant a. Les couples fournis sont :
mots_exemple = [["oou","ooai"],["yee","ieo"],["ye","aaaa"],["a","ieo"],["euee","eyy"],["ao","au"],["e","a"]]Lire les connexions du schéma
- u vers a
- y vers i
- y vers a
- a vers i
- u vers y
- o vers u
- e vers a
Question 8
#Écrire premiere_diff, premier indice différent ou longueur du préfixe si l’un est préfixe de l’autre.
Indice
Avancez tant que les caractères existent et sont identiques.
Comprendre la correction
def premiere_diff(mot1, mot2):
i = 0
while i < len(mot1) and i < len(mot2) and mot1[i] == mot2[i]:
i += 1
return iLes deux tests de longueur précèdent les accès aux caractères, ce qui évite un dépassement sur un préfixe. À la sortie, soit un mot est terminé, soit les deux lettres diffèrent. On obtient 2 pour oou/ooai et 3 pour aio/aioiee. La fonction n’établit pas encore quel mot est plus petit : elle localise seulement la décision.
def dico_adj(mots):
adj = {}
for mot1, mot2 in mots:
ident = premiere_diff(mot1, mot2)
if ident < len(mot1) and ident < len(mot2):
petite = mot1[ident]
grande = mot2[ident]
if petite not in adj:
adj[petite] = [grande]
else:
adj[petite].append(grande)
return adjQuestion 9
#Donner dico_adj(mots_exemple).
Indice
Pour chaque couple, ne retenez que sa première différence.
Comprendre la correction
{'u': ['a', 'y'], 'y': ['i', 'a'], 'a': ['i'],
'o': ['u'], 'e': ['a']}Les lettres sans arc sortant, ici i, ne sont pas nécessairement des clés du dictionnaire, même si elles restent des sommets. L’ordre des valeurs suit celui des couples lus. Le code fourni ajoute des doublons si une même contrainte apparaît plusieurs fois, contrairement à la description d’un arc unique ; l’exemple ne contient pas cette répétition. Un test d’appartenance avant append pourrait éviter ces doublons sans changer l’ordre possible.
def parcours(adj, s, deja_vus, tri):
if s in adj:
for v in adj[s]:
if v not in deja_vus:
deja_vus.append(v)
parcours(adj, v, deja_vus, tri)
tri.append(s)Question 10
#Quel type de parcours réalise parcours ?
Indice
L’appel récursif descend vers un voisin avant de reprendre les suivants.
Comprendre la correction
Un parcours en profondeur, avec ajout du sommet à tri après le traitement de ses successeurs. On obtient donc un ordre de fin de parcours, ou postordre. Cette position de append est essentielle : inverser ensuite ces fins permet un tri topologique d’un graphe orienté sans cycle.
Question 11
#Écrire trier(mots) selon l’algorithme fourni, puis inverser tri et la renvoyer.
Indice
Tous les sommets doivent être explorés, y compris ceux absents des clés du dictionnaire.
Comprendre la correction
def trier(mots):
adj = dico_adj(mots)
tri = []
deja_vus = []
for voyelle in "aeiouy":
if voyelle not in deja_vus:
deja_vus.append(voyelle)
parcours(adj, voyelle, deja_vus, tri)
tri.reverse()
return triLa boucle porte sur les six voyelles et pas seulement sur adj, afin d’inclure aussi les sommets sans arc sortant ou sans contrainte. reverse modifie la liste en place et renvoie None : return tri.reverse() serait faux. Avec l’ordre de départ aeiouy et les listes fournies, on obtient ["o","u","y","e","a","i"]. D’autres permutations peuvent satisfaire les mêmes contraintes.
La garantie vient du contexte : les couples sont issus d’un véritable ordre secret, donc les contraintes sont compatibles et le graphe sans cycle. Si des couples arbitraires étaient saisis, il faudrait vérifier les cycles et rejeter un préfixe placé après son extension (par exemple aa avant a), car le code dico_adj seul ignore ce dernier conflit. Sans ces validations, inverser un postordre ne certifie pas toujours un classement valide.
Proposer un alphabet et tester toutes ses contraintesUn atelier pour expérimenter
Comparez plusieurs alphabets candidats. Le tableau vérifie les sept couples avec la première différence décisive. Deux ordres différents peuvent tous deux être compatibles.
Lire le résultat de l’expérience initiale
Alphabet compatible avec tous les couples
Une paire de mots renseigne uniquement sa première différence. Les lettres qui ne sont pas ordonnées l’une par rapport à l’autre peuvent admettre plusieurs placements.
| Mot avant | Mot après | Contrainte | Verdict |
|---|---|---|---|
| oou | ooai | u avant a | Respecté |
| yee | ieo | y avant i | Respecté |
| ye | aaaa | y avant a | Respecté |
| a | ieo | a avant i | Respecté |
| euee | eyy | u avant y | Respecté |
| ao | au | o avant u | Respecté |
| e | a | e avant a | Respecté |
Un tri topologique trouve un ordre compatible avec les contraintes, pas nécessairement l’unique alphabet initial.
Exercice 3 · 8 points
Robots : interpréter des commandes et relayer des messages
Un robot reçoit une chaîne de commandes : A avance d’un pas, D tourne de 90° à droite et G de 90° à gauche. Un entier répète l’action ou le bloc parenthésé qui suit : 12A vaut douze A ; 3(AD) vaut ADADAD. Les blocs peuvent s’imbriquer. La figure 1 part vers la droite : 3ADA avance trois pas à droite, tourne à droite puis avance d’un pas vers le bas.
Question 1
#Représenter le trajet de 4(AG).
Indice
Chaque répétition contient une avance et un quart de tour à gauche.
Comprendre la correction
La chaîne développée est AGAGAGAG. Depuis une orientation vers la droite, le robot avance à droite, tourne vers le haut, avance, tourne vers la gauche, avance, tourne vers le bas, avance, puis se remet orienté vers la droite. Il trace un carré de côté un pas et retrouve sa position et son orientation initiales.
| Avancée | Position (x,y) | Direction après G |
|---|---|---|
| 0 | (0,0) | Droite initialement |
| 1 | (1,0) | Haut |
| 2 | (1,1) | Gauche |
| 3 | (0,1) | Bas |
| 4 | (0,0) | Droite |
Avant exécution, on vérifie : caractères dans 0123456789ADG(), aucun entier en fin de chaîne ou devant une parenthèse fermante, et parenthésage équilibré. in et not in testent la présence : a in bateau est vrai, y in bateau faux, a not in bateau faux, y not in bateau vrai.
def caracteres_valides(chaine):
valides = "0123456789ADG()"
intrus = [c for c in chaine if c not in valides]
...Question 2
#Compléter caracteres_valides pour renvoyer vrai si intrus est vide.
Indice
La validité des caractères est une condition nécessaire mais pas suffisante de syntaxe.
Comprendre la correction
return intrus == []La compréhension collecte tous les caractères hors de l’alphabet autorisé. Une liste vide signifie qu’aucun intrus n’a été trouvé. return len(intrus) == 0 ou return not intrus conviennent aussi. Cette étape ne vérifie ni les parenthèses ni la position des nombres.
def entiers_valides(chaine):
chiffres = "0123456789"
if chaine[len(chaine)-1] in chiffres:
...
for indice in range(1, len(chaine)):
if chaine[indice] == ")":
if chaine[indice-1] in chiffres:
...
...Question 3
#Compléter entiers_valides aux lignes 4,8,9.
Indice
Les deux situations interdites doivent toutes deux renvoyer False.
Comprendre la correction
def entiers_valides(chaine):
chiffres = "0123456789"
if chaine[len(chaine)-1] in chiffres:
return False
for indice in range(1, len(chaine)):
if chaine[indice] == ")":
if chaine[indice-1] in chiffres:
return False
return TrueUn chiffre final annonce une répétition sans commande ; un chiffre devant ) annonce une répétition sans contenu dans ce bloc. Le return True se place après le parcours complet. Le squelette suppose une chaîne non vide : une extension robuste commence par if chaine == "": return True si l’on décide qu’une commande vide ne fait rien, ou par un refus explicite si elle n’est pas autorisée.
Question 4
#Écrire parenthesage_correct avec le compteur décrit.
PythonRefuser une commande avant qu’elle ferme un bloc inexistantÉcrivez votre solution et mettez-la à l’épreuve
Écrivez parenthesage_correct(chaine). Utilisez le compteur décrit dans le sujet pour vérifier les parenthèses ouvrantes et fermantes, en ignorant les autres caractères. Le compteur ne doit jamais devenir négatif pendant la lecture et doit être nul à la fin. La fonction ne vérifie ici ni la syntaxe complète des commandes du robot, ni les entiers, ni d’autres sortes de délimiteurs.
def parenthesage_correct(chaine):
# À vous de jouer
passLes cas de test proposés :
- Aucun bloc à vérifier : Une chaîne sans parenthèse satisfait cette vérification locale, sans être certifiée comme programme valide.
- Des blocs imbriqués : Le compteur peut dépasser 1 : l’imbrication n’est pas une erreur.
- Autant d’ouvertures que de fermetures ne suffit pas : Dès le premier caractère, on ferme un bloc qui n’existe pas.
- Un bloc jamais fermé : Un compteur positif à la fin signale des ouvertures restantes.
- Une fermeture en trop après un bloc correct : Il ne faut pas retourner
Truedès le premier retour du compteur à zéro. - Des blocs consécutifs restent autorisés : Le compteur peut revenir plusieurs fois à zéro avant la fin de la chaîne.
Indice
Un bilan nul à la fin ne suffit pas : chaque préfixe doit rester non négatif.
Comprendre la correction
def parenthesage_correct(chaine):
parenthese = 0
for caractere in chaine:
if caractere == "(":
parenthese += 1
elif caractere == ")":
parenthese -= 1
if parenthese < 0:
return False
return parenthese == 0Le compteur donne le nombre de parenthèses ouvertes non encore fermées dans le préfixe lu. Il ne doit jamais devenir négatif : )( aurait un bilan final nul mais reste incorrect. À la fin, zéro garantit que toutes les ouvertures sont fermées. Le contrôle autorise naturellement une chaîne sans parenthèse et des blocs imbriqués.
def lire_nombre(chaine, indice):
chiffres = "0123456789"
nombre = ""
while chaine[indice] in chiffres:
nombre = nombre + chaine[indice]
indice = indice + 1
return (int(nombre), indice - 1)Question 5
#Pour lire_nombre("AD179AGA",2), donner nombre et indice après chacune des trois itérations.
Indice
L’indice renvoyé est celui du dernier chiffre, donc indice-1.
Comprendre la correction
| Itération | nombre (chaîne) | indice après incrément |
|---|---|---|
| 1 | "1" | 3 |
| 2 | "17" | 4 |
| 3 | "179" | 5 |
L’indice 5 pointe sur A, premier caractère non numérique : la boucle s’arrête et renvoie (179,4). Le nombre est accumulé en texte puis converti en entier. La validité préalable garantit qu’un nombre n’arrive pas en fin de chaîne, sinon le while sans borne pourrait lire au-delà.
Exemples : lire_bloc("2(AD)A",1) → ("AD",4) ; lire_bloc("2(AD)3(2AGA)",6) → ("2AGA",11) ; lire_bloc("2(3(AD)G)2A",1) → ("3(AD)G",8).
def lire_bloc(chaine, indice):
indice = indice + 1
caractere = chaine[indice]
bloc = ""
compteur = 1
while compteur > 0:
bloc = ...
indice = ...
caractere = ...
if caractere == "(":
compteur += 1
if caractere == ")":
compteur -= 1
return (bloc, indice)Question 6
#Compléter lire_bloc aux lignes 7,8,9.
Indice
Il faut fermer le niveau d’imbrication de départ, pas s’arrêter au premier ).
Comprendre la correction
bloc = bloc + caractere
indice = indice + 1
caractere = chaine[indice]Ces trois lignes complètent le squelette et donnent les résultats des exemples fournis. Le compteur distingue la parenthèse fermante du bloc des fermetures internes. Mais le squelette ne compte pas correctement un premier caractère de bloc qui serait lui-même une parenthèse, et échoue sur un bloc vide. Ces cas ne sont pas exclus par les trois validations précédentes. Pour une fonction correcte sur tout parenthésage validé, voici une version qui compte chaque caractère avant de conclure :
def lire_bloc(chaine, indice):
debut = indice + 1
compteur = 1
while compteur > 0:
indice += 1
if chaine[indice] == '(':
compteur += 1
elif chaine[indice] == ')':
compteur -= 1
return chaine[debut:indice], indiceOn conserve l’indice de début, avance depuis la parenthèse ouvrante et compte toutes les ouvertures/fermetures. Quand le compteur revient à 0, la tranche exclut les deux parenthèses extérieures et conserve les éventuelles parenthèses intérieures. Cette version traite aussi () et ((AD)). Elle suppose que l’indice reçu pointe bien sur une ouverture et que le parenthésage a déjà été validé.
execute_mouvement(caractere) est déjà implémentée pour A, D et G.
def lire_parcours(chaine):
chiffres = "0123456789"
indice = 0
nombre = 1
while indice < len(chaine):
car_lu = chaine[indice]
if car_lu in "AGD":
for k in range(nombre):
execute_mouvement(car_lu)
nombre = 1
elif car_lu in chiffres:
t = ...
nombre = t[0]
indice = t[1]
elif car_lu == "(":
t = ...
bloc = t[0]
indice = t[1]
for k in range(nombre):
...
nombre = 1
indice += 1Question 7
#Compléter lire_parcours aux lignes 12,16,20.
Indice
Le bloc est du même langage que la chaîne entière : il appelle le même interpréteur.
Comprendre la correction
t = lire_nombre(chaine, indice) # ligne 12
t = lire_bloc(chaine, indice) # ligne 16
lire_parcours(bloc) # ligne 20Le résultat de chaque lecteur contient un contenu et l’indice du dernier caractère consommé. La boucle principale ajoute ensuite 1 pour passer à la suite. Une commande simple est répétée nombre fois ; un bloc est interprété récursivement le même nombre de fois. Après consommation, nombre revient à 1 pour qu’une répétition ne s’applique pas aussi aux instructions suivantes.
Avec 2(3(AD)G)2A, on interprète deux fois un bloc contenant lui-même trois répétitions de AD puis G, avant d’avancer deux fois. La version robuste de lire_bloc précédente permet les imbrications générales. Le volume d’actions exécutées peut être bien plus grand que la longueur du texte ; un système réel devrait limiter les répétitions et vérifier l’environnement avant de déplacer un robot.
Partie B : communication par radio
Deux robots proches communiquent directement ; sinon le message est relayé. Figure 2 : chaîne 4-46-12-17, donc 4 rejoint 12 via 46. Chaque robot route selon RIP ; distance compte les transmissions. Figure 3 : chaîne 46-57-22-91-4. La table initiale de 91 est :
| Destination | Prochain robot | Distance |
|---|---|---|
| 4 | 4 | 1 |
| 46 | 22 | 3 |
| 22 | 22 | 1 |
| 57 | 22 | 2 |
La figure 4 ajoute seulement l’arête 4-46, formant un cycle entre les cinq robots.
Question 8
#Le robot 4 peut désormais joindre directement 46. Quelles modifications dans la table de 91 ?
Indice
Comparez chaque ancienne distance au trajet permis par la nouvelle arête.
Comprendre la correction
Seule la route vers 46 s’améliore : prochain robot 4, distance 2, par 91→4→46. Les routes vers 4 et 22 restent directes à distance 1 ; celle vers 57 reste via 22 à distance 2, car 91→4→46→57 demanderait trois transmissions.
| Table de 87 : destination | Prochain robot | Distance |
|---|---|---|
| 91 | 91 | 1 |
| 63 | 63 | 1 |
| 36 | 63 | 2 |
Question 9
#87 contacte 91 avec sa table. Décrire les nouvelles lignes de la table de 91.
Indice
Pour une destination annoncée, ajoutez le saut de 91 au voisin 87.
Comprendre la correction
| Destination ajoutée | Prochain robot | Distance |
|---|---|---|
| 87 | 87 | 1 |
| 63 | 87 | 2 |
| 36 | 87 | 3 |
Le contact direct ajoute 87 à distance 1. Les destinations annoncées 63 à distance 1 et 36 à distance 2 sont connues par 91 avec un saut supplémentaire. L’annonce de 91 lui-même ne doit pas créer une route de retour vers soi. Les anciennes routes restent inchangées puisqu’aucune amélioration vers elles n’est indiquée.
Partie C : programmer le routage
class Module_comm:
def __init__(self, id_robot):
self.identifiant = id_robot
self.table_routage = {}Exemple de table initiale pour 91 :
{4:{"prochain":4,"distance":1},
46:{"prochain":22,"distance":3},
22:{"prochain":22,"distance":1},
57:{"prochain":22,"distance":2}}Question 10
#Écrire Module_comm.ajouter_voisin(identifiant).
Indice
Un voisin direct ne passe par aucun autre robot intermédiaire.
Comprendre la correction
def ajouter_voisin(self, identifiant):
self.table_routage[identifiant] = {
"prochain": identifiant,
"distance": 1
}Un voisin direct est sa propre prochaine étape et coûte une transmission. L’identifiant est la clé de la table ; prochain et distance sont les clés du sous-dictionnaire. On suppose que l’identifiant fourni n’est pas celui du robot lui-même.
Le nombre maximal de sauts est fixé à 15. Les opérateurs in et not in testent les clés d’un dictionnaire. Exemple : dans {4:{prochain:31,distance:2},46:{prochain:22,distance:3}}, 4 in D et 5 not in D sont vrais.
def nombre_sauts(self, identifiant):
if identifiant not in self.table_routage:
return 16
else:
return ...Question 11
#Compléter nombre_sauts : une destination inconnue donne 16.
Indice
Le résultat demandé est dans le champ distance de la route.
Comprendre la correction
return self.table_routage[identifiant]["distance"]L’accès est sûr dans le else car la présence a été testée. La valeur 16 sert à représenter l’inaccessibilité dans ce modèle où les routes utilisables sont limitées à 15 sauts. Il faut renvoyer la distance, pas le prochain robot ni le sous-dictionnaire entier.
Question 12
#Écrire voisins(), liste des robots directement joignables.
Indice
Un seul saut signifie une communication radio directe.
Comprendre la correction
def voisins(self):
resultat = []
for identifiant in self.table_routage:
if self.table_routage[identifiant]["distance"] == 1:
resultat.append(identifiant)
return resultatDans une table cohérente, la distance 1 caractérise un voisin direct. L’ordre de la liste n’est pas imposé. On ne renvoie pas tous les prochains robots sans déduplication : plusieurs routes distantes pourraient partager le même prochain voisin.
Question 13
#Écrire communiquer_extrait_table(voisin), qui renvoie les routes dont ce voisin n’est pas le prochain robot.
Indice
Évitez de renvoyer à un voisin les routes que vous connaissez justement grâce à lui.
Comprendre la correction
def communiquer_extrait_table(self, voisin):
extrait = {}
for destination, route in self.table_routage.items():
if route["prochain"] != voisin:
extrait[destination] = route.copy()
return extraitLe filtre porte sur prochain, pas seulement sur la destination. Par exemple, si 46 se rejoint via 22, la route de 46 ne doit pas être annoncée à 22, même si 46 ≠ 22. La copie du sous-dictionnaire préserve la table interne si le destinataire du résultat le modifie ensuite. L’extrait conserve la même structure que la table et n’incrémente pas les distances : le récepteur ajoutera son propre saut lors du traitement.
Lire une commande de robot et suivre son trajetUn atelier pour expérimenter
Choisissez une instruction, puis le nombre d’actions à exécuter. Les parenthèses sont développées par un interpréteur ; le robot démarre en (0,0) vers la droite.
Lire le résultat de l’expérience initiale
Position (0,0), orientation Droite
A change la position ; D et G changent seulement l’orientation. Les répétitions portent sur l’action ou le bloc qui suit, puis leur multiplicateur revient à 1.
| Étape | Action | Position | Direction |
|---|---|---|---|
| 1 | A | (1,0) | Droite |
| 2 | G | (1,0) | Haut |
| 3 | A | (1,1) | Haut |
| 4 | G | (1,1) | Gauche |
| 5 | A | (0,1) | Gauche |
| 6 | G | (0,1) | Bas |
| 7 | A | (0,0) | Bas |
| 8 | G | (0,0) | Droite |
Un interpréteur combine une lecture syntaxique (nombres et blocs) et une exécution ; chacune doit respecter ses propres invariants.
Du sujet à la méthode
Votre prochaine séance de révision
- Ne vous fiez pas seulement au nom de la fonction : suivez l’état réellement modifié.
- Une hypothèse de validité doit être annoncée pour les indices, les parenthèses et le tri topologique.
- Pour un routage, distinguez destination finale, voisin de prochain saut et nombre de transmissions.
Retrouver ces notions dans d’autres sujets
Toutes les annales de NSI · Le guide pour préparer le bac NSI 2027
Énoncé : sujet 26-NSIJ2JA1 (PDF). Corrigé et explications pédagogiques proposés par Sofien.
