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
Faire converger les tables de routage avec RIP
Cet exercice porte sur les graphes, les protocoles réseaux et la programmation orientée objet. Les sommets du graphe sont des routeurs ; ses arêtes sont les liaisons entre eux. On souhaite d’abord parcourir ce graphe en largeur depuis A, puis simuler les échanges de tables de routage.
Partie A : du graphe aux routes
Lire les connexions du schéma
- A relié à B
- A relié à C
- A relié à E
- C relié à D
- C relié à E
- D relié à E
- D relié à F
Question 1
#Dire lequel des parcours suivants est un parcours en largeur depuis A, en justifiant : ABCDEF, ABCEDF, ABCDFE.
Indice
Commencez par encercler tous les voisins directs de A.
Comprendre la correction
ABCEDF convient. Les sommets à distance 1 de A sont B, C et E ; ils doivent tous être visités avant D, qui est à distance 2. F est ensuite à distance 3. Dans les deux autres propositions, D apparaît avant E alors que E est un voisin direct de A.
Un parcours en largeur explore par couches de distance. L’ordre de B, C et E pourrait varier avec l’ordre des voisins ; parmi les trois séquences proposées, une seule respecte les couches.
Le sujet résume RIP en trois règles. a. Initialisation : chaque routeur ajoute ses voisins directs, accessibles en un saut, sans routeur intermédiaire. b. Transmission : chaque routeur envoie régulièrement sa table à ses voisins. c. Mise à jour : une route inconnue est ajoutée ; une route plus longue est ignorée ; une route strictement plus courte remplace l’ancienne. Une égalité ne déclenche pas de remplacement dans cette règle.
Une itération correspond à la réception des tables de tous les voisins, puis à la mise à jour. Après stabilisation, les tables ne changent plus. On exclut les pannes de routeur et les coupures de liaison. La table initiale de A est :
| Destination | Nombre de sauts | Prochain routeur |
|---|---|---|
| B | 1 | - |
| C | 1 | - |
| E | 1 | - |
Question 2
#Donner la table de routage de F à l’initialisation du protocole RIP.
Indice
Ne mettez dans la table que les voisins directs visibles sur le graphe.
Comprendre la correction
| Destination | Nombre de sauts | Prochain routeur |
|---|---|---|
| D | 1 | - |
F n’a qu’un voisin : D. Au départ, il ne connaît donc aucune route vers A, B, C ou E. Le tiret signifie qu’aucun routeur intermédiaire n’est nécessaire ; dans la classe de la partie B, cette convention sera représentée par None.
Question 3
#Donner la table de routage de A après une première itération de RIP. Deux réponses sont possibles.
Indice
Une annonce reçue d’un voisin coûte un saut de plus que ce que ce voisin annonce.
Comprendre la correction
| Destination | Nombre de sauts | Prochain routeur |
|---|---|---|
| B | 1 | - |
| C | 1 | - |
| E | 1 | - |
| D | 2 | C ou E |
A reçoit de C une annonce « D est à 1 saut » et ajoute son propre saut vers C : D est à 2 sauts via C. L’annonce d’E donne le même coût. Comme le sujet ne remplace une route que par une route strictement plus courte, la première des deux annonces retenue décide du prochain routeur.
À ce premier échange synchrone, C et E n’annoncent encore que leurs voisins directs. Aucun des deux n’est relié directement à F : A ne connaît donc pas encore de route vers F. N’ajoutez pas un chemin que vous voyez sur le dessin avant que l’information ait pu se propager.
Question 4
#Donner le numéro de l’itération de RIP à partir duquel les tables des routeurs du réseau ne varient plus.
Indice
Cherchez la plus grande distance minimale entre deux routeurs, pas le plus long chemin avec des détours.
Comprendre la correction
Les routes définitives sont acquises à l’issue de la troisième itération. Le trajet le plus court le plus long relie B à F : B → A → C → D → F, soit 4 sauts (on peut passer par E plutôt que C). L’initialisation connaît déjà les routes à un saut ; chaque échange synchrone gagne un saut : 2 sauts après l’itération 1, 3 après l’itération 2 et 4 après l’itération 3.
Pour lever toute ambiguïté de comptage : la troisième itération peut encore modifier une table ; la quatrième est la première itération qui constate qu’aucune modification n’est nécessaire. « Stabilisé après trois échanges » et « premier échange sans modification : le quatrième » décrivent donc le même déroulement.
Question 5
#On relie maintenant les routeurs E et F. Donner la nouvelle table de routage de A après stabilisation de RIP. Deux réponses sont possibles.
Indice
Comparez le trajet vers F avant et après l’ajout de E-F.
Comprendre la correction
| Destination | Nombre de sauts | Prochain routeur |
|---|---|---|
| B | 1 | - |
| C | 1 | - |
| E | 1 | - |
| D | 2 | C ou E |
| F | 2 | E |
La nouvelle arête crée A → E → F, de coût 2. D reste à deux sauts via C ou E ; les trois routes directes restent de coût 1. La nouvelle liaison n’est pas un routeur supplémentaire : on compte toujours les arêtes traversées.
Partie B : des objets qui échangent des routes
Chaque objet Routeur possède nom, une chaîne qui l’identifie, et voisins, une liste d’objets Routeur directement connectés. nb_sauts associe à chaque routeur accessible sa distance en sauts. prochain associe à chaque destination le premier routeur d’un chemin minimal ; pour une liaison directe, la valeur choisie par le sujet est None. Les routeurs sont initialement déconnectés :
class Routeur:
def __init__(self, nom):
self.nom = nom
self.voisins = []
self.nb_sauts = {}
self.prochain = {}A = Routeur('A')
B = Routeur('B')
C = Routeur('C')
D = Routeur('D')
E = Routeur('E')
F = Routeur('F')
liste_routeurs = [A, B, C, D, E, F]La méthode relie deux routeurs et ne fait rien s’ils sont déjà connectés. Les lignes à compléter sont :
def relie(self, autre):
if autre not in self.voisins:
self.voisins.append(...)
self.nb_sauts[autre] = ...
self.prochain[autre] = ...
if self not in autre.voisins:
autre.relie(...)Question 6
#Recopier et compléter le code de la méthode relie.
Indice
Les deux extrémités doivent connaître la liaison, avec un coût direct de 1.
Comprendre la correction
def relie(self, autre):
if autre not in self.voisins:
self.voisins.append(autre)
self.nb_sauts[autre] = 1
self.prochain[autre] = None
if self not in autre.voisins:
autre.relie(self)Le voisin est l’objet autre, pas son nom. Il devient aussi une clé des dictionnaires : sa distance vaut 1 et son prochain routeur intermédiaire vaut None. Le dernier appel construit la liaison dans l’autre sens.
Pourquoi la récursion s’arrête-t-elle ? Avant l’appel réciproque, autre a déjà été ajouté à self.voisins. Lorsque les rôles sont inversés, le test final voit que la première moitié de la liaison existe. Il ne déclenche pas une troisième construction. Le premier test empêche également les doublons lors d’un nouvel appel identique.
Question 7
#Écrire la méthode relie_liste de la classe Routeur qui prend en paramètre une liste lst et relie self à chacun de ses routeurs. Par exemple, A.relie_liste([B, C, E]) crée aussi les liaisons en sens inverse.
Indice
La méthode écrite à la question précédente réalise déjà toutes les mises à jour utiles.
Comprendre la correction
def relie_liste(self, lst):
for routeur in lst:
self.relie(routeur)La boucle parcourt les objets de la liste et délègue chaque connexion à relie. Il serait fragile de ne modifier que voisins : on oublierait les deux dictionnaires et la connexion inverse. Réutiliser une méthode dont le contrat est déjà établi évite cette duplication.
Si lst est vide, la boucle n’effectue aucun appel. Si un routeur y figure deux fois, relie empêche les voisins en double.
Question 8
#Après A.relie_liste([B, C, E]), écrire les instructions manquantes pour obtenir le graphe de la figure 1.
Indice
Cochez les trois arêtes déjà créées, puis parcourez les quatre restantes.
Comprendre la correction
C.relie_liste([D, E])
D.relie_liste([E, F])A-B, A-C et A-E existent déjà. Il reste C-D, C-E, D-E et D-F. Ces deux appels les créent dans les deux sens ; aucun appel concernant B seul n’est nécessaire. D’autres regroupements sont possibles si et seulement si les sept arêtes du graphe sont présentes, sans arête supplémentaire.
def met_a_jour_table(self, autre):
for r in autre.nb_sauts:
if r != self:
if (r not in self.nb_sauts or
self.nb_sauts[r] > ...):
self.nb_sauts[r] = ...
self.prochain[r] = ...Question 9
#Recopier et compléter le code de la méthode met_a_jour_table qui applique la règle c.
Indice
Le prochain routeur vu par self est son voisin autre, pas le prochain routeur annoncé par autre.
Comprendre la correction
def met_a_jour_table(self, autre):
for r in autre.nb_sauts:
if r != self:
if (r not in self.nb_sauts or
self.nb_sauts[r] > autre.nb_sauts[r] + 1):
self.nb_sauts[r] = autre.nb_sauts[r] + 1
self.prochain[r] = autrePour atteindre r en passant par autre, on paie un saut jusqu’à ce voisin, puis les autre.nb_sauts[r] sauts qu’il annonce. Les trois compléments sont donc autre.nb_sauts[r] + 1, la même expression, puis autre.
Le test r != self évite d’ajouter une route vers soi via un détour. Le or est ici utile : si la destination est inconnue, Python n’évalue pas l’accès self.nb_sauts[r] qui provoquerait une erreur. Pour une destination connue, la comparaison doit être stricte afin de ne pas changer de prochain routeur à coût égal.
Question 10
#Écrire la méthode itere_rip qui met à jour la table de self à partir de celle de chacun de ses voisins.
Indice
Un voisin est un objet de self.voisins ; sa table est traitée par met_a_jour_table.
Comprendre la correction
def itere_rip(self):
for voisin in self.voisins:
self.met_a_jour_table(voisin)On reçoit une annonce de chaque objet contenu dans self.voisins. La méthode précédente sait comparer les routes ; cette méthode organise seulement la réception de toutes les annonces. Il faut parcourir les voisins, et non toutes les destinations déjà présentes dans la table, qui ne sont pas nécessairement directement connectées.
Question 11
#Écrire une fonction qui prend une liste de routeurs l_routeurs et réalise une itération de RIP pour tous ses routeurs.
Indice
Parcourez la liste et appelez une fois la méthode de chaque objet.
Comprendre la correction
def iteration_rip(l_routeurs):
for routeur in l_routeurs:
routeur.itere_rip()Il s’agit d’une fonction indépendante de la classe : elle ne reçoit pas de paramètre self. Chaque objet exécute sa propre méthode de réception.
Cette traduction met les tables à jour en place et dans l’ordre de la liste. Un routeur traité plus tard peut lire une table déjà améliorée durant ce même passage. Le nombre de passages du programme peut donc différer du nombre d’échanges synchrones de la partie A ; les routes finales restent celles de plus petit nombre de sauts.
Question 12
#On suppose désormais que met_a_jour_table renvoie True si elle a modifié la table, et False sinon. Modifier itere_rip pour qu’elle renvoie True si au moins une réception a changé sa table.
Indice
Cumulez les changements avec un drapeau qui ne redevient pas faux au milieu de la boucle.
Comprendre la correction
def itere_rip(self):
modifie = False
for voisin in self.voisins:
if self.met_a_jour_table(voisin):
modifie = True
return modifieLe drapeau part de False. Dès qu’un voisin provoque une amélioration, il devient True et doit le rester jusqu’au retour. Les voisins suivants sont néanmoins tous traités. Une réception sans changement ne doit pas effacer le changement précédent.
Attention au court-circuit : modifie = modifie or self.met_a_jour_table(voisin) cesse d’appeler la méthode dès que modifie vaut True. Le bloc if proposé garantit au contraire que chaque annonce est réellement examinée.
Le programme principal a créé A, B, C, D, E, F, la liste liste_routeurs et les connexions de la figure 1. On utilise les versions des méthodes qui renvoient un booléen.
Question 13
#Compléter le programme principal pour mettre à jour tous les routeurs jusqu’à ce qu’aucune table n’ait besoin de changer. Il n’est pas demandé de réécrire les connexions.
Indice
Un passage supplémentaire permet de constater que la stabilisation est atteinte.
Comprendre la correction
modifie = True
while modifie:
modifie = False
for routeur in liste_routeurs:
if routeur.itere_rip():
modifie = TrueLe premier True force un premier passage. Au début de chaque passage, on remet le drapeau à False ; un seul changement suffit à programmer un passage supplémentaire. La boucle s’arrête uniquement après avoir testé tous les routeurs sans obtenir de modification.
Dans ce réseau fini et sans panne, les mises à jour ajoutent des destinations ou diminuent strictement un nombre entier de sauts. On ne peut donc pas améliorer indéfiniment les tables. N’écrivez pas un return au milieu de ce programme principal et ne réinitialisez pas le drapeau pour chaque routeur.
Voir une route se propager de voisin en voisinUn atelier pour expérimenter
Choisissez un routeur, puis le nombre d’échanges synchrones depuis l’initialisation. Ajoutez la liaison E-F pour voir quand une route plus courte apparaît. Les égalités sont départagées par l’ordre alphabétique des voisins dans cette simulation.
Lire le résultat de l’expérience initiale
1 destinations connues depuis B.
À l’initialisation, seuls les voisins directs sont connus.
| Destination | Sauts | Prochain routeur |
|---|---|---|
| A | 1 | - |
| C | Inconnu | - |
| D | Inconnu | - |
| E | Inconnu | - |
| F | Inconnu | - |
Le graphe montre tous les chemins à l’observateur ; un routeur, lui, ne connaît que ce que ses voisins lui ont déjà annoncé.
Exercice 2 · 6 points
Randonnées : relier des parkings aux lacs avec SQL et Python
Susie recense les randonnées allant d’un parking à un lac. Les altitudes sont des entiers exprimés en mètres. La relation rando contient deux clés étrangères : depart référence parking.idP et arrivee référence lac.idL. Les clés primaires sont idP, idR et idL.
Les clauses SQL utilisables comprennent SELECT, FROM, WHERE avec AND et OR, JOIN ... ON, UPDATE, INSERT, DELETE, DISTINCT et ORDER BY. Voici les extraits fournis :
parking
| idP | commune | altitude | coord_GPS |
|---|---|---|---|
| 1 | Chamonix | 1026 | (45.98;6.89) |
| 2 | Argentiere | 1429 | (45.99;6.92) |
| 3 | Passy | 600 | (45.92;6.72) |
| 4 | Passy | 1181 | (45.95;6.71) |
| 5 | Nevache | 2022 | (45.05;6.52) |
rando
| idR | depart | arrivee |
|---|---|---|
| 1 | 1 | 1 |
| 2 | 2 | 1 |
| 3 | 1 | 2 |
| 4 | 3 | 3 |
lac
| idL | nom | altitude |
|---|---|---|
| 1 | Lac Blanc | 2354 |
| 2 | Lacs Noirs | 2564 |
| 3 | Lac Vert | 1266 |
| 4 | Lac Rouge | 2585 |
Question 1
#Indiquer ce que renvoie cette requête lorsqu’on l’applique aux extraits :
SELECT nom
FROM lac
WHERE altitude <= 2000;Indice
La clause SELECT détermine ce que l’on affiche ; WHERE détermine les lignes retenues.
Comprendre la correction
| nom |
|---|
| Lac Vert |
On conserve les lignes dont l’altitude est au plus 2 000 m, puis on ne projette que la colonne nom. Le Lac Vert est à 1 266 m ; les trois autres lacs de l’extrait dépassent 2 000 m. Le résultat est une table d’une colonne et d’une ligne, pas une altitude.
Question 2
#Indiquer les noms des lacs qu’on peut atteindre depuis le parking de Chamonix d’après cette base de données.
Indice
Suivez successivement parking.idP, rando.depart, rando.arrivee et lac.idL.
Comprendre la correction
Lac Blanc et Lacs Noirs. Le parking de Chamonix porte l’identifiant 1. Dans rando, les lignes de départ 1 mènent aux lacs 1 et 2. Dans lac, ces identifiants correspondent aux deux noms demandés.
Le numéro de la randonnée et celui du lac jouent des rôles différents : la randonnée 3 mène au lac 2. Suivre directement idR jusqu’à idL donnerait une association incorrecte.
À partir de cette question, les requêtes portent sur la totalité de la base, pas seulement sur les extraits affichés.
Question 3
#Donner une requête obtenant les coordonnées GPS des parkings de Passy situés à une altitude strictement comprise entre 800 et 1 000 mètres.
SQLTrouver les bons parkings de PassyÉcrivez votre solution et mettez-la à l’épreuve
Écrivez la requête donnant les coordonnées GPS des parkings de Passy dont l’altitude est strictement comprise entre 800 et 1 000 mètres. Chaque résultat doit correspondre à un parking qui satisfait les trois conditions.
SELECT ...
FROM parking
WHERE ...;Les cas de test proposés :
- Extrait du sujet : aucun parking ne convient : Les cinq parkings proviennent de l’extrait officiel. Les deux parkings de Passy sont hors de l’intervalle demandé : une requête correcte renvoie ici une table vide.
- Cas complémentaire : les deux bornes sont exclues : Jeu complémentaire pédagogique, distinct des données officielles. Les coordonnées sont des repères de test. Seul le parking à 900 m à Passy satisfait les trois conditions ; 800 et 1 000 sont exclus.
- Cas complémentaire : conserver tous les résultats : Jeu complémentaire pédagogique. Les deux altitudes situées juste à l’intérieur sont retenues ; il ne faut ni renvoyer seulement le premier parking ni remplacer les inégalités par des égalités.
Indice
La question demande une condition ouverte aux deux extrémités.
Comprendre la correction
SELECT coord_GPS
FROM parking
WHERE commune = 'Passy'
AND altitude > 800
AND altitude < 1000;Les trois conditions doivent être vraies simultanément. Les comparaisons sont strictes : un parking situé exactement à 800 m ou 1 000 m ne serait pas retenu. Utiliser BETWEEN 800 AND 1000 serait ici incorrect, car cette expression inclut ses deux bornes.
Aucun des deux parkings de Passy affichés dans l’extrait ne convient, mais on travaille désormais sur la base entière. Cela n’autorise pas à conclure que la requête complète renverrait nécessairement une table vide.
Question 4
#Donner une requête obtenant les noms des lacs atteignables depuis le parking situé à 1 300 mètres d’altitude dans la commune de Cordon. Ce parking existe dans la base.
Indice
Il faut traverser rando : parking et lac ne possèdent pas de clé les reliant directement.
Comprendre la correction
SELECT DISTINCT l.nom
FROM parking AS p
JOIN rando AS r ON r.depart = p.idP
JOIN lac AS l ON l.idL = r.arrivee
WHERE p.commune = 'Cordon'
AND p.altitude = 1300;La première jointure relie le parking aux randonnées qui en partent ; la seconde donne les noms des lacs d’arrivée. Les alias p, r et l rendent explicite l’origine de chaque colonne, notamment altitude, qui existe dans deux relations.
DISTINCT évite d’afficher deux fois un même nom si plusieurs randonnées conduisent au même lac. Le filtre d’altitude doit porter sur le parking, pas sur le lac : confondre ces deux altitudes changerait le sens de la recherche.
Question 5
#Ajouter le lac d’Anterne, à 2 059 m, et une randonnée qui part du parking d’identifiant 3 pour y arriver. Le parking existe déjà ; le lac et la randonnée n’existent pas encore. On peut utiliser 42 pour le lac et 100 pour la randonnée.
Indice
Insérez d’abord la ligne qui sera référencée par la clé étrangère.
Comprendre la correction
INSERT INTO lac (idL, nom, altitude)
VALUES (42, 'Lac d''Anterne', 2059);
INSERT INTO rando (idR, depart, arrivee)
VALUES (100, 3, 42);On crée le lac avant de le référencer dans une randonnée. Dans le cas contraire, arrivee = 42 désignerait une clé inexistante et violerait la contrainte d’intégrité référentielle. Le parking 3 est déjà présent : aucune insertion dans parking n’est demandée.
L’apostrophe du nom est doublée à l’intérieur de la chaîne SQL : d''Anterne. Ce sont deux apostrophes dans la requête pour une seule dans la valeur enregistrée. Expliciter les noms des colonnes évite aussi de dépendre d’un ordre supposé.
Question 6
#Susie a saisi le nom Lc d Anterne par erreur. Donner la requête qui le corrige.
Indice
Mettez à jour une colonne de la ligne identifiée par 42.
Comprendre la correction
UPDATE lac
SET nom = 'Lac d''Anterne'
WHERE idL = 42;La clé primaire cible exactement le lac ajouté. Sans WHERE, tous les lacs recevraient le même nom. Il ne faut ni supprimer la randonnée ni changer son arrivée : l’identifiant 42 reste le même, seul le nom est modifié.
Question 7
#Le parking d’identifiant 28 a été remplacé par un parc. Donner les requêtes qui suppriment ce parking de la base.
Indice
Repérez la relation qui possède une clé étrangère vers parking.
Comprendre la correction
DELETE FROM rando
WHERE depart = 28;
DELETE FROM parking
WHERE idP = 28;On supprime d’abord les randonnées qui référencent ce parking, puis le parking lui-même. Cela évite de laisser un depart pointant vers une ligne disparue. Le sujet ne prévoit pas de suppression en cascade automatique : les deux requêtes rendent l’ordre explicite.
Ne supprimez pas les lacs d’arrivée de ces randonnées : ils continuent d’exister et peuvent rester accessibles depuis d’autres parkings. Une suppression doit se limiter à ce que demande le problème.
Susie veut compter les départs en Python. La table rando est représentée par une liste d’objets de cette classe :
class Rando:
def __init__(self, idR, depart, arrivee):
self.idR = idR
self.depart = depart
self.arrivee = arriveeQuestion 8
#Compléter les lignes 3 et 5 de get_parking pour obtenir la liste des identifiants des parkings de départ, sans doublon :
def get_parking(randos):
parkings = []
for ...:
if rando.depart not in parkings:
...
return parkingsAvec les randonnées Rando(1, 1, 1), Rando(2, 2, 1) et Rando(3, 1, 2), la fonction doit renvoyer [1, 2].
Indice
Le test porte sur l’attribut depart ; c’est aussi cet attribut qu’il faut ajouter.
Comprendre la correction
def get_parking(randos):
parkings = []
for rando in randos:
if rando.depart not in parkings:
parkings.append(rando.depart)
return parkingsLe parcours fournit successivement un objet Rando. Son attribut depart est l’identifiant à ajouter. Dans l’exemple : on ajoute 1, puis 2, puis on ignore le second départ 1, déjà présent. La fonction garde donc l’ordre de première apparition.
Ajouter l’objet entier rando serait une erreur de type de résultat. La liste vide constitue un bon cas limite : le résultat doit être [] sans accès à un premier élément qui n’existe pas.
Question 9
#Compléter la ligne 4 de get_nb_rando, qui compte les départs depuis le parking demandé :
def get_nb_rando(parking, randos):
nb = 0
for rando in randos:
if ...:
nb = nb + 1
return nbAvec le parking 1 et les trois randonnées de la question précédente, le résultat doit être 2.
Indice
La question porte sur le départ, pas sur idR ni arrivee.
Comprendre la correction
def get_nb_rando(parking, randos):
nb = 0
for rando in randos:
if rando.depart == parking:
nb = nb + 1
return nbOn compare un identifiant à un identifiant : rando.depart == parking. Une randonnée d’arrivée 1 mais de départ 2 ne doit pas être comptée pour le parking 1. Le compteur est augmenté seulement si le prédicat est vrai, puis renvoyé après avoir examiné toutes les randonnées.
Si le parking n’apparaît jamais ou si la liste est vide, le compteur reste à 0. Ne placez pas return nb dans la boucle : cela arrêterait le comptage après le premier objet.
Question 10
#Écrire nb_rando_par_parking, qui prend une liste de randonnées et renvoie le dictionnaire associant à chaque parking de départ son nombre de randonnées. Pour l’exemple précédent, on attend {1: 2, 2: 1}.
Indice
Pour chaque objet, sa clé est depart et sa contribution au compteur vaut 1.
Comprendre la correction
def nb_rando_par_parking(randos):
resultat = {}
for parking in get_parking(randos):
resultat[parking] = get_nb_rando(parking, randos)
return resultatOn réutilise les deux contrats précédents : get_parking fournit les clés sans doublon, puis get_nb_rando calcule leur valeur. Cette décomposition facilite une première solution correcte.
On peut aussi compter en un seul parcours :
def nb_rando_par_parking(randos):
resultat = {}
for rando in randos:
parking = rando.depart
if parking not in resultat:
resultat[parking] = 0
resultat[parking] += 1
return resultatAvant chaque incrément, la clé existe. Sur les départs 1, 2, 1, le dictionnaire devient successivement {1: 1}, {1: 1, 2: 1}, puis {1: 2, 2: 1}. La liste des randonnées seule ne permet pas de connaître les parkings de la base qui n’ont aucun départ : ils ne figurent pas dans ce résultat.
Suivre une randonnée à travers les trois relationsUn atelier pour expérimenter
Choisissez un parking de l’extrait et une altitude maximale pour le lac. Le tableau expose chaque étape de la jointure : départ, randonnée et arrivée. Le filtre d’altitude porte ici sur le lac, contrairement à la question 3 qui filtre les parkings.
Lire le résultat de l’expérience initiale
1 randonnée(s) répondent aux critères.
On relie depart à idP et arrivee à idL. Aucun produit cartésien n’est nécessaire : seuls les identifiants qui se correspondent sont associés.
| Parking | Randonnée | Lac | Nom | Altitude |
|---|---|---|---|---|
| 1 | 1 | 1 | Lac Blanc | 2354 |
Une jointure donne du sens aux identifiants : elle relie les lignes qui représentent la même randonnée, sans confondre leurs rôles.
Revoir les notions de cet exercice
- Comprendre le modèle relationnel
- Clés primaires, clés étrangères et contraintes d’intégrité
- SQL : sélectionner, filtrer et trier des résultats
- SQL : comprendre et écrire des jointures
- SQL : insérer, modifier et supprimer des données
- Programmation objet : classes, objets, attributs et méthodes
- Dictionnaires : accès par clé et choix d’une structure
Exercice 3 · 8 points
Objectif somme : résoudre une grille avec des masques binaires
Le jeu Objectif somme possède un plateau de 5 × 5 cases, contenant initialement des chiffres de 1 à 9. On retire certaines cases pour que la somme restante de chaque ligne et de chaque colonne soit égale à sa cible. Il faut conserver au moins un chiffre par ligne. Les lignes sont notées L0 à L4, les colonnes C0 à C4.
Partie A : modélisation du problème
| Ligne / cible | C0 : 15 | C1 : 13 | C2 : 5 | C3 : 2 | C4 : 9 |
|---|---|---|---|---|---|
| L0 : 13 | 7 | 9 | 2 | 3 | 2 |
| L1 : 9 | 8 | 6 | 3 | 5 | 1 |
| L2 : 12 | 7 | 7 | 3 | 2 | 7 |
| L3 : 6 | 6 | 4 | 5 | 8 | 2 |
| L4 : 4 | 8 | 6 | 8 | 8 | 4 |
| Ligne / cible | C0 : 15 | C1 : 13 | C2 : 5 | C3 : 2 | C4 : 9 |
|---|---|---|---|---|---|
| L0 : 13 | · | 9 | 2 | · | 2 |
| L1 : 9 | 8 | · | · | · | 1 |
| L2 : 12 | 7 | · | 3 | 2 | · |
| L3 : 6 | · | 4 | · | · | 2 |
| L4 : 4 | · | · | · | · | 4 |
L1 contient initialement [8, 6, 3, 5, 1] et C3 contient [3, 5, 2, 8, 8]. Dans la suite, les cibles sont supposées être des entiers entre 1 et 45.
Question 1
#Expliquer pourquoi les cibles sont supposées être des entiers entre 1 et 45.
Indice
Combien de chiffres peut-on conserver au maximum, et quelle est leur plus grande valeur ?
Comprendre la correction
Une somme de chiffres entiers est un entier. Cinq cases contenant au plus 9 chacune donnent au maximum 5 × 9 = 45. Garder au moins un chiffre non nul sur une ligne donne une somme au moins égale à 1.
Le sujet choisit aussi des cibles de colonnes positives : il exclut donc les colonnes entièrement vidées. Cette exclusion des colonnes vides vient du choix des cibles ; la seule obligation de garder un chiffre par ligne ne suffirait pas à l’imposer. Il ne faut pas confondre ces deux conditions.
Question 2
#Donner la plus petite et la plus grande cible que la ligne [6, 4, 5, 8, 2] peut atteindre.
Indice
Conserver un seul chiffre permet de minimiser ; tous les chiffres sont positifs.
Comprendre la correction
La plus petite cible est 2, en conservant uniquement le dernier chiffre. La plus grande est 25, en conservant tous les chiffres : 6 + 4 + 5 + 8 + 2 = 25. Vider toute la ligne donnerait 0, mais c’est interdit.
Ces bornes ne signifient pas que tous les entiers intermédiaires sont nécessairement atteignables pour n’importe quelle ligne. Les valeurs accessibles dépendent des sommes de sous-ensembles.
En Python, plateau est une liste de cinq listes de cinq entiers entre 0 et 9. Zéro représente désormais une case vide. Un jeu ajoute deux listes de cibles :
plateau_ex = [[7, 9, 2, 3, 2],
[8, 6, 3, 5, 1],
[7, 7, 3, 2, 7],
[6, 4, 5, 8, 2],
[8, 6, 8, 8, 4]]
ciblesLignes_ex = [13, 9, 12, 6, 4]
ciblesColonnes_ex = [15, 13, 5, 2, 9]Question 3
#Écrire extraireLigne, qui reçoit un plateau et un indice i entre 0 et 4 inclus, et renvoie la ligne Li. Pour l’exemple, l’indice 0 donne [7, 9, 2, 3, 2].
Indice
Le premier indice sélectionne la sous-liste correspondant à une ligne.
Comprendre la correction
def extraireLigne(plateau, i):
return plateau[i]Le plateau est une liste dont chaque élément est déjà une ligne. Un seul accès suffit donc. Cette version renvoie la liste qui se trouve dans le plateau, pas une copie : modifier un élément de la ligne retournée modifierait aussi le plateau. Pour une copie indépendante, on pourrait écrire return plateau[i][:], mais le sujet ne l’exige pas.
Question 4
#Écrire extraireColonne, qui reçoit un plateau et un indice i de 0 à 4 et renvoie la colonne Ci. Avec l’indice 1, l’exemple donne [9, 6, 7, 4, 6].
Indice
Pour une colonne, l’indice de colonne reste fixe ; on traverse toutes les lignes.
Comprendre la correction
def extraireColonne(plateau, i):
colonne = []
for ligne in plateau:
colonne.append(ligne[i])
return colonneCette fois, la colonne n’existe pas comme une sous-liste déjà rangée dans le plateau. On prend l’élément d’indice i dans chaque ligne et on construit une nouvelle liste. Il faut garder i fixe pendant que la ligne change.
Une compréhension est équivalente : return [ligne[i] for ligne in plateau]. Confondre plateau[i][j] et plateau[j][i] échange les rôles des axes : les exemples du sujet servent justement à repérer cette inversion.
Partie B : simplifier le problème
Vider une case revient à remplacer son chiffre par 0. Deux règles permettent d’éliminer des valeurs impossibles sans chercher toutes les combinaisons.
Question 5
#Donner en Python le plateau correspondant à la solution de la figure 1.
Indice
Transcrivez les lignes une par une en remplaçant chaque case vide par zéro.
Comprendre la correction
[[0, 9, 2, 0, 2],
[8, 0, 0, 0, 1],
[7, 0, 3, 2, 0],
[0, 4, 0, 0, 2],
[0, 0, 0, 0, 4]]Chaque ligne doit garder cinq positions : une case vidée devient 0, elle ne disparaît pas de la liste. La première ligne totalise 9 + 2 + 2 = 13 et la première colonne 8 + 7 = 15. Ces vérifications rapides confirment que les positions ont été conservées.
Règle 1 : toute valeur d’une ligne ou colonne qui dépasse sa cible peut être éliminée. Par exemple L4, de cible 4, devient [0, 0, 0, 0, 4]. Cette règle s’appuie sur la positivité des chiffres : aucun autre chiffre conservé ne pourrait compenser une valeur déjà trop grande.
Question 6
#Appliquer la règle 1 à chaque ligne du plateau initial et donner le plateau obtenu en Python.
Indice
Comparez chaque chiffre à la cible de sa ligne, pas à la somme entière de cette ligne.
Comprendre la correction
[[7, 9, 2, 3, 2],
[8, 6, 3, 5, 1],
[7, 7, 3, 2, 7],
[6, 4, 5, 0, 2],
[0, 0, 0, 0, 4]]Les trois premières cibles de lignes sont 13, 9 et 12 : aucun chiffre de leurs lignes ne les dépasse. Pour L3, la cible 6 élimine le 8. Pour L4, la cible 4 élimine 8, 6, 8 et 8.
La consigne de cette question ne demande que le traitement des lignes. N’anticipez pas celui des colonnes : par exemple le 3 en position L0-C3 est encore présent ici, même si la cible de C3 vaut 2.
Question 7
#Compléter les lignes 4 et 10 de regle1, qui applique la règle aux lignes puis aux colonnes :
def regle1(plateau, ciblesLignes, ciblesColonnes):
for i in range(5):
tab = extraireLigne(plateau, i)
cible = ...
for j in range(5):
if tab[j] > cible:
plateau[i][j] = 0
for j in range(5):
tab = extraireColonne(plateau, j)
cible = ...
for i in range(5):
if tab[i] > cible:
plateau[i][j] = 0Indice
Associez chaque indice à la liste de cibles correspondante.
Comprendre la correction
def regle1(plateau, ciblesLignes, ciblesColonnes):
for i in range(5):
tab = extraireLigne(plateau, i)
cible = ciblesLignes[i]
for j in range(5):
if tab[j] > cible:
plateau[i][j] = 0
for j in range(5):
tab = extraireColonne(plateau, j)
cible = ciblesColonnes[j]
for i in range(5):
if tab[i] > cible:
plateau[i][j] = 0La cible de la ligne i est ciblesLignes[i]. Celle de la colonne j est ciblesColonnes[j]. Les variables de parcours gardent donc leur rôle : i pour la ligne, j pour la colonne.
La fonction modifie le plateau en place. Elle ne renvoie pas un nouveau plateau. Un appel doit s’écrire regle1(plateau, ciblesLignes, ciblesColonnes), sans affecter son résultat à plateau, faute de quoi on remplacerait celui-ci par None.
Règle 2 : si une ligne ou colonne de cible paire ne contient qu’un seul chiffre impair, ce chiffre doit être éliminé. Sinon, la somme serait impaire quel que soit le choix des chiffres pairs. Dans L3 de cible 6, le seul impair est 5 : on peut donc l’enlever.
Question 8
#Écrire unImpair, qui prend une liste d’entiers et renvoie True si elle contient exactement un entier impair, et False sinon.
Indice
Comptez les valeurs qui ont pour reste 1 dans la division par 2.
Comprendre la correction
def unImpair(tab):
nb = 0
for valeur in tab:
if valeur % 2 == 1:
nb += 1
return nb == 1Le reste modulo 2 repère chaque valeur impaire. On compte ces valeurs, puis on teste si le compteur vaut exactement 1. Trois valeurs impaires donnent donc False, même si leur nombre est impair : la propriété demandée n’est pas « un nombre impair de valeurs impaires ».
assert unImpair([6, 4, 5, 8, 2]) is True
assert unImpair([0, 2, 4]) is False
assert unImpair([1, 3, 5]) is False
assert unImpair([]) is FalseLes zéros représentant les cases vides sont pairs. Ils ne doivent pas augmenter le compteur.
Question 9
#Compléter les lignes 4, 5, 11 et 12 de regle2 :
def regle2(plateau, ciblesLignes, ciblesColonnes):
for i in range(5):
ligne = extraireLigne(plateau, i)
...
if ...:
for j in range(5):
if plateau[i][j] % 2 == 1:
plateau[i][j] = 0
for j in range(5):
colonne = extraireColonne(plateau, j)
...
if ...:
for i in range(5):
if plateau[i][j] % 2 == 1:
plateau[i][j] = 0Indice
Traduisez littéralement les deux hypothèses de la règle avec and.
Comprendre la correction
def regle2(plateau, ciblesLignes, ciblesColonnes):
for i in range(5):
ligne = extraireLigne(plateau, i)
cible = ciblesLignes[i]
if cible % 2 == 0 and unImpair(ligne):
for j in range(5):
if plateau[i][j] % 2 == 1:
plateau[i][j] = 0
for j in range(5):
colonne = extraireColonne(plateau, j)
cible = ciblesColonnes[j]
if cible % 2 == 0 and unImpair(colonne):
for i in range(5):
if plateau[i][j] % 2 == 1:
plateau[i][j] = 0Les deux conditions sont nécessaires : une cible paire et un seul impair encore présent. Si la cible est impaire, cet impair peut au contraire être indispensable. Si deux impairs sont présents, leur somme peut être paire : en éliminer un arbitrairement serait injustifié.
Les colonnes sont extraites après les modifications des lignes. Le test doit donc porter sur leur état actuel. La fonction conserve la même convention que regle1 : modification du plateau en place.
Les deux règles ne résolvent pas tous les plateaux. On étudie maintenant une seule ligne de cinq entiers : quels chiffres conserver pour atteindre une cible ? Pour [6, 4, 5, 8, 2] de cible 6, on peut garder le 6 seul, masque [1, 0, 0, 0, 0], ou garder 4 et 2, masque [0, 1, 0, 0, 1]. Un 1 signifie « conserver cette position », un 0 « ne pas la conserver ».
Partie C : énumérer les masques
Question 10
#Expliquer pourquoi [1, 1, 0, 0, 1] résout la ligne [1, 2, 3, 5, 2] avec la cible 5. Donner tous les autres masques solutions.
Indice
Classez les cas selon que le 5 est conservé, puis selon que le 3 est conservé.
Comprendre la correction
Le masque donné conserve les positions 0, 1 et 4, donc 1 + 2 + 2 = 5. Les autres masques sont :
[0, 1, 1, 0, 0] # 2 + 3
[0, 0, 1, 0, 1] # 3 + 2
[0, 0, 0, 1, 0] # 5Les deux chiffres 2 sont égaux mais occupent des positions différentes ; ils produisent deux masques distincts lorsqu’on les associe au 3. Pour vérifier l’exhaustivité : soit on garde le 5 seul, soit on utilise le 3 avec l’un des deux 2, soit on n’utilise ni 3 ni 5 et il faut alors garder 1 et les deux 2.
Question 11
#Écrire somme, qui prend une liste de cinq entiers et un masque de cinq bits, puis renvoie la somme des chiffres sélectionnés. L’appel somme([1, 5, 3, 4, 8], [0, 1, 1, 0, 1]) doit donner 16.
Indice
Un masque permet de sélectionner par multiplication par 0 ou 1.
Comprendre la correction
def somme(tab, masque):
total = 0
for i in range(5):
total += tab[i] * masque[i]
return totalChaque produit vaut soit la valeur de la case, si le bit vaut 1, soit zéro. On additionne les cinq contributions. Dans l’exemple, on obtient 0 + 5 + 3 + 0 + 8 = 16. Une version avec if masque[i] == 1 est également correcte.
La liste et le masque ne sont pas modifiés : ils pourront être réutilisés pour d’autres tests. Vérifiez le masque tout nul, qui donne 0, et le masque tout à 1, qui donne la somme de la liste entière.
On interprète chaque masque comme un entier binaire sur cinq bits. Par exemple [0, 1, 0, 0, 1] représente 0 × 16 + 1 × 8 + 0 × 4 + 0 × 2 + 1 × 1 = 9.
Question 12
#Donner l’écriture binaire sur cinq bits de 26 sous la forme d’une liste.
Indice
Décomposez 26 en puissances de deux de 16 à 1.
Comprendre la correction
[1, 1, 0, 1, 0]On décompose 26 = 16 + 8 + 2. Avec les poids 16, 8, 4, 2, 1 de gauche à droite, cela donne 1, 1, 0, 1, 0. L’ordre des bits est le même que celui des positions du masque.
Question 13
#Expliquer pourquoi cinq bits ne représentent que les entiers de 0 à 31.
Indice
Le comptage inclut zéro.
Comprendre la correction
Chaque bit possède deux valeurs possibles. Il existe donc 2⁵ = 32 configurations. En représentation non signée, elles vont de 00000, soit 0, à 11111, soit 16 + 8 + 4 + 2 + 1 = 31.
Il faut distinguer le nombre de valeurs représentables, 32, de la plus grande valeur, 31. Cet écart d’une unité reviendra dans la borne de range à la question 15.
Question 14
#Écrire dec2bin, qui prend un entier de 0 à 31 et renvoie la liste de ses cinq bits. Par exemple, dec2bin(9) doit donner [0, 1, 0, 0, 1].
Indice
Écrivez les restes de 9 // 2 puis recommencez avec le quotient.
Comprendre la correction
def dec2bin(n):
bits = [0, 0, 0, 0, 0]
for i in range(4, -1, -1):
bits[i] = n % 2
n = n // 2
return bitsLes restes des divisions successives par 2 donnent d’abord les bits de poids faible. On remplit donc la liste de droite à gauche. Pour 9, les restes successifs sont 1, 0, 0, 1, 0 ; placés aux indices 4, 3, 2, 1, 0, ils donnent la liste demandée.
range(4, -1, -1) inclut l’indice 0 et réalise toujours cinq tours, ce qui conserve les zéros en tête. Le quotient entier // est indispensable : une division réelle introduirait des nombres flottants.
assert dec2bin(0) == [0, 0, 0, 0, 0]
assert dec2bin(9) == [0, 1, 0, 0, 1]
assert dec2bin(31) == [1, 1, 1, 1, 1]Le sujet rappelle que append ajoute un élément en fin de liste :
solutions = []
solutions.append(1)
solutions.append(2)
# solutions vaut maintenant [1, 2]Question 15
#Écrire masques_solutions, qui reçoit une liste de cinq entiers et une cible, puis renvoie la liste de tous les masques solutions. On peut utiliser dec2bin, somme et append.
PythonTrouver toutes les façons d’atteindre une cibleÉcrivez votre solution et mettez-la à l’épreuve
Écrivez masques_solutions(tab, cible) pour une liste de cinq entiers. Un masque est une liste de cinq bits : 1 conserve la valeur de même position et 0 l’écarte. Renvoyez tous les masques dont la somme sélectionnée vaut cible. dec2bin et somme sont disponibles. Les tests acceptent tout ordre de résultats, mais chaque masque doit apparaître une seule fois.
def masques_solutions(tab, cible):
# À vous de jouer
passLes cas de test proposés :
- La combinaison vide compte aussi : Le masque 00000 réalise zéro et doit être examiné.
- Le dernier masque ne doit pas être oublié : 31 doit être inclus dans l’intervalle des entiers parcourus.
- Des valeurs égales ne sont pas la même case : Il existe cinq sélections distinctes : les positions comptent, même si les valeurs sont identiques.
- Une cible impossible : Une somme de nombres pairs ne peut pas donner 3.
- Plusieurs solutions indépendantes : 3 seul et 1 + 2 conviennent. Les résultats ne doivent pas partager une même liste de bits modifiée en boucle.
Indice
Une boucle énumère les entiers, une condition vérifie la somme et un append conserve les réussites.
Comprendre la correction
def masques_solutions(tab, cible):
solutions = []
for n in range(32):
masque = dec2bin(n)
if somme(tab, masque) == cible:
solutions.append(masque)
return solutionsOn examine exhaustivement les 32 configurations, convertit chacune en masque, puis conserve celles dont la somme vaut la cible. range(32) inclut 31 : range(31) oublierait le masque qui garde tous les chiffres.
Il faut placer le retour après la boucle et conserver toutes les solutions, même si une première a déjà été trouvée. Chaque appel à dec2bin crée une liste indépendante : les masques enregistrés ne sont pas plusieurs références vers une unique liste modifiée au tour suivant.
Pour cinq cases, cela fait seulement 32 masques. Une généralisation à n cases examinerait 2ⁿ possibilités : cette recherche exhaustive devient rapidement coûteuse. Ce constat explique l’intérêt des règles de simplification, sans les confondre avec une preuve qu’elles résolvent tout.
Partie D : retour au jeu complet
Un masque valable pour une ligne ne suffit pas à résoudre le jeu : les colonnes doivent être respectées elles aussi.
Question 16
#Écrire teste_solution, qui reçoit un plateau, les cibles de lignes et celles des colonnes, et renvoie True si toutes les sommes correspondent, False sinon.
Indice
Rejeter au premier écart ; accepter seulement après le dernier contrôle.
Comprendre la correction
def teste_solution(plateau, ciblesLignes, ciblesColonnes):
for i in range(5):
if sum(extraireLigne(plateau, i)) != ciblesLignes[i]:
return False
for j in range(5):
if sum(extraireColonne(plateau, j)) != ciblesColonnes[j]:
return False
return TrueUne seule somme incorrecte suffit à rejeter le plateau : le retour False peut donc être immédiat. Au contraire, on ne renvoie True qu’après avoir vérifié les cinq lignes et les cinq colonnes. Des lignes correctes ne garantissent pas des colonnes correctes.
Les cibles étant positives, une ligne entièrement vide échouerait automatiquement. Cette fonction vérifie exactement les sommes demandées. Elle ne peut pas vérifier que chaque case conservée vient du plateau d’origine : ce plateau n’est pas un paramètre de la fonction.
assert teste_solution(solution, ciblesLignes_ex, ciblesColonnes_ex)
assert not teste_solution(plateau_ex, ciblesLignes_ex, ciblesColonnes_ex)À vous de résoudre le plateau : cinq masques, dix ciblesUn atelier pour expérimenter
Chaque nombre de 0 à 31 est un masque de cinq bits pour une ligne. Un bit à 1 conserve la case, un bit à 0 la vide. Modifiez les cinq masques : une ligne peut être correcte alors qu’une colonne ne l’est pas encore. La table compare les dix sommes aux cibles du sujet.
Lire le résultat de l’expérience initiale
0 cibles atteintes sur 10.
Les bits se lisent dans le même ordre que les colonnes, avec le poids 16 à gauche. Une valeur 0 dans la grille désigne une case vidée, jamais un déplacement des autres cases.
| Ligne ou colonne | Masque sur 5 bits | Somme | Cible | État |
|---|---|---|---|---|
| L0 | 11111 | 23 | 13 | À ajuster |
| L1 | 11111 | 23 | 9 | À ajuster |
| L2 | 11111 | 26 | 12 | À ajuster |
| L3 | 11111 | 25 | 6 | À ajuster |
| L4 | 11111 | 34 | 4 | À ajuster |
| C0 | - | 36 | 15 | À ajuster |
| C1 | - | 32 | 13 | À ajuster |
| C2 | - | 21 | 5 | À ajuster |
| C3 | - | 26 | 2 | À ajuster |
| C4 | - | 16 | 9 | À ajuster |
Résoudre chaque ligne séparément produit des candidats. Seule la vérification croisée des lignes et colonnes confirme une solution complète.
Du sujet à la méthode
Votre prochaine séance de révision
- Pour RIP, distinguez l’initialisation, les échanges synchrones et les mises à jour en place du programme.
- En SQL, suivez les clés entre les relations et vérifiez l’ordre des mutations avant de lancer les requêtes.
- Dans Objectif somme, gardez les positions des cases et testez les colonnes aussi soigneusement que les lignes.
- Refaites les questions 12-13 de l’exercice 1 et 14-16 de l’exercice 3 : elles concentrent le raisonnement sur les boucles, leurs bornes et leur arrêt.
Retrouver ces notions dans d’autres sujets
Toutes les annales de NSI · Le guide pour préparer le bac NSI 2027
Énoncé : sujet 25-NSIJ1NC1 (PDF) · Publication d’origine (nouvel onglet). Corrigé et explications pédagogiques proposés par Sofien.
