Aller au contenu principal

Prouver un algorithme : invariant et terminaison

Ce que ce chapitre apporte 5 points
  • Distinguer ce qu'un jeu d'essai établit de ce qu'il n'établira jamais.
  • Énoncer l'invariant d'une boucle et vérifier ses trois obligations.
  • Repérer sur une trace le tour exact où un invariant supposé se rompt.
  • Poser un variant pour établir qu'une boucle finit par s'arrêter.
  • Séparer la correction d'un algorithme de sa terminaison.

Chaque nuit, un programme de supervision cherche la température la plus basse relevée dans une chambre froide, et déclenche une alerte quand elle passe sous un seuil. Il a réussi tous ses essais de recette. Rendra-t-il le bon minimum sur les relevés de cette nuit, qu'aucun essai n'a vus ? Prouver un algorithme, c'est établir deux choses : un invariant qui reste vrai à chaque tour, et une terminaison qui garantit l'arrêt. Un algorithme qui rend le bon résultat sur trois exemples n'est pas un algorithme juste : c'est un algorithme qui n'a pas encore été pris en défaut. Ce chapitre donne l'outil qui permet d'affirmer davantage, et de l'affirmer pour toutes les données d'un coup : l'invariant de boucle, doublé du variant qui garantit l'arrêt. Il raisonne sur les boucles du chapitre sur les répétitions et les met à l'épreuve sur deux algorithmes déjà rencontrés : la recherche dichotomique de diviser pour régner et le tri par insertion du chapitre sur les tris.

Jusqu'ici, la façon de contrôler un algorithme a toujours été la même : le dérouler sur un jeu d'essai et comparer la sortie à ce qui était attendu. Cette méthode est indispensable, et elle a un défaut que rien ne corrigera jamais : elle ne parle que des données essayées. Un jeu d'essai qui échoue prouve qu'il y a une faute. Un jeu d'essai qui passe ne prouve rien.

La question posée ici est donc concrète, et elle se pose devant n'importe quelle boucle un peu longue : comment savoir qu'elle fait bien ce qu'on croit, alors qu'il est impossible d'essayer toutes les entrées possibles. La réponse tient en une phrase écrite au bon endroit.

Trois essais qui passent ne prouvent rien

Voici un algorithme qui cherche le plus petit élément d'un tableau, par exemple la température la plus basse d'un relevé. Il est presque juste, et c'est ce « presque » qui fait tout le chapitre.

Algorithme
pas 1 / 15
Début
t ← [8, 5, 9, 7, 2]
min ← t[1]
Pour i de 2 à longueur(t)
Si t[i] < t[i - 1] Alors
min ← t[i]
FinSi
FinPour
Écrire "minimum annoncé : ", min
Fin

programme principal

t[8, 5, 9, 7, 2]

Le résultat affiché est 2, et c'est bien le plus petit élément du tableau. Un deuxième essai sur [3, 1, 4] rendrait 1, un troisième sur [6, 6, 6] rendrait 6. Trois essais, trois succès : l'algorithme paraît correct, et il serait mis en production sans hésiter.

Il est faux. La même ligne, sur un autre tableau :

Algorithme
pas 1 / 15
Début
t ← [8, 5, 9, 7, 6]
min ← t[1]
Pour i de 2 à longueur(t)
Si t[i] < t[i - 1] Alors
min ← t[i]
FinSi
FinPour
Écrire "minimum annoncé : ", min
Fin

programme principal

t[8, 5, 9, 7, 6]

Le résultat affiché est 6, alors que le plus petit élément vaut 5. Une seule case a changé par rapport au tableau précédent, et la réponse est devenue fausse.

Ce qu'un jeu d'essai peut dire, et ce qu'il ne dira jamais

Un jeu d'essai qui échoue prouve la présence d'une faute, et il en désigne même souvent l'endroit. Un jeu d'essai qui passe ne prouve rien : il établit seulement que la faute, s'il y en a une, ne se voit pas sur ces données-là. Aucun nombre d'essais réussis ne transforme le second cas en premier.

La faute, ici, est visible à l'œil une fois qu'elle est nommée : la comparaison porte sur t[i] < t[i - 1], c'est-à-dire sur l'élément précédent du tableau, alors qu'elle devrait porter sur le minimum retenu jusque-là. L'algorithme compare chaque valeur à sa voisine au lieu de la comparer au record en cours. Reste à savoir comment on trouve ce genre de faute sans avoir la chance de tomber sur le bon jeu d'essai.

L'invariant de boucle

L'idée est de renoncer à parler des données et de parler de la boucle elle-même. Plutôt que de demander « que vaut le résultat sur ce tableau », on énonce une propriété qui décrit ce que les variables contiennent à chaque tour, et on montre que la boucle la préserve.

Le cas le plus simple est celui d'une somme, comme le total des consommations relevées sur quatre jours. La boucle ci-dessous additionne les cases d'un tableau, et la trace affiche à chaque tour la valeur de somme et celle de i.

Algorithme
pas 1 / 21
Début
somme ← 0
i ← 1
t ← [4, 7, 2, 9]
TantQue i <= longueur(t)
Écrire "avant le tour ", i, " : somme = ", somme
somme ← somme + t[i]
i ← i + 1
FinTantQue
Écrire "à la sortie : i = ", i, " et somme = ", somme
Fin

programme principal

somme0
À manipuler
Avancer avec « suivant ▶ » jusqu'au pas 6, où la ligne 7, somme ← somme + t[i], vient d'être exécutée : l'encadré des variables, sous le listing, montre somme à 4 alors que i vaut encore 1. Aucune case ne précède t[1], et le total de ces cases vaut 0 : à cet instant précis, somme ne vaut plus le total des cases t[1] à t[i - 1]. Un pas de plus, i ← i + 1, et l'accord revient, 4 étant le total de t[1] seul. Relever ensuite i et somme à chaque pas où la ligne 5, celle du TantQue, est surlignée : les pas 4, 8, 12, 16 et 20. Constater qu'à chacun de ces tests somme vaut exactement le total des cases qui précèdent t[i], et que l'accord ne se rompt qu'au milieu d'un tour, entre les lignes 7 et 8.

Voici ce que valent les deux variables au moment où la condition est testée, tour après tour, avec en dernière colonne le total des cases qui précèdent t[i] :

Au testisommeTotal de t[1] à t[i - 1]
avant le 1er tour10total d'aucune case, soit 0
avant le 2e tour244
avant le 3e tour3114 + 7
avant le 4e tour4134 + 7 + 2
à la sortie5224 + 7 + 2 + 9

i et somme changent à chaque ligne, et pourtant une chose ne change jamais : somme est toujours égale à la dernière colonne. Une relation entre les variables qui tient d'un tour à l'autre porte un nom.

Définition

Un invariant de boucle est une propriété des variables qui est vraie avant le premier tour et qui reste vraie après chaque tour, quel que soit le nombre de tours effectués. Il ne décrit pas une valeur particulière, il décrit une relation qui tient toujours.

L'invariant de cette boucle s'énonce ainsi : au moment de tester la condition, somme contient le total des cases t[1] à t[i - 1]. Il se lit directement dans la figure, parce que les deux variables qu'il met en relation sont justement celles que le moteur affiche.

La dernière ligne du tableau est celle qui conclut : à la sortie, i vaut 5, donc l'invariant affirme que somme contient le total des cases t[1] à t[4], c'est-à-dire de tout le tableau. Le résultat cherché tombe alors sans qu'il ait été nécessaire de connaître les valeurs du tableau.

Vérification rapideon peut se reprendre

1.Un jeu d'essai qui passe sur dix tableaux différents prouve quoi ?

2.L'invariant d'une boucle décrit quoi ?

Les trois obligations d'un invariant

Vérifier un invariant sur cinq lignes de trace est un bon exercice de lecture, mais ce n'est encore qu'un jeu d'essai déguisé. La preuve proprement dite tient en trois points, et il faut les trois.

  1. L'initialisation. L'invariant est vrai avant d'entrer dans la boucle. Ici, somme vaut 0 et i vaut 1 : l'invariant réclame le total des cases t[1] à t[0], c'est-à-dire d'aucune case, et le total d'aucune case vaut bien 0.
  2. La conservation. Si l'invariant est vrai au début d'un tour, il est encore vrai au début du tour suivant. La trace le montre sur le tour 3 : il commence avec i = 3 et somme = 11, le total de t[1] et t[2]. Le corps ajoute t[3], qui vaut 2, puis fait passer i à 4. Au test suivant, l'invariant réclame le total de t[1] à t[4 - 1], c'est-à-dire t[1] + t[2] + t[3], soit 13, et somme vaut bien 13. Le même raisonnement vaut pour un tour quelconque, sans connaître les valeurs : si somme contient le total jusqu'à t[i - 1], le corps lui ajoute t[i], et elle contient le total jusqu'à t[i] ; puis i augmente de 1, et « jusqu'à t[i] » avec l'ancienne valeur de i se lit « jusqu'à t[i - 1] » avec la nouvelle. Toute la difficulté tient dans cette dernière phrase : la même lettre désigne deux nombres, avant et après i ← i + 1, et l'invariant se relit toujours avec la valeur que i a au moment du test.
  3. La conclusion. Combiné à la raison qui fait sortir de la boucle, l'invariant donne le résultat voulu. On sort quand i > longueur(t), et comme i augmente de 1 par tour, on sort avec i = longueur(t) + 1 exactement : somme contient le total de tout le tableau.

Les deux premiers points sont un raisonnement par récurrence. L'initialisation est le cas de base, la conservation est l'hérédité, et la conclusion est ce qu'on en tire une fois la boucle terminée. C'est exactement le schéma employé sur les suites, décrit dans le chapitre sur les suites. Un invariant de boucle n'est rien d'autre qu'une récurrence appliquée au numéro du tour.

Où placer l'énoncé

L'invariant s'écrit en commentaire juste au-dessus de la ligne du TantQue ou du Pour, et il porte sur l'état des variables au moment du test. Le fixer à cet instant précis évite la moitié des discussions : un invariant vrai en milieu de corps mais faux au test ne sert à rien, puisque c'est au test que la boucle décide de continuer ou de sortir.

Une boucle Pour ne montre pas son test. Pour elle, l'invariant s'énonce après chaque tour, en fonction de la valeur de i que la figure affiche, et la conclusion se lit après le dernier tour, où i vaut la borne de fin : le compteur d'un Pour garde la dernière valeur qu'il a prise.

Vérification rapideon peut se reprendre

Quelles sont les trois obligations à vérifier sur un invariant ?

Un invariant qui se casse, et le tour exact où il se casse

Retour à l'algorithme faux du début. L'invariant qu'il faudrait pour conclure est évident : après le tour i, min contient le plus petit des éléments t[1] à t[i]. Si cette propriété tient après chaque tour, alors après le dernier, où i vaut longueur(t), min contient le plus petit de tout le tableau, et l'algorithme est juste.

La figure suivante vérifie cet invariant elle-même. Une variable reference suit le plus petit élément réellement rencontré, calculé correctement ; à la fin de chaque tour, la comparaison entre min et reference dit si l'invariant tient encore.

Algorithme
pas 1 / 30
Début
t ← [8, 5, 9, 7, 6]
min ← t[1]
reference ← t[1]
Pour i de 2 à longueur(t)
Si t[i] < t[i - 1] Alors
min ← t[i]
FinSi
Si t[i] < reference Alors
reference ← t[i]
FinSi
Si min = reference Alors
Écrire "tour ", i, " : min = ", min, " = plus petit de t[1..", i, "] : invariant tenu"
Sinon
Écrire "tour ", i, " : min = ", min, " alors que le plus petit de t[1..", i, "] vaut ", reference, " : invariant rompu"
FinSi
FinPour
Fin

programme principal

t[8, 5, 9, 7, 6]

La sortie produite est sans ambiguïté :

tour 2 : min = 5 = plus petit de t[1..2] : invariant tenu
tour 3 : min = 5 = plus petit de t[1..3] : invariant tenu
tour 4 : min = 7 alors que le plus petit de t[1..4] vaut 5 : invariant rompu
tour 5 : min = 6 alors que le plus petit de t[1..5] vaut 5 : invariant rompu

L'invariant tient deux tours, et se rompt au tour 4. À dérouler la figure jusque-là, le moment se voit dans la valeur de min, affichée sous le listing : elle passe de 5 à 7. Une variable censée contenir un minimum vient d'augmenter, ce qui est absurde en soi, et la ligne responsable est celle qui vient d'être exécutée.

Le premier jeu d'essai cachait exactement la même rupture
Sur le tableau [8, 5, 9, 7, 2] du début, l'invariant se rompt aussi au tour 4, de la même façon et pour la même raison. Simplement, le dernier élément valait 2, il était plus petit que tout le reste, et le tour 5 a remis min sur la bonne valeur par accident. Le résultat final était juste alors que l'algorithme ne l'était pas.
C'est le scénario le plus coûteux qui soit : une faute qui se corrige toute seule sur les données d'essai et qui ressort plus tard, sur des données réelles, sans que personne pense à remettre l'algorithme en cause.

L'invariant fait alors office de cahier des charges pour la correction. Il faut que chaque nouvelle valeur soit comparée à min, le record en cours, et non à la case précédente :

Algorithme
pas 1 / 13
Début
t ← [8, 5, 9, 7, 6]
min ← t[1]
// invariant : après le tour i, min est le plus petit de t[1] à t[i]
Pour i de 2 à longueur(t)
Si t[i] < min Alors
min ← t[i]
FinSi
FinPour
Écrire "minimum = ", min
Fin

programme principal

t[8, 5, 9, 7, 6]

La conservation se démontre maintenant en une phrase. Si, après le tour i - 1, min est le plus petit de t[1] à t[i - 1], alors le plus petit de t[1] à t[i] est soit ce même min, soit t[i] : le test du tour i choisit exactement le bon des deux. L'initialisation est vraie parce que min ← t[1] fait de min le plus petit d'une portion d'un seul élément, ce qui est l'invariant pris juste avant le premier tour. La conclusion suit, et l'algorithme est correct pour tous les tableaux, pas seulement pour trois. L'invariant figure désormais en commentaire au-dessus du Pour, là où l'encadré sur la place de l'énoncé demande de l'écrire : le moteur ignore cette ligne, le lecteur non.

La terminaison se prouve à part : le variant

Tout ce qui précède établit une chose, et une seule : si la boucle s'arrête, le résultat est le bon. C'est ce que l'on appelle la correction partielle, et le « si » n'est pas une formalité.

Une boucle Pour s'arrête d'elle-même, puisque son nombre de tours est fixé à l'entrée et que modifier son compteur dans le corps n'y change rien. La question se pose pour le TantQue, dont rien ne garantit a priori que la condition deviendra fausse.

Définition

Un variant de boucle est une quantité entière, toujours positive ou nulle, qui décroît strictement à chaque tour. Son existence prouve que la boucle s'arrête : une suite d'entiers positifs qui décroît strictement ne peut pas être infinie.

La recherche dichotomique fournit le variant le plus lisible du parcours. Ici, elle cherche la valeur 7 dans un relevé de huit mesures rangées par ordre croissant. La portion encore à explorer va de bas à haut ; sa taille, haut - bas + 1, est le variant. La figure la calcule à chaque tour, pour qu'elle se lise dans la trace au même titre que les autres variables.

Algorithme
pas 1 / 24
Début
t ← [2, 5, 9, 12, 17, 23, 30, 41]
x ← 7
bas ← 1
haut ← longueur(t)
variant ← haut - bas + 1
TantQue bas <= haut
milieu ← (bas + haut) DIV 2
Si t[milieu] < x Alors
bas ← milieu + 1
Sinon
haut ← milieu - 1
FinSi
variant ← haut - bas + 1
FinTantQue
Écrire "absent, variant final : ", variant
Fin

programme principal

t[2, 5, 9, 12, 17, 23, 30, 41]

La variable variant, calculée pour être lue dans la trace mais absente de la condition, prend successivement les valeurs 8, 3, 1 puis 0. Elle décroît strictement et ne descend jamais sous zéro : partie de 8, elle garantit que la boucle s'arrête en huit tours au plus. Le raisonnement ne dépend pas des valeurs : chaque tour remplace bas par milieu + 1 ou haut par milieu - 1, et dans les deux cas la portion perd au moins la case milieu. La dichotomie fait en réalité bien mieux, puisque chaque tour garde au plus la moitié de la portion, soit quatre tours au plus sur ce tableau de huit cases ; mais pour prouver l'arrêt, la décroissance stricte suffit.

Il suffit d'effacer le - 1 pour que la garantie disparaisse. Dans la version suivante, l'auteur a écrit haut ← milieu au lieu de haut ← milieu - 1.

Algorithme
pas 1 / 501
Début
t ← [2, 5, 9, 12, 17, 23, 30, 41]
x ← 7
bas ← 1
haut ← longueur(t)
TantQue bas <= haut
milieu ← (bas + haut) DIV 2
Si t[milieu] < x Alors
bas ← milieu + 1
Sinon
haut ← milieu
FinSi
FinTantQue
Écrire "sortie avec bas = ", bas
Fin

programme principal

t[2, 5, 9, 12, 17, 23, 30, 41]

Le déroulement s'interrompt de lui-même : le moteur refuse d'aller au-delà de cinq cents pas et le signale. Les trois premiers tours sont irréprochables, haut descend de 8 à 4 puis à 3, et bas monte à 3. Puis bas et haut valent tous les deux 3, milieu vaut 3, la valeur cherchée est plus petite que t[3], donc haut ← milieu réaffecte 3 à une variable qui valait déjà 3. Le variant reste bloqué à 1, et rien ne le fera plus descendre.

Décroître ne suffit pas, il faut décroître strictement

Une quantité qui décroît au sens large peut stagner indéfiniment, et c'est exactement ce qui arrive ici. La condition de sortie bas <= haut est pourtant bien écrite, la boucle est pourtant bien bornée par un test : rien de tout cela ne sert si un tour peut laisser l'état inchangé. La preuve de terminaison ne porte pas sur la condition, elle porte sur ce que le corps fait au variant.

Correction = correction partielle + terminaison. L'invariant établit que le résultat est bon si la boucle s'arrête. Le variant établit qu'elle s'arrête. Les deux ensemble, et seulement les deux ensemble, permettent de dire qu'un algorithme est correct.
Les deux moitiés se perdent séparément : une boucle qui ne termine pas peut avoir un invariant parfait, et une boucle qui termine toujours peut rendre n'importe quoi.

Vérification rapideon peut se reprendre

1.Un variant de boucle est une quantité qui doit faire quoi ?

2.Un invariant correctement démontré établit quoi, exactement ?

Preuve complète d'une recherche dichotomique

Reste à mettre les deux moitiés bout à bout sur un algorithme utile. La recherche dichotomique, étudiée dans le chapitre diviser pour régner, cherche une valeur dans un tableau trié en coupant en deux à chaque tour. Son invariant est écrit en commentaire au-dessus du TantQue.

Algorithme
pas 1 / 16
Début
t ← [2, 5, 9, 12, 17, 23, 30, 41]
x ← 23
bas ← 1
haut ← longueur(t)
trouve ← 0
// invariant : si x figure dans t, son indice est compris entre bas et haut
TantQue bas <= haut ET trouve = 0
milieu ← (bas + haut) DIV 2
Si t[milieu] = x Alors
trouve ← milieu
SinonSi t[milieu] < x Alors
bas ← milieu + 1
Sinon
haut ← milieu - 1
FinSi
FinTantQue
Écrire "indice : ", trouve
Fin

programme principal

t[2, 5, 9, 12, 17, 23, 30, 41]

L'invariant. Si x figure dans le tableau, alors son indice est compris entre bas et haut. La formulation compte : l'invariant ne promet pas que x est présent, il promet que la portion bas à haut est le seul endroit où il peut encore se trouver.

L'initialisation. Avant le premier tour, bas vaut 1 et haut vaut longueur(t) : la portion est le tableau entier, et l'invariant est vrai sans rien supposer.

La conservation. Le tableau étant trié, si t[milieu] < x, alors toutes les cases d'indice inférieur ou égal à milieu contiennent des valeurs inférieures à x, donc x ne peut pas s'y trouver, et bas ← milieu + 1 conserve l'invariant. Le cas symétrique se traite de la même façon. Et quand t[milieu] = x, ni bas ni haut ne changent : l'invariant reste vrai sans rien faire. C'est ici, et nulle part ailleurs, que l'hypothèse « le tableau est trié » est utilisée : la preuve désigne elle-même la condition d'emploi de l'algorithme.

La conclusion. La boucle s'arrête pour deux raisons possibles. Soit trouve est devenu non nul, et t[trouve] = x a été testé directement. Soit bas > haut, la portion est vide, et l'invariant affirme que x ne pouvait être que là : x est donc absent du tableau.

La terminaison. Un tour se termine de deux façons. Si t[milieu] = x, trouve devient non nul et la boucle s'arrête au test suivant : ce tour est le dernier, même si la portion n'a pas rétréci. Dans les deux autres branches, bas passe à milieu + 1 ou haut à milieu - 1, et la taille de la portion, haut - bas + 1, perd au moins la case milieu : elle décroît strictement sans jamais descendre sous zéro. La taille seule n'est donc pas un variant de cette boucle, puisqu'elle ne bouge pas dans la branche qui trouve. Les deux cas se réunissent en un seul variant : la taille de la portion, augmentée de 1 tant que trouve vaut 0. Dans la branche qui trouve, la taille reste la même mais le 1 ajouté disparaît ; dans les deux autres, la taille baisse d'au moins 1 et le 1 reste. Ce variant est entier, jamais négatif, et il baisse d'au moins 1 à chaque tour, quelle que soit la branche prise. C'est exactement la vérification que demande la méthode : la branche qui trouve est la branche rare, et c'est précisément celle où la taille seule ne bouge pas.

Le déroulement de la figure l'illustre, sans rien prouver de plus : ce variant vaut 9 avant le premier tour, puis 5 quand bas passe de 1 à 5, puis 4 quand milieu tombe sur 6 et que trouve prend la valeur 6. La valeur 23 est bien en sixième position, et deux tours ont suffi là où un parcours case par case en aurait demandé six.

La preuve est aussi une documentation

Les quatre points ci-dessus disent, sans rien ajouter d'autre, que l'algorithme réclame un tableau trié, qu'il rend 0 quand la valeur est absente, et qu'il s'arrête toujours. Une preuve bien écrite remplace le paragraphe d'explications qu'on ne prend jamais le temps d'écrire, et elle a l'avantage d'être vérifiable.

Mettre en pratique

Un invariant donne la valeur d'une variable à un tour choisi sans dérouler la boucle, et un variant borne le nombre de tours avant toute exécution. L'un et l'autre se calculent.

Ce que l'invariant et le variant permettent de prévoir

  • 1.

    La boucle de la somme parcourt cette fois le tableau [6, 3, 8, 5, 2]. Que vaut somme au moment du test où i vaut 4 ?

  • 2.

    Que vaut i à la sortie de cette boucle TantQue, sur ce tableau de cinq cases ?

  • 3.

    Une recherche dichotomique explore un relevé trié de 48 mesures. Combien de tours au plus la seule décroissance stricte du variant haut - bas + 1 garantit-elle ?

  • 4.

    Chaque tour garde en fait au plus la moitié de la portion. Combien de tours au plus la recherche fait-elle réellement sur ces 48 mesures ?

  • 5.

    L'algorithme de la preuve complète cherche cette fois 30 dans le tableau [3, 8, 15, 21, 30, 42]. Que vaut son variant, la taille de la portion augmentée de 1 tant que trouve vaut 0, à la sortie de la boucle ?

La troisième réponse et la quatrième ne se contredisent pas : l'une est ce que la preuve de terminaison garantit, l'autre ce que la dichotomie fait vraiment.

Énoncer l'invariant de la boucle extérieure du tri par insertion, et le vérifier sur la trace.
Algorithme
pas 1 / 63
Début
t ← [7, 3, 9, 1, 5, 2]
n ← longueur(t)
Pour i de 2 à n
v ← t[i]
j ← i - 1
TantQue j >= 1 ET t[j] > v
t[j + 1] ← t[j]
j ← j - 1
FinTantQue
t[j + 1] ← v
Écrire "après le tour ", i, " : ", t[1], " ", t[2], " ", t[3], " ", t[4], " ", t[5], " ", t[6]
FinPour
Fin

programme principal

t[7, 3, 9, 1, 5, 2]

L'invariant de la boucle extérieure, énoncé après chaque tour comme pour tout Pour : après le tour i, les cases t[1] à t[i] contiennent les i premiers éléments d'origine, triés entre eux. Avant le premier tour, il tient déjà pour la seule case t[1], triée par nature. La trace le confirme à chaque ligne :

après le tour 2 : 3 7 9 1 5 2
après le tour 3 : 3 7 9 1 5 2
après le tour 4 : 1 3 7 9 5 2
après le tour 5 : 1 3 5 7 9 2
après le tour 6 : 1 2 3 5 7 9

Après le tour 2, les deux premières cases sont triées ; après le tour 3, les trois premières, et ainsi jusqu'au tour 6. Après ce dernier tour, i vaut 6, c'est-à-dire longueur(t), comme l'affiche la figure : l'invariant affirme alors que les cases t[1] à t[6], c'est-à-dire le tableau entier, sont triées.

Deux précisions que l'invariant impose et qu'il serait tentant d'oublier : les cases triées sont bien les éléments d'origine et non d'autres valeurs, ce qui interdit une version qui perdrait un élément en route ; et la partie droite du tableau, elle, n'est pas concernée, ce que la trace montre bien avec le 2 qui reste en dernière position jusqu'au dernier tour. Le tour 3 ne change rien au tableau, ce qui est normal : le 9 est déjà à sa place parmi les valeurs déjà triées, et l'invariant est tout de même conservé.

Le variant de la boucle intérieure est j : il décroît de 1 à chaque passage, et il ne descend jamais sous 0, puisque la boucle n'entame un passage que si j >= 1. La boucle extérieure, un Pour, n'en demande pas : son nombre de tours est fixé à l'entrée, cinq ici. Si l'on tient à lui en donner un, longueur(t) - i convient : il vaut 4 au premier tour et 0 au dernier, sans jamais devenir négatif, puisque i s'arrête à longueur(t).

Le détail des trois tris et de leurs coûts est traité dans le chapitre sur les tris.

L'algorithme suivant est censé compter les éléments strictement positifs d'un tableau. Énoncer son invariant et dire s'il tient.
Algorithme
pas 1 / 15
Début
t ← [3, -1, 4, 5]
compte ← 0
Pour i de 1 à longueur(t)
Si t[i] > 0 Alors
compte ← 1
FinSi
FinPour
Écrire compte
Fin

programme principal

t[3, -1, 4, 5]
Le corrigé

L'invariant voulu, énoncé après chaque tour comme pour tout Pour, serait : « après le tour i, compte contient le nombre d'éléments strictement positifs parmi t[1] à t[i] ».

L'initialisation tient : avant le premier tour, compte vaut 0 et la portion est vide. La conservation ne tient pas : compte ← 1 ne dépend pas de la valeur précédente de compte, si bien qu'au deuxième élément strictement positif la propriété devient fausse. Sur le tableau de la figure, l'invariant tient après les tours 1 et 2, où compte vaut 1 pour un seul élément strictement positif, et se rompt au tour 3 : 4 est le deuxième élément strictement positif, et compte reste à 1. La figure affiche 1 là où 3 était attendu.

Le jeu d'essai qui masque la faute est facile à fabriquer : tout tableau contenant au plus un élément strictement positif donne la bonne réponse. La correction est compte ← compte + 1, et l'invariant devient alors conservé.

Proposer un variant pour la boucle suivante, qui divise n par 2 tant que n est pair, l'entier n lu au départ étant positif. Dire ensuite pour quelles valeurs de départ elle s'arrête.
Algorithme
pas 1 / 7
séparées par des virgules, décimales au point
Début
Lire n
TantQue n MOD 2 = 0
n ← n DIV 2
FinTantQue
Écrire "n vaut ", n
Fin

programme principal

nsaisi12
Le corrigé

n lui-même convient, à une condition : que n vaille au moins 1 au départ. Tant que n est pair et au moins égal à 2, n DIV 2 est un entier au moins égal à 1 et strictement plus petit que n : la quantité reste positive et décroît strictement. La boucle s'arrête donc pour tout n supérieur ou égal à 1 ; avec 12, la figure rend 3 en 7 pas.

Le vrai piège de l'énoncé est n = 0. Zéro est pair, 0 DIV 2 vaut 0, et le variant ne bouge plus : la boucle tourne sans fin. Entrer 0 dans le champ de saisie de la figure le confirme, puisque le moteur bute alors sur ses cinq cents pas. C'est le variant qui fait voir ce cas, parce qu'il cesse de décroître, et la preuve désigne ainsi d'elle-même la condition d'emploi de l'algorithme, comme l'hypothèse « le tableau est trié » pour la dichotomie.

L'autre piège est d'aller chercher un compteur de tours : un variant n'a pas à être une variable nouvelle, et il n'a pas à compter quoi que ce soit. Toute quantité entière positive qui décroît strictement fait l'affaire, et la plus simple est presque toujours une variable déjà présente.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Dans la bibliothèque d'exercices