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
- Spécifier un problème simple en indiquant ses entrées, ses sorties et les conditions qu’elles doivent remplir.
- Décrire un algorithme en étapes numérotées, sans ambiguïté, avec des décisions et des répétitions explicites.
- Suivre à la main un algorithme avec un tableau de suivi et déterminer son résultat pour une entrée donnée.
- 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.
Énoncé courant : « trouver le plus grand de trois nombres ». Spécification :
- Entrée : trois nombres , , (entiers ou décimaux, sans autre condition).
- Sortie : un nombre .
- Relation : est l’une des trois données, et aucune d’elles n’est plus grande que ; c’est-à-dire , et .
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 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 , la sortie est , 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 et , tout le monde est d’accord : le quotient et le reste sont les seuls entiers qui vérifient
Ainsi, pour et , ce sont et , parce que . Mais l’énoncé « diviser deux entiers » ne dit pas que faire si est strictement négatif, et la question n’est pas oiseuse : pour et , il y a deux réponses raisonnables.
Première spécification : le reste n’est jamais strictement négatif, . Alors , et la sortie est , . 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 se tronque en , et : la sortie est , .
Les deux vérifient ; 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 , 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 : avec , il n’existe pas de et de 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 ou , et s’appellent des variables. On écrit « prend la valeur » pour dire que l’on calcule avec la valeur actuelle de et que le résultat devient la nouvelle valeur de ; 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.
Le problème est celui que l’on a spécifié dans la section précédente : en entrée, deux entiers et ; en sortie, deux entiers et avec et . L’idée est celle du partage : on retire de des groupes de tant que c’est possible, on compte les groupes retirés, et ce qui reste est le reste.
- prend la valeur , et prend la valeur .
- Tant que est vérifiée, répéter les étapes 3 et 4 ; quand elle cesse de l’être, continuer à l’étape 5.
- prend la valeur .
- prend la valeur ; revenir à l’étape 2.
- Rendre et , 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.
On suit la division par soustractions successives avec et . 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
Étape 1 : on donne les valeurs initiales. L’entrée vérifie la précondition, parce que et .
Première itération. À l’étape 2, on vérifie que , de sorte que l’on exécute le corps : l’étape 3 retire un groupe de cinq et l’étape 4 le compte.
Deuxième itération : , et le corps s’exécute de nouveau.
Troisième itération : , et le corps s’exécute de nouveau.
À l’étape 2, la condition échoue, parce que : l’algorithme continue à l’étape 5 et rend et . 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, , et l’étape 2 a été exécutée une fois de plus que le corps, 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 , par exemple et , la condition échoue dès la première fois, le corps ne s’exécute aucune fois, et la sortie est , , ce qui est correct, parce que . 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 , prend la valeur » 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 , et non vérifiée, ), 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.
La figure dessine les trois formes avec des instructions génériques et et une condition , puis les compose dans le diagramme de la division par soustractions successives.
Démonstration
Séquence : on exécute puis ; il y a une seule entrée et une seule sortie.
Décision : si est vérifiée, on exécute ; sinon, . Les deux branches se rejoignent en une seule sortie.
Répétition : on évalue ; si elle est vérifiée, on exécute et l’on évalue de nouveau ; sinon, on sort. La condition est évaluée une fois de plus que le corps.
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 il n’y a pas plus de 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.
Dans la division par soustractions successives, le variant est . Tant que la répétition continue, , de sorte que est un entier naturel. À chaque itération, prend la valeur , et comme (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 itérations (en fait, exactement après itérations, le quotient).
On remarquera où l’on a utilisé la précondition . Si l’on exécute l’algorithme avec , l’étape 3 laisse inchangé, la condition 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 était strictement négatif, 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 est pair : « tant que , prend la valeur ; si l’on arrive à zéro, était pair ». La quantité décroît à chaque itération, et pourtant la procédure ne se termine pas si est impair : avec , elle passe par , , , , et la condition n’échoue jamais. Ce qui n’est pas rempli, c’est l’autre condition : 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 et à décider à la fin selon la valeur qui reste, ou ; maintenant est un entier naturel à chaque itération, parce qu’on ne soustrait que lorsque , 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 , tant que , si est pair on le divise par deux, et s’il est impair on le remplace par . Avec , la suite est , et elle se termine. Personne n’a trouvé de 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
É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 et .
Solution
Idée : on parcourt l’entrée en gardant le plus grand vu jusqu’ici dans une variable . Première étape : prend la valeur . Après elle, est le maximum des données vues, qui se réduisent à .
Deuxième étape : si , prend la valeur . Troisième étape : si , prend la valeur . Quatrième étape : rendre . Ce sont trois instructions en séquence, dont deux sont des décisions sans branche alternative.
Trace pour : après la première étape, ; à la deuxième, est faux et ne change pas ; à la troisième, est faux et ne change pas. On rend .
Trace pour : après la première étape, ; à la deuxième, et passe à ; à la troisième, est faux et ne change pas. On rend , ce qui est correct : l’égalité n’exige aucun traitement particulier.
Justification : après chaque étape, 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 ; sinon, elle ne dépasse pas . À la fin, on a vu les trois, et vérifie la postcondition.
Si l’on avait écrit au lieu de , le résultat serait le même, parce qu’en cas d’égalité on affecte une valeur égale. Le choix entre et importerait si l’on demandait aussi la position du maximum : avec , on rend la première des positions à égalité, et avec la dernière ; la formule appelle cette position .
Dans le calendrier grégorien, une année est bissextile si elle est divisible par , sauf celles divisibles par , qui ne le sont que si elles sont en outre divisibles par . Un étudiant propose l’algorithme « si l’année est divisible par , rendre oui ; sinon, rendre non ». Trouver une année pour laquelle il échoue et écrire un algorithme correct, avec sa spécification.
Solution
Spécification. Entrée : un entier (le calendrier grégorien est en vigueur depuis octobre 1582). Sortie : oui ou non. Relation : la sortie est oui exactement quand est divisible par et non par , ou est divisible par .
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 et non par : est divisible par , et l’algorithme dit oui ; mais n’a pas été bissextile.
Algorithme corrigé, en trois décisions enchaînées. Étape 1 : si est divisible par , rendre oui et terminer. Étape 2 : si est divisible par , rendre non et terminer. Étape 3 : si est divisible par , rendre oui ; sinon, rendre non.
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 , l’année recevrait oui avant d’arriver à l’exception. Trace pour quatre années, une par cas : (étape 1 : oui), (étape 2 : non), (étape 3 : oui), (étape 3 : non).
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 , et n’aurait rien révélé.
Suivre à la main la division par soustractions successives avec et , et avec et . Ensuite, justifier que pour toute entrée admissible l’algorithme se termine après exactement itérations et que sa sortie vérifie la postcondition.
Solution
Trace avec , : passe par , , , , , et par , , , , . La cinquième vérification, , échoue. Sortie : , . Ici, le cas limite est la division exacte : la quatrième itération a lieu parce que , et c’est pourquoi la condition doit être et non .
Trace avec , : la première vérification, , échoue ; le corps ne s’exécute aucune fois. Sortie : , , ce qui est correct.
Une relation qui se conserve. Avant la première itération, . À chaque itération, diminue de et augmente de , de sorte que ne change pas. Par conséquent, l’égalité est vraie après chaque itération.
Terminaison. Tant que l’on répète, , et est un entier naturel ; à chaque itération, il diminue de . Une suite strictement décroissante d’entiers naturels est finie : la répétition se termine.
À la fin, la condition a échoué : . De plus , parce qu’on ne soustrait que lorsque . Avec la relation qui se conserve, la sortie vérifie la postcondition ; et comme compte les itérations, le nombre d’itérations est le quotient.
Exercices en ligne
Suivre à la main l’algorithme suivant et écrire dans la case la valeur qu’il rend, seulement le nombre.
- prend la valeur , et prend la valeur .
- Tant que est vérifiée, répéter les étapes 3 et 4 ; quand elle cesse de l’être, continuer à l’étape 5.
- Si est pair, prend la valeur ; s’il est impair, prend la valeur .
- prend la valeur ; revenir à l’étape 2.
- Rendre .
Le navigateur vérifie ta réponse instantanément, sans la noter.
Indice
Faites le tableau : passe de à à la première itération. Comptez une itération pour chaque changement de , jusqu’à ce que vaille .
Voir la solution
7L’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 et : la deuxième itération doit avoir lieu même si le reste vaut exactement .
Voir la solution
Entrée : a (entier, a >= 0), b (entier, b > 0)1. q <- 02. r <- a3. tant que r >= b répéter : r <- r - b q <- q + 14. rendre q, rProblè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.
É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 : et , et qu’elle admet .
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 , , . Sortie : trois nombres , , . Relation : , et est un réarrangement de , 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 ( 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. est ordonné mais contient un qui n’y était pas ; a quatre valeurs ; remplit les deux conditions. Sans la condition de réarrangement, serait une sortie correcte pour n’importe quelle entrée.
Écrire un algorithme en étapes numérotées qui reçoive un entier naturel et rende ses chiffres du dernier au premier (pour , il rend , , , ), en n’utilisant que le quotient et le reste de la division par . Le suivre à la main pour et pour , et justifier qu’il se termine pour tout .
Indice
Le reste de la division par 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 , qui a un chiffre.
Réponse
Un algorithme correct : (1) rendre le reste de la division de par ; (2) prend la valeur du quotient de la division de par ; (3) si , 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 , il rend , qui est son unique chiffre. Si l’on posait la question au début (« tant que »), pour 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 : on rend , , , , et passe par , , , . Terminaison : le variant est , un entier naturel ; si l’on revient à l’étape 1, c’est parce que , et pour le quotient par est strictement plus petit que , de sorte que décroît strictement à chaque itération.
On dispose de pièces de , et unités, en quantité illimitée. Spécification : en entrée, un entier naturel ; en sortie, trois entiers naturels , , avec et le plus petit nombre possible de pièces, . Quelqu’un propose l’algorithme suivant. Trouver une entrée pour laquelle il échoue, expliquer chaque défaut et le corriger.
- , et prennent la valeur .
- Si , prend la valeur et prend la valeur .
- Tant que : prend la valeur et prend la valeur .
- Tant que : prend la valeur et prend la valeur .
- Rendre , et .
Indice
La somme des pièces vaut toujours ; ce qui peut échouer, c’est leur nombre. Essayez un multiple exact de et un montant de plus de , 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 : avec , elle ne rend aucune pièce de et rend dix pièces de , au lieu d’une. Deuxièmement, l’étape 2 est une décision là où il faut une répétition : avec , l’algorithme tel qu’il est écrit rend une pièce de , six de et dix de (dix-sept pièces), et même avec l’étape 3 déjà corrigée il rendrait une pièce de et sept de (huit), au lieu de deux de et deux de (quatre). L’entrée montre les deux défauts : l’algorithme rend une pièce de , quatre de et dix de , quinze pièces, au lieu de deux. Correction : étape 2, « tant que , … » ; étape 3, « tant que , … ». 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 ne peut se payer qu’avec des pièces de , et de ce qui reste il convient de payer le plus possible avec des pièces de , parce que cinq de valent autant qu’une de . Avec d’autres pièces, par exemple de , et pour , prendre toujours la plus grande ne donne plus le minimum ( contre ).