Introduction
La machine RAM nous a permis de définir formellement la notion d'algorithme et de complexité, malheureusement le prix à payer pour cette rigueur est une écriture très fastidieuse des algorithmes. Le troisième exemple d'algorithme du chapitre sur la machine RAM est particulièrement simple à décrire en français : soit \(T\) une liste de \(n\) valeurs sur un ensemble totalement ordonné. Pour toutes les valeurs \(i\) allant de \(1\) à \(n-1,\) on échange les valeurs \(T[i]\) et \(T[i+1]\) si \(T[i]>T[i+1]\). Le programme correspondant sur la machine RAM est bien plus complexe à rédiger ! Pour des raisons d'efficacité, les algorithmes seront décrits dans un pseudo-langage algorithmique plus proche de la langue naturelle.
Le langage algorithmique que nous allons employer tout au long du cours est à la fois rudimentaire et très évolué. Deux raisons principales motivent l'utilisation d'un langage algorithmique plutôt qu'un langage de programmation (même si l'on peut trouver des analogies avec le Python par exemple).
La première est qu'il est en quelque sorte un plus grand commun dénominateur des langages de programmation, ce qui le rend à la fois universel et nécessairement simple compte tenu de leur diversité. Son apprentissage est quasi immédiat et sa proximité avec la langue naturelle permet également de décrire facilement les objets que l'on manipule. Par exemple, l'appartenance d'un objet \(x\) à un ensemble \(E\) s'écrira très classiquement \(x\in E\).
La seconde est qu'il évite de cumuler deux types de difficultés, celles qui concernent la conception et l'analyse d'un algorithme et celles qui sont spécifiques au langage de programmation utilisé. On peut par exemple avoir besoin de manipuler des listes dans l'écriture d'un algorithme sans que leur gestion soit un sujet d'étude en soit. Le langage Python dispose d'un arsenal à cet effet alors que le langage C demande un travail conséquent et fastidieux pour développer la structure de données ad hoc. C'est justement le rôle des tp de bien faire la part des choses et le travail d'implantation est une tâche à part entière.
Nous ne fournirons pas de grammaire formelle pour ce langage, à dessein. Ce qui pourrait apparaître comme un manque de rigueur nous permet en fait de nous affranchir à chaque fois que nécessaire, de difficultés qui ne sont pas au centre de l'algorithme étudié. Ainsi, si nous avons ponctuellement besoin d'une liste de \(n\) entiers compris entre \(1\) et \(K\) choisis aléatoirement sans que la génération de cette liste ne soit la question centrale de l'algorithme étudié, nous nous autoriserons à employer ponctuellement un algorithme auxilaire ALEA(1,K) sans même le définir. La liberté ainsi gagnée permet de nous concentrer sur la nature du problème à résoudre plutôt que sur des questions annexes qui peuvent nécessiter à elles seules une étude détaillée, la généralsation d'une bonne séquence pseudo-aléatoire en faisant partie.
Ce pseudo-langage manipule des objets plus proches des modèles abstraits que des structures de données. Nous utiliserons par exemple librement les listes et les opérations usuelles sur les listes, sauf encore une fois, dans le cas où ces opérations sont le sujet d'étude de l'algorithme.
Description générale d'un algorithme
La description littérale d'un algorithme que nous allons faire tente de conserver les éléments clés que nous avons mis en évidence dans le schéma de l'introduction. On y trouve son nom suivi de la liste des données à traiter suivie du type du résultat final (la nature de l'ensemble d'arrivée).
ALGORITHME NomAlgorithme(liste_données):type_résultat DONNEES liste_données: types VARIABLES GLOBALES liste_variables_globales: types VARIABLES liste_variables: types DEBUT liste_instructions FIN
Par convention chaque terme des listes de données (sauf dans l'entête) occupe une ligne de l'algorithme (ou sont regroupés par type). Le mot réservé RENVOYER (ou RETOURNER) permet de renvoyer le résultat du calcul effectué par l'algorithme et par convention l'algorithme s'arrête. Comme pour la composition des applications, on peut parfaitement composer des algorithmes en considérant que la valeur renvoyée par un premier algorithme est l'entrée d'un second.
Comme pour les langages de programmation, le passage des variables est un passage par valeur, c'est-à-dire que lors de l'appel d'un algorithme, le contenu des variables d'appel est copié dans leurs paramètres respectifs, les variables d'appels ne sont donc pas modifiées par l'exécution de l'algorithme. On peut aussi faire opérer un algorithme directement sur la donnée qui lui est fournie en entrée, dans ce cas celle-ci est précédée du symbole @. C'est l'équivalent en langage pseudo-algorithmique du passage par adresse.
Affectation et structures de contrôle
Affectation
Si \(y\) est une variable, alors on interprète \(x\leftarrow y\) comme l'affectation de la valeur de la variable \(y\) à \(x\). Plus généralement, on peut placer à droite du symbole d'affectation une expression arithmétique ou logique et dans ce cas, c'est le résultat de l'évaluation de cette expression qui est affecté à \(x\). Par exemple, soit \(y\) une variable dont la valeur est 3, l'affectation \(x\leftarrow 2(y + 1)\) donne la valeur 8 à \(x\).
Par défaut, un algorithme est exécuté séquentiellement, dans l'ordre de lecture des instructions qui le composent.
Ruptures de séquence
Si/Alors
SI condition ALORS bloc instructions FSI
Son fonctionnement consiste à exécuter la séquence des instructions du bloc uniquement si la condition est satisfaite. Le bloc d'instructions est indenté et est délimité par les mots réservés SI et FSI.
Si/Alors/Sinon
SI condition ALORS bloc instructions #1 SINON bloc instructions #2 FSI
Son fonctionnement consiste à exécuter exclusivement l'un des deux blocs d'instructions, le premier si la condition est satisfaite le second sinon. Les blocs d'instructions sont indentés et délimités par les mots réservés SI, SINON et FSI.
Tant que
Il s'agit de la rupture de séquence qui permet de réaliser des boucles:TQ condition FAIRE bloc instructions FTQSon fonctionnement consiste à vérifier que la condition est satisfaite avant d'exécuter les instructions du bloc et à recommencer au début du bloc tant qu'elle l'est. Si la condition n'est pas satisfaite, le programme continue après le bloc délimité par les mots réservés TQ et FTQ.
Quelques notations pour les modèles de données
Nous désignerons indifférement par \(u_i\) ou \(u[i]\) le \(i\)ème élément d'une structure \(u\) de type énuméré (modèle liste, suite, mot, ensemble, tableau, etc.). Dans le même esprit si \(u\) est une structure de type énuméré, nous noterons \(u[a\!:\!b]\) la sous-structure de \(u\) dont les termes sont ceux dont les indices appartiennent à l'intervalle entier \(\ab{a}{b}\). Selon les situations et pour simplifier les écritures nous commencerons l'indexation de ces structures par 0 ou par 1, en le précisant bien entendu.
Quelques exemples
L'algorithme suivant renvoie la somme des nombres entiers pairs contenus dans la liste d'entrée :
ALGORITHME Additionner(L):réel donnees L: liste de nombres réels variables i: entier S: réel debut i ← 1 S ← 0 TQ i ≤ #L faire SI (2 | L[i]) alors S ← S + L[i] FSI i ← i + 1 FTQ RENVOYER S FINL'algorithme suivant prend en entrée un entier naturel \(n\) et renvoie l'entier \(n/2\) si \(n\) est pair et \(3n+1\) s'il est impair:
ALGORITHME PairOuImpair(n):entier donnees n: entier variables S: entier debut SI (2 | n) alors S ← n / 2 SINON S ← 3 * n + 1 FSI RENVOYER S fin
ALGORITHME Syracuse(u0):entier donnees u0: entier variables u: entier n: entier debut u ← u0 n ← 0 TQ u > 1 faire u ← PairOuImpair(u) n ← n + 1 FTQ RENVOYER n fin
Cet algorithme ne respecte peut-être pas la condition d'arrêt imposée par notre définition d'un algorithme. En effet, il s'appuie sur une conjecture célèbre en mathématiques :
Travaux pratiques
Dans les différents sujets, vous aurez parfois à utiliser l'utilitaire Gnuplot pour tracer des courbes représentatives de fonctions. Ce logiciel a été développé par gnu et les tracés se font à partir de commandes écrites dans un langage spécifique. À l'instar de Python, on exécute le script monscript.gp en lançant la commande gnuplot monscript.gp et on lance l'interprète de commande simplement grâce à gnuplot.
Par exemple, la commande plot sin(x) va ouvrir une fenêtre et tracer le graphe de la fonction réelle définie par \(x\mapsto \sin x\). Pour tracer un nouveau graphe dans la même fenêtre, il suffit d'utiliser replot au lieu de plot. On peut également tracer un graphe à partir d'un fichier texte contenant un couple de valeurs par ligne, avec plot "mongraphe.txt". Comme toujours, pour en savoir plus, rtfm.
NB : La phrase devra être récupérée via la ligne de commande. Utilisez les paramètres de la fonction int main(int argc, char *argv[]).
La suite de Syracuse de terme initial \(u_0:=u\) est qualifiée de vol \(u\). On note \(t(u)\) le temps de vol de \(u,\) c'est-à-dire \[ t(u):=\min\{n\in\N\such (u_0=u)\wedge(u_n=1)\}. \]
On note \(p(u)\) le plafond du vol \(u,\) c'est-à-dire l'altitude maximale atteinte durant ce vol : \[ p(u):=\max\{u_i\such i\in\ab{0}{t(u)}\}. \]
(1) Écrivez une fonction uint Syracuse(uint n) qui renvoie la valeur \(n/2\) si \(n\) est pair et la valeur \(3n+1\) si \(n\) est impair.
(2) Écrivez une fonction void Vol(uint u, uint *temps, uint *plafond, tbool afficher) qui calcule \(t(u)\) et \(p(u)\). Si le paramètre affiche est vrai, alors la fonction doit afficher tous les couples \((n,u_n)\) pour \(n\in\ab{0}{t(u)}\) (un couple par ligne uniquement séparés par un espace). Quelle est la durée et l'altitude maximale du vol qui commence à l'altitude \(u_0:=27\) ? Même question pour le vol qui commence à l'altitude \(u_0=4\,563\,281\).
(3) Écrivez une fonction void GenererGraphes(uint n, const char *datat, const char *datap) qui crée deux fichiers dont les noms sont en paramètres, et qui contiennent les graphes partiels des deux fonctions \(t\) et \(p\) respectivement pour \(u\in\ab{1}{n}\).
(4) Tracez les graphes de ces deux fonctions dans la même fenêtre avec Gnuplot pour différentes valeurs de \(n\). Indication: commande plot "datat.txt" with lines, "datap.txt" with lines.