Épreuve écrite · 2025 · Jour 2

Bac NSI 2025 Asie jour 2

Ce sujet demande de manipuler des dates, de simuler un ordonnanceur puis de réduire un arbre sans modifier ses décisions. Les pièges sont concrets : compter les jours avec la bonne convention, ne pas inverser le sens des priorités, et traiter les descendants avant leur parent. Le corrigé distingue le comportement exact des programmes imprimés des corrections nécessaires à leurs quelques coquilles.

Les 35 questions se répartissent en trois exercices indépendants valant 6, 6 et 8 points. L’épreuve dure 3 h 30 sans calculatrice.

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

Une classe Date : bissextiles, différences de dates et timestamp

On crée une classe Date pour stocker un jour, un mois et une année sous forme d’entiers. Le tableau des longueurs des mois commence à janvier d’indice 0.

class Date:
    def __init__(self, jour, mois, annee):
        self.jour = ...
        self.mois = ...
        self.annee = ...
        self.nb_jours_par_mois = [31,28,31,30,31,30,31,31,30,31,30,31]

    def get_jour(self):
        return self.jour
    def get_mois(self):
        return self.mois
    def get_annee(self):
        return ...
    def set_jour(self, jour):
        self.jour = jour
    def set_mois(self, mois):
        self.mois = ...
    def set_annee(self, annee):
        self.annee = annee
    def est_bissextile(self):
        ...

Partie A : accès et modification des données

Question 1

#

Compléter les lignes 3 à 5 du constructeur pour affecter les trois paramètres aux attributs correspondants.

Comprendre la correction
self.jour = jour
self.mois = mois
self.annee = annee

À gauche, self désigne l’objet que l’on initialise ; à droite, on utilise les arguments reçus. Le paramètre et l’attribut peuvent avoir le même nom sans être la même variable.

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

Question 4

#

Compléter get_annee, qui renvoie l’attribut annee.

Comprendre la correction
def get_annee(self):
    return self.annee

Un accesseur retourne la donnée sans modifier l’objet. Écrire print(self.annee) afficherait une valeur mais ne la renverrait pas au programme appelant.

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

Question 5

#

Compléter set_mois, qui remplace le mois par la valeur reçue.

Comprendre la correction
def set_mois(self, mois):
    self.mois = mois

Il s’agit d’une modification de l’objet. Le sujet suppose des données valides ; cette méthode seule ne vérifie pas que le jour existe dans le nouveau mois.

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

Février a généralement 28 jours, mais 29 dans les années bissextiles. On dispose de est_bissextile, qui utilise uniquement l’année de l’objet.

self.nb_jours_par_mois = [31,28,31,30,31,30,31,31,30,31,30,31]
if ...:
    self.nb_jours_par_mois[...] = 29

Question 6

#

Compléter les lignes 7 et 8 pour ajuster février à 29 jours si l’année est bissextile.

Comprendre la correction
if self.est_bissextile():
    self.nb_jours_par_mois[1] = 29

Février est le deuxième mois, donc l’élément d’indice 1. On appelle la méthode sur l’instance déjà initialisée, afin qu’elle lise sa valeur d’année.

La liste étant créée dans chaque constructeur, chaque objet possède ses propres longueurs de mois. En revanche, le set_annee imprimé ne les recalcule pas : après un changement d’année, une implémentation complète devrait remettre février à 28 ou 29 selon la nouvelle valeur.

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

Partie B : dans l’année courante

Question 7

#

Écrire est_bissextile. Une année est divisible par 4 mais pas par 100, ou divisible par 400. Rappel : a % n == 0 teste la divisibilité.

Indice

Traitez séparément les multiples de 100, puis ceux de 400.

Comprendre la correction
def est_bissextile(self):
    return (self.annee % 4 == 0 and self.annee % 100 != 0) or self.annee % 400 == 0

Les parenthèses rendent les deux possibilités visibles. 2024 est divisible par 4 et pas par 100 : vrai. 1900 est divisible par 100 mais pas par 400 : faux. 2000 satisfait le second cas : vrai. Ces trois tests évitent la règle incomplète « divisible par 4 ».

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)
def nb_jours_passes(self):
    nb_jours = self.jour
    mois = self.mois - 2
    while mois >= 0:
        nb_jours = nb_jours + self.nb_jours_par_mois[mois]
        mois = mois - 1
    return nb_jours

Question 8

#

Quel sera le résultat de d1 = Date(20, 3, 2001) puis d1.nb_jours_passes() ?

Comprendre la correction

79 : 20 jours de mars + 28 de février + 31 de janvier. 2001 n’est pas bissextile. La méthode inclut le jour courant : le 1 er janvier renverrait 1, pas 0.

Voir la question dans le sujet PDF, p. 3 (nouvel onglet)
def nb_jours_restants(self):
    j = 365
    if ...:
        j = 366
    return j - ...

Question 9

#

Compléter nb_jours_restants, qui renvoie 365 ou 366 moins le nombre de jours déjà passés.

Comprendre la correction
def nb_jours_restants(self):
    j = 365
    if self.est_bissextile():
        j = 366
    return j - self.nb_jours_passes()

Le nombre total dépend de l’année bissextile. Comme les jours passés incluent aujourd’hui, les jours restants commencent demain : le 31 décembre donne 0. Ces deux méthodes utilisent donc des conventions complémentaires.

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

Partie C : entre deux dates

nb_jours_depuis(other) doit renvoyer le nombre de jours depuis other jusqu’à l’instance courante, ou -1 si other est postérieure. Le code du sujet est :

def nb_jours_depuis(self, other):
    if other.get_annee() > self.get_annee():
        return -1
    if other.get_annee() == self.get_annee():
        if other.nb_jours_passes() > self.nb_jours_passes():
            return -1
        if other.nb_jours_passes() == self.nb_jours_passes():
            return 0
    nb_jours = self.nb_jours_passes() + other.nb_jours_restants()
    for annee in range(other.get_annee() + 1, self.get_annee()):
        d_suivant = date(1, 1, annee)
        if d_suivant.est_bissextile():
            nb_jours += 366
        else:
            nb_jours += 365
    return nb_jours
d1 = Date(15, 6, 2024)
d2 = Date(15, 6, 2024)
d3 = Date(15, 7, 2024)
d4 = Date(15, 6, 2025)
d5 = Date(15, 6, 2022)

Question 10

#

Avec les instances fournies, donner les résultats de d1.nb_jours_depuis(d2), d1.nb_jours_depuis(d3), d1.nb_jours_depuis(d4) et d1.nb_jours_depuis(d5). 2024 est bissextile.

Indice

Repérez quels retours arrivent avant l’appel de la classe mal orthographiée.

Comprendre la correction
AppelRésultat viséPourquoi
d1 depuis d20Dates égales
d1 depuis d3-1Le 15 juillet 2024 est après le 15 juin 2024
d1 depuis d4-1L’année 2025 est après 2024
d1 depuis d5731199 jours après le 15 juin 2022 + 365 jours de 2023 + 167 jours de 2024

Attention au code réellement imprimé : la quatrième instruction appelle date(1, 1, annee) alors que la classe définie est Date. Sans autre définition, elle produit donc une NameError. Le résultat 731 correspond à la correction de cette majuscule ; les trois premiers retours arrivent avant cette ligne.

Une autre limite apparaît pour deux dates différentes d’une même année, si other est antérieure : le code additionne à tort le début et la fin de l’année. Pour un objet pleinement fonctionnel, voici la méthode corrigée, distincte du programme fourni :

def nb_jours_depuis(self, other):
    if other.get_annee() > self.get_annee():
        return -1
    if other.get_annee() == self.get_annee():
        difference = self.nb_jours_passes() - other.nb_jours_passes()
        return difference if difference >= 0 else -1
    nb_jours = self.nb_jours_passes() + other.nb_jours_restants()
    for annee in range(other.get_annee() + 1, self.get_annee()):
        d_suivant = Date(1, 1, annee)
        nb_jours += 366 if d_suivant.est_bissextile() else 365
    return nb_jours

Dans le cas d’années différentes, on additionne la fin de la première année, les années complètes intermédiaires et le début de la dernière. Dans la même année, une simple différence de numéros de jour suffit.

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

Le sujet appelle timestamp le nombre de secondes depuis le 1 er janvier 1970 à 00:00 et donne pour exemple 1704063600 pour le 01/01/2024 à 00:00.

def timestamp(self):
    d = ...
    return self.nb_jours_depuis(d) * 24 * 3600

Question 11

#

Compléter la création de d dans timestamp, qui renvoie le nombre de secondes depuis le 1 er janvier 1970.

Comprendre la correction
def timestamp(self):
    d = Date(1, 1, 1970)
    return self.nb_jours_depuis(d) * 24 * 3600

La référence est une autre instance de Date. On convertit les jours en secondes avec 24 × 3600 = 86400. Il faut la méthode nb_jours_depuis corrigée ci-dessus pour un fonctionnement général.

Le 1 er janvier 1970 est une origine conventionnelle de temps Unix, pas la date de démarrage de tous les systèmes Unix. La classe ne gère ni fuseau ni heure d’été. Pour le 1 er janvier 2024 à minuit UTC, ce calcul donne 1704067200. Le nombre 1704063600 imprimé dans le sujet correspond à une heure plus tôt en UTC, donc à minuit dans un fuseau UTC+1 ; sans précision de fuseau, les deux notions ne doivent pas être confondues.

Voir la question dans le sujet PDF, p. 5 (nouvel onglet)
Le jour de plus qui change les calculsUn atelier pour expérimenter

Changez l’année, le mois et le jour. Observez le rang dans l’année et les jours restant après la date. Février réagit aux exceptions de siècle.

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

20/3/2024 : jour 80 de l’année

Année bissextile : février compte 29 jours.

MoisJours dans le moisContribution au cumul
13131
22929
33120
4300
5310
6300
7310
8310
9300
10310
11300
12310

La divisibilité par 4 seule ne suffit pas. Les conventions « jour inclus » et « jours restant après » doivent rester cohérentes pour que les différences de dates soient justes.

Revoir les notions de cet exercice

Exercice 2 · 6 points

Ordonnancement à files multiples et prévention de la famine

On souhaite gérer l’ordre d’exécution des processus sur un processeur. Une valeur numérique plus petite donne une priorité plus élevée : passer de 2 à 3 rend moins prioritaire. Une liste contient une file par valeur, de 0 vers les priorités décroissantes.

À l’arrivée, un nouveau processus reçoit la valeur 0 et entre dans la première file. À chaque cycle : si le CPU est libre, élire le premier processus de la file non vide la plus prioritaire ; s’il contient un processus terminé, le retirer ; sinon augmenter son temps d’utilisation d’une unité. S’il existe un processus en attente de priorité supérieure ou égale, retirer le processus courant, diminuer sa priorité d’un cran, le remettre dans la file correspondante et élire le meilleur disponible. Sinon, diminuer sa priorité d’un cran et le laisser s’exécuter.

Question 1

#

Donner le nom du programme qui gère l’ordre d’exécution des processus.

Comprendre la correction

Il s’agit de l’ordonnanceur, composant du système d’exploitation chargé de choisir quel processus prêt obtient le processeur.

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

Question 2

#

Donner les différents états possibles d’un processus.

Comprendre la correction

Les états centraux sont prêt (il peut s’exécuter mais attend le CPU), élu ou en exécution (il utilise le CPU), et bloqué ou en attente (il attend un événement, par exemple une entrée-sortie). Selon le diagramme de cycle de vie utilisé, on ajoute nouveau et terminé.

Attendre son tour dans une file de l’ordonnanceur correspond à prêt, pas à bloqué : le processus ne manque alors que du processeur.

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

Question 3

#

Choisir la structure la plus adaptée aux processus d’une même priorité : liste, file ou pile.

Comprendre la correction

La file (proposition 2) assure le premier arrivé, premier servi. Une pile favoriserait le dernier arrivé et pourrait faire attendre indéfiniment les plus anciens. Une liste Python peut implémenter cette file, mais le comportement abstrait recherché est FIFO.

Voir la question dans le sujet PDF, p. 7 (nouvel onglet)
class Processus:
    ...(self, ..., priorite, temps_CPU):
        ...priorite = priorite
        ...PID = ...
        self.temps_utilisation = 0
        self.temps_CPU = temps_CPU

Question 4

#

Compléter le constructeur de Processus, qui stocke PID, priorite, temps_utilisation et temps_CPU.

Comprendre la correction
class Processus:
    def __init__(self, PID, priorite, temps_CPU):
        self.priorite = priorite
        self.PID = PID
        self.temps_utilisation = 0
        self.temps_CPU = temps_CPU

temps_utilisation commence à 0 tandis que temps_CPU désigne la quantité totale de CPU nécessaire. La priorité reçue est stockée telle quelle ; le programme d’arrivée lui passe 0.

Voir la question dans le sujet PDF, p. 7 (nouvel onglet)
P1 = Processus(PID=1, priorite=0, temps_CPU=10)
P2 = Processus(PID=2, priorite=0, temps_CPU=7)
P3 = Processus(PID=3, priorite=0, temps_CPU=5)

Question 5

#

Compléter la simulation jusqu’au cycle 5. Au départ liste_files = [[P3, P2, P1], [], []] ; cycle 1 : CPU=P1 et [[P3,P2],[],[]] ; cycle 2 : CPU=P2 et [[P3],[P1],[]].

Indice

La tête de file se trouve à droite dans les états donnés.

Comprendre la correction
CycleCPU après gestionFiles, tête à droiteRaison
3P3[[], [P2, P1], []]P2 consomme une unité puis cède à P3 de priorité 0.
4P3[[], [P2, P1], []]P3, priorité 0 au test, est plus prioritaire que les attentes de valeur 1. Il reste élu et passe à 1.
5P1[[], [P2], [P3]]P3, valeur 1 au test, doit céder ; il passe à 2. P1 est le plus ancien de la file 1.

Les listes du sujet sont écrites avec la tête de file à droite, comme le montre le retrait de P1 au cycle 1. Le test des attentes compare à la priorité de p avant de la diminuer. Au cycle 4, la troisième file demeure vide : les points de suspension du modèle n’imposent pas qu’elle contienne un processus.

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

Question 6

#

Expliquer pourquoi un processus nécessitant 1000 unités de CPU risque de ne jamais terminer face à de nombreux processus de 4 unités, chaque court processus terminé étant remplacé par un nouveau. Préciser l’évolution de la priorité du processus long.

Comprendre la correction

À mesure qu’il consomme du CPU, le processus long voit sa valeur numérique de priorité augmenter : il devient de moins en moins prioritaire. Les nouveaux processus courts arrivent toujours avec la valeur 0. S’ils alimentent continuellement les files prioritaires, le processus long peut rester dans une file de faible priorité sans être réélu.

C’est une famine : le système continue à travailler, mais un processus ne reçoit pas assez de CPU pour terminer. Il ne faut pas confondre ce phénomène avec un interblocage, où plusieurs processus s’attendent mutuellement.

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

Question 7

#

On ajoute un compteur temps_d_attente et un seuil Max_Temps. Le compteur augmente à chaque cycle d’attente ; au-delà du seuil, la priorité remonte. Pourquoi ce mécanisme évite-t-il la famine du processus long ?

Comprendre la correction

Le vieillissement rétablit progressivement la priorité d’un processus qui attend. Sa valeur numérique diminue jusqu’à rejoindre les files servies ; dans une même priorité, l’ordre FIFO empêche les nouveaux arrivants de passer systématiquement devant lui. Il reçoit de nouvelles tranches de CPU et peut accumuler les 1000 unités nécessaires.

Cette conclusion suppose que la remontée déplace effectivement le processus dans la bonne file et que le compteur est géré de manière cohérente. Un simple attribut qui augmente sans modifier l’ordonnancement ne suffit pas.

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

Question 8

#

Écrire meilleur_priorite(liste_files), qui renvoie None si toutes les files sont vides, sinon la valeur de priorité la plus élevée. Pour [[], [p2], [p3,p1]], elle doit renvoyer 1.

Comprendre la correction
def meilleur_priorite(liste_files):
    for priorite in range(len(liste_files)):
        if len(liste_files[priorite]) > 0:
            return priorite
    return None

On explore les indices dans l’ordre croissant : le premier indice non vide est la plus petite valeur, donc la meilleure priorité. Le retour None vient après la boucle pour ne pas s’arrêter dès la première file vide. Une liste de files vide est traitée naturellement.

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

Question 9

#

Écrire prioritaire, qui renvoie et retire le premier processus parmi les plus prioritaires, ou None si tout est vide. On peut utiliser liste.pop(i).

Comprendre la correction
def prioritaire(liste_files):
    priorite = meilleur_priorite(liste_files)
    if priorite is None:
        return None
    return liste_files[priorite].pop()

La fonction précédente donne la file à utiliser. La tête est à droite dans la représentation de la simulation : pop(), équivalent à pop(-1), enlève donc le bon processus. Le cas None doit être testé avant d’indexer la liste.

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

Question 10

#

Écrire gerer(p, liste_files), qui récupère le processus élu et les files d’attente, et implémente le programme donné au début de l’exercice.

PythonFaire céder le processeur au bon momentÉcrivez votre solution et mettez-la à l’épreuve

Écrivez gerer(p, liste_files), qui renvoie le prochain processus élu. Si p vaut None, élisez le prioritaire. Si p a déjà terminé, renvoyez None sans élire immédiatement son successeur. Sinon, exécutez une unité, comparez la meilleure priorité en attente à l’ancienne priorité de p, puis augmentez la priorité numérique de p de 1. S’il doit céder (priorité en attente inférieure ou égale), replacez-le à gauche de sa nouvelle file et élisez le prioritaire. Les files sortent par la droite. Créez les files manquantes si nécessaire. Aucun vieillissement global n’est ajouté.

def gerer(p, liste_files):
    # À vous de jouer
    pass

Les cas de test proposés :

  • CPU libre : élire sans exécuter : L’élection consomme la tête de file mais ne réalise pas encore d’unité de calcul.
  • Un processus déjà terminé : La fin libère le processeur ; l’élection suivante aura lieu au prochain appel.
  • Aucune attente : Le processus continue, mais son temps et sa priorité numérique évoluent.
  • Comparer avant de dégrader la priorité : B est moins prioritaire que l’ancienne priorité 0 de A. Comparer après incrément ferait céder A trop tôt.
  • À égalité, céder et créer la nouvelle file : Le processus exécuté rejoint la priorité 1, même si cette file n’existait pas encore.
  • Respecter FIFO dans la file de retour : Insérer A à gauche conserve C à droite, donc C sera servi avant A.
Indice 1

Traitez d’abord CPU vide et processus terminé.

Indice 2

Comparez les priorités avant de dégrader celle du processus courant.

Comprendre la correction
def gerer(p, liste_files):
    if p is None:
        return prioritaire(liste_files)
    if p.temps_utilisation >= p.temps_CPU:
        return None
    p.temps_utilisation += 1
    attente = meilleur_priorite(liste_files)
    doit_ceder = attente is not None and attente <= p.priorite
    p.priorite += 1
    if doit_ceder:
        while len(liste_files) <= p.priorite:
            liste_files.append([])
        liste_files[p.priorite].insert(0, p)
        return prioritaire(liste_files)
    return p

La fonction renvoie le nouveau processus élu, ou None si le CPU devient libre. Elle respecte la succession du texte : un cycle qui constate la fin retire le processus ; l’élection suivante interviendra au prochain appel. Lors d’un travail réel, elle mémorise la nécessité de céder avant d’augmenter la valeur de priorité.

L’insertion à gauche, insert(0, p), place le nouvel arrivant derrière les éléments qui sortiront à droite. Si la priorité atteint un indice encore absent, on ajoute des files vides : sans cela, un processus long finirait par provoquer une erreur d’indice. Le vieillissement de Q7 n’est pas demandé dans cette fonction et n’est pas ajouté implicitement.

Voir la question dans le sujet PDF, p. 8 (nouvel onglet)
Le CPU change de main, les priorités changent de sensUn atelier pour expérimenter

Avancez cycle par cycle dans la simulation des trois processus. La priorité numérique augmente quand le processus devient moins prioritaire ; les files sont lues de droite à gauche comme dans le sujet.

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

CPU = P1, valeur de priorité 1

Le temps est incrémenté seulement dans la branche « processus en cours et non terminé ». Un cycle d’élection ou de retrait n’ajoute pas d’unité de CPU dans le pseudo-algorithme du sujet.

CycleCPUValeur prioritéFiles, tête à droiteAction
1P100: [P3, P2]Élection sans consommation dans ce cycle
2P200: [P3] ; 1: [P1]P1 cède le CPU
3P301: [P2, P1]P2 cède le CPU
4P311: [P2, P1]Le processus conserve le CPU
5P111: [P2] ; 2: [P3]P3 cède le CPU

Le sens numérique de la priorité, le côté de sortie de la file et le moment où l’on teste chaque condition font partie de l’algorithme. Les changer modifie la simulation.

Revoir les notions de cet exercice

Exercice 3 · 8 points

Base hospitalière, réduction d’arbres et contrôle modulo 97

Cet exercice comporte trois parties indépendantes. Le contexte hospitalier sert à étudier des données et un arbre de décision fictif : les résultats de cet arbre sont ceux du modèle scolaire, sans validation clinique.

Partie A : base de données

TableAttributs et clés
Patientnom_patient TEXT (clé primaire), prenom TEXT, numero_secu INT, age INT
Symptomenom_patient TEXT (clé primaire et clé étrangère vers Patient.nom_patient), toux TEXT, fievre TEXT, nausee TEXT, anosmie TEXT
Maladienom_maladie TEXT (clé primaire) ; autres attributs inutiles à cet exercice
Diagnosticnom_patient TEXT (clé primaire et clé étrangère vers Patient.nom_patient), nom_maladie TEXT (clé étrangère vers Maladie.nom_maladie)
Patient.nom_patientprenomnumero_secuage
HeartmanAlice20705352380018717
DouglasBob10001750015557224
WoodsCaroll25812593061074765
Symptome.nom_patienttouxfievrenauseeanosmie
HeartmanOuiNonNonOui
DouglasNonOuiOuiNon
WoodsOuiOuiNonNon
Maladie.nom_maladie
Covid-19
Gastroentérite

Les lignes de Diagnostic sont laissées non renseignées dans le sujet. On peut utiliser les mots SQL AND, FROM, INSERT, INTO, JOIN, ON, SELECT, SET, UPDATE, VALUES, WHERE et la fonction COUNT.

Question 1

#

Écrire une requête donnant les noms et prénoms des patients ayant strictement plus de 60 ans.

Comprendre la correction
SELECT nom_patient, prenom
FROM Patient
WHERE age > 60;

Le filtre exclut une personne de 60 ans exactement. Dans l’extrait, la seule ligne renvoyée est Woods, Caroll, âgée de 65 ans.

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

Question 2

#

Alice Heartman ne tousse plus. Écrire la mise à jour correspondante.

Comprendre la correction
UPDATE Symptome
SET toux = 'Non'
WHERE nom_patient = 'Heartman';

Le symptôme est stocké dans Symptome et non dans Patient. La colonne contient les chaînes Oui et Non ; on conserve ce codage au lieu d’y introduire le booléen Python False.

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

Question 3

#

Écrire une requête comptant les patients diagnostiqués Covid-19 qui toussent.

SQLCompter les patients qui toussent avec le diagnostic demandéÉcrivez votre solution et mettez-la à l’épreuve

Comptez les patients dont le diagnostic est Covid-19 et dont le champ toux vaut Oui. Les données servent uniquement à travailler les relations SQL du sujet.

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

Les cas de test proposés :

  • Patients officiels, diagnostics pédagogiques : Patient, Symptome et les noms de maladies viennent du sujet. Sa table Diagnostic est vide dans l’extrait : les trois diagnostics de ce test sont ajoutés explicitement pour vérifier la requête, sans portée médicale.
  • Cas complémentaire : chaque filtre compte : Jeu complémentaire pédagogique, distinct des données officielles. Deux lignes satisfont les deux conditions, une seule la maladie, une seule la toux. Le total doit compter uniquement leur intersection.
Comprendre la correction
SELECT COUNT(*)
FROM Diagnostic JOIN Symptome
ON Diagnostic.nom_patient = Symptome.nom_patient
WHERE Diagnostic.nom_maladie = 'Covid-19'
AND Symptome.toux = 'Oui';

Le diagnostic et le symptôme appartiennent à deux tables : la jointure les associe par le patient. Puis les deux filtres doivent être vrais. Comme chaque table admet au plus une ligne par patient selon ses clés, la jointure ne multiplie pas ici les lignes d’un même patient.

On ne peut pas donner un résultat numérique à partir du document : la table Diagnostic n’y est pas remplie.

Voir la question dans le sujet PDF, p. 10 (nouvel onglet)
INSERT INTO Patients VALUES ('Douglas', 'Patrick', 168077230253829, 55);

Question 4

#

Pourquoi l’insertion proposée produit-elle une erreur ?

Comprendre la correction
INSERT INTO Patients VALUES ('Douglas', 'Patrick', 168077230253829, 55);

Le problème de modèle visé est le doublon de clé primaire : Douglas existe déjà pour Bob, donc une seconde ligne de même nom_patient ne peut pas être insérée.

À la lettre, la requête emploie aussi Patients au pluriel, alors que la table du schéma est Patient. Sans table supplémentaire, cette différence produit d’abord une erreur de table inconnue. Corriger le nom en Patient laisse subsister le conflit de clé, qui motive la question suivante.

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

Question 5

#

Proposer une modification du schéma relationnel qui résout ce problème.

Comprendre la correction

Ajouter un identifiant unique id_patient à Patient et en faire la clé primaire. Le nom devient un attribut ordinaire, autorisant les homonymes. Les tables Symptome et Diagnostic doivent alors remplacer leurs références par id_patient, vers la nouvelle clé.

TablePartie du schéma modifiée
Patientid_patient (PK), nom_patient, prenom, numero_secu, age
Symptomeid_patient (PK et FK vers Patient), toux, fievre, nausee, anosmie
Diagnosticid_patient (PK et FK vers Patient), nom_maladie (FK vers Maladie)

Le numéro de sécurité sociale pourrait être envisagé si son unicité, sa présence et sa validité sont imposées. Un identifiant interne évite de dépendre de ces hypothèses externes.

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

Partie B : arbre de décision binaire

Un nœud interne porte un symptôme, une feuille un résultat + ou -. Si le symptôme est présent, on descend à droite ; sinon à gauche. Le sujet donne comme exemple un patient sans toux ni anosmie, avec fièvre et nausées : le chemin non, oui, oui, non aboutit à -.

NonOuiNonOuiNonOuiNonOuiNonOuiNonOuiNonOuiNonOuiNonOuiNonOuiNonOuiNonOuiNonOuiNonOuiNonOui-+anosmie--anosmienausee++anosmie--anosmienauseefievre++anosmie-+anosmienausee++anosmie++anosmienauseefievretoux
Figure 1 : arbre binaire du sujet, non à gauche et oui à droite
Lire les connexions du schéma
  • anosmie relié à - : Non
  • anosmie relié à + : Oui
  • anosmie relié à - : Non
  • anosmie relié à - : Oui
  • nausee relié à anosmie : Non
  • nausee relié à anosmie : Oui
  • anosmie relié à + : Non
  • anosmie relié à + : Oui
  • anosmie relié à - : Non
  • anosmie relié à - : Oui
  • nausee relié à anosmie : Non
  • nausee relié à anosmie : Oui
  • fievre relié à nausee : Non
  • fievre relié à nausee : Oui
  • anosmie relié à + : Non
  • anosmie relié à + : Oui
  • anosmie relié à - : Non
  • anosmie relié à + : Oui
  • nausee relié à anosmie : Non
  • nausee relié à anosmie : Oui
  • anosmie relié à + : Non
  • anosmie relié à + : Oui
  • anosmie relié à + : Non
  • anosmie relié à + : Oui
  • nausee relié à anosmie : Non
  • nausee relié à anosmie : Oui
  • fievre relié à nausee : Non
  • fievre relié à nausee : Oui
  • toux relié à fievre : Non
  • toux relié à fievre : Oui

Question 6

#

D’après l’arbre de la figure 1, donner le diagnostic pour un patient qui tousse et a de la fièvre, mais sans nausée ni anosmie.

Comprendre la correction

Le résultat du modèle est positif (+). On suit oui à toux, oui à fièvre, non à nausée et non à anosmie. La feuille correspondante est étiquetée +.

Voir la question dans le sujet PDF, p. 11 (nouvel onglet)
class Noeud:
    def __init__(self, valeur, gauche=None, droit=None):
        self.valeur = valeur
        self.gauche = gauche
        self.droit = droit

    def est_feuille(self):
        return self.gauche == None and self.droit == None

    def symptome(self):
        assert not self.est_feuille()
        return self.valeur

    def diagnostic(self):
        assert self.est_feuille()
        return self.valeur

Question 7

#

Préciser la signification de l’assertion de la méthode symptome.

Comprendre la correction

assert not self.est_feuille() exige un nœud interne. Une feuille contient un diagnostic, pas un symptôme à consulter. Avec les assertions actives, appeler cette méthode sur une feuille lève une AssertionError au lieu d’interpréter le résultat comme une question.

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

Question 8

#

Nommer un attribut et une méthode de Noeud.

Comprendre la correction

valeur est un attribut. est_feuille est une méthode. Les attributs gauche et droit, ou les méthodes symptome et diagnostic, sont d’autres réponses possibles.

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

Les symptômes sont un dictionnaire de booléens. Exemple :

patient = {'toux': True, 'fievre': True, 'nausee': False, 'anosmie': False}
def applique(arbre, patient):
    if arbre.est_feuille():
        ...
    else:
        if patient[arbre.symptome()]:
            ...
        else:
            ...

Question 9

#

Compléter récursivement applique(arbre, patient), qui renvoie le diagnostic du modèle.

Indice 1

Le cas de base utilise diagnostic, pas symptome.

Indice 2

La convention associe True au fils droit.

Comprendre la correction
def applique(arbre, patient):
    if arbre.est_feuille():
        return arbre.diagnostic()
    else:
        if patient[arbre.symptome()]:
            return applique(arbre.droit, patient)
        else:
            return applique(arbre.gauche, patient)

Le cas d’arrêt est une feuille : son résultat est déjà disponible. Sinon, le dictionnaire dit si le symptôme du nœud est présent. La branche vraie descend à droit, la branche fausse à gauche, conformément à la convention du sujet. Chaque appel doit être retourné pour que le résultat remonte jusqu’au premier appel.

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

Question 10

#

Donner la taille de l’arbre de la figure 1. Une feuille seule compte pour 1.

Comprendre la correction

Il comporte 31 nœuds : 1 + 2 + 4 + 8 + 16, des niveaux de la racine aux feuilles. Les 16 résultats doivent être comptés ; ne donner que les 15 nœuds de symptômes oublierait les feuilles.

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

Règle de la figure 2 : un nœud dont les deux enfants sont des feuilles portant le même diagnostic est remplacé par une feuille portant ce diagnostic.

NonOuiQuestion++Résultat +
Figure 2 : deux branches identiques permettent de supprimer la question
Lire les connexions du schéma
  • Question relié à + : Non
  • Question relié à + : Oui

Question 11

#

Appliquer la règle de réduction à la figure 1 et dessiner le nouvel arbre.

Comprendre la correction
NonOuiNonOuiNonOuiNonOuiNonOuiNonOuiNonOuiNonOui-+anosmie-nausee+-nauseefievre+-+anosmienausee+fievretoux
Arbre réduit : les mêmes décisions avec 17 nœuds
Lire les connexions du schéma
  • anosmie relié à - : Non
  • anosmie relié à + : Oui
  • nausee relié à anosmie : Non
  • nausee relié à - : Oui
  • nausee relié à + : Non
  • nausee relié à - : Oui
  • fievre relié à nausee : Non
  • fievre relié à nausee : Oui
  • anosmie relié à - : Non
  • anosmie relié à + : Oui
  • nausee relié à + : Non
  • nausee relié à anosmie : Oui
  • fievre relié à nausee : Non
  • fievre relié à + : Oui
  • toux relié à fievre : Non
  • toux relié à fievre : Oui

On commence par les nœuds anosmie dont les deux feuilles sont identiques. Six de ces huit nœuds disparaissent, chacun remplacé par une feuille, soit 12 nœuds retirés. Dans la branche toux oui et fièvre oui, les deux nouveaux résultats sont tous deux + : le nœud nausée disparaît à son tour avec deux nœuds supplémentaires.

La taille passe ainsi de 31 à 17, sans changer aucun des 16 résultats possibles. Il faut recommencer vers le haut après les premières réductions, car elles peuvent rendre un parent réductible.

Voir la question dans le sujet PDF, p. 13 (nouvel onglet)
def reduire(self):
    if self.est_feuille():
        return
    self.gauche.reduire()
    self....
    if self.gauche.est_feuille() and ... \
       and ... == ...:
        self.valeur = ...
        self.gauche = ...
        self.droite = ...

Question 12

#

Compléter la méthode reduire, qui applique cette règle récursivement sans changer les décisions.

Indice

Une simplification des enfants peut rendre le parent simplifiable.

Comprendre la correction
def reduire(self):
    if self.est_feuille():
        return
    self.gauche.reduire()
    self.droit.reduire()
    if (self.gauche.est_feuille() and self.droit.est_feuille()
            and self.gauche.diagnostic() == self.droit.diagnostic()):
        self.valeur = self.gauche.diagnostic()
        self.gauche = None
        self.droit = None

On réduit d’abord les deux sous-arbres, puis on inspecte le nœud courant : c’est une logique de parcours postfixe. Tester les deux enfants avant de les avoir réduits manquerait des simplifications devenues possibles plus bas.

Le sujet imprime self.droite dans sa dernière ligne, mais le constructeur crée self.droit. Il faut conserver droit partout. Lors de la réduction, on copie la valeur avant de supprimer les références aux enfants, sinon on perdrait l’accès au diagnostic.

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

Partie C : contrôle d’intégrité

Le sujet représente le numéro de sécurité sociale par 15 chiffres : les 13 premiers forment n, les deux derniers la clé k. On impose que n + k soit multiple de 97. Exemple :

2070535238001 + 87 = 97 × 21345724104

% donne le reste et // le quotient entier : 13 % 3 = 1 et 13 // 3 = 4.

def verifie(num_secu):
    n = num_secu // 100
    k = num_secu % 100
    return ...

Question 13

#

Compléter verifie(num_secu), qui indique si la somme des treize premiers chiffres n et des deux derniers chiffres k est multiple de 97.

Comprendre la correction
def verifie(num_secu):
    n = num_secu // 100
    k = num_secu % 100
    return (n + k) % 97 == 0

La division entière supprime les deux derniers chiffres ; le reste modulo 100 les extrait. Le test final porte ensuite sur la somme, pas sur le numéro complet. Il vérifie la relation arithmétique demandée, sans garantir qu’un numéro existe ou correspond à une personne.

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

Question 14

#

Écrire cle(n), qui renvoie la clé à partir des treize premiers chiffres.

Comprendre la correction
def cle(n):
    return 97 - n % 97

Si n a pour reste r modulo 97, ajouter 97 - r atteint le multiple suivant. La clé est comprise entre 1 et 97 ; quand r vaut 0, elle vaut 97.

La seule équation énoncée admettrait aussi 0 lorsque n est déjà multiple de 97. La formule ci-dessus choisit la convention usuelle de clé 1 à 97. Ne pas confondre cette clé de contrôle, calculable par tous, avec une clé secrète de chiffrement.

Voir la question dans le sujet PDF, p. 14 (nouvel onglet)
Simplifier un arbre sans changer sa réponseUn atelier pour expérimenter

Choisissez les quatre booléens du modèle scolaire et comparez les parcours dans l’arbre complet et l’arbre réduit. Les questions supprimées ne changeaient aucune décision.

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

Résultat du modèle : + dans les deux arbres

Il s’agit uniquement du modèle fictif de l’exercice. Une réduction remplace une question devenue inutile par le résultat commun à ses deux branches.

ÉtapeArbre completArbre réduit
1toux : Ouitoux : Oui
2fievre : Ouifievre : Oui
3nausee : NonRésultat déjà atteint
4anosmie : NonRésultat déjà atteint

Réduire depuis les feuilles vers la racine permet de propager les simplifications. La preuve de correction repose sur l’égalité des réponses des deux branches supprimées.

Revoir les notions de cet exercice

Du sujet à la méthode

Votre prochaine séance de révision

  • Lisez les conventions avant tout calcul : ici, les jours passés incluent aujourd’hui et la tête de file est dessinée à droite.
  • Une simulation d’ordonnanceur doit noter séparément priorité, temps consommé et processus élu.
  • Sur une récursion qui modifie l’arbre, sauvegardez la valeur utile avant de couper les références aux enfants.

Retrouver ces notions dans d’autres sujets

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

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