• Aucun résultat trouvé

Article pp.691-699 du Vol.25 n°5 (2006)

N/A
N/A
Protected

Academic year: 2022

Partager "Article pp.691-699 du Vol.25 n°5 (2006)"

Copied!
9
0
0

Texte intégral

(1)

Algorithmes et algorithmique : relire les classiques

Ma boîte aux lettres se remplit régulièrement d’ouvrages consacrés à l’introduction à l’algorithmique, envoyés par les éditeurs pour examen en vue d’une adoption comme manuel de cours ou de référence. La lecture de ces ouvrages engendre trop souvent la nostalgie de certains « grands classiques » qui ne sont plus disponibles ni réédités. Pourquoi ? Parce que trop d’auteurs semblent avoir abandonné les chemins prometteurs débroussaillés dans les premiers temps de l’informatique au profit de présentations simplifiées gommant tous les défis intellectuels liés à la conception des algorithmes. Certains auteurs de textes récents ont, nous semble-t-il, tendance à sous-estimer les capacités des étudiants.

Un texte d’introduction à une discipline doit mettre en exergue une série de repères, qui permettront, d’une part, de pouvoir résoudre d’une manière créative les problèmes qui relèvent du domaine, et d’autre part, de disposer de concepts pour évaluer les évolutions futures du domaine. Si ces repères sont absents ou s’ils sont introduits d’une manière superficielle, les aspects les plus structurants du domaine ne marqueront pas le lecteur qui risque de ne plus être guidé que par la nouveauté.

Comme par l’espérance que ses problèmes actuels de conception de solutions seront sans doute régler par le « prochain langage », par la « prochaine librairie » de fonctions préprogrammées.

1. Exemple de sous-estimation des capacités des étudiants

Dans un texte d’introduction à l’algorithmique, le point crucial est la présentation de la commande de répétition. La présentation va-t-elle au delà de la simple explication de la syntaxe et de la sémantique opérationnelle de la commande ? Introduit-elle une méthode de conception d’une répétition ?

A titre d’exemple, considérons un ouvrage récent (publié en 2005), en langue française, destiné à un public d’étudiants de premier et second cycles (IUT, BTS, universités et écoles d’ingénieurs). Dès le début, l’auteur fait bien la distinction entre pseudo-langage et langage de programmation utilisé pour la mise en œuvre ; l’importance des performances est soulignée.

Pour la commande de répétition, l’auteur introduit une distinction utile entre les boucles déterministes, dont le nombre d’itérations est connu à l’avance, et les boucles non déterministes, dont le nombre d’itérations dépendra d’une condition booléenne. Ensuite, abordant la compréhension du fonctionnement d’une boucle,

(2)

l’auteur – comme c’est souvent le cas dans les ouvrages récents – ne fait pas grande confiance dans les capacités d’abstractions des étudiants, et ne tente pas non plus de leur faire découvrir l’intérêt de l’abstraction :

La construction d’une boucle suppose souvent que l’on ajuste les valeurs initiales et le test booléen en la faisant tourner à la main. Cette opération consiste à simuler l’exécution de la boucle à la place de l’ordinateur, et à noter à chaque itération les valeurs des différentes variables. On a souvent recours à ce système pour comprendre le dysfonctionnement d’une instruction répétitive, lors d’un développement. Il permet également de comprendre quel calcul effectue une boucle écrite par un autre. C’est la méthode utilisée pour assimiler les algorithmes rencontrés dans la littérature informatique. ... Faire tourner une boucle à la main est le seul moyen fiable pour la comprendre ou la corriger. Il faut appliquer

« mécaniquement » les instructions, et comparer leurs résultats avec les valeurs espérées à chaque itération, ou à la fin de la boucle. Les différences permettent de trouver les instructions défaillantes.

Que la technique « faire tourner une boucle à la main » soit proposée ne me gêne pas trop ; mais, je ne peux accepter que cette technique soit présentée comme le seul moyen fiable pour comprendre une boucle !

Pour construire une boucle, l’auteur distingue trois phases : la constitution du corps de boucle, la définition des valeurs initiales et l’écriture du test d’arrêt. Pour illustrer la méthode, un algorithme de calcul de la m-ième puissance de x est développé.

Pour le corps de boucle, la méthode est de trouver comme passer de l’étape N à l’étape N+1, en supposant connu le résultat de l’étape N. Dans l’exemple, on trouve la commande « résultat := résultat * x », à laquelle on associe un compteur pour repérer le nombre d’itérations. D’où le corps de boucle :

– compteur := compteur +1 – résultat := résultat * x.

Pour définir les commandes initiales, il est suggéré de trouver quelles sont les valeurs requises pour obtenir le résultat attendu à la fin de la première itération, c’est-à-dire x1, avec un compteur d’itérations à la valeur unitaire. Pour obtenir ces valeurs avec les deux commandes composées précédemment, il faut partir respectivement de 0 et de 1 pour « compteur » et pour « résultat ». D’où les commandes d’initialisation :

– compteur := 0 – résultat := 1.

Pour le test d’arrêt, la méthode proposée est de choisir un test a priori et de faire tourner la boucle à la main pour vérifier le résultat.

Comme premier essai : (compteur ” P

(3)

En simulant le calcul de x3, on constate (en faisant tourner la boucle à la main) que l’on obtient x4. Le test est donc modifié en (compteur < m).

La méthode proposée a deux gros défauts :

– la conception des commandes d’initialisation est liée à l’examen des commandes qui composent le corps de la boucle ;

– l’expression du test d’arrêt résulte de la vérification de tests candidats.

Cette présentation est surprenante ; en effet, elle gomme plus de trente années de réflexions méthodologiques en informatique. Elle préconise une méthode peu rigoureuse, peu systématique et intellectuellement peu satisfaisante. Il est étonnant que des notions aussi simples que l’invariant de boucle, connu depuis (Hoare, 1969), et la fonction de terminaison ne soient pas révélées aux étudiants.

2. Version utilisant l’invariant de boucle

A titre de comparaison, développons le même algorithme en utilisant la notion d’invariant de boucle (Hoare, 1969), (Wirth, 1973). Nous utilisons comme langage de conception d’algorithmes, le langage des commandes gardées (Dijkstra, 1976).

Rappelons que, dans la méthode de l’invariant de boucle de Hoare, étant donné l’objectif de la boucle exprimé par la postcondition R, il faut déterminer une condition P, l’invariant, qui est vérifiée aux points spécifiés dans le schéma de programme suivant :

Sinit {P} ; do B -> {B and P} S {P} od {P and non B}

Le choix de P et de B est guidé par l’identité : R = (P and non B).

Il suffit de montrer que P est vérifié : – après Sinit ;

– après S, en supposant que {B and P} est vrai.

Si c’est le cas, les deux autres occurrences de P dans le schéma seront vérifiées vu la sémantique opérationnelle de la commande.

Le but de l’algorithme à concevoir, le calcul de la M-ième puissance de x peut être exprimé par la postcondition :

R = (y = X M) où M et X sont des constantes données, (M • and X>0).

Comme invariant P, choisissons une forme affaiblie de la postcondition : (y = X m), où m est variable.

(4)

Le gardien B est immédiatement déduit de la relation R = (P and non B). En effet, connaissant P, on obtient R si la condition (m = M) est vérifiée, puisque :

(y = X M) = ((y = X m) and (m = M)).

On en déduit que le gardien est (m  0).

Les commandes d’initialisation doivent attribuer aux deux variables y et m des valeurs telles que l’invariant P est vérifié. Puisque la valeur minimale de M est zéro, nous pouvons initialiser avec les commandes y := 1 ; m := 0 qui respectent l’invariant.

Pour le corps de la boucle, il est clair qu’il faut y introduire une commande qui accroît la valeur de la variable m afin d’atteindre la valeur M à partir d’une valeur initiale de zéro. Il reste à définir comment rétablir l’invariant en fin d’itération si m est augmentée d’une unité. Pour retrouver l’invariant, il faut multiplier la valeur de y par X.

Plus de formalisme peut être introduit en utilisant les préconditions les plus faibles ; le calcul de wp (« y := y*X ; m := m+1 », y = X m) donne wp (« y := y*X ; m := m+1 », y = X m) = (y*X = X m+1) = (y = X m), condition garantie avant l’action de l’itération puisqu’elle est identique à l’invariant.

On obtient l’algorithme : y := 1 ; m := 0 {P}

; do (m  0) ->

{B and P}

y := y*X ; m := m+1 {P}

od {P and non B}

La terminaison peut être prouvée par la fonction t = (M - m). (Pour l’explication complète des détails de la méthode, voir (Dijkstra, 1976).)

La méthode de l’invariant a comme avantage :

– les commandes d’initialisation sont composées sans faire référence aux commandes présentes dans le corps de boucle ;

– le choix de la condition booléenne résulte d’un raisonnement sur la relation entre l’invariant et la postcondition ; ce qui élimine tout « essai et erreur ».

Ces deux aspects de l’intérêt de la méthode sont fréquemment ignorés, alors qu’ils en constituent l’un des points essentiels. De plus, il ne faut point perdre de vue l’intérêt de l’invariant comme documentation de la boucle ; il est en quelque sorte l’explication de la boucle.

(5)

3. Comparer les deux méthodes

La différence entre les deux méthodologies, l’une, celle de l’invariant basée sur un schéma de raisonnement, l’autre, celle du test par simulation des itérations, est très importante. Comme la composition des commandes qui forment l’itération ne semble pas très différente d’une méthode à l’autre, certains considèrent que la méthode de l’invariant de boucle introduit un formalisme excessif dans la conception d’une commande de répétition. De plus, comme de nombreux exemples d’applications de la méthode utilisent une formalisation mathématique de l’expression de l’invariant, le processus semble trop complexe et trop lourd à manipuler : une escouade de symboles pour exprimer des évidences. Il n’en est rien, car la méthode peut très bien être appliquée d’une manière semi-formelle, où l’invariant est exprimé par une expression précise en langage naturel. Si le concepteur suit bien l’idée de composer le schéma de répétition en respectant scrupuleusement la nécessité de garantir l’invariant aux endroits prévus, il bénéficiera d’un guide pour la composition. Il en résulte que des algorithmes élégants et efficaces émergent sans trop de difficultés.

A titre d’exemple, nous prendrons l’énoncé d’un exercice dont la résolution est donnée dans l’ouvrage précité, dans le contexte d’une série de problèmes destinés à conforter les étudiants dans la maîtrise de la conception d’algorithmes. Nous donnerons l’énoncé du problème, et sa résolution en suivant la méthode de l’invariant de boucle. Nous avons pu tester que des étudiants de première année (Bac+1), formés à la méthode de l’invariant, recréaient aisément la solution proposée ci-après. A titre de comparaison, nous donnerons la solution proposée dans l’ouvrage précité, en appliquant la méthode « faire tourner une boucle à la main ».

3.1. Le problème

Soit un entier positif X, (X > 0), on demande de déterminer si X est un nombre factoriel, c’est-à-dire s’il existe un entier n tel que (X = n!).

3.2. Schéma de résolution

Un solution évidente est de générer successivement les factorielles jusqu’à une factorielle qui jouera le rôle de sentinelle, c’est-à-dire qu’elle marquera la fin de la série. Cette factorielle sera soit égale ou supérieure à la valeur de X.

Rapprochant ce schéma de résolution du schéma d’une commande de répétition, nous voyons que nous avons établi une condition de terminaison de la boucle. Cette condition de terminaison nous donne une indication pour le choix du gardien de la boucle.

(6)

Utilisons une variable y pour contenir la valeur du plus récent élément généré dans la suite des factorielles ; cette variable respecte la condition (y = n!).

La variable y contient la factorielle « sentinelle » si la condition (y • X) est vraie.

Cette condition nous donne le gardien de la boucle : (y < X). En effet, quand la boucle se termine, la négation du gardien est vraie.

Nous pouvons maintenant composer la condition invariante qui va expliquer le principe de la commande de répétition : (y est la dernière factorielle générée) and (toutes les factorielles précédemment générées ont des valeurs inférieures à X)

En d’autres termes : (y = n!) and (Pour toutes les valeurs de k, avec (1” k < n), (k! < X)).

Les commandes d’initialisation doivent attribuer à y et à n des valeurs qui vérifient cet invariant. Nous devons simplement attribuer à y la valeur de la première factorielle de la série à générer, soit (1!). La deuxième partie de l’invariant est vérifiée ; en effet, nous avons une quantification universelle sur un intervalle vide pour k. D’où les commandes : n := 1 ; y := 1

Nous pouvons rassembler les divers éléments dans un premier schéma de programme :

n := 1 ; y := 1 {P}

; do (y < X) ->

{B and P}

« y devient la factorielle suivante dans la série » {P}

od {P and non B}

Quand l’itération est activée, la condition {B and P} est vérifiée, c’est-à-dire : {(y < X) and ((y = n!)and

(Pour toutes les valeurs de k, avec (1” k < n), (k! < X)))}.

Pour composer la commande « y devient la factorielle suivante dans la série », vu que (y = n!) est vérifiée avant l’activation, il nous suffit d’activer les deux commandes :

n := n +1 ; y := y *n

Vu le gardien (y < X), l’ancienne valeur de y vérifie la condition pour être ajoutée à la série des factorielles qui sont inférieures à X. Il en résulte que l’invariant est rétabli après l’itération.

(7)

En fin de boucle, le gardien est faux, soit (y • X). Il ne reste plus qu’à tester la valeur de y par rapport à X pour résoudre le problème posé. On obtient le programme complet suivant :

n := 1 ; y := 1 {P}

; do (y < X) ->

{B and P}

n := n +1 ; y := y *n {P}

od {P and non B}

; if (y >X) -> print (X, « n’est pas un nombre factoriel ») [] (y = X) -> print (X, « est égal à la factorielle de », n) fi

La terminaison de la boucle est évidente, vu la croissance de la valeur de y. Une fonction de terminaison est : t = (X - y).

Dans l’ouvrage précité, le programme suivant est obtenu, (nous le réécrivons dans notre métalangage des commandes gardées et en utilisant les mêmes noms de variables que ceux que nous avons utilisés) :

n := 0 ; y := 1 ; trouvé := false

; do (y ”X) and non trouvé ->

n := n +1 ; y := factorielle(n)

; if (y =X) -> trouvé := true [] (y X) -> skip

fi od

; if non trouvé -> print (X, « n’est pas un nombre factoriel ») [] trouvé -> print (X, « est égal à la factorielle de », n) fi

(Pour rappel, dans le langage des commandes gardées, « skip » est la commande

« vide », « sans effet ».)

Cette solution appelle trois remarques :

(8)

– la variable booléenne trouvé est inutile ; son introduction dans la solution résulte de l’absence de méthodologie de raisonnement pour la construction d’une boucle ; son introduction est clairement une conséquence d’un raisonnement intuitif ; – le gardien (y”X) est apparemment trop restrictif : l’égalité ne serait pas nécessaire. Mais elle est bien requise ; en effet, si elle est absente, le cas où (X = 1) ne serait pas correctement traité : la boucle ne serait pas activée, et trouvé garderait la valeur initiale false. Ce gardien est une conséquence d’une méthode qui implique de concevoir les commandes d’initialisation en fonction du contenu de l’itération ;

– l’utilisation d’une fonction « factorielle » a un effet négatif sur les performances. Dans le contexte du problème posé, il est important de montrer l’intérêt de l’exploitation de l’itération, c’est-à-dire d’exploiter le résultat obtenu à l’itération précédente.

4. Une triste constatation

L’algorithmique doit être enseignée comme une formation au raisonnement, à un type de raisonnement très utile dans la résolution de problèmes. Cette orientation était claire et faisait l’objet d’un large consensus, il y a une trentaine d’années ; l’algorithmique était même vue comme susceptible de prendre la place de la

« géométrie à la Euclide » dans la formation générale. Cet objectif n’a pas été atteint ; d’une part, les cours d’introduction à la programmation sont souvent des cours d’introduction à un langage de programmation particulier, avec toutes ses particularités ; d’autre part, dans les cours spécifiquement « algorithmique », l’étude mathématique de la complexité des algorithmes prend souvent le pas sur les méthodes de conception d’algorithmes, ou bien la conception d’algorithmes corrects est présentée dans un contexte tellement formel qu’elle semble impraticable.

A une époque où, de part le monde, tous les départements d’informatique constatent une diminution du nombre de leurs étudiants, il serait peut être utile de réfléchir à ce que nos étudiants apprennent vraiment. Nos programmes de cours sont- ils suffisamment orientés vers la formation au raisonnement ou glissent-ils trop vers l’accumulation de connaissances et recettes (les librairies à utiliser) ? A lire les supports de cours qui sont publiés, je crains que la deuxième branche de l’alternative soit la plus fréquente. Rappelez-vous que, dans l’ouvrage précité, l’auteur n’hésite pas à affirmer que « faire tourner une boucle à la main » est la méthode utilisée pour assimiler les algorithmes rencontrés dans la littérature informatique. Peut-on croire qu’un tel procédé puisse attirer les meilleurs étudiants quand ils le comparent à ce que l’étude de la physique ou des sciences du vivant peut leur offrir ? Il est temps de relire les « classiques » du domaine (par exemple, (Wirth 1973), (Wirth 1976), ou les ouvrages de J. Arsac (Arsac 1977), (Arsac 1991)), ceux d’une époque où les capacités des étudiants n’étaient pas sous-estimées.

REMARQUE. — Il ne m’a pas paru utile de donner la référence de l’ouvrage d’où sont issus les extraits utilisés dans cet article. En effet, cet ouvrage n’est pas unique

(9)

dans son genre ; le citer explicitement impliquerait de citer également d’autres textes qui présentent les mêmes travers.

5. Bibliographie

Arsac J., La construction de programmes structurés, Dunod, 1977.

Arsac J., Préceptes pour programmer, Dunod, 1991.

Dahl O.-J., Dijkstra E. W., Hoare C. A. R., Structured Programming, Academic Press, 1972.

Dijkstra E. W., A Discipline of Programming, Prentice-Hall, 1976.

Hoare C. A. R., “An axiomatic basis for computer programming”, Communications of ACM, vol. 12, n° 10, octobre 1969, p. 576-580, 583.

Wirth N., Systematic Programming: An Introduction, Prentice-Hall Series in Automatic Computation, 1973.

Wirth N., Algorithms + Data Structures = Programs, Prentice-Hall Series in Automatic Computation, 1976.

P.-A. de Marneffe Institut Montefiore, Université de Liège PA.deMarneffe@ulg.ac.be

Références

Documents relatifs

Celui des « Les virus émergents » a été rédigé par deux chercheurs de l’Institut Pasteur : Antoine Gessain, spé- cialiste de l’épidémiologie des virus oncogènes, et

23-27 JUILLET 2006 Glasgow (GB) BioScience 2006 www.bioscience2006.org. 29 JUILLET-1 er AOÛT 2006 Omaha,

M is en mots et en images de façon accessible, on voit quelques exemples de recherche fondamentale dont les applications ne sont pas évidentes immédiatement mais qui deviennent

B éatrice de Montera, biologiste de formation et impliquée dans l’éthique du clonage, s’efforce de tracer la limité entre « l’amélioration » (que l’on peut ima- giner,

14-15 MARS 2006 Palais des Congrès, Paris Profession : Bio-Entrepreneur 2006, Entreprendre en biotech santé. Ces journées sont organisées dans le cadre de Medec, salon européen de

Un cadre formel pour la spécification multivue de systèmes avioniques, 43-72 Une approche multiagent pour la gestion de la communication dans les réseaux. de capteurs sans

[r]

Ainsi, dans ce numéro, nous trouvons un premier article relatif aux architectures reconfigurables qui, depuis maintenant une bonne quinzaine d’années, sont sources d’inspiration