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éeTape tes réponses et vérifie-les question par 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, et tes réponses justes restent remplies quand tu reviens.
•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 n cases, au plus ⌊log2n⌋+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 i tours, les i premières cases contiennent les i 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+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 n 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 t 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.
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), puis le nombre moyen de connexions par heure.
b) Donnez la valeur de combien_au_dessus(t,13).
c) present(t,7) renvoie False, alors que 7 figure deux fois dans t. Expliquez précisément pourquoi, dites pour quelles valeurs de x la fonction répond juste, puis corrigez-la en déplaçant une seule ligne.
d) Écrivez indice(t,x), qui renvoie l'indice de la PREMIÈRE case contenant x, et −1 si x est absent. Donnez indice(t,7), indice(t,25) et indice(t,5).
Voir la correction
Réponses
a)total(t)=112, soit 14 connexions par heure en moyenne
b)4
c)La fonction ne regarde que t[0] : elle est juste pour x=12 et pour toute valeur absente, fausse pour les cinq autres valeurs présentes
d)1, 2 et −1
a)
La somme vaut 12+7+25+7+19+3+25+14=112, et le tableau compte 8 cases, donc la moyenne vaut 112/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 0 pour une addition, mise à jour à chaque case. Vérification rapide : la plus petite valeur vaut 3 et la plus grande 25, 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 à 13 sont 25, 19, 25 et 14, donc la fonction renvoie 4. C'est le schéma du COMPTEUR, un accumulateur dont la mise à jour est conditionnelle : on part de 0 et on ajoute 1 quand le test réussit. Le piège habituel est d'écrire c=1 à l'initialisation, ce qui compte une case qui n'existe pas, ou d'écrire c=c+v au lieu de c=c+1, ce qui répond à une autre question. La valeur 14 compte, car le test est strict dans le sens attendu ; si l'énoncé avait dit « au moins 13 », il aurait fallu >=.
c)
Le else est DANS la boucle. Au tout premier tour, v vaut 12 ; le test v==7 échoue, on passe au else, et le returnFalse termine la fonction avant même d'avoir regardé la deuxième case. La fonction ne décide donc jamais que sur t[0]. Elle répond juste par accident dans deux cas : quand x vaut 12, où elle renvoie True à raison, et quand x est absent du tableau, où False est la bonne réponse. Elle se trompe sur les cinq autres valeurs présentes, 7, 25, 19, 3 et 14. La correction tient en un décalage : le returnFalse doit sortir de la boucle et se placer APRÈS elle, sans else. 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 : foriinrange(len(t)), et si t[i]==x alors returni. Après la boucle, et seulement après, return−1. La valeur 7 apparaît aux indices 1 et 3 ; le premier return rencontré est celui de l'indice 1, donc la fonction renvoie 1. La valeur 25 apparaît aux indices 2 et 6, donc elle renvoie 2. La valeur 5 est absente, donc −1. Le choix de −1 comme code d'absence tient à ce qu'aucun indice valide n'est négatif : rendre 0 serait ambigu, puisque 0 est un indice parfaitement légitime.
Coche ici les exercices faits ou à revoir : un compte gratuit, sans mot de passe, retient tes coches et tes réponses justes d'une visite à l'autre, te dit quel chapitre attaquer ensuite et te permet de demander l'exercice qui te manque. Crée ton espace, un courriel suffit.
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] et seuil=8.
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), puis écrivez l'invariant de la boucle, en une phrase qui relie c et i.
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=1 à l'initialisation. Le deuxième place la ligne i=i+1 À L'INTÉRIEUR du if. Le troisième écrit whilei<=len(t). Pour chacun, dites lequel des trois points tombe, ou si c'est la terminaison, et ce que le programme fait réellement.
Voir la correction
Réponses
a)compte(t,8)=4 ; invariant : c est le nombre d'indices j strictement inférieurs à i tels que t[j]>seuil
b)Vrai à l'entrée avec c=0 et i=0, préservé par un tour, et à la sortie i=len(t) donne le compte total
c)Variant len(t)−i, il décroît de 1 à chaque tour et vaut 0 à la sortie
d)Premier : l'initialisation, il renvoie 5. Deuxième : la terminaison, boucle infinie. Troisième : l'accès sort du tableau, erreur d'indice
a)
Les valeurs strictement supérieures à 8 sont 11, 15, 9 et 11, donc la fonction renvoie 4. L'invariant s'écrit : « avant chaque tour, c est égal au nombre d'indices j vérifiant j<i et t[j]>seuil, et i reste compris entre 0 et 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] 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, i vaut 0 et c vaut 0 ; il n'existe aucun indice j<0, donc le compte attendu est bien 0. L'invariant est vrai. CONSERVATION : supposons-le vrai avec la valeur i. Le tour examine exactement la case t[i] et ajoute 1 à c si et seulement si t[i]>seuil, puis fait passer i à i+1. Le compte des indices j<i+1 est donc le compte des indices j<i augmenté de ce même 1 conditionnel : l'invariant est encore vrai. SORTIE : la boucle s'arrête quand i n'est plus strictement inférieur à len(t), donc quand i=len(t) ; l'invariant dit alors que c 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)−i. C'est un entier, il est positif ou nul tant que la boucle tourne puisque i<len(t), et chaque tour fait croître i de 1, donc il décroît strictement de 1. Un entier positif qui décroît strictement ne peut pas décroître indéfiniment : la boucle s'arrête, après exactement 7 tours ici. À la sortie il vaut 0. 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, c vaut 1 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 5 au lieu de 4. 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+1 dans le if, le compteur d'indice n'avance que lorsque la condition est vraie ; ici t[0]=4 n'est pas supérieur à 8, donc i reste à 0 et la boucle tourne indéfiniment. Le variant len(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 i≤len(t) au moment de l'accès, mais la condition i<=len(t) autorise un tour de trop ; à i=len(t)=7, l'accès 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], donc n=6. La figure montre l'état du tableau APRÈS le premier tour.
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(n−1) et non range(n) ? Le résultat changerait-il ?
c) Un camarade écrit m=0 au lieu de m=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 i tours, les i 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=5 sans être trié.
Voir la correction
Réponses
a)[4,8,41,15,23,16], puis inchangé, puis [4,8,15,41,23,16] ; final [4,8,15,16,23,41]
b)Après i tours, les i premières cases contiennent les i 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 4 en case 1 : la partie « elles ne bougeront plus » tombe
d)Vrai, mais il ne conclut pas ; par exemple [1,2,3,4,5,0]
a)
Premier tour, i=0 : le minimum de tout le tableau est 4, en case 4 ; on l'échange avec la case 0, ce qui donne [4,8,41,15,23,16]. Deuxième tour, i=1 : le minimum du reste est 8, déjà en case 1 ; l'échange se fait avec elle-même et le tableau ne bouge pas. Troisième tour, i=2 : le minimum du reste est 15, en case 3 ; on obtient [4,8,15,41,23,16]. Les deux derniers tours amènent 16 puis laissent 23 en place, et le tableau final est [4,8,15,16,23,41]. Vérification : la somme des six valeurs vaut 107 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 i tours, les i premières cases contiennent les i plus petits éléments du tableau, rangés en ordre croissant, et elles ne seront plus modifiées ». La boucle s'arrête à n−1 parce qu'après n−1 tours l'invariant dit déjà que les n−1 premières cases portent les n−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) 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=0, la recherche du minimum repart toujours de la case 0, qui est déjà définitive. Au deuxième tour, i vaut 1 et m vaut 0 ; la boucle interne compare t[j] à t[0]=4 pour j allant de 2 à 5, et aucune de ces valeurs n'est inférieure à 4, donc m reste 0. L'échange final porte alors sur les cases 1 et 0 et produit [8,4,41,15,23,16] : la case 0, 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 i premières cases contiennent les i 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=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] le vérifie et n'est pourtant pas trié. Tout l'intérêt de l'invariant fort tient dans les trois mots « les i 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] avant la boucle, et le t[j+1]=v après. On travaille sur t=[9,4,7,2,6].
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 for, puis le nombre de fois où le test t[j]>v est évalué à chaque tour et au total.
b) Pourquoi la dernière ligne s'écrit-elle t[j+1]=v et non t[j]=v ? Donnez le tableau obtenu au premier tour par la version fausse.
c) Donnez un variant de la boucle while et prouvez sa terminaison. Que vaut-il au minimum ?
d) Un camarade supprime la ligne v=t[i] et remplace v par t[i] partout ailleurs. Déroulez le premier tour et expliquez ce qui est perdu.
Voir la correction
Réponses
a)[4,9,7,2,6], [4,7,9,2,6], [2,4,7,9,6], [2,4,6,7,9] ; 1, 2, 3 et 3 évaluations, soit 9 au total
b)La boucle décrémente j une fois de trop, donc la case libre est j+1. La version fausse donne [9,9,7,2,4]
c)Variant j+1, entier positif ou nul qui décroît de 1 par tour, minimum 0
d)La première écriture écrase t[i] : la valeur 4 est perdue et le tableau devient [9,9,7,2,6]
a)
Tour i=1 : v=4, on compare t[0]=9 à 4, le décalage a lieu, j passe à −1 et la boucle s'arrête sur la première condition ; on pose 4 en case 0, d'où [4,9,7,2,6], avec 1 évaluation du test. Tour i=2 : v=7, on décale 9, puis t[0]=4 n'est pas supérieur à 7 et l'on s'arrête ; [4,7,9,2,6], 2 évaluations. Tour i=3 : v=2, les trois valeurs 9, 7 et 4 sont décalées et j tombe à −1 ; [2,4,7,9,6], 3 évaluations. Tour i=4 : v=6, on décale 9 puis 7, et 4 arrête la boucle ; [2,4,6,7,9], 3 évaluations. Total : 1+2+3+3=9. Contrôle : la somme vaut 28 avant et après.
b)
La boucle while ne s'arrête qu'APRÈS avoir décrémenté j une fois de trop : soit parce que j est devenu −1, soit parce que t[j] n'est plus supérieur à v et que la case j doit rester où elle est. Dans les deux cas, la case libre est celle d'indice j+1, la dernière d'où l'on a décalé une valeur. Écrire t[j]=v pose la valeur une case trop à gauche, et écrase une valeur qui, elle, était bien placée. Au premier tour, v vaut 4, le décalage donne [9,9,7,2,6] et j vaut −1 ; l'affectation t[−1]=4 ne lève aucune erreur en Python, puisque l'indice −1 désigne la DERNIÈRE case : le tableau devient [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+1. C'est un entier, il vaut au moins 0 tant que la boucle tourne puisque la condition impose j≥0, et chaque tour fait décroître j de 1, 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 0, c'est-à-dire j=−1, c'est-à-dire quand la valeur mise de côté est plus petite que toutes les précédentes. On choisit j+1 plutôt que j 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] elle-même. Or la toute première itération du while écrit t[j+1]=t[j] avec j+1=i : elle écrase précisément cette case. Au premier tour, i vaut 1 et t[1]=4 ; le décalage écrit t[1]=t[0]=9, donc t[i] vaut maintenant 9. La condition t[j]>t[i] devient 9>9, fausse, et la boucle s'arrête ; l'affectation finale t[j+1]=t[i] recopie 9 sur lui-même. Le tableau devient [9,9,7,2,6] et la valeur 4 a disparu. La ligne 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 A, B, C et D ont été chronométrés sur les mêmes données, pour quatre tailles qui doublent à chaque ligne. Les temps sont en millisecondes.
a) Pour A, B et C, 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 B et celui de C pour n=16000, puis pour n=64000. Donnez le temps de C à 64000 en secondes, au dixième.
c) Le programme D ne rentre dans aucune des trois catégories : son temps AUGMENTE, mais pas proportionnellement. Que fait le doublement de n à son temps, et que vaut son temps pour n=64000 ?
d) Pour n=1000, C n'est que 16 fois plus lent que B. Calculez le rapport pour n=64000. Que faut-il en conclure sur une mesure faite à une seule taille ?
Voir la correction
Réponses
a)A : rapport 1, coût constant. B : rapport 2, coût linéaire. C : rapport 4, coût quadratique
b)B : 24,6 ms puis 98,4 ms. C : 6216 ms puis 99456 ms, soit 99,5 s
c)Le doublement AJOUTE 1,4 ms : c'est la signature d'un coût logarithmique. À 64000, 20,4 ms
d)Environ 1011 fois plus lent : une mesure à une seule taille ne dit rien du coût
a)
Programme A : 0,8 puis 0,8 puis 0,9 puis 0,8, donc un rapport voisin de 1 à chaque doublement. Le temps ne dépend pas de n : coût CONSTANT, et les petits écarts sont du bruit de mesure. Programme B : 3,1/1,5≈2,07, puis 6,2/3,1=2, puis 12,3/6,2≈1,98. Le rapport vaut 2 : coût LINÉAIRE, le temps est proportionnel à n. Programme C : 97/24≈4,04, puis 389/97≈4,01, puis 1554/389≈3,99. Le rapport vaut 4 : coût QUADRATIQUE, car doubler n multiplie n2 par 4. Le rapport est l'outil, pas la différence : une différence de temps ne distingue pas les trois cas.
b)
Pour B, chaque doublement multiplie par 2 : 12,3×2=24,6 ms à 16000, puis 24,6×2×2=98,4 ms à 64000, deux doublements plus loin. Pour C, chaque doublement multiplie par 4 : 1554×4=6216 ms à 16000, puis 6216×16=99456 ms à 64000, soit environ 99,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 8 au lieu de 4×4 : trois doublements se traduisent par 23 pour un linéaire et par 43 pour un quadratique, jamais par la même valeur.
c)
De 12,0 à 13,4, puis à 14,8, puis à 16,2 : le temps augmente de 1,4 milliseconde à chaque doublement, et non d'un facteur. Une quantité qui gagne une constante quand n double est proportionnelle au nombre de doublements, c'est-à-dire à log2n : c'est la signature du coût LOGARITHMIQUE, celui de la recherche dichotomique. De 8000 à 64000 il y a trois doublements, donc 16,2+3×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=64000, le rapport vaut 99456/98,4≈1011. Il était de 16 à n=1000 : 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.
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], qui compte dix cases.
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) puis dicho(t,20) : donnez à chaque tour les valeurs de g, d et m, le nombre de tours et la valeur renvoyée. Combien de tours au maximum pour dix cases, et pour 1000 cases ?
b) Première version fausse : whileg<d. Que renvoie-t-elle pour x=3 ? Décrivez la situation qu'elle manque toujours.
c) Deuxième version fausse : g=m au lieu de g=m+1. Déroulez la recherche de 88 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], qui n'est pas trié. Que renvoie dicho(u,55) ? Et dicho(u,18) ? Qu'est-ce qui rend ce cas plus dangereux que les deux précédents ?
Voir la correction
Réponses
a)67 : deux tours, renvoie 7. 20 : quatre tours, renvoie −1. Au maximum 4 tours pour dix cases, 10 pour mille
b)Elle renvoie −1 : elle manque toujours la valeur d'un intervalle réduit à une seule case
c)g se bloque à 8 et m aussi : boucle infinie, la TERMINAISON n'a plus de réponse
d)dicho(u,55) renvoie 2 par chance, dicho(u,18) renvoie −1 alors que 18 est en case 3. Aucune erreur n'est signalée
a)
Recherche de 67 : premier tour, g=0, d=9, m=4, et t[4]=31<67, donc g=5 ; deuxième tour, g=5, d=9, m=7, et t[7]=67, on renvoie 7. Deux tours. Recherche de 20 : m=4 et 31>20 donne d=3 ; puis g=0, d=3, m=1 et 11<20 donne g=2 ; puis m=2 et 18<20 donne g=3 ; puis g=d=3, m=3 et 24>20 donne d=2 ; la condition g≤d est alors fausse et l'on renvoie −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 n par deux, plus un : pour n=10 cela fait 4, et pour n=1000 cela fait 10, puisque 29=512<1000≤1024=210. Dix comparaisons pour mille cases, contre mille pour un parcours séquentiel.
b)
Avec whileg<d, la boucle s'arrête dès que g et d 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 3 : m=4 et 31>3 donne d=3 ; puis g=0, d=3, m=1 et 11>3 donne d=0 ; alors g=d=0, la condition est fausse, et la fonction renvoie −1 alors que 3 occupe la case 0. 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=m, l'intervalle ne rétrécit plus toujours. Recherche de 88 : m=4 et 31<88 donne g=4 ; m=(4+9)//2=6 et 55<88 donne g=6 ; m=7 et 67<88 donne g=7 ; m=8 et 70<88 donne g=8 ; m=(8+9)//2=8 à nouveau, et g reste 8. À partir de là, g, d et m ne bougent plus et la boucle tourne indéfiniment. La question sans réponse est celle de la TERMINAISON : le variant d−g ne décroît plus, parce que la division entière fait retomber m sur g dès que d=g+1. Le +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 31 et pour 67 : elle passe les premiers tests et se bloque sur le dernier élément.
d)
dicho(u,55) renvoie 2, parce que u[2] vaut justement 55 et que la toute première comparaison tombe dessus : la réponse est juste par hasard, pas par raisonnement. dicho(u,18), lui, compare 55 à 18, conclut qu'il faut chercher à gauche, ramène d à 1, compare u[0]=31 à 18, ramène d à −1 et renvoie −1, alors que 18 occupe la case 3. 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 10 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.
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 10, 7 et 1 point. Déroulez 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]. Combien de jetons pour 14 ? 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], déroulez le rendu de 87. Le glouton est ici optimal. Qu'est-ce qui distingue ce système du précédent ?
d) Un disque de 100 mégaoctets doit recevoir des fichiers de 34, 21, 55, 13, 8 et 40 mégaoctets. Appliquez le glouton « le plus petit d'abord », puis le glouton « le plus grand d'abord ». Le meilleur remplissage possible vaut 97 mégaoctets. Que conclure ?
Voir la correction
Réponses
a)Le glouton donne 5 jetons, 10+1+1+1+1 ; la meilleure solution en donne 2, 7+7. Le glouton n'est pas optimal
b)14 jetons. Le tri décroissant est nécessaire mais pas suffisant
c)5 pièces, 50+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 : 4 fichiers, 76 Mo. Le plus grand d'abord : 2 fichiers, 95 Mo. Chacun est optimal pour SON critère, et aucun n'atteint 97
a)
Le glouton prend d'abord 10, il reste 4 ; il essaie 7, qui ne tient pas ; il prend quatre fois 1. Total : 10+1+1+1+1, soit CINQ jetons. La meilleure solution est 7+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 14, il le rend mal.
b)
Dans l'ordre croissant, la boucle while épuise d'abord les jetons de 1 : elle en prend quatorze, le reste tombe à 0, et les valeurs 7 et 10 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] é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 50, il reste 37 ; puis 20, il reste 17 ; puis 10, il reste 7 ; puis 5, il reste 2 ; puis 2, il reste 0. Cinq pièces : 50+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] ne l'est pas, puisque deux jetons de 7 dépassent un jeton de 10. 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 : 8, puis 13, puis 21, puis 34, ce qui fait 76 mégaoctets ; le suivant, 40, porterait le total à 116 et ne tient pas. Résultat : QUATRE fichiers, 76 mégaoctets. Le plus grand d'abord : 55, puis 40, ce qui fait 95 ; aucun des quatre restants ne tient dans les cinq mégaoctets libres. Résultat : DEUX fichiers, 95 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, 97 mégaoctets, que donne la combinaison 55+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=64 essais ici, et 260, 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 6 : la version qui écrit g=m au lieu de g=m+1 renvoie correctement 4 pour la valeur 31 et 7 pour la valeur 67, et boucle indéfiniment sur 88. 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 3 montre un invariant parfaitement vrai, « les i premières cases sont triées entre elles », qui reste vrai pour [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,001n2 millisecondes et un algorithme linéaire dont le temps vaut 10n millisecondes se croisent à n=10000 ; en dessous, le quadratique est le plus rapide, et pour n=1000 il met 1 seconde contre 10. 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], elle affirme que 18 est absent alors qu'il occupe la case 3. 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 14 avec des jetons de 10, 7 et 1 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 260 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 20, sont [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.
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 30 copies et 20 questions.
d) Une copie est absente du fichier : la liste ne contient plus que neuf notes, celle de 20 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.
Voir la correction
Réponses
a)Moyenne 12,7 ; coût linéaire
b)La médiane demande les valeurs rangées ; elle vaut 12,5, et le tri coûte au pire 45 comparaisons
c)Question 5, avec 2 bonnes réponses ; 50 comparaisons ici et 600 pour 30 copies et 20 questions
d)Le programme affiche 10,7 au lieu de 11,89 ; il faut diviser par len(notes)
a)
La somme vaut 14+8+17+11+8+20+13+9+15+12=127, donc la moyenne vaut 127/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 1. Contrôle : la moyenne doit tomber entre la plus petite note, 8, et la plus grande, 20.
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 8, 8, 9, 11, 12, 13, 14, 15, 17, 20 ; 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. Au pire, le tri par insertion de dix valeurs coûte 10×9/2=45 comparaisons, c'est-à-dire le cas d'un tableau rangé à l'envers. On note au passage que la médiane, 12,5, est inférieure à la moyenne, 12,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 9, 4, 10, 7 et 2 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=50 comparaisons. Pour 30 copies et 20 questions, cela fait 30×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 127−20=107, et le programme la divise par la constante 10 : il affiche 10,7. La valeur juste est 107/9≈11,89. L'écart est de plus d'un point, et rien ne le signale : la constante 10 était vraie le jour où le programme a été écrit, elle ne l'est plus. La correction tient en un mot : écrire returns/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) vaut 0 : 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 40000 références, censé être rangé par numéro croissant. Le logiciel y fait 3000 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.
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 40000 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 40000 cases coûte au pire 16 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 assert qui la contrôle. Une assertion coûte-t-elle quelque chose ?
Voir la correction
Réponses
a)Invariant : avant le tour i, aucune des i premières paires de cases voisines n'est en désordre. Au pire 39999 comparaisons, coût linéaire
b)Rien : 48000. Une fois au chargement : 87999. Avant chaque recherche : 119997000, soit environ 1364 fois la deuxième
c)Le tri par insertion coûte au pire 799980000 comparaisons, bien plus qu'une journée de recherches séquentielles, 120000000
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 i, aucune des paires de cases voisines d'indices inférieurs à i 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, i a parcouru toutes les paires, donc le tableau est trié. La boucle compare des cases VOISINES, il y en a n−1, soit 39999 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 1, quand les deux premières cases sont déjà en désordre.
b)
Ne rien vérifier : 3000×16=48000 comparaisons par jour, et le risque de réponses fausses. Vérifier une fois au chargement : 39999+48000=87999 comparaisons, soit moins du double, pour une garantie complète sur la journée entière. Vérifier avant chaque recherche : 3000×39999=119997000 comparaisons, c'est-à-dire environ 1364 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(n−1)/2 comparaisons, soit 40000×39999/2=799980000. C'est environ 6,7 fois plus cher que de renoncer à la dichotomie pour la journée et de faire 3000 parcours séquentiels, dont le coût au pire est 3000×40000=120000000 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 False : 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 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 assertest_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.
Il manque quelque chose dans cette série ?Un type d'exercice que ton prof donne, un énoncé qui te bloque, une erreur repérée : dis-le-moi, je lis chaque demande.
L'envoi passe par ton espace gratuit : c'est ce qui me permet de te répondre et de te prévenir quand l'exercice est ajouté. Un courriel suffit, pas de mot de passe. Crée ton espace.
Pas envie de créer un compte ? Écris-moi directement.
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.