NSI Première à Montréal • Algorithmique

Exercices corrigés de NSI Première : algorithmique, preuve et coût

Voici une série d'exercices corrigés d'algorithmique pour la spécialité NSI de première, calibrée sur le programme de l'année : parcours séquentiel, recherche dichotomique, tris par sélection et par insertion, algorithmes gloutons, coût constant, linéaire et quadratique, preuve de correction par un invariant et preuve de terminaison par un variant.

Le fil de la série tient en une phrase : un algorithme qui tourne sur l'exemple du cours n'a rien prouvé. Trois questions se posent à chaque fois, et l'évaluation les pose toutes les trois. L'algorithme donne-t-il le bon résultat, et comment le démontrer autrement que par des essais ? S'arrête-t-il toujours, et sur quel variant ? Combien coûte-t-il, non pas annoncé, mais compté ?

Chaque exercice part d'un programme donné, souvent faux d'un détail, et demande de dire lequel des trois points tombe. C'est la forme des sujets, et c'est aussi la façon la plus rapide de comprendre à quoi servent l'invariant et le variant.

Faites chaque exercice au complet avant d'ouvrir la correction : c'est en cherchant qu'on apprend, pas en lisant la solution.

Série autocorrigée Tape tes réponses sous chaque question : la page te dit juste ou faux avant d'ouvrir la correction. Avec un compte, chaque bonne réponse du premier coup rapporte des points.

Ce chapitre fait partie de NSI en Première
Avant de commencer Fiche de révision : les pièges et la méthode de ce chapitre

Avant ce chapitre

Ces notions sont supposées acquises ici. Si le premier exercice résiste, le blocage vient presque toujours de l'une d'elles, pas du chapitre lui-même.

Remonter plus loin : la chaîne complète (4 chapitres) ↓

Le chemin de remédiation, du plus ancien au plus proche. Un élève qui reprend ce chapitre de zéro le reprend dans cet ordre.

  1. 1Algorithmique et PythonSeconde, Mathématiques
  2. 2Python : types, contrôle, fonctions et tableaux
  3. 3Algorithmique et ScratchQuatrième, Mathématiques
  4. 4Algorithmique et ScratchTroisième, Mathématiques

Rappel de cours

  • PARCOURS SÉQUENTIEL : un seul motif, une valeur de départ et une mise à jour par case. Le résultat se rend APRÈS la boucle, sauf pour un test d'existence, qui peut conclure « oui » dès la première case qui convient mais jamais « non » avant la fin.
  • INVARIANT DE BOUCLE : une propriété vraie avant chaque tour et encore vraie après. Elle se vérifie en trois points, initialisation, conservation par un tour, et conclusion à la sortie. Un invariant vrai qui ne conclut pas ne prouve rien.
  • VARIANT : un entier positif qui décroît STRICTEMENT à chaque tour. Son existence prouve que la boucle s'arrête, et son absence explique les boucles infinies.
  • RECHERCHE DICHOTOMIQUE : dans un tableau TRIÉ de nn cases, au plus log2n+1\lfloor\log_{2}n\rfloor+1 tours. Le tri est une PRÉCONDITION : sur un tableau non trié, la fonction rend une réponse fausse sans lever d'erreur.
  • TRI PAR SÉLECTION : à chaque tour on cherche le minimum du reste et on l'échange avec la première case non fixée. Invariant fort : après ii tours, les ii premières cases contiennent les ii PLUS PETITS éléments, en ordre, et ne bougeront plus.
  • TRI PAR INSERTION : on met la valeur de côté, on décale vers la droite tout ce qui est plus grand, on pose la valeur dans la case libérée, d'indice j+1j+1. La mise de côté n'est pas une commodité : sans elle la valeur est écrasée par le premier décalage.
  • COÛT CONSTANT, LINÉAIRE, QUADRATIQUE : on le reconnaît au test du doublement. Doubler nn laisse le temps inchangé, le double, ou le quadruple. Un temps qui gagne une CONSTANTE à chaque doublement est logarithmique.
  • ALGORITHME GLOUTON : le meilleur choix immédiat, sans retour en arrière. Rapide, un seul parcours, mais optimal seulement dans certains cas. Un contre-exemple suffit à le réfuter, aucun nombre d'exemples ne suffit à le valider.
  • UN ALGORITHME NE SE PROUVE PAS PAR DES TESTS. Les tests trouvent des erreurs, ils ne démontrent jamais leur absence.
  • PRÉCONDITION : une condition que la fonction ne vérifie pas et dont elle a besoin. Elle s'écrit dans la spécification et se contrôle par une assertion, placée là où la donnée change, pas là où on la consulte.

Partie A : les bases (/50)

Exercice 1 : Le parcours séquentiel : quatre schémas, un seul motif

Un serveur enregistre, heure par heure, le nombre de connexions reçues. Le relevé tient dans un tableau tt de huit cases.

Parcourir ce tableau, c'est toujours le même geste : on part d'une valeur de départ, on la met à jour case après case, et l'on rend le résultat À LA SORTIE de la boucle. Ce qui change d'un schéma à l'autre, c'est la valeur de départ et la mise à jour, jamais le motif.

Les trois fonctions ci-dessous suivent ce motif. La troisième le suit mal.

120712527319435256147t, une case par heure
Python
t = [12, 7, 25, 7, 19, 3, 25, 14]

def total(t):
    s = 0
    for v in t:
        s = s + v
    return s

def combien_au_dessus(t, seuil):
    c = 0
    for v in t:
        if v > seuil:
            c = c + 1
    return c

def present(t, x):
    for v in t:
        if v == x:
            return True
        else:
            return False
  • a) Donnez la valeur de total(t)total(t), puis le nombre moyen de connexions par heure.
  • b) Donnez la valeur de combien_au_dessus(t,13)combien\_au\_dessus(t, 13).
  • c) present(t,7)present(t, 7) renvoie FalseFalse, alors que 77 figure deux fois dans tt. Expliquez précisément pourquoi, dites pour quelles valeurs de xx la fonction répond juste, puis corrigez-la en déplaçant une seule ligne.
  • d) Écrivez indice(t,x)indice(t, x), qui renvoie l'indice de la PREMIÈRE case contenant xx, et 1-1 si xx est absent. Donnez indice(t,7)indice(t, 7), indice(t,25)indice(t, 25) et indice(t,5)indice(t, 5).

Tape tes réponses, la page te dit juste ou faux 0/8

a)
b)
c)
d)
Voir la correction

Réponses

  • a) total(t)=112total(t)=112, soit 1414 connexions par heure en moyenne
  • b) 44
  • c) La fonction ne regarde que t[0]t[0] : elle est juste pour x=12x=12 et pour toute valeur absente, fausse pour les cinq autres valeurs présentes
  • d) 11, 22 et 1-1

a) La somme vaut 12+7+25+7+19+3+25+14=11212+7+25+7+19+3+25+14=112, et le tableau compte 88 cases, donc la moyenne vaut 112/8=14112/8=14 connexions par heure. Le schéma est celui de l'ACCUMULATEUR : une variable initialisée à la valeur neutre de l'opération, ici 00 pour une addition, mise à jour à chaque case. Vérification rapide : la plus petite valeur vaut 33 et la plus grande 2525, donc la moyenne doit tomber entre les deux, ce qui est le cas. Une moyenne hors des bornes signale toujours une erreur de somme ou un mauvais diviseur.

b) Les valeurs strictement supérieures à 1313 sont 2525, 1919, 2525 et 1414, donc la fonction renvoie 44. C'est le schéma du COMPTEUR, un accumulateur dont la mise à jour est conditionnelle : on part de 00 et on ajoute 11 quand le test réussit. Le piège habituel est d'écrire c=1c = 1 à l'initialisation, ce qui compte une case qui n'existe pas, ou d'écrire c=c+vc = c + v au lieu de c=c+1c = c + 1, ce qui répond à une autre question. La valeur 1414 compte, car le test est strict dans le sens attendu ; si l'énoncé avait dit « au moins 1313 », il aurait fallu >=>=.

c) Le elseelse est DANS la boucle. Au tout premier tour, vv vaut 1212 ; le test v==7v == 7 échoue, on passe au elseelse, et le returnFalsereturn False termine la fonction avant même d'avoir regardé la deuxième case. La fonction ne décide donc jamais que sur t[0]t[0]. Elle répond juste par accident dans deux cas : quand xx vaut 1212, où elle renvoie TrueTrue à raison, et quand xx est absent du tableau, où FalseFalse est la bonne réponse. Elle se trompe sur les cinq autres valeurs présentes, 77, 2525, 1919, 33 et 1414. La correction tient en un décalage : le returnFalsereturn False doit sortir de la boucle et se placer APRÈS elle, sans elseelse. C'est la règle du schéma d'EXISTENCE : on ne peut conclure « non » qu'une fois toutes les cases vues, alors qu'on peut conclure « oui » dès la première qui convient. Une réponse trop tôt vaut zéro à la question, car la fonction rend un résultat faux sans lever la moindre erreur.

d) On parcourt les INDICES et non les valeurs, puisque c'est un indice qu'on doit rendre : for i in range(len(t))for\ i\ in\ range(len(t)), et si t[i]==xt[i] == x alors return ireturn\ i. Après la boucle, et seulement après, return 1return\ -1. La valeur 77 apparaît aux indices 11 et 33 ; le premier returnreturn rencontré est celui de l'indice 11, donc la fonction renvoie 11. La valeur 2525 apparaît aux indices 22 et 66, donc elle renvoie 22. La valeur 55 est absente, donc 1-1. Le choix de 1-1 comme code d'absence tient à ce qu'aucun indice valide n'est négatif : rendre 00 serait ambigu, puisque 00 est un indice parfaitement légitime.

Exercice 2 : L'invariant de boucle : la phrase, les trois points, le bug qu'il localise

Un invariant de boucle est une phrase vraie AVANT chaque tour et encore vraie APRÈS. Ce n'est pas un commentaire décoratif : c'est l'outil qui prouve qu'un algorithme est correct, et c'est aussi celui qui dit OÙ il se trompe quand il se trompe.

La fonction ci-dessous compte les valeurs d'un tableau qui dépassent un seuil. On travaille sur t=[4,11,6,15,2,9,11]t = [4, 11, 6, 15, 2, 9, 11] et seuil=8seuil = 8.

40111621532495116déjà examiné : i casespas encorei = 4
Python
def compte(t, seuil):
    c = 0
    i = 0
    while i < len(t):
        if t[i] > seuil:
            c = c + 1
        i = i + 1
    return c
  • a) Donnez compte(t,8)compte(t, 8), puis écrivez l'invariant de la boucle, en une phrase qui relie cc et ii.
  • b) Vérifiez les trois points : l'invariant est vrai avant d'entrer dans la boucle, un tour le préserve, et il donne le résultat voulu à la sortie.
  • c) Donnez un variant et prouvez la terminaison. De combien décroît-il à chaque tour, et que vaut-il à la sortie ?
  • d) Trois camarades modifient le code. Le premier écrit c=1c = 1 à l'initialisation. Le deuxième place la ligne i=i+1i = i + 1 À L'INTÉRIEUR du ifif. Le troisième écrit while i<=len(t)while\ i <= len(t). Pour chacun, dites lequel des trois points tombe, ou si c'est la terminaison, et ce que le programme fait réellement.

Tape tes réponses, la page te dit juste ou faux 0/8

a)
b)
c)
d)
Voir la correction

Réponses

  • a) compte(t,8)=4compte(t, 8)=4 ; invariant : cc est le nombre d'indices jj strictement inférieurs à ii tels que t[j]>seuilt[j] > seuil
  • b) Vrai à l'entrée avec c=0c=0 et i=0i=0, préservé par un tour, et à la sortie i=len(t)i=len(t) donne le compte total
  • c) Variant len(t)ilen(t)-i, il décroît de 11 à chaque tour et vaut 00 à la sortie
  • d) Premier : l'initialisation, il renvoie 55. Deuxième : la terminaison, boucle infinie. Troisième : l'accès sort du tableau, erreur d'indice

a) Les valeurs strictement supérieures à 88 sont 1111, 1515, 99 et 1111, donc la fonction renvoie 44. L'invariant s'écrit : « avant chaque tour, cc est égal au nombre d'indices jj vérifiant j<ij < i et t[j]>seuilt[j] > seuil, et ii reste compris entre 00 et len(t)len(t) ». La deuxième moitié de la phrase n'est pas un ornement : c'est elle qui garantit que l'accès t[i]t[i] est légal. Un invariant qui ne dit rien sur les bornes ne prouve rien sur les débordements.

b) INITIALISATION : avant le premier tour, ii vaut 00 et cc vaut 00 ; il n'existe aucun indice j<0j < 0, donc le compte attendu est bien 00. L'invariant est vrai. CONSERVATION : supposons-le vrai avec la valeur ii. Le tour examine exactement la case t[i]t[i] et ajoute 11 à cc si et seulement si t[i]>seuilt[i] > seuil, puis fait passer ii à i+1i+1. Le compte des indices j<i+1j < i+1 est donc le compte des indices j<ij < i augmenté de ce même 11 conditionnel : l'invariant est encore vrai. SORTIE : la boucle s'arrête quand ii n'est plus strictement inférieur à len(t)len(t), donc quand i=len(t)i = len(t) ; l'invariant dit alors que cc compte tous les indices du tableau, ce qui est exactement le résultat demandé. Les trois points doivent être écrits séparément : deux sur trois ne prouvent rien.

c) Le variant est len(t)ilen(t) - i. C'est un entier, il est positif ou nul tant que la boucle tourne puisque i<len(t)i < len(t), et chaque tour fait croître ii de 11, donc il décroît strictement de 11. Un entier positif qui décroît strictement ne peut pas décroître indéfiniment : la boucle s'arrête, après exactement 77 tours ici. À la sortie il vaut 00. Attention à ne pas confondre variant et invariant : le variant DÉCROÎT et prouve l'arrêt, l'invariant NE CHANGE PAS et prouve le résultat.

d) Premier camarade : l'INITIALISATION tombe. Avant le premier tour, cc vaut 11 alors qu'aucune case n'a été examinée ; les deux autres points restent valables, donc l'erreur se propage telle quelle jusqu'à la sortie et la fonction renvoie 55 au lieu de 44. C'est la faute la plus discrète du chapitre, parce que le programme s'exécute sans broncher. Deuxième camarade : la TERMINAISON tombe. Avec i=i+1i = i + 1 dans le ifif, le compteur d'indice n'avance que lorsque la condition est vraie ; ici t[0]=4t[0] = 4 n'est pas supérieur à 88, donc ii reste à 00 et la boucle tourne indéfiniment. Le variant len(t)ilen(t) - i ne décroît plus, et c'est précisément ce que le variant sert à détecter. Troisième camarade : l'invariant garantissait ilen(t)i \le len(t) au moment de l'accès, mais la condition i<=len(t)i <= len(t) autorise un tour de trop ; à i=len(t)=7i = len(t) = 7, l'accès t[7]t[7] sort du tableau et le programme s'arrête sur une erreur d'indice. Des trois, c'est la moins grave, parce qu'elle se voit tout de suite.

Exercice 3 : Le tri par sélection : l'invariant fort et celui qui ne conclut pas

Le tri par sélection range un tableau en cherchant, à chaque tour, le plus petit élément de ce qui reste, et en l'échangeant avec la première case non encore fixée.

On travaille sur t=[23,8,41,15,4,16]t = [23, 8, 41, 15, 4, 16], donc n=6n = 6. La figure montre l'état du tableau APRÈS le premier tour.

4081412153234165définitifreste à traiterminimum du reste
Python
def tri_selection(t):
    n = len(t)
    for i in range(n - 1):
        m = i
        for j in range(i + 1, n):
            if t[j] < t[m]:
                m = j
        t[i], t[m] = t[m], t[i]
    return t
  • a) Donnez l'état du tableau après le premier, le deuxième et le troisième tour de la boucle externe, puis le tableau final.
  • b) Écrivez l'invariant de la boucle externe. Pourquoi la boucle s'écrit-elle range(n1)range(n - 1) et non range(n)range(n) ? Le résultat changerait-il ?
  • c) Un camarade écrit m=0m = 0 au lieu de m=im = i. Déroulez le deuxième tour sur ce tableau et dites quelle partie de l'invariant tombe.
  • d) Un autre camarade propose l'invariant suivant : « après ii tours, les ii premières cases sont triées entre elles ». Cet énoncé est-il vrai ? Permet-il de conclure à la sortie ? Donnez un tableau de six cases qui le vérifie pour i=5i = 5 sans être trié.

Tape tes réponses, la page te dit juste ou faux 0/9

a)
b)
c)
d)
Voir la correction

Réponses

  • a) [4,8,41,15,23,16][4, 8, 41, 15, 23, 16], puis inchangé, puis [4,8,15,41,23,16][4, 8, 15, 41, 23, 16] ; final [4,8,15,16,23,41][4, 8, 15, 16, 23, 41]
  • b) Après ii tours, les ii premières cases contiennent les ii plus petits éléments, rangés en ordre croissant, et elles ne bougeront plus. Le dernier tour serait inutile, le résultat ne change pas
  • c) Le deuxième tour ramène 44 en case 11 : la partie « elles ne bougeront plus » tombe
  • d) Vrai, mais il ne conclut pas ; par exemple [1,2,3,4,5,0][1, 2, 3, 4, 5, 0]

a) Premier tour, i=0i = 0 : le minimum de tout le tableau est 44, en case 44 ; on l'échange avec la case 00, ce qui donne [4,8,41,15,23,16][4, 8, 41, 15, 23, 16]. Deuxième tour, i=1i = 1 : le minimum du reste est 88, déjà en case 11 ; l'échange se fait avec elle-même et le tableau ne bouge pas. Troisième tour, i=2i = 2 : le minimum du reste est 1515, en case 33 ; on obtient [4,8,15,41,23,16][4, 8, 15, 41, 23, 16]. Les deux derniers tours amènent 1616 puis laissent 2323 en place, et le tableau final est [4,8,15,16,23,41][4, 8, 15, 16, 23, 41]. Vérification : la somme des six valeurs vaut 107107 avant comme après, puisqu'un tri ne fait que déplacer des valeurs. Une somme qui change signale une valeur écrasée, faute classique quand on remplace l'échange par deux affectations.

b) L'invariant : « après ii tours, les ii premières cases contiennent les ii plus petits éléments du tableau, rangés en ordre croissant, et elles ne seront plus modifiées ». La boucle s'arrête à n1n - 1 parce qu'après n1n - 1 tours l'invariant dit déjà que les n1n - 1 premières cases portent les n1n - 1 plus petits éléments en ordre : la dernière case ne peut donc contenir que le plus grand, et un tour de plus chercherait le minimum d'un reste réduit à une seule case. Écrire range(n)range(n) ne change donc pas le résultat, cela ajoute seulement un tour qui échange une case avec elle-même. Ce n'est pas une faute, c'est une ligne pour rien, et le correcteur attend que l'on sache dire pourquoi.

c) Avec m=0m = 0, la recherche du minimum repart toujours de la case 00, qui est déjà définitive. Au deuxième tour, ii vaut 11 et mm vaut 00 ; la boucle interne compare t[j]t[j] à t[0]=4t[0] = 4 pour jj allant de 22 à 55, et aucune de ces valeurs n'est inférieure à 44, donc mm reste 00. L'échange final porte alors sur les cases 11 et 00 et produit [8,4,41,15,23,16][8, 4, 41, 15, 23, 16] : la case 00, que l'invariant déclarait définitive, vient d'être modifiée. C'est exactement la clause « elles ne seront plus modifiées » qui tombe, et le tableau final n'est pas trié. L'erreur est fatale et silencieuse, ce qui en fait la plus coûteuse du chapitre : la fonction rend un tableau d'allure plausible.

d) L'énoncé est VRAI : si les ii premières cases contiennent les ii plus petits éléments en ordre croissant, alors elles sont bien triées entre elles. Mais il ne permet pas de conclure. À la sortie, avec i=n1=5i = n - 1 = 5, il affirme seulement que les cinq premières cases sont ordonnées, sans rien dire de la sixième ni du lien entre les deux parties. Le tableau [1,2,3,4,5,0][1, 2, 3, 4, 5, 0] le vérifie et n'est pourtant pas trié. Tout l'intérêt de l'invariant fort tient dans les trois mots « les ii plus petits » : ce sont eux qui interdisent qu'une valeur plus petite traîne dans le reste. La leçon vaut pour tout le chapitre : un invariant peut être vrai et inutile, et le seul test qui compte est de vérifier qu'il DONNE le résultat voulu quand la boucle s'arrête.

Exercice 4 : Le tri par insertion : la valeur mise de côté et la case libérée

Le tri par insertion range un tableau comme on range une main de cartes : on prend la carte suivante, on la met de côté, on décale vers la droite toutes celles qui sont plus grandes, puis on la pose dans la case libérée.

Deux lignes portent toute la difficulté : la mise de côté v=t[i]v = t[i] avant la boucle, et le t[j+1]=vt[j + 1] = v après. On travaille sur t=[9,4,7,2,6]t = [9, 4, 7, 2, 6].

407192364v = 2tour i = 3 : on décale vers la droitedécalage
Python
def tri_insertion(t):
    for i in range(1, len(t)):
        v = t[i]
        j = i - 1
        while j >= 0 and t[j] > v:
            t[j + 1] = t[j]
            j = j - 1
        t[j + 1] = v
    return t
  • a) Donnez l'état du tableau après chacun des quatre tours de la boucle forfor, puis le nombre de fois où le test t[j]>vt[j] > v est évalué à chaque tour et au total.
  • b) Pourquoi la dernière ligne s'écrit-elle t[j+1]=vt[j + 1] = v et non t[j]=vt[j] = v ? Donnez le tableau obtenu au premier tour par la version fausse.
  • c) Donnez un variant de la boucle whilewhile et prouvez sa terminaison. Que vaut-il au minimum ?
  • d) Un camarade supprime la ligne v=t[i]v = t[i] et remplace vv par t[i]t[i] partout ailleurs. Déroulez le premier tour et expliquez ce qui est perdu.

Tape tes réponses, la page te dit juste ou faux 0/9

a)
b)
c)
d)
Voir la correction

Réponses

  • a) [4,9,7,2,6][4, 9, 7, 2, 6], [4,7,9,2,6][4, 7, 9, 2, 6], [2,4,7,9,6][2, 4, 7, 9, 6], [2,4,6,7,9][2, 4, 6, 7, 9] ; 11, 22, 33 et 33 évaluations, soit 99 au total
  • b) La boucle décrémente jj une fois de trop, donc la case libre est j+1j + 1. La version fausse donne [9,9,7,2,4][9, 9, 7, 2, 4]
  • c) Variant j+1j + 1, entier positif ou nul qui décroît de 11 par tour, minimum 00
  • d) La première écriture écrase t[i]t[i] : la valeur 44 est perdue et le tableau devient [9,9,7,2,6][9, 9, 7, 2, 6]

a) Tour i=1i = 1 : v=4v = 4, on compare t[0]=9t[0] = 9 à 44, le décalage a lieu, jj passe à 1-1 et la boucle s'arrête sur la première condition ; on pose 44 en case 00, d'où [4,9,7,2,6][4, 9, 7, 2, 6], avec 11 évaluation du test. Tour i=2i = 2 : v=7v = 7, on décale 99, puis t[0]=4t[0] = 4 n'est pas supérieur à 77 et l'on s'arrête ; [4,7,9,2,6][4, 7, 9, 2, 6], 22 évaluations. Tour i=3i = 3 : v=2v = 2, les trois valeurs 99, 77 et 44 sont décalées et jj tombe à 1-1 ; [2,4,7,9,6][2, 4, 7, 9, 6], 33 évaluations. Tour i=4i = 4 : v=6v = 6, on décale 99 puis 77, et 44 arrête la boucle ; [2,4,6,7,9][2, 4, 6, 7, 9], 33 évaluations. Total : 1+2+3+3=91 + 2 + 3 + 3 = 9. Contrôle : la somme vaut 2828 avant et après.

b) La boucle whilewhile ne s'arrête qu'APRÈS avoir décrémenté jj une fois de trop : soit parce que jj est devenu 1-1, soit parce que t[j]t[j] n'est plus supérieur à vv et que la case jj doit rester où elle est. Dans les deux cas, la case libre est celle d'indice j+1j + 1, la dernière d'où l'on a décalé une valeur. Écrire t[j]=vt[j] = v pose la valeur une case trop à gauche, et écrase une valeur qui, elle, était bien placée. Au premier tour, vv vaut 44, le décalage donne [9,9,7,2,6][9, 9, 7, 2, 6] et jj vaut 1-1 ; l'affectation t[1]=4t[-1] = 4 ne lève aucune erreur en Python, puisque l'indice 1-1 désigne la DERNIÈRE case : le tableau devient [9,9,7,2,4][9, 9, 7, 2, 4]. Deux valeurs sont détruites d'un coup, et aucune exception ne prévient. C'est le meilleur argument pour écrire un jeu de tests plutôt que de se fier à l'absence de message d'erreur.

c) Le variant est j+1j + 1. C'est un entier, il vaut au moins 00 tant que la boucle tourne puisque la condition impose j0j \ge 0, et chaque tour fait décroître jj de 11, donc le variant décroît strictement. Il ne peut donc pas décroître indéfiniment, et la boucle s'arrête : au pire quand le variant atteint 00, c'est-à-dire j=1j = -1, c'est-à-dire quand la valeur mise de côté est plus petite que toutes les précédentes. On choisit j+1j + 1 plutôt que jj pour que le variant reste positif ou nul, ce qui est la forme attendue dans une preuve de terminaison.

d) Sans la mise de côté, la valeur à insérer n'existe plus qu'à un seul endroit : la case t[i]t[i] elle-même. Or la toute première itération du whilewhile écrit t[j+1]=t[j]t[j + 1] = t[j] avec j+1=ij + 1 = i : elle écrase précisément cette case. Au premier tour, ii vaut 11 et t[1]=4t[1] = 4 ; le décalage écrit t[1]=t[0]=9t[1] = t[0] = 9, donc t[i]t[i] vaut maintenant 99. La condition t[j]>t[i]t[j] > t[i] devient 9>99 > 9, fausse, et la boucle s'arrête ; l'affectation finale t[j+1]=t[i]t[j + 1] = t[i] recopie 99 sur lui-même. Le tableau devient [9,9,7,2,6][9, 9, 7, 2, 6] et la valeur 44 a disparu. La ligne v=t[i]v = t[i] n'est donc pas une commodité de lecture : c'est la sauvegarde sans laquelle l'algorithme détruit sa propre donnée. C'est pour la même raison qu'on écrit l'échange du tri par sélection en une seule instruction, plutôt qu'en deux affectations successives.

Exercice 5 : Constant, linéaire, quadratique : le test du doublement

On ne devine pas le coût d'un algorithme, on le constate. Le test du doublement consiste à doubler la taille des données et à regarder ce que devient le temps : inchangé pour un coût constant, doublé pour un coût linéaire, quadruplé pour un coût quadratique.

Quatre programmes AA, BB, CC et DD ont été chronométrés sur les mêmes données, pour quatre tailles qui doublent à chaque ligne. Les temps sont en millisecondes.

nA (ms)B (ms)C (ms)D (ms)1 0000,81,52412,02 0000,83,19713,44 0000,96,238914,88 0000,812,31 55416,2
  • a) Pour AA, BB et CC, calculez le rapport d'un temps au précédent, puis classez chacun en coût constant, linéaire ou quadratique.
  • b) Prédisez le temps de BB et celui de CC pour n=16000n = 16\,000, puis pour n=64000n = 64\,000. Donnez le temps de CC à 6400064\,000 en secondes, au dixième.
  • c) Le programme DD ne rentre dans aucune des trois catégories : son temps AUGMENTE, mais pas proportionnellement. Que fait le doublement de nn à son temps, et que vaut son temps pour n=64000n = 64\,000 ?
  • d) Pour n=1000n = 1\,000, CC n'est que 1616 fois plus lent que BB. Calculez le rapport pour n=64000n = 64\,000. Que faut-il en conclure sur une mesure faite à une seule taille ?

Tape tes réponses, la page te dit juste ou faux 0/9

a)
b)
c)
d)
Voir la correction

Réponses

  • a) AA : rapport 11, coût constant. BB : rapport 22, coût linéaire. CC : rapport 44, coût quadratique
  • b) BB : 24,624{,}6 ms puis 98,498{,}4 ms. CC : 62166\,216 ms puis 9945699\,456 ms, soit 99,599{,}5 s
  • c) Le doublement AJOUTE 1,41{,}4 ms : c'est la signature d'un coût logarithmique. À 6400064\,000, 20,420{,}4 ms
  • d) Environ 10111\,011 fois plus lent : une mesure à une seule taille ne dit rien du coût

a) Programme AA : 0,80{,}8 puis 0,80{,}8 puis 0,90{,}9 puis 0,80{,}8, donc un rapport voisin de 11 à chaque doublement. Le temps ne dépend pas de nn : coût CONSTANT, et les petits écarts sont du bruit de mesure. Programme BB : 3,1/1,52,073{,}1/1{,}5 \approx 2{,}07, puis 6,2/3,1=26{,}2/3{,}1 = 2, puis 12,3/6,21,9812{,}3/6{,}2 \approx 1{,}98. Le rapport vaut 22 : coût LINÉAIRE, le temps est proportionnel à nn. Programme CC : 97/244,0497/24 \approx 4{,}04, puis 389/974,01389/97 \approx 4{,}01, puis 1554/3893,991\,554/389 \approx 3{,}99. Le rapport vaut 44 : coût QUADRATIQUE, car doubler nn multiplie n2n^{2} par 44. Le rapport est l'outil, pas la différence : une différence de temps ne distingue pas les trois cas.

b) Pour BB, chaque doublement multiplie par 22 : 12,3×2=24,612{,}3 \times 2 = 24{,}6 ms à 1600016\,000, puis 24,6×2×2=98,424{,}6 \times 2 \times 2 = 98{,}4 ms à 6400064\,000, deux doublements plus loin. Pour CC, chaque doublement multiplie par 44 : 1554×4=62161\,554 \times 4 = 6\,216 ms à 1600016\,000, puis 6216×16=994566\,216 \times 16 = 99\,456 ms à 6400064\,000, soit environ 99,599{,}5 secondes. On passe donc de moins de deux secondes à plus d'une minute et demie pour huit fois plus de données. Le piège est de multiplier par 88 au lieu de 4×44 \times 4 : trois doublements se traduisent par 232^{3} pour un linéaire et par 434^{3} pour un quadratique, jamais par la même valeur.

c) De 12,012{,}0 à 13,413{,}4, puis à 14,814{,}8, puis à 16,216{,}2 : le temps augmente de 1,41{,}4 milliseconde à chaque doublement, et non d'un facteur. Une quantité qui gagne une constante quand nn double est proportionnelle au nombre de doublements, c'est-à-dire à log2n\log_{2} n : c'est la signature du coût LOGARITHMIQUE, celui de la recherche dichotomique. De 80008\,000 à 6400064\,000 il y a trois doublements, donc 16,2+3×1,4=20,416{,}2 + 3 \times 1{,}4 = 20{,}4 ms. La leçon est qu'un coût logarithmique est pratiquement insensible à la taille : multiplier les données par huit coûte ici un quart de milliseconde de plus par doublement.

d) À n=64000n = 64\,000, le rapport vaut 99456/98,4101199\,456/98{,}4 \approx 1\,011. Il était de 1616 à n=1000n = 1\,000 : il a été multiplié par plus de soixante en passant d'une taille à l'autre. Une mesure faite à une seule taille ne dit donc RIEN du coût, elle ne donne qu'un point ; c'est la façon dont le temps ÉVOLUE qui caractérise l'algorithme. C'est aussi pourquoi un programme quadratique peut paraître acceptable pendant toute la phase de mise au point, sur des jeux de test réduits, et devenir inutilisable le jour de la mise en service. Le réflexe attendu en évaluation est de toujours chronométrer au moins trois tailles qui doublent.

1.00A2.00B4.00C1.10D1234rapport d'un temps au précédent

Partie B : problèmes et raisonnement (/50)

Exercice 6 : La recherche dichotomique : trois versions fausses

La recherche dichotomique coupe en deux à chaque tour l'intervalle des indices où la valeur peut encore se trouver. Elle exige un tableau TRIÉ, et trois détails d'écriture décident de sa justesse.

On travaille sur t=[3,11,18,24,31,42,55,67,70,88]t = [3, 11, 18, 24, 31, 42, 55, 67, 70, 88], qui compte dix cases.

30111182243314425556677708889gmdpremier tour : g = 0, d = 9, m = 4
Python
def dicho(t, x):
    g = 0
    d = len(t) - 1
    while g <= d:
        m = (g + d) // 2
        if t[m] == x:
            return m
        elif t[m] < x:
            g = m + 1
        else:
            d = m - 1
    return -1
  • a) Déroulez dicho(t,67)dicho(t, 67) puis dicho(t,20)dicho(t, 20) : donnez à chaque tour les valeurs de gg, dd et mm, le nombre de tours et la valeur renvoyée. Combien de tours au maximum pour dix cases, et pour 10001\,000 cases ?
  • b) Première version fausse : while g<dwhile\ g < d. Que renvoie-t-elle pour x=3x = 3 ? Décrivez la situation qu'elle manque toujours.
  • c) Deuxième version fausse : g=mg = m au lieu de g=m+1g = m + 1. Déroulez la recherche de 8888 et dites laquelle des trois questions du chapitre n'a plus de réponse.
  • d) Troisième version fausse : on applique la fonction d'origine à u=[31,3,55,18,70,11]u = [31, 3, 55, 18, 70, 11], qui n'est pas trié. Que renvoie dicho(u,55)dicho(u, 55) ? Et dicho(u,18)dicho(u, 18) ? Qu'est-ce qui rend ce cas plus dangereux que les deux précédents ?

Tape tes réponses, la page te dit juste ou faux 0/9

a)
b)
c)
d)
Voir la correction

Réponses

  • a) 6767 : deux tours, renvoie 77. 2020 : quatre tours, renvoie 1-1. Au maximum 44 tours pour dix cases, 1010 pour mille
  • b) Elle renvoie 1-1 : elle manque toujours la valeur d'un intervalle réduit à une seule case
  • c) gg se bloque à 88 et mm aussi : boucle infinie, la TERMINAISON n'a plus de réponse
  • d) dicho(u,55)dicho(u, 55) renvoie 22 par chance, dicho(u,18)dicho(u, 18) renvoie 1-1 alors que 1818 est en case 33. Aucune erreur n'est signalée

a) Recherche de 6767 : premier tour, g=0g = 0, d=9d = 9, m=4m = 4, et t[4]=31<67t[4] = 31 < 67, donc g=5g = 5 ; deuxième tour, g=5g = 5, d=9d = 9, m=7m = 7, et t[7]=67t[7] = 67, on renvoie 77. Deux tours. Recherche de 2020 : m=4m = 4 et 31>2031 > 20 donne d=3d = 3 ; puis g=0g = 0, d=3d = 3, m=1m = 1 et 11<2011 < 20 donne g=2g = 2 ; puis m=2m = 2 et 18<2018 < 20 donne g=3g = 3 ; puis g=d=3g = d = 3, m=3m = 3 et 24>2024 > 20 donne d=2d = 2 ; la condition gdg \le d est alors fausse et l'on renvoie 1-1. Quatre tours. Chaque tour divise par deux le nombre de cases encore possibles, donc le nombre maximal de tours est le nombre de fois où l'on peut diviser nn par deux, plus un : pour n=10n = 10 cela fait 44, et pour n=1000n = 1\,000 cela fait 1010, puisque 29=512<10001024=2102^{9} = 512 < 1\,000 \le 1\,024 = 2^{10}. Dix comparaisons pour mille cases, contre mille pour un parcours séquentiel.

b) Avec while g<dwhile\ g < d, la boucle s'arrête dès que gg et dd deviennent égaux, c'est-à-dire quand il ne reste plus qu'une seule case à examiner, et cette case n'est jamais testée. Recherche de 33 : m=4m = 4 et 31>331 > 3 donne d=3d = 3 ; puis g=0g = 0, d=3d = 3, m=1m = 1 et 11>311 > 3 donne d=0d = 0 ; alors g=d=0g = d = 0, la condition est fausse, et la fonction renvoie 1-1 alors que 33 occupe la case 00. La version manque donc toujours une valeur qui se trouve seule dans le dernier intervalle, ce qui arrive très souvent. L'erreur est particulièrement vicieuse parce que la fonction reste juste pour beaucoup de valeurs, celles trouvées avant le rétrécissement final : un test sur deux ou trois exemples bien choisis ne la révèle pas.

c) Avec g=mg = m, l'intervalle ne rétrécit plus toujours. Recherche de 8888 : m=4m = 4 et 31<8831 < 88 donne g=4g = 4 ; m=(4+9)//2=6m = (4+9)//2 = 6 et 55<8855 < 88 donne g=6g = 6 ; m=7m = 7 et 67<8867 < 88 donne g=7g = 7 ; m=8m = 8 et 70<8870 < 88 donne g=8g = 8 ; m=(8+9)//2=8m = (8+9)//2 = 8 à nouveau, et gg reste 88. À partir de là, gg, dd et mm ne bougent plus et la boucle tourne indéfiniment. La question sans réponse est celle de la TERMINAISON : le variant dgd - g ne décroît plus, parce que la division entière fait retomber mm sur gg dès que d=g+1d = g + 1. Le +1+1 n'est donc pas un ajustement cosmétique : il exclut la case déjà examinée et garantit la décroissance stricte. Noter que cette version donne le bon résultat pour 3131 et pour 6767 : elle passe les premiers tests et se bloque sur le dernier élément.

d) dicho(u,55)dicho(u, 55) renvoie 22, parce que u[2]u[2] vaut justement 5555 et que la toute première comparaison tombe dessus : la réponse est juste par hasard, pas par raisonnement. dicho(u,18)dicho(u, 18), lui, compare 5555 à 1818, conclut qu'il faut chercher à gauche, ramène dd à 11, compare u[0]=31u[0] = 31 à 1818, ramène dd à 1-1 et renvoie 1-1, alors que 1818 occupe la case 33. Ce cas est plus dangereux que les deux précédents parce que le code est CORRECT : c'est sa PRÉCONDITION qui est violée, et rien dans le langage ne la vérifie. Il n'y a ni exception, ni boucle infinie, ni message : seulement une réponse fausse, rendue vite et avec aplomb. C'est la raison d'être de la spécification, et l'exercice 1010 chiffre ce que coûte de la vérifier.

Exercice 7 : Les algorithmes gloutons : rapides, et parfois à côté

Un algorithme glouton prend, à chaque étape, le meilleur choix immédiat, et ne revient jamais en arrière. Il est simple et rapide. Il n'est pas toujours optimal, et savoir QUAND il échoue fait partie du chapitre.

Le glouton du rendu de monnaie parcourt les valeurs de la plus grande à la plus petite et prend chaque valeur tant qu'elle tient dans ce qui reste à rendre.

34f121f255f313f48f540f6255075100capacité du disque : 100 Mo
Python
def rendu(somme, valeurs):
    pris = []
    reste = somme
    for v in valeurs:
        while v <= reste:
            pris.append(v)
            reste = reste - v
    return pris
  • a) Un jeu de société utilise des jetons de 1010, 77 et 11 point. Déroulez rendu(14,[10,7,1])rendu(14, [10, 7, 1]) : combien de jetons le glouton donne-t-il ? Trouvez la meilleure solution possible et concluez.
  • b) On passe la liste dans l'ordre croissant, [1,7,10][1, 7, 10]. Combien de jetons pour 1414 ? Pourquoi le tri décroissant est-il indispensable, et suffit-il à rendre le glouton optimal ?
  • c) Avec le système [50,20,10,5,2,1][50, 20, 10, 5, 2, 1], déroulez le rendu de 8787. Le glouton est ici optimal. Qu'est-ce qui distingue ce système du précédent ?
  • d) Un disque de 100100 mégaoctets doit recevoir des fichiers de 3434, 2121, 5555, 1313, 88 et 4040 mégaoctets. Appliquez le glouton « le plus petit d'abord », puis le glouton « le plus grand d'abord ». Le meilleur remplissage possible vaut 9797 mégaoctets. Que conclure ?

Tape tes réponses, la page te dit juste ou faux 0/9

a)
b)
c)
d)
Voir la correction

Réponses

  • a) Le glouton donne 55 jetons, 10+1+1+1+110+1+1+1+1 ; la meilleure solution en donne 22, 7+77+7. Le glouton n'est pas optimal
  • b) 1414 jetons. Le tri décroissant est nécessaire mais pas suffisant
  • c) 55 pièces, 50+20+10+5+250+20+10+5+2. Ce système est canonique : chaque valeur vaut au moins le double de la précédente
  • d) Le plus petit d'abord : 44 fichiers, 7676 Mo. Le plus grand d'abord : 22 fichiers, 9595 Mo. Chacun est optimal pour SON critère, et aucun n'atteint 9797

a) Le glouton prend d'abord 1010, il reste 44 ; il essaie 77, qui ne tient pas ; il prend quatre fois 11. Total : 10+1+1+1+110 + 1 + 1 + 1 + 1, soit CINQ jetons. La meilleure solution est 7+77 + 7, soit DEUX jetons. Le glouton donne donc une solution valide, la somme est exacte, mais pas la meilleure : en prenant le plus gros jeton il s'est interdit la combinaison qui tombait juste. C'est le contre-exemple à connaître, et il suffit d'un seul pour réfuter « le glouton est toujours optimal ». Remarquer que l'algorithme ne se trompe pas au sens où il rendrait une mauvaise somme : il rend 1414, il le rend mal.

b) Dans l'ordre croissant, la boucle whilewhile épuise d'abord les jetons de 11 : elle en prend quatorze, le reste tombe à 00, et les valeurs 77 et 1010 ne servent jamais. QUATORZE jetons. Le tri décroissant est donc indispensable au principe même du glouton, qui est de prendre le plus gros choix disponible : sans lui, l'algorithme n'est plus glouton, il est simplement mauvais. Mais il ne suffit pas, et la question a le prouve : la liste [10,7,1][10, 7, 1] était bien triée, et le résultat n'était pas optimal. Ordre décroissant et optimalité sont deux choses différentes, et les confondre est l'erreur la plus fréquente sur ce chapitre.

c) Le glouton prend 5050, il reste 3737 ; puis 2020, il reste 1717 ; puis 1010, il reste 77 ; puis 55, il reste 22 ; puis 22, il reste 00. Cinq pièces : 50+20+10+5+2=8750 + 20 + 10 + 5 + 2 = 87. Ici aucune combinaison ne fait mieux. Ce qui distingue ce système, c'est que chaque valeur vaut au moins le double de la précédente, si bien qu'on ne peut jamais remplacer une grosse pièce par un petit nombre de plus petites : on dit que le système est canonique. Le système [10,7,1][10, 7, 1] ne l'est pas, puisque deux jetons de 77 dépassent un jeton de 1010. Retenir la formulation honnête : le glouton du rendu de monnaie est optimal SUR CERTAINS systèmes, et l'énoncé doit dire lequel avant qu'on affirme quoi que ce soit.

d) Le plus petit d'abord : 88, puis 1313, puis 2121, puis 3434, ce qui fait 7676 mégaoctets ; le suivant, 4040, porterait le total à 116116 et ne tient pas. Résultat : QUATRE fichiers, 7676 mégaoctets. Le plus grand d'abord : 5555, puis 4040, ce qui fait 9595 ; aucun des quatre restants ne tient dans les cinq mégaoctets libres. Résultat : DEUX fichiers, 9595 mégaoctets. Chacun est optimal pour le critère qui l'a inspiré, le premier pour le NOMBRE de fichiers, le second pour l'ESPACE occupé, et aucun des deux n'atteint le meilleur remplissage, 9797 mégaoctets, que donne la combinaison 55+21+13+855 + 21 + 13 + 8. La conclusion du chapitre tient en deux phrases. Un glouton se juge toujours par rapport à un critère annoncé, jamais dans l'absolu. Et on l'utilise malgré ses échecs parce qu'il coûte un seul parcours, là où examiner toutes les combinaisons coûterait 26=642^{6} = 64 essais ici, et 2602^{60}, soit plus d'un milliard de milliards, pour soixante fichiers.

Exercice 8 : Cinq affirmations à corriger

Chacune des cinq affirmations ci-dessous est FAUSSE. Pour chacune, donnez un contre-exemple ou l'argument qui la réfute, puis écrivez l'énoncé correct.

Ce sont les cinq raccourcis les plus fréquents sur ce chapitre en évaluation.

  • a) « Un algorithme qui donne le bon résultat sur trois exemples est correct. »
  • b) « Si l'invariant d'une boucle est vrai à la sortie, l'algorithme est correct. »
  • c) « Un algorithme de coût quadratique est toujours plus lent qu'un algorithme de coût linéaire. »
  • d) « La recherche dichotomique fonctionne sur n'importe quel tableau, elle est juste plus efficace s'il est trié. »
  • e) « Un algorithme glouton donne toujours la solution optimale, sinon personne ne l'utiliserait. »
Voir la correction

a) Faux : des exemples ne prouvent rien, ils ne peuvent que réfuter. Contre-exemple pris à l'exercice 66 : la version qui écrit g=mg = m au lieu de g=m+1g = m + 1 renvoie correctement 44 pour la valeur 3131 et 77 pour la valeur 6767, et boucle indéfiniment sur 8888. Trois essais bien choisis l'auraient validée. L'énoncé correct : un algorithme se prouve correct par un invariant de boucle et une terminaison ; les tests servent à trouver des erreurs, jamais à démontrer leur absence.

b) Faux, pour deux raisons. D'abord l'invariant doit DIRE le résultat voulu : l'exercice 33 montre un invariant parfaitement vrai, « les ii premières cases sont triées entre elles », qui reste vrai pour [1,2,3,4,5,0][1, 2, 3, 4, 5, 0], lequel n'est pas trié. Ensuite un invariant ne dit rien de l'arrêt : une boucle infinie n'atteint jamais sa sortie, et son invariant reste vrai à chaque tour. L'énoncé correct : la correction demande trois points sur l'invariant, initialisation, conservation et conclusion à la sortie, PLUS une preuve de terminaison par un variant.

c) Faux : le coût décrit la façon dont le temps ÉVOLUE avec la taille, pas sa valeur. Un algorithme quadratique dont le temps vaut 0,001n20{,}001n^{2} millisecondes et un algorithme linéaire dont le temps vaut 10n10n millisecondes se croisent à n=10000n = 10\,000 ; en dessous, le quadratique est le plus rapide, et pour n=1000n = 1\,000 il met 11 seconde contre 1010. L'énoncé correct : un algorithme quadratique finit toujours par être plus lent, mais seulement à partir d'une certaine taille, que les constantes déterminent.

d) Faux, et c'est la plus dangereuse des cinq. Sur un tableau non trié, la dichotomie ne perd pas en efficacité, elle rend des réponses FAUSSES : sur [31,3,55,18,70,11][31, 3, 55, 18, 70, 11], elle affirme que 1818 est absent alors qu'il occupe la case 33. Aucune erreur n'est levée. L'énoncé correct : le tri est une PRÉCONDITION de la dichotomie, c'est-à-dire une condition sans laquelle le résultat n'est pas défini, et c'est au programme appelant de la garantir.

e) Faux sur les deux moitiés de la phrase. Le rendu de 1414 avec des jetons de 1010, 77 et 11 donne cinq jetons quand deux suffisent : le glouton n'est pas optimal. Et la raison de l'utiliser n'est pas l'optimalité, c'est le COÛT : un seul parcours des valeurs, là où examiner toutes les combinaisons de soixante objets demanderait 2602^{60} essais, soit plus d'un milliard de milliards. L'énoncé correct : un glouton donne rapidement une bonne solution, optimale seulement dans certains cas qu'il faut savoir reconnaître et énoncer.

Exercice 9 : Problème : le correcteur automatique d'un questionnaire

Un professeur fait passer un questionnaire de cinq questions à dix élèves, et écrit un programme qui corrige les copies. Les notes obtenues, sur 2020, sont [14,8,17,11,8,20,13,9,15,12][14, 8, 17, 11, 8, 20, 13, 9, 15, 12].

La figure donne, pour chaque question, le nombre de copies qui y ont répondu juste.

9Q14Q210Q37Q42Q5246810copies ayant répondu juste, sur 10
Python
notes = [14, 8, 17, 11, 8, 20, 13, 9, 15, 12]

def moyenne(notes):
    s = 0
    for n in notes:
        s = s + n
    return s / 10
  • a) Calculez la moyenne de la classe. Quel est le coût de ce calcul en fonction du nombre de copies ?
  • b) Le professeur veut aussi la MÉDIANE. Expliquez pourquoi il faut trier, donnez la médiane, et donnez le nombre de comparaisons que coûte au pire un tri par insertion de ces dix notes.
  • c) La grille complète des réponses compte une ligne par copie et une colonne par question. Donnez le numéro de la question la plus ratée, puis le nombre de comparaisons d'un parcours complet de la grille, ici et pour 3030 copies et 2020 questions.
  • d) Une copie est absente du fichier : la liste ne contient plus que neuf notes, celle de 2020 ayant disparu. Le programme ci-dessus affiche une moyenne sans lever la moindre erreur. Donnez la valeur affichée et la valeur juste, puis corrigez la fonction en une modification.

Tape tes réponses, la page te dit juste ou faux 0/9

a)
b)
c)
d)
Voir la correction

Réponses

  • a) Moyenne 12,712{,}7 ; coût linéaire
  • b) La médiane demande les valeurs rangées ; elle vaut 12,512{,}5, et le tri coûte au pire 4545 comparaisons
  • c) Question 55, avec 22 bonnes réponses ; 5050 comparaisons ici et 600600 pour 3030 copies et 2020 questions
  • d) Le programme affiche 10,710{,}7 au lieu de 11,8911{,}89 ; il faut diviser par len(notes)len(notes)

a) La somme vaut 14+8+17+11+8+20+13+9+15+12=12714+8+17+11+8+20+13+9+15+12 = 127, donc la moyenne vaut 127/10=12,7127/10 = 12{,}7. Le calcul fait un tour de boucle par copie et une addition par tour : son coût est LINÉAIRE en le nombre de copies. Doubler l'effectif double le temps, ce qui est le comportement du schéma d'accumulation vu à l'exercice 11. Contrôle : la moyenne doit tomber entre la plus petite note, 88, et la plus grande, 2020.

b) La médiane est la valeur qui partage l'effectif en deux moitiés de même taille : la définir suppose donc de connaître les notes RANGÉES, ce que la liste d'origine ne donne pas. Une fois triées, les notes sont 88, 88, 99, 1111, 1212, 1313, 1414, 1515, 1717, 2020 ; l'effectif étant pair, la médiane est la moyenne des deux valeurs centrales, la cinquième et la sixième, soit (12+13)/2=12,5(12 + 13)/2 = 12{,}5. Au pire, le tri par insertion de dix valeurs coûte 10×9/2=4510 \times 9 / 2 = 45 comparaisons, c'est-à-dire le cas d'un tableau rangé à l'envers. On note au passage que la médiane, 12,512{,}5, est inférieure à la moyenne, 12,712{,}7 : les deux notes très hautes tirent la moyenne vers le haut sans déplacer la médiane, ce qui est tout l'intérêt de calculer les deux.

c) La figure donne 99, 44, 1010, 77 et 22 bonnes réponses. La question la plus ratée est la cinquième, avec deux réussites seulement sur dix ; la troisième, réussie par tout le monde, n'a rien discriminé. Compter les bonnes réponses demande de parcourir la grille entière et de comparer chaque case à la réponse attendue, soit 10×5=5010 \times 5 = 50 comparaisons. Pour 3030 copies et 2020 questions, cela fait 30×20=60030 \times 20 = 600. Le coût est le PRODUIT des deux dimensions : il est linéaire en le nombre de cases, et c'est cette lecture qu'il faut donner, plutôt que de parler de coût quadratique, qui supposerait que les deux dimensions soient la même grandeur.

d) Avec neuf notes, la somme vaut 12720=107127 - 20 = 107, et le programme la divise par la constante 1010 : il affiche 10,710{,}7. La valeur juste est 107/911,89107/9 \approx 11{,}89. L'écart est de plus d'un point, et rien ne le signale : la constante 1010 était vraie le jour où le programme a été écrit, elle ne l'est plus. La correction tient en un mot : écrire return s/len(notes)return\ s / len(notes). C'est la règle générale du chapitre, une dimension de données ne s'écrit jamais en dur. Il reste à traiter le cas de la liste vide, où len(notes)len(notes) vaut 00 : la division échoue, et c'est une bonne nouvelle, puisqu'une erreur franche vaut mieux qu'un nombre faux. On le dit dans la spécification sous la forme d'une précondition, la liste doit être non vide.

Exercice 10 : Problème : la précondition que personne ne vérifie

Un atelier charge chaque matin, depuis un fichier, un catalogue de 4000040\,000 références, censé être rangé par numéro croissant. Le logiciel y fait 30003\,000 recherches par jour, par dichotomie.

Un matin, le fichier a été modifié à la main et une référence s'est retrouvée hors d'ordre. Aucune erreur n'apparaît, et le magasinier signale des pièces « introuvables » qui sont pourtant en stock.

1040117112821313129414651526hors d'ordreun extrait du catalogue chargé
Python
def est_trie(t):
    for i in range(len(t) - 1):
        if t[i] > t[i + 1]:
            return False
    return True
  • a) Donnez l'invariant de cette boucle. Combien de comparaisons coûte-t-elle au pire pour 4000040\,000 références, et de quel type de coût s'agit-il ?
  • b) Trois stratégies sont possibles : ne rien vérifier, vérifier une fois au chargement, vérifier avant chaque recherche. Sachant qu'une dichotomie sur 4000040\,000 cases coûte au pire 1616 comparaisons, chiffrez le coût quotidien des trois, puis dites combien de fois la troisième est plus chère que la deuxième.
  • c) Si la vérification échoue, faut-il trier le catalogue avec un tri par insertion ? Chiffrez son coût au pire et comparez au coût d'une journée de recherches séquentielles.
  • d) Écrivez la spécification de la fonction de recherche, avec sa précondition, et dites où placer l'instruction assertassert qui la contrôle. Une assertion coûte-t-elle quelque chose ?

Tape tes réponses, la page te dit juste ou faux 0/8

a)
b)
c)
d)
Voir la correction

Réponses

  • a) Invariant : avant le tour ii, aucune des ii premières paires de cases voisines n'est en désordre. Au pire 3999939\,999 comparaisons, coût linéaire
  • b) Rien : 4800048\,000. Une fois au chargement : 8799987\,999. Avant chaque recherche : 119997000119\,997\,000, soit environ 13641\,364 fois la deuxième
  • c) Le tri par insertion coûte au pire 799980000799\,980\,000 comparaisons, bien plus qu'une journée de recherches séquentielles, 120000000120\,000\,000
  • d) Précondition : le tableau est trié par ordre croissant. L'assertion se place une seule fois, au chargement

a) L'invariant : « avant le tour d'indice ii, aucune des paires de cases voisines d'indices inférieurs à ii n'est en désordre ». Il est vrai au départ, où aucune paire n'a été examinée ; chaque tour le préserve, puisqu'il examine exactement la paire suivante et interrompt tout si elle est en désordre ; et à la sortie normale, ii a parcouru toutes les paires, donc le tableau est trié. La boucle compare des cases VOISINES, il y en a n1n - 1, soit 3999939\,999 comparaisons au pire, atteint quand le tableau est effectivement trié et qu'il faut aller jusqu'au bout. Le coût est LINÉAIRE. Au mieux il vaut 11, quand les deux premières cases sont déjà en désordre.

b) Ne rien vérifier : 3000×16=480003\,000 \times 16 = 48\,000 comparaisons par jour, et le risque de réponses fausses. Vérifier une fois au chargement : 39999+48000=8799939\,999 + 48\,000 = 87\,999 comparaisons, soit moins du double, pour une garantie complète sur la journée entière. Vérifier avant chaque recherche : 3000×39999=1199970003\,000 \times 39\,999 = 119\,997\,000 comparaisons, c'est-à-dire environ 13641\,364 fois le coût de la deuxième stratégie. La leçon est nette : une vérification linéaire placée au bon endroit est presque gratuite, la même placée dans la boucle de travail détruit tout le bénéfice de la dichotomie. Le bon endroit est celui où la donnée CHANGE, donc le chargement, et non celui où on la consulte.

c) Le tri par insertion coûte au pire n(n1)/2n(n-1)/2 comparaisons, soit 40000×39999/2=79998000040\,000 \times 39\,999 / 2 = 799\,980\,000. C'est environ 6,76{,}7 fois plus cher que de renoncer à la dichotomie pour la journée et de faire 30003\,000 parcours séquentiels, dont le coût au pire est 3000×40000=1200000003\,000 \times 40\,000 = 120\,000\,000 comparaisons. Trier n'est donc pas la bonne réponse immédiate, d'autant que trier des données fausses ne les rend pas justes : si une référence a été saisie de travers, elle sera rangée au mauvais endroit. La réponse raisonnable est de REFUSER de démarrer et de signaler la ligne fautive, que la fonction peut renvoyer au lieu d'un simple FalseFalse : le coût du diagnostic est nul, et le magasinier sait quoi corriger.

d) La spécification : la fonction prend un tableau d'entiers et une valeur, elle renvoie l'indice d'une case contenant cette valeur, ou 1-1 si elle n'y figure pas ; PRÉCONDITION, le tableau est rangé par ordre croissant. Sans cette dernière ligne, la spécification est fausse, car elle promet un résultat que la fonction ne tient pas. L'assertion assert est_trie(t)assert\ est\_trie(t) se place UNE SEULE FOIS, juste après le chargement du fichier, et jamais à l'intérieur de la fonction de recherche : la question b vient de montrer ce que coûterait ce second choix. Une assertion coûte donc exactement ce que coûte le test qu'elle contient, ici un parcours linéaire une fois par jour, et rien du tout le reste du temps. Elle vaut mieux qu'un commentaire pour une raison simple : un commentaire décrit une intention que rien ne vérifie, une assertion arrête le programme au moment précis où l'intention cesse d'être vraie, avant que la réponse fausse ne soit lue par quelqu'un.

Chapitre précédent Représentation des données : réels et texte Chapitre suivant Traitement de données en tables et types construits

Ce chapitre resservira dans

Les chapitres qui le réclament en amont, plus tard dans l'année ou dans les années suivantes.

Voir aussi

Vous cherchez un tuteur en NSI à Montréal ?

Contactez-moi pour une première séance. Bachelier en informatique de McGill et maîtrise en informatique appliquée de Concordia, je travaille l'algorithmique sur la preuve et le coût, pas seulement sur le code qui tourne.

Site par Studio Squalli