Aller au contenu
Topos Uranos

Résumé

Cette leçon ne contient pas de code, et ce n’est pas un oubli : l’algorithme précède le programme, et qui ne sait pas décrire avec précision ce qu’il faut faire ne saura pas non plus le dire en C++. On apprend d’abord à spécifier un problème, c’est-à-dire à déclarer quelles données il reçoit, quel résultat il doit rendre et quelle relation les unit, et l’on montre qu’un même énoncé admet des spécifications différentes. On définit ensuite l’algorithme comme une procédure finie, précise et générale, et l’on écrit en étapes numérotées celui de la division par soustractions successives. On apprend à suivre un algorithme à la main avec un tableau de suivi, qui sera pendant tout le cours le premier instrument pour comprendre et pour déboguer. On présente les trois façons de combiner des étapes (séquence, décision et répétition), avec lesquelles s’écrit tout algorithme, et l’on apprend à justifier qu’une répétition se termine au moyen d’une quantité qui décroît à chaque itération et ne peut pas descendre indéfiniment.

Objectifs d’apprentissage

  1. Spécifier un problème simple en indiquant ses entrées, ses sorties et les conditions qu’elles doivent remplir.
  2. Décrire un algorithme en étapes numérotées, sans ambiguïté, avec des décisions et des répétitions explicites.
  3. Suivre à la main un algorithme avec un tableau de suivi et déterminer son résultat pour une entrée donnée.
  4. Distinguer, dans une procédure donnée, la séquence, la décision et la répétition, et justifier qu’une répétition se termine.

Qu’est-ce qu’un problème algorithmique

Un problème algorithmique est une relation entre deux sortes de données : celles que l’on reçoit, appelées entrée, et celles que l’on doit rendre, appelées sortie. Le résoudre ne consiste pas à obtenir une réponse pour un cas, mais à disposer d’une méthode qui, pour chaque entrée admissible, produise une sortie qui satisfasse la relation. « Combien font 17 divisé par 5 ? » est une question ; « étant donné deux entiers, obtenir leur quotient et leur reste » est un problème, parce qu’il parle de toutes les paires d’entiers à la fois.

Or le langage courant est imprécis, et le même énoncé peut s’entendre de plusieurs manières. C’est pourquoi, avant de résoudre, on spécifie : on écrit ce qui entre, ce qui sort et quelles conditions chaque chose doit remplir. La spécification a trois parties. L’entrée énumère les données avec leur sorte (entiers, décimaux, mots) et les conditions qu’on leur impose, que l’on appelle précondition : qui utilise la solution s’engage à les respecter. La sortie énumère les résultats. La relation, ou postcondition, dit ce que la sortie doit vérifier par rapport à l’entrée : qui écrit la solution s’engage à la garantir chaque fois que la précondition a été respectée. Il s’ensuit que la spécification est un contrat entre qui pose le problème et qui le résout, et qu’en dehors de la précondition la solution ne promet rien.

ExempleLe maximum de trois nombres

Énoncé courant : « trouver le plus grand de trois nombres ». Spécification :

  • Entrée : trois nombres xx, yy, zz (entiers ou décimaux, sans autre condition).
  • Sortie : un nombre mm.
  • Relation : mm est l’une des trois données, et aucune d’elles n’est plus grande que mm ; c’est-à-dire m≥xm \geq x, m≥ym \geq y et m≥zm \geq z.

La relation ne dit pas « le plus grand des trois », ce qui est justement ce que l’on voulait préciser, mais deux conditions vérifiables. La première exige que mm soit l’une des données (sans elle, le nombre 1000 vérifierait la seconde pour presque toute entrée) ; la seconde, qu’aucune ne le dépasse. Et la spécification tranche au passage un doute que l’énoncé laissait ouvert : si deux données sont à égalité pour le maximum, comme dans (4,9,9)(4, 9, 9), la sortie est 99, et il n’y a là aucune erreur.

Un même énoncé admet des spécifications différentes, et choisir entre elles est une décision, non un oubli. Le cas le plus instructif est celui de la division entière. Pour des entiers a≥0a \geq 0 et b>0b > 0, tout le monde est d’accord : le quotient qq et le reste rr sont les seuls entiers qui vérifient

a=b⋅q+r,0≤r<ba = b \cdot q + r, \quad 0 \leq r < b

Ainsi, pour a=17a = 17 et b=5b = 5, ce sont q=3q = 3 et r=2r = 2, parce que 17=5⋅3+217 = 5 \cdot 3 + 2. Mais l’énoncé « diviser deux entiers » ne dit pas que faire si aa est strictement négatif, et la question n’est pas oiseuse : pour a=−7a = -7 et b=2b = 2, il y a deux réponses raisonnables.

ExempleDeux spécifications de la division à dividende strictement négatif

Première spécification : le reste n’est jamais strictement négatif, 0≤r<b0 \leq r < b. Alors −7=2⋅(−4)+1-7 = 2 \cdot (-4) + 1, et la sortie est q=−4q = -4, r=1r = 1. C’est celle que l’on étudie en mathématiques, dans la leçon « Divisibilité et division euclidienne » du cours Algèbre et géométrie I.

Seconde spécification : le quotient est le résultat de la division avec décimales, dont on écarte la partie décimale, c’est-à-dire arrondi vers zéro ; le reste est ce qui manque, et il a le signe du dividende. Alors −7/2=−3,5-7 / 2 = -3{,}5 se tronque en −3-3, et −7=2⋅(−3)+(−1)-7 = 2 \cdot (-3) + (-1) : la sortie est q=−3q = -3, r=−1r = -1.

Les deux vérifient a=b⋅q+ra = b \cdot q + r ; elles diffèrent par la condition imposée au reste. Aucune n’est une erreur, mais un programme qui utilise l’une en croyant utiliser l’autre donnera des résultats faux avec les nombres strictement négatifs. On peut l’annoncer dès maintenant : C++ adopte la seconde, comme on le verra dans la leçon Arithmétique ; qui attend la première aura une surprise.

Il existe une troisième option, la plus fréquente en pratique : exclure le cas par la précondition. Si la spécification exige a≥0a \geq 0, le problème des dividendes strictement négatifs disparaît, non parce qu’on l’a résolu, mais parce que qui utilise la solution s’est engagé à ne pas le poser. Ce que l’on ne peut pas faire, c’est laisser la question sans réponse. Et pour la même raison la spécification exige b>0b > 0 : avec b=0b = 0, il n’existe pas de qq et de rr qui vérifient la relation, et le contrat doit le dire.

Une erreur fréquente consiste à spécifier par un exemple : « la sortie de 17 et 5 est 3 et 2 ». Un exemple illustre, mais ne spécifie pas, parce qu’il ne dit pas ce qui arrive avec les autres entrées ; une infinité de relations différentes coïncident sur un cas. La spécification s’écrit comme une condition générale, et les exemples servent ensuite à la vérifier.

Algorithmes

Une fois le problème spécifié, on cherche une méthode pour le résoudre. Un algorithme est une suite d’instructions qui, appliquée à toute entrée vérifiant la précondition, produit en un nombre fini d’étapes une sortie qui vérifie la postcondition. La définition contient trois exigences.

  • Finitude. L’algorithme s’écrit avec un nombre fini d’instructions et, pour chaque entrée admissible, il se termine après en avoir exécuté un nombre fini. Une procédure qui, pour une certaine entrée, continue indéfiniment n’est pas un algorithme pour cette entrée.
  • Précision. Chaque instruction a une seule signification, et qui l’exécute sait à tout moment quoi faire et quelle est l’instruction suivante. Aucune décision n’est laissée à l’appréciation de qui exécute.
  • Généralité. L’algorithme résout le problème pour toutes les entrées admissibles, pas seulement pour un cas.

Avec une certaine liberté, on appelle aussi algorithme une procédure précise et qui se termine, mais qui pour une certaine entrée admissible ne vérifie pas la postcondition : en toute rigueur, c’est un algorithme pour un autre problème, et l’on dira qu’il est incorrect pour le problème spécifié. C’est ainsi que parleront les problèmes de cette leçon, qui réfutent des algorithmes incorrects par un contre-exemple.

Une recette de cuisine ressemble à un algorithme, et c’est pourquoi elle est la comparaison habituelle ; mais presque aucune n’en est un. « Ajouter une pincée de sel » n’est pas précis, parce qu’une pincée n’est pas une quantité ; « cuire au four jusqu’à ce que ce soit doré » dépend d’un jugement que deux personnes porteraient différemment ; « battre plusieurs fois » ne dit pas combien. Un cuisinier comble ces lacunes par son expérience ; un ordinateur n’en a aucune. Il s’ensuit l’exigence qui gouverne tout le cours : un algorithme doit pouvoir être suivi par qui ne comprend pas le problème, sans rien deviner. Si, pour exécuter une étape, il faut savoir à quoi elle sert, l’étape est mal écrite.

Les algorithmes de ce cours s’écrivent d’abord en étapes numérotées, en français, avec quelques conventions. Les données qui changent pendant l’algorithme se nomment par des lettres ou des mots, comme qq ou rr, et s’appellent des variables. On écrit « rr prend la valeur r−br - b » pour dire que l’on calcule r−br - b avec la valeur actuelle de rr et que le résultat devient la nouvelle valeur de rr ; l’ancienne valeur est perdue. Dans la forme abrégée, que l’on utilise pour prendre des notes, la même instruction s’écrit avec une flèche vers la gauche, qui au clavier est r <- r - b. Les instructions s’exécutent dans l’ordre de leur numéro, sauf si l’une d’elles dit autre chose.

ExempleLa division par soustractions successives

Le problème est celui que l’on a spécifié dans la section précédente : en entrée, deux entiers a≥0a \geq 0 et b>0b > 0 ; en sortie, deux entiers qq et rr avec a=b⋅q+ra = b \cdot q + r et 0≤r<b0 \leq r < b. L’idée est celle du partage : on retire de aa des groupes de bb tant que c’est possible, on compte les groupes retirés, et ce qui reste est le reste.

  1. qq prend la valeur 00, et rr prend la valeur aa.
  2. Tant que r≥br \geq b est vérifiée, répéter les étapes 3 et 4 ; quand elle cesse de l’être, continuer à l’étape 5.
  3. rr prend la valeur r−br - b.
  4. qq prend la valeur q+1q + 1 ; revenir à l’étape 2.
  5. Rendre qq et rr, et terminer.

L’étape 2 est une question que l’on pose chaque fois : avant la première itération (ou premier tour) et après chacune. Les étapes 3 et 4 forment le corps de la répétition, ce qui se répète. Rien n’est laissé à l’appréciation de qui exécute : à chaque instant, il n’y a qu’une instruction suivante.

L’algorithme est lent (pour diviser un million par trois, il fait plus de trois cent mille itérations), et la méthode scolaire de la division est beaucoup plus rapide ; mais il est correct, et sa simplicité le rend idéal pour apprendre à raisonner sur les algorithmes. Le cours reviendra sur la différence entre un algorithme correct et un algorithme efficace dans la leçon Le coût d’un algorithme.

Il convient, enfin, de séparer l’algorithme de son écriture. L’algorithme de la division par soustractions est le même, qu’on l’écrive en français, dans un diagramme, en C++ ou dans tout autre langage de programmation : ce qui le définit, c’est la suite des opérations, non les mots. Un programme est l’écriture d’un algorithme dans un langage qu’une machine peut exécuter, et la leçon Programmes, compilateurs et C++ expliquera comment se fait cette exécution. S’il en est ainsi, un programme peut être faux de deux manières très différentes : parce que l’algorithme est incorrect, et alors aucun langage ne le sauve, ou parce que l’algorithme est correct mais a été mal écrit. Distinguer ces deux cas est la moitié du travail de débogage, et c’est pourquoi le cours écrit toujours d’abord l’algorithme.

Suivre un algorithme à la main

Lire un algorithme ne suffit pas pour savoir ce qu’il fait ; il faut l’exécuter. Le suivre à la main, comme le ferait la machine, sans sauter d’étapes ni deviner de résultats, est la compétence la plus utile d’un programmeur : avec elle, on vérifie un algorithme avant de l’écrire, on prédit la sortie d’un programme avant de l’exécuter et l’on trouve l’erreur de celui qui échoue.

L’instrument est le tableau de suivi : une colonne pour le numéro de l’étape exécutée, une colonne par variable et, si c’est utile, une pour les conditions évaluées ; et une ligne par étape exécutée. Dans chaque ligne, on note la nouvelle valeur de la variable qui a changé à cette étape et l’on laisse en blanc ce qui n’a pas changé, de sorte que la valeur actuelle d’une variable est la dernière écrite dans sa colonne. Les conditions se notent avec leur résultat.

ExempleLa trace de la division de 17 par 5

On suit la division par soustractions successives avec a=17a = 17 et b=5b = 5. La flèche de la figure indique l’étape qui vient d’être exécutée ; la ligne pointillée rappelle que l’étape 4 revient à l’étape 2.

Démonstration

  1. (q,r)=(0,17)(q, r) = (0, 17)

    Étape 1 : on donne les valeurs initiales. L’entrée vérifie la précondition, parce que 17≥017 \geq 0 et 5>05 > 0.

  2. (q,r)=(1,12)(q, r) = (1, 12)

    Première itération. À l’étape 2, on vérifie que 17≥517 \geq 5, de sorte que l’on exécute le corps : l’étape 3 retire un groupe de cinq et l’étape 4 le compte.

  3. (q,r)=(2,7)(q, r) = (2, 7)

    Deuxième itération : 12≥512 \geq 5, et le corps s’exécute de nouveau.

  4. (q,r)=(3,2)(q, r) = (3, 2)

    Troisième itération : 7≥57 \geq 5, et le corps s’exécute de nouveau.

  5. 17=5⋅3+2,0≤2<517 = 5 \cdot 3 + 2, \quad 0 \leq 2 < 5

    À l’étape 2, la condition échoue, parce que 2<52 < 5 : l’algorithme continue à l’étape 5 et rend q=3q = 3 et r=2r = 2. La sortie vérifie la postcondition.

Le tableau complet a douze lignes : l’étape 1, trois itérations de trois lignes chacune (étapes 2, 3 et 4), la vérification qui échoue et le rendu du résultat.

Deux observations sur le tableau. La première : le nombre d’itérations est égal au quotient, 33, et l’étape 2 a été exécutée une fois de plus que le corps, 44 fois ; cela arrive toujours dans une répétition de cette forme, parce que la dernière vérification est celle qui échoue. La seconde : la trace enseigne quelque chose que le texte de l’algorithme ne dit pas à première vue ; si a<ba < b, par exemple a=3a = 3 et b=5b = 5, la condition échoue dès la première fois, le corps ne s’exécute aucune fois, et la sortie est q=0q = 0, r=3r = 3, ce qui est correct, parce que 3=5⋅0+33 = 5 \cdot 0 + 3. Oublier le cas où la répétition ne s’exécute pas est l’une des erreurs les plus fréquentes dans la conception d’algorithmes, et la trace avec de petites entrées la révèle.

La trace d’un cas ne démontre pas que l’algorithme est correct : elle vérifie un cas. Mais une trace bien choisie (un cas courant, un cas limite, le cas où la répétition ne s’exécute pas) révèle la plupart des erreurs, et celle qui échoue dit où elles sont. La leçon Concevoir des boucles correctes apprendra à passer des cas à une démonstration.

Séquence, décision et répétition

Vus de près, les algorithmes se construisent avec très peu de pièces. Il y a des instructions élémentaires (donner une valeur à une variable, rendre un résultat) et trois manières de les combiner.

  • Séquence : une instruction après une autre, dans l’ordre. Les étapes 3 et 4 de la division sont une séquence : d’abord on soustrait, ensuite on compte.
  • Décision : selon qu’une condition est vérifiée ou non, on exécute une partie ou une autre (ou une partie ou rien). « Si y>my > m, mm prend la valeur yy » est une décision.
  • Répétition : tant qu’une condition est vérifiée, on exécute une partie encore et encore. L’étape 2 de la division, avec son corps, est une répétition.

Chaque forme a une seule entrée et une seule sortie, et son intérieur peut être à son tour n’importe laquelle des trois : le corps d’une répétition peut contenir des décisions, une branche d’une décision peut contenir une répétition, et ainsi de suite. S’il en est ainsi, avec les trois formes on construit des algorithmes de n’importe quelle complexité, comme avec quelques pièces qui s’emboîtent on construit n’importe quel édifice. Qu’elles suffisent à exprimer tout algorithme est un résultat connu de la théorie de la programmation (Corrado Böhm et Giuseppe Jacopini l’ont démontré en 1966) ; on ne le démontre pas ici, mais tout le cours le confirme : les programmes que l’on y écrira, aussi longs soient-ils, n’utiliseront rien d’autre que ces trois formes, écrites en C++ comme if pour la décision et while ou for pour la répétition, dans les leçons Décisions et Boucles.

Une notation utile pour voir la forme d’un algorithme est l’organigramme : chaque instruction est un rectangle, chaque condition un losange à deux sorties (vérifiée, que la figure étiquette 11, et non vérifiée, 00), et les flèches disent quelle est l’étape suivante. C’est une notation, non une fin : elle sert à embrasser d’un coup d’œil un petit algorithme, mais elle ne passe pas à l’échelle, et personne n’écrit un grand programme en le dessinant.

ExempleTrois formes et leur composition

La figure dessine les trois formes avec des instructions génériques AA et BB et une condition cc, puis les compose dans le diagramme de la division par soustractions successives.

Démonstration

  1. A;BA ; B

    Séquence : on exécute AA puis BB ; il y a une seule entrée et une seule sortie.

  2. c=1⇒A,c=0⇒Bc = 1 \Rightarrow A, \quad c = 0 \Rightarrow B

    Décision : si cc est vérifiée, on exécute AA ; sinon, BB. Les deux branches se rejoignent en une seule sortie.

  3. c,A,c,A,…,cc, A, c, A, \ldots, c

    Répétition : on évalue cc ; si elle est vérifiée, on exécute AA et l’on évalue de nouveau cc ; sinon, on sort. La condition est évaluée une fois de plus que le corps.

  4. a=b⋅q+r,0≤r<ba = b \cdot q + r, \quad 0 \leq r < b

    Composition : la division par soustractions successives est une séquence (les valeurs initiales, la répétition, le rendu du résultat) dont la répétition a pour corps une autre séquence (soustraire, compter).

Distinguer les trois formes dans une procédure écrite en langage courant est le premier pas pour la traduire. Le signe d’une décision est un « si », un « au cas où » ou un « selon » ; celui d’une répétition, un « tant que », un « jusqu’à ce que », un « pour chaque » ou un « autant de fois que ». Une procédure sans aucun de ces mots est une séquence, et si elle doit traiter des entrées de taille variable (une liste de nombres, un texte), il lui manque une répétition.

L’algorithme se termine-t-il ?

La finitude fait partie de la définition de l’algorithme, et une répétition est le seul endroit où elle peut se perdre : une séquence de cinq étapes exécute cinq étapes, et une décision exécute l’une de ses branches ; mais une répétition exécute son corps autant de fois que la condition est vérifiée, et rien ne garantit d’avance qu’elle cesse de l’être.

Pour justifier qu’une répétition se termine, on cherche une quantité, calculée à partir des variables, qui remplisse deux conditions : c’est un entier naturel (un entier positif ou nul) tant que la répétition continue, et elle décroît strictement à chaque itération. S’il en est ainsi, la répétition se termine : une suite d’entiers naturels strictement décroissante ne peut pas être infinie, parce que chaque itération la fait baisser d’au moins un et qu’à partir d’un entier naturel nn il n’y a pas plus de nn marches jusqu’à zéro. Cette quantité s’appelle un variant ou une fonction de borne, et la leçon Concevoir des boucles correctes l’utilisera de manière systématique.

ExempleLe variant de la division par soustractions

Dans la division par soustractions successives, le variant est rr. Tant que la répétition continue, r≥b>0r \geq b > 0, de sorte que rr est un entier naturel. À chaque itération, rr prend la valeur r−br - b, et comme b>0b > 0 (précondition), la nouvelle valeur est strictement plus petite. Par conséquent, la répétition se termine, et elle le fait au plus après aa itérations (en fait, exactement après qq itérations, le quotient).

On remarquera où l’on a utilisé la précondition b>0b > 0. Si l’on exécute l’algorithme avec b=0b = 0, l’étape 3 laisse rr inchangé, la condition r≥0r \geq 0 est toujours vérifiée et la répétition ne se termine jamais : l’algorithme n’est pas faux, on lui a donné une entrée que le contrat excluait. Et si bb était strictement négatif, rr croîtrait à chaque itération. La précondition n’est pas un ornement : c’est exactement ce qu’il faut pour que le raisonnement fonctionne.

Les deux conditions du variant sont nécessaires, et il est facile de se tromper sur l’une ou l’autre. Soit cette procédure, qui prétend décider si un entier naturel nn est pair : « tant que n≠0n \neq 0, nn prend la valeur n−2n - 2 ; si l’on arrive à zéro, nn était pair ». La quantité nn décroît à chaque itération, et pourtant la procédure ne se termine pas si nn est impair : avec n=5n = 5, elle passe par 33, 11, −1-1, −3-3, et la condition n≠0n \neq 0 n’échoue jamais. Ce qui n’est pas rempli, c’est l’autre condition : nn cesse d’être un entier naturel, parce que la condition de la répétition ne l’empêche pas de descendre sous zéro. La correction consiste à répéter tant que n≥2n \geq 2 et à décider à la fin selon la valeur qui reste, 00 ou 11 ; maintenant nn est un entier naturel à chaque itération, parce qu’on ne soustrait que lorsque n≥2n \geq 2, et il décroît : la procédure se termine.

Décider si une répétition se termine n’est pas toujours facile. Un exemple célèbre : en partant d’un entier naturel n≥1n \geq 1, tant que n≠1n \neq 1, si nn est pair on le divise par deux, et s’il est impair on le remplace par 3n+13n + 1. Avec n=6n = 6, la suite est 6,3,10,5,16,8,4,2,16, 3, 10, 5, 16, 8, 4, 2, 1, et elle se termine. Personne n’a trouvé de nn pour lequel elle ne se termine pas, et on l’a vérifié par ordinateur pour des quantités énormes de valeurs ; mais personne n’a réussi à démontrer qu’elle se termine toujours : c’est la conjecture de Collatz, un problème ouvert des mathématiques. Aucune quantité connue ne décroît à chaque itération. La morale, pour qui programme, est modeste et ferme : un algorithme dont on ne sait pas justifier la terminaison n’est pas terminé.

Erreurs fréquentes

  • Étapes ambiguës. « Répéter plusieurs fois », « jusqu’à ce que ce soit bien », « prendre un nombre adéquat ». Chaque étape doit pouvoir s’exécuter sans comprendre le problème ; sinon, elle est mal écrite.
  • Oublier le cas où la répétition ne s’exécute pas. Toute répétition dont la condition est au début peut exécuter son corps zéro fois. Il faut toujours se demander ce que rend l’algorithme dans ce cas, et la trace avec une entrée minimale y répond.
  • Spécifier par un exemple. « Pour 17 et 5, cela donne 3 et 2 » n’est pas une spécification ; la relation s’écrit comme une condition générale, et l’exemple l’illustre.
  • Confondre l’algorithme avec son programme. Un programme qui échoue peut avoir un algorithme faux ou une écriture fausse. Qui n’a pas écrit l’algorithme avant ne sait pas laquelle des deux choses chercher.
  • Tenir la terminaison pour acquise. Qu’une quantité décroisse ne suffit pas : elle doit en outre être un entier naturel tant que l’on répète.

Problèmes résolus

Problème résolu 1Le plus grand de trois nombres, avec égalités

Écrire un algorithme qui vérifie la spécification du maximum de trois nombres donnée au début de la leçon, et le suivre à la main pour les entrées (7,2,5)(7, 2, 5) et (4,9,9)(4, 9, 9).

Solution

  1. m=xm = x

    Idée : on parcourt l’entrée en gardant le plus grand vu jusqu’ici dans une variable mm. Première étape : mm prend la valeur xx. Après elle, mm est le maximum des données vues, qui se réduisent à xx.

  2. m=max⁡(x,y,z)m = \max(x, y, z)

    Deuxième étape : si y>my > m, mm prend la valeur yy. Troisième étape : si z>mz > m, mm prend la valeur zz. Quatrième étape : rendre mm. Ce sont trois instructions en séquence, dont deux sont des décisions sans branche alternative.

  3. (x,y,z)=(7,2,5)⇒m=7(x, y, z) = (7, 2, 5) \Rightarrow m = 7

    Trace pour (7,2,5)(7, 2, 5) : après la première étape, m=7m = 7 ; à la deuxième, 2>72 > 7 est faux et mm ne change pas ; à la troisième, 5>75 > 7 est faux et mm ne change pas. On rend 77.

  4. (x,y,z)=(4,9,9)⇒m=9(x, y, z) = (4, 9, 9) \Rightarrow m = 9

    Trace pour (4,9,9)(4, 9, 9) : après la première étape, m=4m = 4 ; à la deuxième, 9>49 > 4 et mm passe à 99 ; à la troisième, 9>99 > 9 est faux et mm ne change pas. On rend 99, ce qui est correct : l’égalité n’exige aucun traitement particulier.

  5. m≥x,m≥y,m≥zm \geq x, \quad m \geq y, \quad m \geq z

    Justification : après chaque étape, mm est l’une des données vues et aucune d’elles ne le dépasse. C’est vrai après la première, et chaque décision le conserve : si la nouvelle donnée est plus grande, elle devient mm ; sinon, elle ne dépasse pas mm. À la fin, on a vu les trois, et mm vérifie la postcondition.

  6. (x,y,z)=(4,9,9)⇒p∈{2,3}(x, y, z) = (4, 9, 9) \Rightarrow p \in \{2, 3\}

    Si l’on avait écrit y≥my \geq m au lieu de y>my > m, le résultat serait le même, parce qu’en cas d’égalité on affecte une valeur égale. Le choix entre >> et ≥\geq importerait si l’on demandait aussi la position du maximum : avec >>, on rend la première des positions à égalité, et avec ≥\geq la dernière ; la formule appelle cette position pp.

Problème résolu 2L’année bissextile, corrigée par un contre-exemple

Dans le calendrier grégorien, une année est bissextile si elle est divisible par 44, sauf celles divisibles par 100100, qui ne le sont que si elles sont en outre divisibles par 400400. Un étudiant propose l’algorithme « si l’année est divisible par 44, rendre oui ; sinon, rendre non ». Trouver une année pour laquelle il échoue et écrire un algorithme correct, avec sa spécification.

Solution

  1. (4∣n∧¬(100∣n))∨400∣n(4 \mid n \wedge \neg (100 \mid n)) \vee 400 \mid n

    Spécification. Entrée : un entier n≥1583n \geq 1583 (le calendrier grégorien est en vigueur depuis octobre 1582). Sortie : oui ou non. Relation : la sortie est oui exactement quand nn est divisible par 44 et non par 100100, ou est divisible par 400400.

  2. 1900=4⋅475,1900=400⋅4+3001900 = 4 \cdot 475, \quad 1900 = 400 \cdot 4 + 300

    L’algorithme proposé est juste pour la plupart des années, et c’est pourquoi il paraît correct. Pour le réfuter, il suffit d’une année divisible par 100100 et non par 400400 : 19001900 est divisible par 44, et l’algorithme dit oui ; mais 19001900 n’a pas été bissextile.

  3. 400∣n,100∣n,4∣n400 \mid n, \quad 100 \mid n, \quad 4 \mid n

    Algorithme corrigé, en trois décisions enchaînées. Étape 1 : si nn est divisible par 400400, rendre oui et terminer. Étape 2 : si nn est divisible par 100100, rendre non et terminer. Étape 3 : si nn est divisible par 44, rendre oui ; sinon, rendre non.

  4. 2000,1900,2024,20232000, \quad 1900, \quad 2024, \quad 2023

    L’ordre des questions est essentiel : elles vont de la règle la plus particulière à la plus générale. Si l’on posait d’abord la question du 44, l’année 19001900 recevrait oui avant d’arriver à l’exception. Trace pour quatre années, une par cas : 20002000 (étape 1 : oui), 19001900 (étape 2 : non), 20242024 (étape 3 : oui), 20232023 (étape 3 : non).

  5. 4∣1900∧¬(400∣1900)4 \mid 1900 \wedge \neg (400 \mid 1900)

    L’enseignement ne concerne pas seulement les calendriers : un algorithme se réfute par un seul contre-exemple, et les contre-exemples se cherchent aux frontières des règles, non dans les cas courants. Essayer 20242024, 20202020 et 20162016 n’aurait rien révélé.

Problème résolu 3Combien d’itérations fait la division par soustractions

Suivre à la main la division par soustractions successives avec a=20a = 20 et b=5b = 5, et avec a=0a = 0 et b=7b = 7. Ensuite, justifier que pour toute entrée admissible l’algorithme se termine après exactement qq itérations et que sa sortie vérifie la postcondition.

Solution

  1. 20=5⋅4+020 = 5 \cdot 4 + 0

    Trace avec a=20a = 20, b=5b = 5 : rr passe par 2020, 1515, 1010, 55, 00, et qq par 00, 11, 22, 33, 44. La cinquième vérification, 0≥50 \geq 5, échoue. Sortie : q=4q = 4, r=0r = 0. Ici, le cas limite est la division exacte : la quatrième itération a lieu parce que 5≥55 \geq 5, et c’est pourquoi la condition doit être ≥\geq et non >>.

  2. 0=7⋅0+00 = 7 \cdot 0 + 0

    Trace avec a=0a = 0, b=7b = 7 : la première vérification, 0≥70 \geq 7, échoue ; le corps ne s’exécute aucune fois. Sortie : q=0q = 0, r=0r = 0, ce qui est correct.

  3. b⋅(q+1)+(r−b)=b⋅q+r=a\dato{conserva}{b \cdot (q + 1) + (r - b) = b \cdot q + r = a}

    Une relation qui se conserve. Avant la première itération, b⋅q+r=b⋅0+a=ab \cdot q + r = b \cdot 0 + a = a. À chaque itération, rr diminue de bb et qq augmente de 11, de sorte que b⋅q+rb \cdot q + r ne change pas. Par conséquent, l’égalité est vraie après chaque itération.

  4. r0>r1>r2>⋯≥0r_0 > r_1 > r_2 > \cdots \geq 0

    Terminaison. Tant que l’on répète, r≥b>0r \geq b > 0, et rr est un entier naturel ; à chaque itération, il diminue de b≥1b \geq 1. Une suite strictement décroissante d’entiers naturels est finie : la répétition se termine.

  5. a=b⋅q+r,0≤r<ba = b \cdot q + r, \quad 0 \leq r < b

    À la fin, la condition a échoué : r<br < b. De plus r≥0r \geq 0, parce qu’on ne soustrait bb que lorsque r≥br \geq b. Avec la relation qui se conserve, la sortie vérifie la postcondition ; et comme qq compte les itérations, le nombre d’itérations est le quotient.

Exercices en ligne

Exercice 1Compter les itérations

Suivre à la main l’algorithme suivant et écrire dans la case la valeur qu’il rend, seulement le nombre.

  1. nn prend la valeur 2929, et kk prend la valeur 00.
  2. Tant que n>1n > 1 est vérifiée, répéter les étapes 3 et 4 ; quand elle cesse de l’être, continuer à l’étape 5.
  3. Si nn est pair, nn prend la valeur n/2n / 2 ; s’il est impair, nn prend la valeur n+1n + 1.
  4. kk prend la valeur k+1k + 1 ; revenir à l’étape 2.
  5. Rendre kk.

Le navigateur vérifie ta réponse instantanément, sans la noter.

Indice

Faites le tableau : nn passe de 2929 à 3030 à la première itération. Comptez une itération pour chaque changement de nn, jusqu’à ce que nn vaille 11.

Exercice 2La condition qui manque

L’algorithme suivant, écrit sous la forme abrégée, doit calculer le quotient et le reste de la division par soustractions successives, mais il lui manque la condition de la répétition. Remplacer la ligne de tirets bas par la condition correcte, sans changer le reste.

Le navigateur vérifie ta réponse instantanément, sans la noter.

Indice

Suivez à la main l’algorithme avec a=10a = 10 et b=5b = 5 : la deuxième itération doit avoir lieu même si le reste vaut exactement 55.

Problèmes proposés

Chaque problème a une indication, qu’il vaut mieux n’ouvrir qu’après avoir cherché, et une réponse, qui apparaît après l’ouverture de l’indication.

Problème proposé 1Spécifier l’ordre de trois nombres

Écrire la spécification du problème « ordonner trois nombres » (entrée, sortie et relation) et signaler quelles décisions elle oblige à prendre que l’énoncé ne prenait pas. Ensuite, vérifier que la spécification écarte ces deux sorties pour l’entrée (5,2,5)(5, 2, 5) : (2,5,6)(2, 5, 6) et (2,5,5,5)(2, 5, 5, 5), et qu’elle admet (2,5,5)(2, 5, 5).

Indice

Une sortie « ordonnée » peut n’avoir rien à voir avec l’entrée. Quelle condition lie les nombres de la sortie à ceux de l’entrée, répétitions comprises ? Croissant ou décroissant ? Strict ou non ?

Réponse

Entrée : trois nombres xx, yy, zz. Sortie : trois nombres uu, vv, ww. Relation : u≤v≤wu \leq v \leq w, et (u,v,w)(u, v, w) est un réarrangement de (x,y,z)(x, y, z), c’est-à-dire qu’il contient les mêmes valeurs avec les mêmes répétitions. Décisions que l’énoncé ne prenait pas : le sens (ici, du plus petit au plus grand) ; que l’ordre n’est pas strict (≤\leq et non <<, parce qu’avec des valeurs répétées l’ordre strict est impossible) ; et que la sortie doit être un réarrangement. (2,5,6)(2, 5, 6) est ordonné mais contient un 66 qui n’y était pas ; (2,5,5,5)(2, 5, 5, 5) a quatre valeurs ; (2,5,5)(2, 5, 5) remplit les deux conditions. Sans la condition de réarrangement, (0,0,0)(0, 0, 0) serait une sortie correcte pour n’importe quelle entrée.

Problème proposé 2Les chiffres d’un nombre, de droite à gauche

Écrire un algorithme en étapes numérotées qui reçoive un entier naturel nn et rende ses chiffres du dernier au premier (pour 40724072, il rend 22, 77, 00, 44), en n’utilisant que le quotient et le reste de la division par 1010. Le suivre à la main pour 40724072 et pour 00, et justifier qu’il se termine pour tout nn.

Indice

Le reste de la division par 1010 est le dernier chiffre, et le quotient est le nombre sans lui. Décidez si la condition se pose avant ou après avoir rendu un chiffre, et regardez ce qui arrive avec n=0n = 0, qui a un chiffre.

Réponse

Un algorithme correct : (1) rendre le reste de la division de nn par 1010 ; (2) nn prend la valeur du quotient de la division de nn par 1010 ; (3) si n>0n > 0, revenir à l’étape 1 ; sinon, terminer. La question vient après le corps, de sorte que le corps s’exécute au moins une fois : pour n=0n = 0, il rend 00, qui est son unique chiffre. Si l’on posait la question au début (« tant que n>0n > 0 »), pour n=0n = 0 on ne rendrait rien ; c’est l’erreur qui consiste à oublier le cas où la répétition ne s’exécute pas. Trace pour 40724072 : on rend 22, 77, 00, 44, et nn passe par 407407, 4040, 44, 00. Terminaison : le variant est nn, un entier naturel ; si l’on revient à l’étape 1, c’est parce que n>0n > 0, et pour n≥1n \geq 1 le quotient par 1010 est strictement plus petit que nn, de sorte que nn décroît strictement à chaque itération.

Problème proposé 3Rendre la monnaie en pièces

On dispose de pièces de 5050, 1010 et 11 unités, en quantité illimitée. Spécification : en entrée, un entier naturel mm ; en sortie, trois entiers naturels c50c_{50}, c10c_{10}, c1c_1 avec 50c50+10c10+c1=m50 c_{50} + 10 c_{10} + c_1 = m et le plus petit nombre possible de pièces, c50+c10+c1c_{50} + c_{10} + c_1. Quelqu’un propose l’algorithme suivant. Trouver une entrée pour laquelle il échoue, expliquer chaque défaut et le corriger.

  1. c50c_{50}, c10c_{10} et c1c_1 prennent la valeur 00.
  2. Si m≥50m \geq 50, c50c_{50} prend la valeur c50+1c_{50} + 1 et mm prend la valeur m−50m - 50.
  3. Tant que m>10m > 10 : c10c_{10} prend la valeur c10+1c_{10} + 1 et mm prend la valeur m−10m - 10.
  4. Tant que m>0m > 0 : c1c_1 prend la valeur c1+1c_1 + 1 et mm prend la valeur m−1m - 1.
  5. Rendre c50c_{50}, c10c_{10} et c1c_1.
Indice

La somme des pièces vaut toujours mm ; ce qui peut échouer, c’est leur nombre. Essayez un multiple exact de 1010 et un montant de plus de 100100, et demandez-vous quelle façon de combiner des étapes utilise l’étape 2 et laquelle elle devrait utiliser.

Réponse

Il y a deux défauts. Premièrement, l’étape 3 utilise m>10m > 10 : avec m=10m = 10, elle ne rend aucune pièce de 1010 et rend dix pièces de 11, au lieu d’une. Deuxièmement, l’étape 2 est une décision là où il faut une répétition : avec m=120m = 120, l’algorithme tel qu’il est écrit rend une pièce de 5050, six de 1010 et dix de 11 (dix-sept pièces), et même avec l’étape 3 déjà corrigée il rendrait une pièce de 5050 et sept de 1010 (huit), au lieu de deux de 5050 et deux de 1010 (quatre). L’entrée m=100m = 100 montre les deux défauts : l’algorithme rend une pièce de 5050, quatre de 1010 et dix de 11, quinze pièces, au lieu de deux. Correction : étape 2, « tant que m≥50m \geq 50, … » ; étape 3, « tant que m≥10m \geq 10, … ». Ainsi corrigé, l’algorithme prend toujours la plus grande pièce qui tient, et avec ces trois pièces cela donne le plus petit nombre : le chiffre des unités de mm ne peut se payer qu’avec des pièces de 11, et de ce qui reste il convient de payer le plus possible avec des pièces de 5050, parce que cinq de 1010 valent autant qu’une de 5050. Avec d’autres pièces, par exemple de 11, 33 et 44 pour m=6m = 6, prendre toujours la plus grande ne donne plus le minimum (4+1+14 + 1 + 1 contre 3+33 + 3).