Épreuve écrite · 2026 · Jour 2

Bac NSI 2026 Antilles-Guyane jour 2

Le sujet du 17 juin 2026 fait manipuler trois formes d’état : la disponibilité d’une salle, les sommets déjà découverts d’un graphe et les valeurs de découpe déjà calculées. Les explications insistent sur les invariants et les détails du code imprimé, notamment une trace de parcours incomplète et un cache que l’on peut initialiser de plusieurs façons. 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

Espaces de coworking : requêtes et objets cohérents

Une agence loue des espaces de travail. Deux parties indépendantes étudient une base de réservations puis un modèle objet de disponibilité. En SQL, on peut utiliser SELECT, FROM, WHERE, AND, OR, JOIN ... ON, INSERT, UPDATE, DELETE, AS, DISTINCT, ORDER BY, COUNT, AVG, MAX, MIN et SUM.

RelationAttributsClé primaire
salleid_salle, intitule, type, nb_places, tarif_jourid_salle
reservationid, id_client, id_salle, date, dureeid
clientid_client, societe, num_telid_client
id_salleintituletypenb_placestarif_jour
1B1Bureau3140
2RavelAmphi902300
3RARéunion10350
4B2Bureau4155
5RBRéunion8300
idid_clientid_salledateduree
1232024-03-241
2112023-12-171
3232024-04-162
4142024-02-051
id_clientsocietenum_tel
1Dupont&co.0812121212
2FleursJ0950505050

Question 1

#

Donner et justifier les clés étrangères de reservation.

Indice

Cherchez les deux informations de réservation qui identifient des lignes d’autres tables.

Comprendre la correction

reservation.id_client référence client.id_client et reservation.id_salle référence salle.id_salle. Une réservation doit concerner un client et une salle existants. Plusieurs réservations peuvent partager ces valeurs, alors que leur propre identifiant id reste unique.

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

Question 2

#

Donner le résultat de SELECT intitule FROM salle WHERE nb_places > 5 ORDER BY tarif_jour sur les extraits.

Indice

Filtrez d’abord la capacité, puis triez les lignes restantes sur leur tarif.

Comprendre la correction
intitule
RB
RA
Ravel

Les salles B1 et B2 sont exclues. Les tarifs des trois restantes valent 300, 350 et 2 300 ; ORDER BY sans DESC trie en ordre croissant. L’ordre du résultat est donc imposé, même si la colonne tarif_jour n’est pas affichée.

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

Question 3

#

Lister sans doublon les id_salle réservés depuis janvier 2024 ; le comparateur > peut être utilisé sur les dates.

Indice

Depuis janvier inclut le premier jour de janvier.

Comprendre la correction
SELECT DISTINCT id_salle
FROM reservation
WHERE date >= '2024-01-01';

Le seuil inclusif comprend tout janvier, y compris le 1er janvier. Avec des dates de jour au format ISO, date > '2023-12-31' est une écriture équivalente. DISTINCT retire les répétitions : la salle 3 a plusieurs réservations. Sur l’extrait, les identifiants sont 3 et 4, sans ordre garanti.

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

Question 4

#

Ajouter le client EN, téléphone 0144624055, puis sa réservation du bureau 4 pour une journée le 12 juin 2024, avec id_client 3 et id 5.

Indice

La clé étrangère du client impose l’ordre des deux insertions.

Comprendre la correction
INSERT INTO client (id_client, societe, num_tel)
VALUES (3, 'EN', '0144624055');

INSERT INTO reservation (id, id_client, id_salle, date, duree)
VALUES (5, 3, 4, '2024-06-12', 1);

La création du client précède la référence à son identifiant. Le téléphone est une chaîne : son zéro initial doit être conservé. La durée vaut 1 jour. On ne crée pas de nouvelle salle : l’identifiant 4 existe déjà.

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

Question 5

#

La salle RAVEL contient désormais 95 personnes. Mettre à jour la base.

Indice

La clé primaire identifie la salle sans ambiguïté de nom.

Comprendre la correction
UPDATE salle
SET nb_places = 95
WHERE id_salle = 2;

L’identifiant 2 désigne la salle Ravel dans l’extrait. Le filtre par identifiant évite de dépendre de la différence de casse entre Ravel dans la table et RAVEL dans la phrase. On modifie la capacité, pas le tarif ni le type.

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

Question 6

#

Obtenir le temps total de réservation de la salle 6.

Indice

Le mot total porte ici sur la durée, pas sur le nombre de lignes.

Comprendre la correction
SELECT SUM(duree)
FROM reservation
WHERE id_salle = 6;

On additionne les durées, en jours, de toutes ses réservations. COUNT donnerait le nombre de réservations, ce qui serait différent pour une réservation de plusieurs jours. La salle 6 ne figure pas dans les extraits, mais ils ne décrivent pas nécessairement toute la base. Si aucune ligne ne correspond, SUM renvoie NULL en SQL ; COALESCE(SUM(duree), 0) serait une amélioration possible si l’application attend un zéro.

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

Question 7

#

Lister sans doublon les noms de sociétés ayant réservé une salle de réunion.

SQLIdentifier les sociétés ayant réservé une réunionÉcrivez votre solution et mettez-la à l’épreuve

Listez sans doublon les noms des sociétés qui ont réservé au moins une salle de type Réunion. Une société peut avoir effectué plusieurs réservations.

SELECT ...
FROM ...
WHERE ...;

Les cas de test proposés :

  • Réservations officielles : Les lignes reprennent les trois tableaux du sujet. Fleurs J a deux réservations de la salle RA mais ne doit apparaître qu’une fois.
  • Cas complémentaire : le rôle des trois tables : Jeu complémentaire pédagogique, distinct des données officielles. Les identifiants de société et de salle sont volontairement croisés : ils ne peuvent pas être joints directement entre eux.
Indice

Le chemin relationnel est client → reservation → salle.

Comprendre la correction
SELECT DISTINCT c.societe
FROM client AS c
JOIN reservation AS r ON r.id_client = c.id_client
JOIN salle AS s ON s.id_salle = r.id_salle
WHERE s.type = 'Réunion';

La société est dans client, l’association société/salle dans reservation et le type dans salle. Les trois tables sont nécessaires. DISTINCT empêche de répéter une société ayant réservé plusieurs réunions. Dans les données affichées, FleursJ apparaît une seule fois malgré ses deux réservations de RA.

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

Partie B : salles et séminaires en Python

Le modèle simplifié n’utilise plus de date : une salle est disponible ou occupée. Si elle est disponible, occupant doit être la chaîne vide.

class Salle:
    def __init__(self, intitule, type_salle, nb_places, tarif_jour):
        self.intitule = intitule
        self.type_salle = type_salle
        self.nb_places = nb_places
        self.tarif = tarif_jour
        self.dispo = True
        self.occupant = ""

    def reserver(self, client):
        if self.dispo:
            self.dispo = False
            self.occupant = client
            return self.tarif
        return False

Question 8

#

Donner un exemple d’attribut et un exemple de méthode de Salle.

Indice

Les affectations self.nom définissent les attributs ; def définit une méthode.

Comprendre la correction

nb_places est un attribut ; reserver est une méthode. Un attribut contient un état propre à l’instance, une méthode décrit une opération effectuée avec cet état. intitule, type_salle, tarif, dispo et occupant seraient aussi des exemples d’attributs.

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

Question 9

#

Écrire liberer pour rendre la salle disponible et sans occupant.

Indice

Une libération modifie la disponibilité et l’identité de l’occupant, pas les caractéristiques de la salle.

Comprendre la correction
def liberer(self):
    self.dispo = True
    self.occupant = ""

Les deux attributs changent ensemble pour maintenir l’invariant : disponible implique aucun occupant. Oublier de vider occupant laisserait une salle déclarée libre mais encore associée à un client. Il ne faut pas réinitialiser son tarif, sa capacité ou son intitulé.

Voir la question dans le sujet PDF, p. 4 (nouvel onglet)
S1 = Salle("B1", "Bureau", 3, 140)
S2 = Salle("RAVEL", "Amphi", 90, 2300)
S3 = Salle("RA", "Réunion", 10, 350)
S4 = Salle("B2", "Bureau", 4, 155)
S5 = Salle("RB", "Réunion", 8, 300)
S1.reserver("Dupont&co")
S2.reserver("FleursJ")
liste_salles = [S1, S2, S3, S4, S5]
def salles_dispos(liste_salles):
    res = []
    for ...:
        if ...:
            res.append(...)
    return res

Question 10

#

Compléter salles_dispos pour renvoyer les intitulés des salles disponibles.

Indice

Lisez l’état de chaque objet puis ajoutez seulement son intitulé.

Comprendre la correction
def salles_dispos(liste_salles):
    res = []
    for salle in liste_salles:
        if salle.dispo:
            res.append(salle.intitule)
    return res

La boucle parcourt des objets Salle : on lit donc leurs attributs avec un point. Le résultat contient les intitulés et non les objets entiers. Après les deux réservations du programme fourni, on obtient ["RA", "B2", "RB"], dans l’ordre de la liste initiale. La fonction ne modifie aucune salle.

Voir la question dans le sujet PDF, p. 4 (nouvel onglet)
class Seminaire:
    def __init__(self, client):
        self.liste_salles = []
        self.prix_total = 0
        self.client = client

    def ajouter_reservation(self, salle):
        if salle.dispo:
            ...
            ...
            return True
        return False

Question 11

#

Compléter ajouter_reservation : ajouter une salle libre au séminaire, la réserver et modifier le prix total ; renvoyer True en cas de succès, False sinon.

Indice

Réutilisez reserver pour ne pas dupliquer la logique de disponibilité.

Comprendre la correction
def ajouter_reservation(self, salle):
    if salle.dispo:
        self.liste_salles.append(salle)
        self.prix_total += salle.reserver(self.client)
        return True
    return False

L’appel salle.reserver(self.client) réalise deux actions liées : il change l’occupation de la salle et renvoie son tarif. On ajoute ce tarif au total existant, au lieu de le remplacer. Le test préalable empêche d’insérer deux fois une même salle déjà occupée et d’ajouter False au total. Pour une salle libre à 350 € et un total initial de 155 €, le nouveau total vaut 505 € ; la salle devient indisponible pour tout autre séminaire qui partagerait la même instance.

Voir la question dans le sujet PDF, p. 5 (nouvel onglet)
Construire un séminaire sans réserver deux fois une salleUn atelier pour expérimenter

Choisissez les salles souhaitées. B1 et RAVEL sont déjà occupées dans le scénario initial. Le modèle accepte uniquement les salles libres et calcule le total correspondant.

Lire le résultat de l’expérience initiale

1 salle(s) réservée(s) pour le séminaire

Une salle occupée est refusée et n’augmente pas le prix. Une salle acceptée devient occupée par le client du séminaire.

Salle demandéeRésultatTarif ajouté
RAAcceptée350

La méthode d’un objet doit faire évoluer ensemble les attributs qui décrivent le même état.

Revoir les notions de cet exercice

Exercice 2 · 6 points

Réseaux : route minimale, chemin pondéré et mémoire des prédécesseurs

Un réseau comporte six routeurs. Les coûts des liaisons sont inversement proportionnels à leurs débits. Les sous-réseaux sont en /24 : les 24 premiers bits définissent leur identifiant réseau.

205010012050200R1R2R3R4R6R5
Réseau initial et coûts de liaison
Lire les connexions du schéma
  • R1 relié à R2 : 20
  • R2 relié à R3 : 50
  • R2 relié à R6 : 100
  • R3 relié à R4 : 1
  • R4 relié à R5 : 20
  • R4 relié à R6 : 50
  • R5 relié à R6 : 200
Sous-réseauInterface 1Interface 2
192.168.1.0/24R1 : 192.168.1.1R2 : 192.168.1.4
10.1.1.0/24R2 : 10.1.1.1R3 : 10.1.1.2
192.168.4.0/24R2 : 192.168.4.2R6 : 192.168.4.1
10.1.6.0/24R3 : 10.1.6.1R4 : 10.1.6.2
10.2.6.0/24R4 : 10.2.6.1R5 : 10.2.6.2
192.168.2.0/24R4 : 192.168.2.2R6 : 192.168.2.1
10.2.7.0/24R5 : 10.2.7.3R6 : 10.2.7.1

Question 1

#

Pourquoi R2 ne peut-il pas utiliser 192.168.2.3 pour son interface liée à R1 ?

Indice

Un /24 fixe les trois premiers octets.

Comprendre la correction

Le lien R1-R2 appartient à 192.168.1.0/24. Les trois premiers octets de chaque interface doivent donc être 192.168.1. L’adresse proposée 192.168.2.3 appartient à un autre sous-réseau, 192.168.2.0/24. Le dernier octet pourrait varier, mais pas le troisième.

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

RIP utilise le nombre de routeurs à traverser pour atteindre le réseau destination. Le tableau initial fournit les trois lignes connectées et 10.1.1.0/24 via 10.1.6.1 à distance 1.

Question 2

#

Compléter la table de routage RIP de R4 avec chaque sous-réseau destination, sa passerelle et sa distance.

Indice

Une passerelle doit être directement joignable depuis R4.

Comprendre la correction
DestinationPasserelle depuis R4Distance
10.1.6.0/24connecté0
10.2.6.0/24connecté0
192.168.2.0/24connecté0
10.1.1.0/2410.1.6.1 (R3)1
192.168.4.0/24192.168.2.1 (R6)1
10.2.7.0/2410.2.6.2 (R5) ou 192.168.2.1 (R6)1
192.168.1.0/2410.1.6.1 (R3) ou 192.168.2.1 (R6)2

Une destination est ici un sous-réseau, pas un routeur. Les réseaux directement connectés ont distance 0 selon la table imposée. Pour les destinations à égalité de nombre de routeurs intermédiaires, plusieurs passerelles sont valables ; la table RIP seule ne privilégie pas le coût numérique du lien. L’adresse de passerelle est l’interface du prochain routeur sur un réseau directement partagé avec R4.

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

Question 3

#

R3-R4 a un débit de 10 Gbit/s et un coût de 1. Calculer le débit de R1-R2, de coût 20.

Indice

Inversement proportionnel signifie que le produit reste constant.

Comprendre la correction

0,5 Gbit/s, soit 500 Mbit/s. Le produit coût × débit est constant : 1 × 10 = 20 × débit, en Gbit/s. Le débit de R1-R2 est donc 10/20. Le coût vingt fois plus grand correspond à un débit vingt fois plus faible, pas plus élevé.

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

Question 4

#

Donner le chemin RIP de R1 à R5.

Indice

Cherchez le minimum de liens, pas la plus petite somme affichée.

Comprendre la correction

R1 → R2 → R6 → R5, avec trois liens inter-routeurs. Le chemin R1-R2-R3-R4-R5 en utilise quatre. RIP ignore ici le coût 200 du dernier lien.

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

Question 5

#

R2-R6 tombe en panne. Donner le nouveau chemin RIP de R1 à R5.

Indice

Supprimez seulement la liaison en panne, pas le routeur R6.

Comprendre la correction

R1 → R2 → R3 → R4 → R5, soit quatre liens. R2 ne peut plus rejoindre directement R6, et passer de R4 par R6 ajouterait encore un détour inutile.

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

Question 6

#

Après rétablissement de R2-R6, donner la route OSPF R1-R5 et la comparer à RIP.

Indice

Comparez les sommes sur les trois chemins simples principaux.

Comprendre la correction

R1 → R2 → R3 → R4 → R5, coût 20 + 50 + 1 + 20 = 91. La route RIP par R6, plus courte en nombre de liens, coûte 20 + 100 + 200 = 320. OSPF garde donc le chemin utilisé pendant la panne malgré le rétablissement, car ses liaisons sont globalement moins coûteuses. Le trajet par R2-R6-R4-R5 coûterait 190, lui aussi supérieur à 91.

Voir la question dans le sujet PDF, p. 7 (nouvel onglet)
RouteurAdresses, toutes en /24
R1192.168.1.1 ; 114.156.4.3 ; 178.115.8.2
R2192.168.1.4 ; 10.1.1.1 ; 192.168.4.2
R310.1.1.2 ; 10.1.6.1 ; 192.162.2.2
R410.1.6.2 ; 10.2.6.1 ; 192.168.2.2
R510.2.6.2 ; 10.2.7.3 ; 12.14.8.2
R6192.168.2.1 ; 10.2.7.1 ; 10.7.2.2 ; 192.168.4.1
R7114.156.4.2 ; 12.14.8.1 ; 192.162.2.8
R810.7.2.3 ; 178.115.8.3

Question 7

#

Dessiner le graphe du réseau mis à jour d’après les nouvelles adresses.

Indice

Deux interfaces dont les trois premiers octets sont identiques partagent ici un lien.

Comprendre la correction

Les sept anciennes liaisons sont conservées. Ajouter R1-R7 via 114.156.4.0/24, R1-R8 via 178.115.8.0/24, R3-R7 via 192.162.2.0/24, R5-R7 via 12.14.8.0/24 et R6-R8 via 10.7.2.0/24.

R1R2R3R4R5R6R7R8
Réseau après ajout de R7 et R8
Lire les connexions du schéma
  • R1 relié à R2
  • R2 relié à R3
  • R2 relié à R6
  • R3 relié à R4
  • R4 relié à R5
  • R4 relié à R6
  • R5 relié à R6
  • R1 relié à R7
  • R1 relié à R8
  • R3 relié à R7
  • R5 relié à R7
  • R6 relié à R8

On compare les trois premiers octets, puisque toutes les interfaces sont en /24. Il faut lire exactement 192.162.2 pour R3-R7 : ce n’est pas 192.168.2, réseau de R4-R6.

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

Partie B : réseau routier

Les villes V1 à V6 forment le même graphe non pondéré que les six routeurs de la partie A : remplacer chaque R par V dans le schéma. Un automobiliste va de V1 à V5. Le dictionnaire fourni est :

route = {
    "V1": [
        "V2"
    ],
    "V2": [
        "V1",
        "V3",
        "V6"
    ],
    "V3": [
        "V2",
        "V4"
    ],
    "V4": [
        "V3",
        "V5",
        "V6"
    ],
    "V5": [
        "V4",
        "V6"
    ],
    "V6": [
        "V2",
        "V4",
        "V5"
    ]
}

Question 8

#

Le GPS propose A, minimisant le temps de parcours, et B, minimisant le nombre de villes traversées. Associer RIP et OSPF.

Indice

L’un compte les étapes, l’autre additionne des poids.

Comprendre la correction

RIP correspond à B : chaque ville joue le rôle d’un routeur traversé. OSPF correspond à A : les poids des liens peuvent représenter les durées, dont la somme est minimisée. L’analogie porte sur les métriques, pas sur une identité entre débit informatique et durée routière.

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

Question 9

#

Donner l’ordre du parcours en largeur depuis V3.

Indice

Marquez les sommets dès leur insertion dans la file pour éviter les doublons.

Comprendre la correction

V3, V2, V4, V1, V6, V5, avec l’ordre des voisins du dictionnaire. La file commence par V3 ; elle reçoit V2 puis V4. V2 découvre V1 et V6 ; V4 découvre V5, tandis que V6 est déjà découvert. Les couches de distances sont donc {V3}, {V2,V4}, puis {V1,V6,V5}.

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

La fonction est présentée comme un calcul de plus court chemin quand les arêtes ont le même poids. pop(0) retire et renvoie le premier élément ; reverse inverse la liste en place ; break interrompt la boucle en cours.

def itineraire_court(graphe, depart, arrivee):
    deja_visites = []
    a_visiter = []
    a_visiter.append(depart)
    precedent = {depart: None}
    while a_visiter != []:
        sommet = a_visiter.pop(0)
        if sommet not in deja_visites:
            deja_visites.append(sommet)
        for voisin in graphe[sommet]:
            if voisin not in deja_visites:
                a_visiter.append(voisin)
                precedent[voisin] = sommet
            if voisin == arrivee:
                a_visiter = []
                break
    if arrivee not in precedent:
        return []
    chemin = []
    ville = arrivee
    while ville != None:
        chemin.append(ville)
        ville = precedent[ville]
    chemin.reverse()
    return chemin

Début de tableau imprimé : tour 1 {V1: None} ; tour 2 {V1: None,V2:V1} ; tour 3 {V1:None,V2:V1,V3:V2}.

Question 10

#

Compléter l’évolution de precedent lors de itineraire_court(route, "V1", "V5").

Indice

Écrivez la file et le dictionnaire après chaque boucle sur les voisins.

Comprendre la correction
InstantDictionnaire precedent
Début tour 1{'V1': None}
Début tour 2{'V1': None, 'V2': 'V1'}
Début tour 3{'V1': None, 'V2': 'V1', 'V3': 'V2', 'V6': 'V2'}
Début tour 4{'V1': None, 'V2': 'V1', 'V3': 'V2', 'V6': 'V2', 'V4': 'V3'}
Fin tour 4{'V1': None, 'V2': 'V1', 'V3': 'V2', 'V6': 'V2', 'V4': 'V6', 'V5': 'V6'}

La troisième ligne imprimée omet V6, alors que le deuxième tour parcourt les deux voisins encore non visités V3 et V6 de V2. Le code découvre ensuite V4 depuis V3, puis réécrit son prédécesseur à V6 au quatrième tour, car V4 est déjà en file mais n’a pas encore été extrait. Les états ci-dessus suivent exactement le programme. Il faut préciser « début » ou « fin » de tour pour éviter un décalage de lecture.

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

Question 11

#

Que renvoie itineraire_court(route, "V1", "V5") ?

Indice

Remontez le dictionnaire depuis V5 puis inversez la liste collectée.

Comprendre la correction
['V1', 'V2', 'V6', 'V5']

À l’arrivée, les prédécesseurs donnent V5 ← V6 ← V2 ← V1 ← None. La boucle construit donc [V5,V6,V2,V1], puis reverse remet le chemin dans le sens du départ.

Ce résultat est bien un plus court chemin pour l’exemple, mais le programme n’est pas fiable sur tout graphe : un sommet déjà en file peut voir son prédécesseur réécrit avec un chemin plus long. Exemple : A relié à B et C ; B relié à C ; C relié à D. Avec les voisins de A dans l’ordre B,C, le code peut remplacer le prédécesseur de C par B et renvoyer A-B-C-D au lieu de A-C-D. La réparation consiste à marquer un sommet dès sa première découverte, par exemple en testant voisin not in precedent, et à ne jamais réécrire son prédécesseur ensuite.

Voir la question dans le sujet PDF, p. 10 (nouvel onglet)
Parcours en largeur : suivre la découverte, puis remonter le cheminUn atelier pour expérimenter

Choisissez le départ et l’arrivée sur le réseau routier. Le modèle utilise un parcours qui marque les sommets dès leur insertion, puis affiche les distances et les prédécesseurs.

Lire le résultat de l’expérience initiale

V3 → V4 → V5

Chaque prédécesseur est fixé une seule fois, à la première découverte. La file garantit alors que cette première route possède un nombre minimal de liens.

Ordre de découverteDistancePrédécesseur
V30Départ
V21V3
V41V3
V12V2
V62V2
V52V4

Une file ne suffit pas : la règle de marquage des sommets est essentielle pour préserver les distances minimales.

Revoir les notions de cet exercice

Exercice 3 · 8 points

Scierie : valoriser le stock et optimiser les découpes

La scierie stocke des planches de longueur entière entre 1 et 10 mètres. Leur prix en euros dépend de la longueur. La liste globale est prix = [0, 1, 5, 8, 9, 10, 17, 17, 20, 24, 30] : prix[n] est le prix d’une planche de n mètres. L’indice 0 sert de convention et aucune planche de longueur nulle n’est vendue.

Longueur (m)12345678910
Prix (€)1589101717202430

Le dictionnaire effectif associe aux longueurs présentes leurs quantités strictement positives. Si trois planches mesurent 2 m, effectif[2] vaut 3. Une longueur absente du stock n’a pas de clé dans le dictionnaire.

Question 1

#

Initialiser effectif pour trois planches de 1 m, quatre de 2 m et une de 4 m.

Indice

Ne permutez pas longueur et nombre de planches.

Comprendre la correction
effectif = {1: 3, 2: 4, 4: 1}

Les clés représentent des longueurs et les valeurs des quantités. Les longueurs absentes ne sont pas ajoutées avec une quantité nulle, conformément à la représentation choisie.

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

Question 2

#

Calculer en Python la valeur totale des planches de 10 m, dont au moins une est présente.

Indice

Vérifiez les unités de chaque facteur.

Comprendre la correction
effectif[10] * prix[10]

On multiplie leur nombre par 30 €, prix d’une planche de 10 m. La garantie de présence rend l’accès effectif[10] valide. Écrire 10 * effectif[10] calculerait la longueur totale en mètres, pas la valeur en euros.

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

Question 3

#

Donner une expression vraie si le stock contient des planches de 5 m.

Indice

La représentation exclut les clés correspondant à un stock nul.

Comprendre la correction
5 in effectif

L’opérateur in sur un dictionnaire teste ses clés. Comme une clé n’existe que si son effectif est positif, ce test répond exactement à la question. effectif[5] > 0 provoquerait KeyError si la longueur 5 est absente.

Voir la question dans le sujet PDF, p. 11 (nouvel onglet)
def valeur_stock(effectif):
    valeur_totale = ...
    for longueur in ...:
        valeur_totale = valeur_totale + ...
    return valeur_totale

Question 4

#

Compléter valeur_stock pour la vente des planches sans découpe.

Indice

Additionnez une contribution par longueur, pas une contribution par clé de valeur arbitraire.

Comprendre la correction
def valeur_stock(effectif):
    valeur_totale = 0
    for longueur in effectif:
        valeur_totale += effectif[longueur] * prix[longueur]
    return valeur_totale

Chaque longueur présente contribue quantité × prix unitaire. Pour {1:3,2:4,4:1}, le résultat est 3×1 + 4×5 + 1×9 = 32 €. L’itération parcourt les clés du dictionnaire ; elle ne suppose pas que toutes les longueurs de 1 à 10 sont présentes. Un dictionnaire vide renvoie 0.

Voir la question dans le sujet PDF, p. 12 (nouvel onglet)
effectif = {1: 3, 5: 5}
autre_effectif = {1: 2, 2: 2}
mise_a_jour(effectif, autre_effectif)
# effectif vaut {1: 5, 2: 2, 5: 5}

Question 5

#

Écrire mise_a_jour(effectif, autre_effectif), qui ajoute les quantités du camion au stock en place.

Indice

La nouvelle quantité s’ajoute à l’ancienne, elle ne la remplace pas lorsqu’une clé existe.

Comprendre la correction
def mise_a_jour(effectif, autre_effectif):
    for longueur in autre_effectif:
        if longueur in effectif:
            effectif[longueur] += autre_effectif[longueur]
        else:
            effectif[longueur] = autre_effectif[longueur]

Deux cas sont nécessaires : augmenter une quantité existante ou créer une nouvelle longueur. Sur l’exemple, la longueur 1 passe de 3 à 5 ; la longueur 2 est ajoutée avec quantité 2 ; la longueur 5 reste à 5. La fonction modifie le dictionnaire reçu, sans devoir en renvoyer un nouveau. On parcourt autre_effectif : l’ajout de clés à effectif ne modifie donc pas la structure parcourue.

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

Question 6

#

Écrire vendre(effectif, longueur), sachant qu’une planche est disponible ; supprimer la clé si sa quantité devient zéro.

Indice

Le test de quantité nulle se fait après avoir retiré une planche.

Comprendre la correction
def vendre(effectif, longueur):
    effectif[longueur] -= 1
    if effectif[longueur] == 0:
        del effectif[longueur]

La précondition garantit que l’accès existe et que le décrément ne devient pas négatif. On supprime uniquement quand la dernière planche est vendue. Cela maintient l’invariant utilisé à la question 3 : une clé présente correspond à un stock positif. Par exemple {1:3,5:1} devient {1:2,5:1} après vendre(...,1), puis {1:2} après vendre(...,5).

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

Partie B : optimiser une planche

On peut découper en morceaux de longueurs entières et les vendre séparément. Pour 1 m, la meilleure valeur est 1 €. Pour 2 m : entière 5 €, ou 1+1 donnant 2 €, donc 5 €. Pour 3 m : entière 8 €, 2+1 donnant 6 €, ou 1+1+1 donnant 3 €, donc 8 €.

Question 7

#

Déterminer la valeur maximale d’une planche de 4 m en autorisant les découpes entières.

Indice

Comparez des partitions de longueur totale 4, pas seulement deux morceaux de tailles différentes.

Comprendre la correction
DécoupeValeur (€)
49
3 + 19
2 + 210
2 + 1 + 17
1 + 1 + 1 + 14

10 € en deux planches de 2 m. Les cinq partitions couvrent toutes les possibilités sans distinguer l’ordre des morceaux. Une coupe n’est donc pas toujours avantageuse, mais celle-ci rapporte 1 € de plus que la vente entière.

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

Pour 1 ≤ n ≤ 10 : si n ≤ 3, valmax(n) = prix[n] ; sinon valmax(n) = max(prix[n], prix[n-1]+valmax(1), prix[n-2]+valmax(2), …, prix[1]+valmax(n-1)).

Question 8

#

Expliquer le cas d’arrêt de la formule récursive de valmax.

Indice

Le cas de base est fondé sur les trois optimisations déjà démontrées.

Comprendre la correction

Pour n ≤ 3, on renvoie prix[n] parce que les comparaisons précédentes ont prouvé qu’aucune découpe n’améliore ces trois petites longueurs. Les autres appels portent sur des longueurs strictement plus petites, donc ils finissent par atteindre ces cas. Ce choix de base dépend du tableau de prix fourni ; il ne serait pas automatiquement vrai avec d’autres tarifs.

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

Question 9

#

Compléter les calculs de valmax(1), valmax(2), valmax(3), valmax(4).

Indice

La récurrence ne force pas la découpe : prix[n] reste un candidat.

Comprendre la correction

valmax(1) = prix[1] = 1 ; valmax(2) = prix[2] = 5 ; valmax(3) = prix[3] = 8. Puis :

valmax(4) = max(prix[4], prix[3] + valmax(1),
                prix[2] + valmax(2), prix[1] + valmax(3))
          = max(9, 8 + 1, 5 + 5, 1 + 8)
          = 10

L’option prix[4] conserve la possibilité de ne pas couper. Les autres options choisissent un morceau vendu entier et optimisent le reste.

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

Question 10

#

Justifier la formule pour n ≥ 4.

Indice

Choisissez un morceau final, puis raisonnez sur la meilleure manière de vendre le reste.

Comprendre la correction

Toute vente optimale a deux formes. Soit la planche reste entière et rapporte prix[n]. Soit elle est découpée : choisir l’un des morceaux finaux de longueur n-k, avec 1 ≤ k ≤ n-1, rapporte prix[n-k], et les k mètres restants doivent être vendus de façon optimale, soit valmax(k). S’ils ne l’étaient pas, remplacer leur découpe par une meilleure augmenterait la valeur totale, contredisant l’optimalité.

Le maximum sur tous les k couvre donc toutes les découpes possibles. On ne suppose pas qu’il n’y aura que deux morceaux : le morceau restant est lui-même optimisé récursivement et peut contenir de nombreuses coupes. Le modèle ignore les pertes de scie et les coûts de découpe, car aucune donnée de ce type n’est fournie.

Voir la question dans le sujet PDF, p. 13 (nouvel onglet)
def valmax(n):
    if ...:
        ...
    else:
        maximum = prix[n]
        for longueur in range(1, ...):
            maximum = max(maximum, ... + ...)
    return maximum

Question 11

#

Compléter valmax.

Indice

Reliez chaque variable à la formule : prix[n-longueur] + valmax(longueur).

Comprendre la correction
def valmax(n):
    if n <= 3:
        return prix[n]
    else:
        maximum = prix[n]
        for longueur in range(1, n):
            maximum = max(maximum, prix[n-longueur] + valmax(longueur))
    return maximum

longueur désigne ici la longueur du reste optimisé. La partie vendue entière mesure n-longueur. range(1,n) évite une planche vide et garantit que l’appel récursif porte sur une longueur strictement inférieure. maximum est initialisé au prix de la planche entière. Le return du cas de base évite d’utiliser un maximum non initialisé quand n ≤ 3.

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

Question 12

#

Pourquoi valmax(7) appelle-t-elle deux fois valmax(5) ?

Indice

Dessinez les arcs 7→5 et 7→6→5.

Comprendre la correction

Une première fois directement lorsque la boucle de valmax(7) atteint longueur = 5. Une seconde fois dans l’appel valmax(6), lui-même déclenché lorsque la boucle principale atteint longueur = 6. Les appels sur des longueurs inférieures à 5 ne peuvent pas rappeler 5, donc il y en a exactement deux. Le résultat calculé la première fois est perdu après le retour : rien ne le mémorise pour la seconde branche.

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

Partie C : mémoriser les résultats

memo = [0, 1, 5, 8, -1, -1, -1, -1, -1, -1, -1]

Une case à -1 est inconnue. Une case déjà renseignée est renvoyée directement ; sinon on calcule récursivement puis on l’enregistre.

def valmax_dynamique(n):
    if ...:
        ...
    else:
        maximum = prix[n]
        for longueur in range(1, ...):
            maximum = max(maximum, ... + ...)
        memo[n] ...
    return maximum

Question 13

#

Compléter valmax_dynamique en utilisant memo.

PythonVendre les meilleures découpes sans recalculer les mêmes longueursÉcrivez votre solution et mettez-la à l’épreuve

Complétez valmax_dynamique(n) pour une longueur entière de 1 à 10. Les listes globales prix et memo sont celles du sujet ; -1 signifie « pas encore calculé ». Une planche peut être vendue entière ou découpée sans perte de matière ni coût supplémentaire. Renvoyez la recette maximale et mémorisez toute nouvelle réponse dans memo[n]. Les appels récursifs doivent réutiliser cette même fonction.

def valmax_dynamique(n):
    # À vous de jouer
    pass

Les cas de test proposés :

  • Une longueur déjà mémorisée : Les trois premières longueurs ont déjà leur valeur optimale dans le cache initial.
  • Découper améliore la vente : Deux morceaux de 2 m rapportent 5 + 5, contre 9 pour la planche entière.
  • Plusieurs découpes successives : 6 + 1 et 2 + 2 + 3 rapportent 18. Il faut comparer les possibilités, pas imposer une coupe au milieu.
  • Parfois la planche entière reste meilleure : Il faut conserver prix[n] comme candidat ; découper n’est pas une obligation.
  • Une réponse déjà connue est réellement utilisée : Avec cette valeur optimale déjà présente, aucun autre sous-problème ne doit être calculé.
  • Toutes les longueurs de la table : Le tableau des recettes attendues provient de l’examen exhaustif des découpages entiers, pas d’un simple tri des prix.
Indice

Le cache est indexé par le sous-problème : ici la longueur n.

Comprendre la correction
def valmax_dynamique(n):
    if memo[n] != -1:
        return memo[n]
    else:
        maximum = prix[n]
        for longueur in range(1, n):
            maximum = max(
                maximum, prix[n-longueur] + valmax_dynamique(longueur))
        memo[n] = maximum
    return maximum

Une valeur différente de -1 est déjà connue et peut être renvoyée sans calcul supplémentaire. Sinon on explore les découpes, puis on mémorise le maximum une fois le calcul terminé. Les appels récursifs doivent utiliser valmax_dynamique, pas l’ancienne valmax, sinon la mémoire serait contournée. Si les tarifs changent, le cache doit être réinitialisé, car ses valeurs dépendent de prix.

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

Question 14

#

Combien d’appels récursifs effectue valmax_dynamique(6), après valmax_dynamique(5) ? Justifier.

Indice

La boucle range(1,6) s’exécute cinq fois, même si tous les résultats demandés sont déjà mémorisés.

Comprendre la correction

Cinq appels récursifs directs, sur les longueurs 1, 2, 3, 4 et 5. Ces valeurs sont toutes en cache, donc chacun renvoie immédiatement, sans produire de nouvel appel récursif. Dire zéro confondrait « aucun calcul récursif supplémentaire sous ces appels » avec « aucun appel ». Si l’on comptait aussi l’appel initial sur 6, on aurait six invocations au total.

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

Question 15

#

Que se passerait-il si memo ne contenait initialement que des -1 ?

Indice

Que fait précisément for longueur in range(1,1) ?

Comprendre la correction

Avec le code complété ci-dessus, il terminerait tout de même et calculerait les bonnes valeurs pour les longueurs 1 à 10. Les appels portent toujours sur des entiers positifs plus petits. Lorsque n = 1, range(1,1) est vide : maximum reste prix[1], cette valeur est mémorisée puis renvoyée. Pour 2 et 3, les petites découpes sont ensuite comparées et redonnent 5 et 8. Il n’y a donc ni boucle infinie ni accès à un indice négatif.

La différence est un petit surcroît de travail pour redécouvrir les bases déjà connues. Le cas de terminaison n’est plus une lecture immédiate d’un cache initial, mais la boucle vide au plus petit n. Ce détail montre pourquoi il faut analyser le programme exact au lieu d’affirmer qu’une récursion doit forcément posséder une instruction explicite if n == 1.

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

Question 16

#

Avec memo réinitialisé comme indiqué, écrire valeur_max_stock(effectif), qui optimise chaque planche et totalise le revenu.

Indice

Remplacez le prix unitaire de valeur_stock par sa valeur optimale, en gardant la quantité.

Comprendre la correction
def valeur_max_stock(effectif):
    total = 0
    for longueur in effectif:
        total += effectif[longueur] * valmax_dynamique(longueur)
    return total

Les planches sont indépendantes dans le modèle : on peut optimiser chacune sans contrainte commune ni coût de découpe. Pour chaque longueur, une valeur optimale est multipliée par la quantité. Le cache global est partagé entre toutes les longueurs du stock ; il ne faut pas le réinitialiser dans la boucle. Pour {1:3,2:4,4:1}, on obtient 3×1 + 4×5 + 1×10 = 33 €, contre 32 € sans découpe. Le stock reste inchangé : cette fonction estime une valeur, elle ne simule pas les ventes ni les coupes.

Voir la question dans le sujet PDF, p. 15 (nouvel onglet)
Découper ou garder entière : chercher le meilleur revenuUn atelier pour expérimenter

Choisissez une longueur et l’état initial du cache. La table donne les meilleures découpes et le nombre d’appels réellement nécessaires pour la nouvelle demande.

Lire le résultat de l’expérience initiale

6 m : 17 € avec 6 m

Une lecture en cache reste un appel de fonction, mais ne relance pas la recherche des découpes. Les planches entières restent un choix possible.

LongueurPrix entièreValeur optimaleUne découpe optimaleCache initial
1111Connu
2552Connu
3883Connu
49102 + 2Connu
510133 + 2Connu
617176Inconnu

La programmation dynamique conserve l’optimum d’un sous-problème. Elle évite de le recalculer, pas nécessairement de l’appeler.

Revoir les notions de cet exercice

Du sujet à la méthode

Votre prochaine séance de révision

  • Tracez les programmes tels qu’ils sont écrits avant de proposer des améliorations : certaines lignes du sujet comportent des incohérences.
  • Dans une récurrence, expliquez ce que représente chaque sous-problème et pourquoi sa taille diminue.
  • Pour compter des appels, distinguez invocation, calcul effectif et lecture d’un résultat déjà en cache.

Retrouver ces notions dans d’autres sujets

Toutes les annales de NSI · Le guide pour préparer le bac NSI 2027

Énoncé : sujet 26-NSIJ2AG1 (PDF). Corrigé et explications pédagogiques proposés par Sofien.