Adloun

Corrigé bac NSI 2025 Métropole jour 1 — Exercice 2 : Liste de tâches d'Alice : file de priorité et planning Pomodoro

Sujet officiel du baccalauréat, spécialité numérique et sciences informatiques, session 2025. Corrigé rédigé par Ibrahim Alame.

Travailler ce sujet sur Adloun Sujet officiel (PDF) Corrigé complet (PDF)

Énoncé

Cet exercice porte sur l'algorithmique, les structures de données, et la gestion de processus.

On cherche à créer une application de type liste de tâches à faire pour aider Alice à planifier sa journée. Pour cela Alice saisit les informations concernant chacune des tâches qu'elle doit effectuer : elle indique un nom pour la tâche, ainsi que la durée qu'elle estime nécessaire afin de la réaliser. On représente une tâche saisie par Alice à l'aide d'un objet de type Tache, muni de quatre attributs :

Avancer de n minutes (n entier positif) dans une tâche consiste à diminuer de n la durée restante de cette tâche. Une tâche est terminée si la durée restante est négative ou nulle.

Lors de la phase de planification de ses tâches (aucune d'entre elles n'est commencée), Alice liste les tâches suivantes qui doivent être effectuées :

NuméroNomDuréeDurée restante
1Répondre aux e-mails4545
2Ranger ma chambre6060
3Réviser la NSI9090
4S'entraîner aux échecs3030
5Apprendre le vocabulaire de chinois3030
6Lire Fondation6060
7Écrire ma lettre au Père Noël2020

On dispose de la classe Tache ci-dessous pour représenter les tâches :


class Tache:
    def __init__(self, numero, nom, duree):
        self.numero = numero
        self.nom = nom
        self.duree_initiale = duree
        self.duree_restante = duree

    def __repr__(self):
        return '<t'+str(self.numero)+'>'

1. Donner le code Python qui permet d'instancier deux variables tache1 et tache2 représentant les tâches :

On supposera dans la suite que les variables tache1, tache2, …, tache7 représentent les tâches établies par Alice lors de la phase de planification.

La méthode __repr__ renvoie une représentation de l'instance sous forme d'une chaîne de caractères. La fonction print utilise cette méthode. Ainsi on a :


>>> print(tache1)
<t1>

2. Recopier et compléter le code de la méthode avancer de la classe Tache qui permet d'avancer la tâche self de n minutes.


    def avancer(self, n):
        ...

3. Recopier et compléter le code de la méthode est_terminee de la classe Tache qui renvoie True si la tâche est terminée, ou False sinon.


    def est_terminee(self):
        ...

Afin d'aider Alice à planifier sa journée, on lui propose d'associer à chacune des tâches une priorité. La priorité d'une tâche est représentée par un entier de la manière suivante : 1 est la priorité minimale et, plus le nombre est grand, plus la tâche associée est prioritaire.

Pour stocker toutes les tâches à effectuer, on utilise une file, dans laquelle les éléments sont des tuples (tache, priorite). Les éléments stockés dans la file doivent respecter les deux conditions ci-après.

Par exemple, si la file de tâches f est la file :


[début] (<t3>, 4) (<t1>, 3) (<t2>, 3) (<t4>, 1) (<t5>, 1) [fin]

Cela signifie que :

4. Représenter l'état de la file f lorsqu'on lui ajoute successivement la tâche numéro 6 avec la priorité 2, puis la tâche numéro 7 avec la priorité 4 en respectant les conditions 1 et 2 décrites ci-dessus.

On suppose déjà définies les méthodes suivantes pour la classe File :

5. En repartant de la file f suivante :


[début](<t3>, 4)(<t1>, 3)(<t2>, 3)(<t4>, 1)(<t5>, 1)[fin]

donner la valeur de f.defiler()[0], et représenter le contenu de la file f après l'exécution de cette instruction.

6. En repartant de la file f suivante :


[début](<t3>, 4)(<t1>, 3)(<t2>, 3)(<t4>, 1)(<t5>, 1)[fin]

donner la valeur de f.examiner()[1], et représenter le contenu de la file f après l'exécution de cette instruction.

On souhaite écrire une fonction ajouter_file_prio qui prend en paramètres :

et qui ajoute le tuple (t, p) à la bonne position dans la file f.

On utilise une file auxiliaire f_aux que l'on remplit en défilant les éléments en début de file f tant que la priorité du premier élément de la file est supérieure ou égale à p. Puis on enfile l'élément (t, p) dans la file auxiliaire. On défile ensuite tous les éléments restants de f dans f_aux et enfin on enfile dans f tous les éléments de f_aux.

7. Recopier et compléter le code de la fonction ajouter_file_prio.


def ajouter_file_prio(f, t, p):
    f_aux = File()
    while ...:
        ...
    ...enfiler(...)
    while not ...:
        ...
    while not ...:
        ...

8. Donner le coût d'exécution temporel dans le pire des cas de la fonction ajouter_file_prio, en fonction du nombre m d'éléments de la file f.

Une fois qu'Alice a entré les tâches qu'elle doit effectuer, leur durée estimée, ainsi que la priorité à laquelle elle doit les effectuer, l'application lui propose un planning en utilisant la technique dite Pomodoro :

On rappelle les tâches à effectuer ci-dessous, classées par ordre de priorité. On considérera que les tâches sont ajoutées à la file de priorité dans l'ordre du tableau ci-dessous :

NuméroNomDuréePriorité
3Réviser la NSI904
7Écrire ma lettre au Père Noël204
1Répondre aux e-mails453
2Ranger ma chambre603
6Lire Fondation602
4S'entraîner aux échecs301
5Apprendre le vocabulaire de chinois301

9. Indiquer pour chaque bloc de 25 minutes la tâche qui avance, en suivant le modèle proposé, jusqu'à la fin de toutes les tâches.

On fera particulièrement attention au cas où la tâche n'est pas terminée : celle-ci est rajoutée à la file des tâches à effectuer (dont elle avait été supprimée) avec la même priorité qu'initialement, en respectant les conditions 1 et 2 de l'énoncé.

10. Écrire le code d'une fonction planning qui prend en paramètre une file de priorité f dont les éléments sont des tuples (tache, prio), et qui renvoie une liste de tâches, dans l'ordre dans lequel elles vont être effectuées par tranche de 25 minutes avec la méthode Pomodoro.

Par exemple, si tache1, tache2 et tache3 sont les tâches numéro 1, numéro 2 et numéro 3, alors le programme suivant :


file = File()
for t, p in [(tache1, 3), (tache2, 3), (tache3, 4)]:
    ajouter_file_prio(file, t, p)
print(planning(file))

produit l'affichage :


[<t3>, <t3>, <t3>, <t3>, <t1>, <t2>, <t1>, <t2>, <t2>]

Corrigé

1. On appelle le constructeur de la classe avec, dans l'ordre des paramètres de __init__, le numéro, le nom et la durée :


tache1 = Tache(1, 'Répondre aux e-mails', 45)
tache2 = Tache(2, 'Ranger ma chambre', 60)

Le constructeur initialise lui-même duree_restante à 45 (puis 60) : on ne passe que trois arguments, self étant fourni automatiquement.

2. Avancer de n minutes, c'est diminuer de n l'attribut duree_restante :


    def avancer(self, n):
        self.duree_restante = self.duree_restante - n

(ou self.duree_restante -= n). La méthode ne renvoie rien : elle modifie l'état de l'objet. Par exemple, pour une tâche de 45 minutes, avancer(25) laisse 20, puis un second avancer(25) laisse : la durée restante peut devenir négative, l'énoncé le prévoit.

3. Une tâche est terminée si sa durée restante est négative ou nulle :


    def est_terminee(self):
        return self.duree_restante <= 0

Ce que le correcteur attend : on renvoie directement le booléen de la comparaison (pas de if ... return True else return False), et la comparaison est bien large () : une tâche dont il reste exactement 0 minute est terminée.

4. On applique les deux conditions. La tâche 6 a la priorité 2 : elle se place après tous les éléments de priorité supérieure ou égale à 2 (&lt;t3&gt;, &lt;t1&gt;, &lt;t2&gt;) et avant ceux de priorité 1 :


[début] (<t3>, 4) (<t1>, 3) (<t2>, 3) (<t6>, 2) (<t4>, 1) (<t5>, 1) [fin]

Puis la tâche 7 a la priorité 4, comme &lt;t3&gt; déjà présente : par la condition 2, elle se place derrière &lt;t3&gt; (insérée plus tôt) mais devant les éléments de priorité 3 :


[début] (<t3>, 4) (<t7>, 4) (<t1>, 3) (<t2>, 3) (<t6>, 2) (<t4>, 1) (<t5>, 1) [fin]

5. f.defiler() retire et renvoie le premier élément de la file, le tuple (&lt;t3&gt;, 4) ; l'indice [0] en extrait la première composante : la valeur de f.defiler()[0] est la tâche &lt;t3&gt; (l'objet tache3). Comme defiler supprime l'élément, la file devient :


[début] (<t1>, 3) (<t2>, 3) (<t4>, 1) (<t5>, 1) [fin]

6. f.examiner() renvoie le premier élément (&lt;t3&gt;, 4) sans le retirer ; [1] en extrait la seconde composante : f.examiner()[1] vaut 4, la priorité de la tâche 3. La file n'est pas modifiée :


[début] (<t3>, 4) (<t1>, 3) (<t2>, 3) (<t4>, 1) (<t5>, 1) [fin]

Ce que le correcteur attend : la différence entre defiler (qui consomme la tête) et examiner (qui la consulte seulement) est le point de la question.

7. On traduit ligne à ligne la description : on transvase dans f_aux la tête de f tant que sa priorité (deuxième composante du tuple, d'indice 1) est supérieure ou égale à p, on enfile le nouvel élément, on vide le reste de f dans f_aux, puis on remet tout dans f :


def ajouter_file_prio(f, t, p):
    f_aux = File()
    while not f.est_vide() and f.examiner()[1] >= p:
        f_aux.enfiler(f.defiler())
    f_aux.enfiler((t, p))
    while not f.est_vide():
        f_aux.enfiler(f.defiler())
    while not f_aux.est_vide():
        f.enfiler(f_aux.defiler())

Le test &gt;= p (et non &gt; p) garantit la condition 2 : un élément de même priorité déjà présent reste devant le nouveau. La condition not f.est_vide() est placée avant f.examiner() dans le and : si f est vide, ou si toute la file a été transvasée (nouvelle priorité plus petite que toutes les autres), Python n'évalue pas examiner() sur une file vide (évaluation paresseuse). À la ligne 5, c'est un tuple que l'on enfile, d'où les doubles parenthèses enfiler((t, p)) : les parenthèses extérieures sont celles de l'appel, les intérieures celles du couple. Aux lignes 4, 7 et 9, en revanche, on enfile directement l'élément renvoyé par defiler(), qui est déjà un tuple.

Vérification sur l'exemple de la question 4 : avec p = 2, les trois premiers éléments (priorités 4, 3, 3) passent dans f_aux, (&lt;t6&gt;, 2) est enfilé, puis (&lt;t4&gt;, 1) et (&lt;t5&gt;, 1) : on retrouve bien la file attendue.

8. Chaque opération du type abstrait file (enfiler, defiler, examiner, est_vide) est de coût constant, en : c'est l'hypothèse usuelle, vérifiée par l'implémentation du cours à l'aide d'une file à deux bouts (un deque). Dans le pire des cas, la nouvelle priorité p est plus petite que toutes celles de la file : la première boucle transvase les éléments (un tour par élément), la deuxième n'en fait aucun, et la troisième remet les éléments dans f. On effectue donc de l'ordre de opérations élémentaires : le coût est linéaire, en . (Si p est plus grande que toutes les priorités, c'est la deuxième boucle qui fait tours : même total. Dans tous les cas, chaque élément de la file est défilé deux fois et enfilé deux fois : une fois de f vers f_aux, une fois de f_aux vers f.)

9. On construit d'abord la file en insérant les tâches dans l'ordre du tableau avec ajouter_file_prio :


[début] (<t3>, 4) (<t7>, 4) (<t1>, 3) (<t2>, 3) (<t6>, 2) (<t4>, 1) (<t5>, 1) [fin]

Puis, à chaque bloc de 25 minutes, on défile la tête, on l'avance de 25, et on la réinsère avec sa priorité si elle n'est pas terminée : par la condition 2 elle repasse derrière les tâches de même priorité, ce qui fait alterner &lt;t1&gt; et &lt;t2&gt;, puis &lt;t4&gt; et &lt;t5&gt;.

BlocMinutesTâcheRestantÉtat de la file après le bloc
10--25``65`(t7,4)(t3,4)(t1,3)(t2,3)(t6,2)(t4,1)(t5,1)`
225--50``, finie`(t3,4)(t1,3)(t2,3)(t6,2)(t4,1)(t5,1)`
350--75``40`(t3,4)(t1,3)(t2,3)(t6,2)(t4,1)(t5,1)`
475--100``15`(t3,4)(t1,3)(t2,3)(t6,2)(t4,1)(t5,1)`
5100--125``, finie`(t1,3)(t2,3)(t6,2)(t4,1)(t5,1)`
6125--150``20`(t2,3)(t1,3)(t6,2)(t4,1)(t5,1)`
7150--175``35`(t1,3)(t2,3)(t6,2)(t4,1)(t5,1)`
8175--200``, finie`(t2,3)(t6,2)(t4,1)(t5,1)`
9200--225``10`(t2,3)(t6,2)(t4,1)(t5,1)`
10225--250``, finie`(t6,2)(t4,1)(t5,1)`
11250--275``35`(t6,2)(t4,1)(t5,1)`
12275--300``10`(t6,2)(t4,1)(t5,1)`
13300--325``, finie`(t4,1)(t5,1)`
14325--350``5`(t5,1)(t4,1)`
15350--375``5`(t4,1)(t5,1)`
16375--400``, finie`(t5,1)`
17400--425``, finiefile vide

La suite des tâches qui avancent, bloc par bloc, est donc :


<t3> <t7> <t3> <t3> <t3> <t1> <t2> <t1> <t2> <t2> <t6> <t6> <t6> <t4> <t5> <t4> <t5>

soit 17 blocs, 425 minutes. Contrôle : chaque tâche occupe blocs, .

Ce que le correcteur attend : au bloc 1, &lt;t3&gt; non terminée est réinsérée avec la priorité 4 derrière &lt;t7&gt; (condition 2) ; c'est donc &lt;t7&gt; qui avance au bloc 2, et non &lt;t3&gt; une seconde fois. De même &lt;t1&gt; et &lt;t2&gt; alternent, puis &lt;t4&gt; et &lt;t5&gt;. Ce mécanisme est celui de l'ordonnancement par tourniquet (round robin) d'un système d'exploitation, avec un quantum de 25 minutes et des niveaux de priorité : d'où le thème « gestion de processus » annoncé.

10. La fonction reproduit la boucle de la question 9 et accumule dans une liste la tâche traitée à chaque bloc :


def planning(f):
    resultat = []
    while not f.est_vide():
        tache, prio = f.defiler()
        tache.avancer(25)
        resultat.append(tache)
        if not tache.est_terminee():
            ajouter_file_prio(f, tache, prio)
    return resultat

On dépaquette le tuple de tête en tache, prio pour réinsérer la tâche avec sa priorité initiale. Sur l'exemple du sujet, la file construite est (&lt;t3&gt;, 4)(&lt;t1&gt;, 3)(&lt;t2&gt;, 3) ; la tâche 3 (90 min) occupe quatre blocs de suite (elle est seule à la priorité 4), puis &lt;t1&gt; (45 min) et &lt;t2&gt; (60 min) alternent : &lt;t1&gt; (reste 20), &lt;t2&gt; (35), &lt;t1&gt; (finie), &lt;t2&gt; (10), &lt;t2&gt; (finie). On obtient bien [&lt;t3&gt;, &lt;t3&gt;, &lt;t3&gt;, &lt;t3&gt;, &lt;t1&gt;, &lt;t2&gt;, &lt;t1&gt;, &lt;t2&gt;, &lt;t2&gt;], l'affichage attendu (les &lt;tk&gt; viennent de __repr__). Terminaison : à chaque tour de boucle, la tâche défilée perd 25 minutes, donc le nombre total de blocs encore nécessaires, somme des des tâches de la file, diminue strictement de 1 (une tâche terminée n'est plus réinsérée) : cet entier positif est un variant, la file finit par être vide.

Poser une question au tuteur sur ce sujet