Épreuve écrite · 2026 · Jour 2

Bac NSI 2026 Asie jour 2

Le sujet combine trois mécanismes de parcours : remonter une pile pour annuler des coups, ordonner des contraintes par un graphe et lire des instructions imbriquées de robot. Les corrections reconstruisent les figures et rendent visibles les invariants et les limites des programmes fournis. Durée : 3 h 30 sans calculatrice, trois exercices indépendants.

Dans ce sujet

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 :

LigneColonne 1Colonne 2Colonne 3
1Vide12
2345
3678

Seules les cases directement adjacentes au vide, horizontalement ou verticalement, peuvent permuter avec lui. Figure 2, grille mélangée :

LigneColonne 1Colonne 2Colonne 3
1538
2Vide12
3764

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.

Voir la question dans le sujet PDF, p. 2 (nouvel onglet)
tab[3] = tab[4]
tab[4] = 0

Question 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
LigneColonne 1Colonne 2Colonne 3
1538
21Vide2
3764

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.

Voir la question dans le sujet PDF, p. 2 (nouvel onglet)

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.

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)
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.

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)
def indice(self, numero):
    assert type(numero) == int, "numero doit être entier"
    ... , "numero de case non valide"
    i = 0
    while ...:
        i += 1
    return i

Question 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.

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)

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] = 0

i 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.

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)

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 += 1

Question 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 += 1

La 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.

Voir la question dans le sujet PDF, p. 4 (nouvel onglet)

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
ÉtapeCoup annulé/jouéPile restante, bas → sommet
Avant résolution-[1,4,5,2]
12[1,4,5]
25[1,4]
34[1]
41[]

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.

Voir la question dans le sujet PDF, p. 4 (nouvel onglet)
class Taquin:
    def __init__(self):
        self.tab = [0,1,2,3,4,5,6,7,8]
        self.pile = Pile()
        self.mode_resolution = False

Pile 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.

Voir la question dans le sujet PDF, p. 5 (nouvel onglet)

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.

Voir la question dans le sujet PDF, p. 5 (nouvel onglet)

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.

Voir la question dans le sujet PDF, p. 5 (nouvel onglet)
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.

LigneColonne 1Colonne 2Colonne 3
114Vide
2352
3678

La pile convient à l’annulation parce que l’opération inverse doit être exécutée dans l’ordre opposé à l’historique.

Revoir les notions de cet exercice

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.

Voir la question dans le sujet PDF, p. 6 (nouvel onglet)

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.

Voir la question dans le sujet PDF, p. 6 (nouvel onglet)

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.

Voir la question dans le sujet PDF, p. 6 (nouvel onglet)

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.

Voir la question dans le sujet PDF, p. 7 (nouvel onglet)

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.

Voir la question dans le sujet PDF, p. 7 (nouvel onglet)

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 i

L’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.

Voir la question dans le sujet PDF, p. 7 (nouvel onglet)
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.

Voir la question dans le sujet PDF, p. 8 (nouvel onglet)

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"]]
ouyiae
Contraintes orientées entre les six voyelles
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 i

Les 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.

Voir la question dans le sujet PDF, p. 9 (nouvel onglet)
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 adj

Question 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.

Voir la question dans le sujet PDF, p. 9 (nouvel onglet)
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.

Voir la question dans le sujet PDF, p. 9 (nouvel onglet)

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 tri

La 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.

Voir la question dans le sujet PDF, p. 10 (nouvel onglet)
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 avantMot aprèsContrainteVerdict
oouooaiu avant aRespecté
yeeieoy avant iRespecté
yeaaaay avant aRespecté
aieoa avant iRespecté
eueeeyyu avant yRespecté
aoauo avant uRespecté
eae avant aRespecté

Un tri topologique trouve un ordre compatible avec les contraintes, pas nécessairement l’unique alphabet initial.

Revoir les notions de cet exercice

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éePosition (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
Voir la question dans le sujet PDF, p. 11 (nouvel onglet)

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.

Voir la question dans le sujet PDF, p. 12 (nouvel onglet)
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 True

Un 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.

Voir la question dans le sujet PDF, p. 12 (nouvel onglet)

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
    pass

Les 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 True dè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 == 0

Le 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.

Voir la question dans le sujet PDF, p. 12 (nouvel onglet)
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érationnombre (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à.

Voir la question dans le sujet PDF, p. 13 (nouvel onglet)

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], indice

On 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é.

Voir la question dans le sujet PDF, p. 13 (nouvel onglet)

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 += 1

Question 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 20

Le 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.

Voir la question dans le sujet PDF, p. 14 (nouvel onglet)

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 :

DestinationProchain robotDistance
441
46223
22221
57222

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.

Voir la question dans le sujet PDF, p. 16 (nouvel onglet)
Table de 87 : destinationProchain robotDistance
91911
63631
36632

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éeProchain robotDistance
87871
63872
36873

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.

Voir la question dans le sujet PDF, p. 16 (nouvel onglet)

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.

Voir la question dans le sujet PDF, p. 17 (nouvel onglet)

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.

Voir la question dans le sujet PDF, p. 18 (nouvel onglet)

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 resultat

Dans 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.

Voir la question dans le sujet PDF, p. 18 (nouvel onglet)

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 extrait

Le 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.

Voir la question dans le sujet PDF, p. 18 (nouvel onglet)
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.

ÉtapeActionPositionDirection
1A(1,0)Droite
2G(1,0)Haut
3A(1,1)Haut
4G(1,1)Gauche
5A(0,1)Gauche
6G(0,1)Bas
7A(0,0)Bas
8G(0,0)Droite

Un interpréteur combine une lecture syntaxique (nombres et blocs) et une exécution ; chacune doit respecter ses propres invariants.

Revoir les notions de cet exercice

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.