Leçons
Leçons
Leçons d’Informatique
Agrégation 2022
2 © 2022 M. Marin
Table des matières
I Leçons 9
3
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
II Développements 193
3 B-arbres 201
4 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
5 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
6 © 2022 M. Marin
Préface
Présentation
Enfin une agrégation d’informatique ! Il aura fallu attendre 2022 pour la création d’une agrégation
entièrement dédiée à l’informatique, remplaçant ainsi l’ancienne agrégation de mathématiques option in-
formatique. Avec elle suit une épreuve orale emblématique : la leçon.
De manière succincte, une leçon consiste à proposer un plan détaillé au jury après 4h de préparation sur
un sujet tiré aléatoirement. Les dix premières minutes de l’oral consiste en une défense du plan dans lequel
sera proposé deux développements. (Les dix premières minutes donnent lieu à la défense du plan ....) Le
jury en choisira alors un que vous présenterez pendant une vingtaine de minutes. Pour plus de détails sur
cette épreuve ainsi que sur l’agrégation d’informatique en général, je vous ramène au site de l’agrégation
[Link]
Nous vous proposons ici un ensemble de plans et développements préparés lors de la première session
de 2022 par des membres de la promotion de l’ENS de Lyon.
Public visé
De manière évidente, ce document est avant tout destiné aux personnes préparant ou allant préparer
l’agrégation d’informatique. Il est nécessaire pour cela d’avoir déjà un certain recul sur l’ensemble des
notions présentées, le réel intérêt du document étant dans les choix et l’organisation des thèmes abordés,
ainsi que dans les références utilisées.
Attention, une leçon doit rester personnelle, adaptée à vous et vos qualités. Il est inutile de reprendre
telle qu’elle une leçon présentée ici puisque vous serez incapable de défendre de manière efficace un plan
qui n’est pas le votre. Ces leçons doivent juste vous servir à ne pas partir de zero dans votre travail, comme
nous avons eu à le faire pour cette première session.
Contenu
Ce document propose pour chaque leçon un plan détaillé ainsi que deux développements possibles. Il
est important de noter que ces leçons et développements ont été écrits lors de la première session, sans
recul sur les années précédentes et sans rapport de jury. Il sera donc important pour le lecteur de prendre
en compte ce rapport pour modifier voir complètement changer ces plans. Enfin, l’ENS de Lyon recrutant
avant tout des personnes venant d’un parcours très théorique, beaucoup de leçons et développement sont
influencés par nos compétences (ou non-compétences). Nous demandons ainsi au lecteur à la fois de la
méfiance et de la compréhension vis-à-vis des plans proposés dans ces leçons (particulièrement pour les
leçons 18 et 21). À l’inverse, pour les personnes venant d’un parcours moins théorique, il est possible que
nous allions trop loin dans certains concepts.
Remerciements
L’ensemble de ces leçons et développements ont été travaillé à l’École Normale Supérieure de Lyon, dans
le cadre d’une formation dédiée à la préparation de l’agrégation d’informatique. Nous tenons à remercier
l’ensemble des étudiants ayant préparé les oraux : Jules Bertrand, Luc Lapointe, Pierre-Marie Marcille,
7
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Malory Marin, Guillaume Rousseau et Émile Sorci. Nous remercions aussi toutes les personnes nous ayant
accompagné au cours de cette année, soit en examinant nos leçons ou soit en en tant que professeurs :
Pascal Koiran, Simon Iosti, Florent De Dinechin, Valentin Bartier, Etienne Mauffret, Yves Robert, Rémi
Pellerin, Eddy Carron, Émile Hazard, Natacha Portier, Nicolas Chappe, Angela Bonifati, Frederic Prost,
Aurelien Garivier, Laurent Lefèvre et Christophe Alias.
Enfin, nous remercions Sasha Darmon et Pierre Marrec pour la relecture finale.
Malory Marin
Ce document est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas
d’Utilisation Commerciale - Pas de Modification 3.0 non transposé.
8 © 2022 M. Marin
Première partie
Leçons
9
Leçon 1
I Introduction
Lorsqu’on programme, il est essentiel de s’assurer de la correction des algorithmes implantés. Tester
notre programme permet seulement de prouver que notre algorithme n’est pas correct, alors on développe
ici des méthodes et outils permettant de démontrer la correction de programmes.
Définition 1.1 (Terminaison) Un programme P termine sur l’entrée x si le calcul de P (x) nécessite
un nombre fini d’étapes.
Définition 1.2 (Spécification) La spécification d’un programme P est la donnée de son domaine
(les arguments de la fonction) ainsi que l’ensemble des résultats attendus.
Exemple 1.1 Le programme P prend en entrée un entier x et retourne l’entier 2x est une spécification de
P . Plus formellement, on pourra écrire ∀x ∈ N, P (x) = 2x.
Définition 1.3 (Correction) Un programme P est partiellement correcte si, sur l’entrée x, P
retourne une valeur conforme à sa spécification lorsqu’il termine.
De plus, si P termine sur toute entrée, on dit qu’il est totalement correcte.
II Terminaison
On cherche ici à montrer la terminaison d’un programme. On distinguera le cas des algorithmes itératifs
et récursifs.
11
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Définition 1.4 Un ensemble (E, ≤) est dit bien fondé s’il n’existe pas de suite infinie d’éléments de
E strictement décroissante.
Exemple 1.2 N muni de la relation d’ordre naturelle est bien fondé, et il en est de même pour N2 muni
de l’ordre lexicographique.
Définition 1.5 Un variant de boucle est une suite (vn )n∈N strictement décroissante à valeurs dans
un ensemble bien fondé (E, ≤) qui dépend de l’ensemble des valeurs des variables et du nombre
d’itérations n passées.
Pour montrer la correction d’un programme, il faut alors montrer que chaque boucle termine c’est-à-dire
que cette boucle admet un variant.
Théorème 1.1 Si chaque boucle d’un programme P admet un variant de boucle, le programme P
termine.
Remarque 1.1 Toute boucle Pour admet un variant trivial. Considérons une boucle Pour i=a...b
faire ..., alors la suite (vn ) définie par vn = b − in est un variant de boucle, où in est la valeur
de i à l’itération n.
Exemple 1.3 Dans l’algorithme d’Euclide suivant, la suite définie par (bn ) où bn est la valeur de b à
l’itération n est un variant de boucle.
Proposition 1.1 Un ensemble (E, ≤) est bien fondé si et seulement si toute partie non vide de E
admet un élément minimal.
12 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Théorème 1.2 Soit f : A → S une fonction récursive et ϕ : A → E avec (E, ≤) bien fondé. Soit
B ⊂ A l’ensemble des minimiseurs de ϕ (cas de bases).
Si, pour tout b ∈ B, f (b) termine et si dans la définition de f (x) (pour x ∈ A) n’apparaissent, en
nombre fini, que des appels à f (y) avec ϕ(y) < ϕ(x), alors f termine pour tout x ∈ A.
2. Déterminer pour tout n ∈ N les valeurs de A(2, n) et A(3, n). En déduire que pour tout n ∈ N,
...
A(4, n) = 22 − 3 avec n − 3 deux empilés.
Exemple 1.4 (Suite de Syracuse) On a seulement conjecturé que la fonction suivante termine.
De plus, il existe un fameux théorème, démontré par Alan Turing, qui affirme qu’il n’existe pas de
programme qui reçoit en entrée un autre programme, et qui renvoie VRAI si le programme termine et
FAUX sinon.
Exercice 1.2 Étant donné un programme P qui reçoit en entrée le code d’un programme Q et décide si
Q termine, trouver une contradiction.
III Correction
On va maintenant donner des méthodes permettant de montrer la correction d’un programme. De
même, on sépare le cas itératif et récursif.
13 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Définition 1.6 Étant donné un programme P et une boucle dans P , un invariant de boucle est un
prédicat dépendant des valeurs des variables et de l’itération vérifiant les propriétés suivantes :
— initialisation : le prédicat est vrai avant l’entrée dans la boucle ;
— conservation : s’il est vrai avant une itération de boucle, il est vrai avant la prochaine itération
de boucle
À la fin de la boucle, le prédicat permet de donner une propriété utile pour prouver la correction du
programme.
Exemple 1.5 (Correction de l’Algorithme d’Euclide) On revient à notre exemple. Dans l’algorithme
d’Euclide, on peut prendre l’invariant suivant : pgcd(a, b) = pgcd(x, y) .
— Avant d’entrer dans la boucle, a (resp. b) contient la valeur x (resp. y) et donc le prédicat est vérifié.
— Ensuite, pour montrer la conservation, il suffit d’utiliser la propriété du pgcd pgcd(a, b) = pgcd(b, a
mod b).
Enfin, à la sortie de la boucle, la condition du Tant que est violée, et donc b = 0 et donc pgcd(a, b) =
pgcd(a, 0) = a. Via l’invariant, on a pgcd(a, b) = pgcd(x, y) = a. L’algorithme est ainsi totalement
correcte.
Calcul de l’enveloppe convexe. Étant donné un ensemble Q de points sur le plan, un problème classique
consiste à trouver l’enveloppe convexe EC(Q), c’est-à-dire le plus petit polygone convexe qui contient
l’ensemble des points de Q. Un algorithme classique pour calculer cette enveloppe convexe est le balayage
de Graham.
Développement 1. Présentation du balayage de Graham et correction.
Proposition 1.2 Soit (E, ≤) un ensemble bien fondé et p un prédicat sur les éléments de E. Si la
propriété suivante est vérifiée :
alors ∀x ∈ E, p(x).
Remarque 1.2 Si x est un élément minimal, alors ∀y < x, p(y) est toujours vrai et donc il suffit de
montrer p(x).
Théorème 1.3 On reprend toutes les données du théorème 1.2 ainsi qu’un prédicat pf sur les valeurs
prises par f .
Si pour tout b ∈ B, pf (b) et si pour tout x ∈ A, en notant Y ⊂ A l’ensemble fini des arguments
des appels récursifs de f (x), on a :
— ∀y ∈ Y, ϕ(y) < ϕ(x),
— (∀y ∈ Y, pf (y)) ⇒ pf (x)
alors ∀x ∈ A, pf (x).
14 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Exercice 1.3 Programmez une fonction OCaml réalisant un tri rapide et montrer qu’elle est totalement
correcte.
n
Exercice 1.4 Programmer une fonction récursive OCaml calculant p via le triangle de Pascal et montrer
qu’elle est totalement correcte.
Exemple 1.6 Pour calculer la distance d’édition entre deux chaı̂nes de caractères en temps raisonnable,
on exhibe d’abord une relation de récurrence complexe avant d’écrire l’algorithme.
Développement 2. Dans le cadre des arbres couvrants, on peut montrer que l’ensemble des forêts d’un
graphe forment un matroı̈de, structure sur lequel le glouton est optimal.
Lors d’une démonstration de la correction manuelle, on peut négliger ce genre de problèmes venant de
la représentation machine.
B Triplet de Hoare
Pour se tourner vers une automatisation des preuves, on va avoir besoin de logiques de programmes.
Une logique de programmes fournit un langage de spécification et des principes de raisonnement sur
les comportements du programme.
On peut alors formaliser la définition de spécification.
Définition 1.7 Les spécifications se présentent généralement comme des assertions logiques por-
tant su le programme :
— préconditions : hypothèses sur les entrées (paramètres des fonctions ; valeurs initiales des va-
riables)
— postconditions : garanties sur les sorties (résultats de fonction ; valeurs finales des variables)
15 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Exemple 1.7 (Frama-C) Des outils comme Frama-C permettent de spécifier un programme écrit en C
et de démontrer automatiquement la validité des triplets de Hoare associés.
Exemple 1.8 (COQ) Pour OCaml, le logiciel de preuve formel COQ permet aussi de prouver la correction
de fonctions de manière semi-automatique. Il donne un langage formel permetant d’écrire des définitions
mathématiques, des algorithmes avec une syntaxe très similaire à OCaml et des théorèmes.
des ; .
La sémantique s’intéresse à la signification d’un programme grammaticalement correcte. Sur l’exemple
précédent, une analyse sémantique nous permet de comprendre que ce programme échange les valeurs
contenues dans x et y. On distinguera trois sémantiques :
— la sémantique opérationnelle : expliquer comment s’exécute un programme ;
— la sémantique dénotationnelle : explique la réalisation du programme ;
— la sémantique axiomatique : donne des propriétés spécifiques sur l’exécution du programme.
Ces trois sémantiques permettent de prouver formellement des résultats de correction et les triplets de
Hoare vu précédemment sont très liés à la sémantique axiomatique.
16 © 2022 M. Marin
Leçon 2
Introduction
Revenons à une définition de base , qu’est-ce que la programmation informatique ? Cela consiste à
concevoir une solution algorithmique à un problème et à la transcrire dans un langage informatique.
Le paradigme de programmation est alors une manière d’approcher un problème et formuler une
solution dans un langage approprié.
Il existe de nombreux langages de programmations, et chaque langage a son propre modèle de conception
avec sa propre syntaxe et ses propres caractéristiques (Python, C, OCaml, Rust, JavaScript,...). Cependant,
ils peuvent présenter beaucoup de liens et de ressemblances, et peuvent plus ou moins adapté selon le
paradigme choisi.
I Monoparadigme
A Impératif
Définition 2.1 La programmation impérative consiste à résoudre un problème via une suite d’ins-
tructions. En particulier, on décrit la structure de contrôle du programme (variables, état de la mémoire,
etc).
Beaucoup de langages sont basés sur ce paradigme, le plus bas niveau étant le code machine (assem-
bleur). Ensuite, Python, C et JavaScript sont aussi adaptés ce paradigme.
17
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
B Déclaratif
Exemple 2.2
— Une requête SQL décrit la solution sans donner l’algorithme de recherche dans la base de donnée.
— Pour écrire un fichier Latex, on décrit seulement le contenue du fichier et on donne sa structure.
HTML et CSS fonctionnent aussi de cette manière.
On remarque alors qu’un langage déclaratif nécessite beaucoup plus d’abstractions qu’un langage
déclaratif. Une abstraction est un objet que l’on peut manipuler sans se soucier de l’implémentation (une
fonction numpy et python par exemple). On peut alors voir un langage de programmation comme un
ensemble d’abstractions.
Une branche importante du paradigme déclaratif constitue le paradigme fonctionnel, qui permet de
définir des fonctions.
C Fonctionnel
Exemple 2.3 (Déclaration de fonction en Python et OCaml) On souhaite définir une fonction per-
mettant d’incrémenter une variable entière, et l’appliquer à 0.
En Python,
def f ( x ) :
r e t u r n x+1
f (0)
En OCaml,
l e t f x = x +1 ; ;
f 0 ;;
L’une des forces du paradigme fonctionnel est de considérer une fonction comme un autre type, per-
mettant de les passer en argument d’une autre fonction par exemple. On appelle cela les fonctions d’ordre
supérieur.
Exemple 2.4 On souhaite écrire une fonction OCaml qui reçoit deux fonctions et retourne la composée
de ces deux fonctions.
l e t comp f g = ( f u n x −> f ( g x ) ) ; ;
Les fonctions d’ordres supérieur peuvent aussi être utilisées par Python ou C par exemple.
Ce paradigme est particulièrement adapté lorsqu’on manipule des structures naturellement récursive
(ou inductive) comme les arbres ou les entiers. Par exemple, le logiciel Coq de preuve assisté par ordina-
teur est codé en OCaml. Ce logiciel permet aussi de créer des compilateurs certifié de C, comme CompCert.
Le fonctionnel peut aussi être vu comme du déclaratif puisqu’on utilise les fonctions comme des boı̂tes
à imbriquer et connecter pour décrire la solution.
Exemple 2.5 La fonction factorielle peut se définir par induction sur les entiers par 0! = 1 et n! =
n × (n − 1)!. La fonction factorielle peut alors s’écrire directement en OCaml :
18 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
l e t r e c f a c t n = match n w i t h
| 0 −> 1
| n −> n ∗ ( f a c t ( n −1)) ; ;
Dans certains cas, il s’agit même de la seule manière connue de programmer une fonction. Par exemple,
la fonction d’Ackermann A : N2 → N défini ci-dessous :
m+1 si n = 0
A(n, m) = A(n − 1, 1) si m = 0
A(n − 1, A(n, m − 1))sinon
Exercice 2.1 Écrire une fonction OCaml qui implémente la fonction d’Ackermann, et montrer qu’elle
termine.
D Orienté Objet
Exemple 2.6 En python, le package numpy propose les objets [Link] que l’on crée via la commande
a = [Link]([...]). Un tel objet possède des attributs comme sa taille ([Link] mais aussi des
méthodes ([Link]() par exemple).
Remarque 2.1 Les méthodes peuvent aussi caractériser le comportement d’un objet avec un autre objet.
En pratique, le programmeur interagit avec un objet seulement par ses méthodes. On parle d’encapsulation.
Un outil puissant pour représenter de manière schématique les classes d’un programme sont les dia-
grammes UML. Chaque classe est représentée par une boı̂te contenant ses attributs et ses méthodes. Les
différentes interactions entre classes sont précisés par des flèches différentes.
Exemple 2.7 On présente ici un schéma UML d’un programme contenant trois objets : maison, fenêtre et
pièce.
Lorsqu’un objet est un sous-ensemble d’un autre objet, on parle d’héritage. Ainsi, le premier objet
hérite des attributs et méthodes de la seconde, auquel on peut ajouter des caractéristiques spécifiques.
19 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
II Paradigmes avancés
A Multiparadigme
En pratique, la plupart des langages ne se réduisent pas à un unique paradigme, mais en favorise certains
par rapport à d’autres. Par exemple, il est possible de faire du fonctionnel en C, même s’il n’est pas du
tout adapté pour cela.
Pourquoi utilisent-on plusieurs paradigmes ?
Abstraction. Le déclaratif n’est qu’une abstraction permettant de raisonner plus facilement. En cas
d’opération complexe, le programmeur peut atteindre les limites des abstractions disponibles. De plus, dans
la programmation orienté objet, les méthodes d’une classe sont aussi une abstraction qui peuvent être
programmer en utilisant un autre paradigme. C’est pour cela que les langages permettant ce paradigme
proposent aussi d’autres paradigmes.
Exercice 2.2 (TP) Implémentation d’un jeu de rôle contenant plusieurs objets (joueur/personnage/mons-
tres/armes).
B Programmation concurrente
Définition 2.5 La programmation concurrente consiste à diviser son programme en plusieurs fils
d’exécution s’exécutant en parallèle.
Ce paradigme peut être très utile lorsque son programme peut facilement être divisé en tâche parallèle.
Processus et Threads.
Définition 2.6 Un processus est une abstraction par le système d’un programme en cours d’exécution.
Un thread ou fil d’exécution d’un processus est une exécution (état des registres et de la pile)
pour un processus.
Ici, le multiparadigme est central puisque chacun des différents threads effectue une tâche bien définie,
le plus souvent programmé avec un paradigme impératif.
Remarque 2.2
1. Il existe de nombreux processus dont on n’a pas conscience, qui tourne continuellement en fond.
Pour l’instant, on pourra considérer que tous les processus s’exécute en parallèle.
2. L’espace d’adressage entre threads d’un même processus est le même, ils partagent les variables
globales. Les piles et états des registres sont cependant distincts.
20 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Définition 2.7 Une condition de concurrence est une situation où deux threads ou processus
exécutent une zone du code d’écriture ou de lecture dans sur une mémoire partagée et dont le résultat
dépend de l’ordre d’exécution des instructions. Une telle zone de code est appelée section critique.
Si plusieurs threads n’entrent jamais dans leur section critique en même temps, on dit qu’il y a
exclusion mutuelle.
Exemple 2.9 Dans le programme précédent où plusieurs threads se comptent, la section critique correspond
à la ligne d’incrémentation du compteur. On peut alors identifier la condition de concurrence.
Ces deux solutions présentent un problème majeur : il s’agit d’attente active. Le processeur exécute du
code qui ne fait rien, et donc de la ressource est perdue.
Définition 2.8 Un sémaphore est une variable qui stocke un entier qui représente le nombre de res-
sources d’un type disponible. Deux opérations sont possibles sur un tel entier : le prendre (décrémenter)
s’il est non nul (sinon, on bloque tant qu’il est nul) ou le libérer de manière réciproque. Ces opérations
sont atomiques.
Un mutex est un sémaphore binaire.
Pour implémenter les sémaphores et mutex dans le système, on utilise des interruptions. Lorsqu’un
thread bloque, il est placé dans un état bloqué special. L’ordonnanceur (cf partie suivante) n’essaye plus
de l’exécuter tant qu’il n’a pas récupérer l’information disant que le mutex ou sémaphore a été libéré. La
librairie pthread permet d’utiliser des mutex, et la librairie semaphore les sémaphores.
Exercice 2.4 Proposer une implémentation en python d’un automate finis déterministe.
Un résultat fameux, admis ici, est la décidabilité de l’arithmétique de Presburger. En effet, il existe
un automate finis déterministe (avec beaucoup d’états) qui étant donné une formule logique du premier
ordre sur les entiers munis de l’addition, décide si elle est vraie ou non.
21 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Recherche de motif. Ce paradigme est aussi très utile en algorithmique du texte. Étant donné un texte
T de longueur n et un motif M de longueur m, l’algorithme naı̈f décidant si M est une sous-chaı̂ne de T
est un O(nm). On peut mieux faire avec ce paradigme.
Développement 8. Recherche de motif à l’aide de l’automate des occurrences.
22 © 2022 M. Marin
Leçon 3
I Introduction
Test de programmes. Dans le sens commun, tester notre programme a tendance à avoir l’objectif de
montrer qu’il fonctionne bien. Cette définition n’est pas correcte.
Définition 3.1 Tester est le processus qui consiste à exécuter un programme avec l’intention de
trouver des erreurs.
Tests en boı̂te noire. La première méthode de test consiste à considérer notre programme comme une
boı̂te noire.
Si quelqu’un veut trouver toutes les erreurs sur le programme, il faut alors tester toutes les entrées
possibles. C’est déjà impossible lorsqu’il y a une infinité de donnée possible (ou même un très grand
nombre).
Ce test doit donc s’effectuer sur un sous-ensemble des entrées possibles, et il n’y a aucun raison qu’il
n’y ait pas une erreur spécifique pour un de ces cas.
Tests en boı̂te blanche. La seconde méthode de test nous permet d’avoir accès au code du programme.
De ce code on peut extraire un graphe de flot de contrôle. Au lieu de tester l’ensemble des données
d’entrées, on peut alors tester l’ensemble des chemins du graphe. Ceci est impossible dès lors que le graphe
contient une boucle.
Remarque 3.1 Tester l’ensemble des chemins n’est pas optimal. Lorsque par exemple le code contient une
instruction de la forme if (a-b) < then ... au lieu de if |a-b| < then ..., tester le chemin
une unique fois ne suffit pas.
23
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Ces deux méthode ne permettent pas de détecter toutes les erreurs, on va donc essayer de les combiner
afin d’avoir des jeux On parle de tests raisonnables.
Principes de tests. On présente ici quelques principes nécessaires lorsqu’on créer un jeu de tests.
1. Une partie nécessaire d’un jeu de test est la définition claire des résultats attendus.
2. Un programmeur doit éviter de tester son propre programme.
3. Un jeu de test doit être écrit avec des conditions d’entrées invalides ou imprévues, mais aussi des
conditions valides et prévus.
4. Vérifier si le programme fait ce qu’il doit faire et s’il ne fait pas ce qu’il est censé ne pas faire.
5. La probabilité qu’une erreur soit présente dans une portion du programme augmente avec le nombre
d’erreurs déjà trouvées à cet endroit.
II Inspection de code
Avant de parler de méthodes de tests par ordinateurs, on va se concentrer sur des méthodes hu-
maines . Les bonnes pratiques de programmations permettent souvent de rendre le code lisible et de
trouver des erreurs de manière plus efficace car trouvées plus tôt.
Inspection. Le but est ici de lire un code et de trouver d’éventuelles erreurs et non pas de les corriger.
Une inspection de code se réalise le plus souvent en groupe, dans lequel se trouve le programmeur.
Définition 3.2 Une inspection de code est un ensemble de procédures et de techniques de détection
d’erreurs pour un groupe de lecture de code.
Remarque 3.2 Lorsque c’est le programmeur seul qui fait l’inspection, on parle plutôt de contrôle de
code (desk-checking), la distinction étant dû à la différence d’efficacité.
Cette méthode présente des avantages : lorsqu’une erreur est trouvé, c’est se nature même qui est
trouvé, et non pas juste un symptôme ; et on repère souvent un ensemble d’erreurs. Une inspection de code
se réalise en trois étapes :
1. les participants prennent connaissance du code à l’avance ;
2. le programmeur explique lui-même son code aux participants (et peut alors lui-même trouver des
erreurs) ;
3. le programme est analysé via une liste d’erreurs classiques.
Une liste d’erreurs classiques peut être construite suivant ces points :
— Erreurs de référence de données (variables non initialisée, oublie d’une allocation de mémoire).
— Erreurs de déclaration de données (variable non déclarée, erreur de typage, homonymes).
— Erreur de calcul (division par zéro, somme d’entiers et de flottants).
— Erreurs de comparaisons (mauvais comparateur, mauvais opérateur booléen).
— Erreurs de contrôle de flot (terminaison des boucles)
Exécution pas à pas. Une autre méthode de test humain est de simuler l’exécution du programme.
Elle permet notamment de trouver plus facilement les erreurs de logique du programmes (comme un
branchement inutile).
24 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Remarque 3.3 Ces exercices sont aussi très bénéfiques au programmeur lui-même, qui reçoit un avis
extérieur sur son code.
L’approche classique consiste à d’abord faire un jeu de test en utilisant une approche boı̂te noire, puis
de le compléter avec des méthodes boı̂te blanche.
Définition 3.3 (Graphe de contrôle) Étant donné un code de programme, le graphe de contrôle
de ce code est un graphe orienté G = (V, E) où :
— V est l’ensemble des instructions élémentaires du code (affectation, test conditionnel, etc), avec
un sommet initial D et final F ;
— ij ∈ E si et seulement si le programme peut passer directement de l’instruction i à l’instruction
j. L’arc ij est étiquetée par la condition de saut (souvent un booléen).
D F
Algorithme 3.1 : PGCD(a,b)
Données : a, b ∈ N∗ a=b
Sorties : pgcd(a, b)
tant que a 6= b faire a≠b
si a < b alors
b ← b − a; a≠b
sinon
a ← a − b; b←b-a a←a-b
retourner a ; a<b a<b a≥b
Un test va parcourir le graphe de D à F . On peut alors donner un critère pour que l’ensemble du jeu
de tests parcourt :
1. tous les sommets ;
2. tous les arcs ;
25 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
IV Test de modèle
Dans certains cas, comme en apprentissage machine, les erreurs sont intrinsèques au problème. La
principale différence entre le test d’un algorithme classique vu jusqu’alors et un algorithme d’apprentissage
vient de l’espace des données :
— pour un algorithme classique, on connaı̂t pour chaque entrée possible, le comportement voulu ;
— tandis qu’en apprentissage machine, on connaı̂t le résultat voulu pour un sous-ensemble des entrées
possible, appelé jeu d’entraı̂nement.
Il y a alors deux niveaux de lectures de notre algorithme d’apprentissage :
— la vue d’un algorithme classique, qui peut être prouvé et testé,
— la vue d’un modèle (de prédiction, de classification) qui peut être testé afin de vérifier s’il est pertinent.
Modèle. On suppose qu’on dispose d’un algorithme d’apprentissage fonctionnant sur un ensemble d’en-
traı̂nement S et un ensemble de paramètre Θ. Le but est de trouver les meilleurs paramètres possibles.
Diviser ses données. Puisque dans notre cas, on n’a un choix limité de donnée pour tester notre al-
gorithme une division s’impose. En effet, il nous faut des données pour entraı̂ner, pour optimiser les
paramètres (validation) et enfin pour tester notre algorithme final. La méthode la plus simple consiste à
couper notre jeu de donnée en 3 (60%, 20%, 20%).
Test du meilleur
modèle
Itération : sélection du
meilleur modèle
26 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Développement 24. Il existe une autre méthode pour optimiser nos paramètres : la validation croisée.
Mesures de performances. Lorsqu’on teste notre algorithme (en phase de vérification et de test), plu-
sieurs choix de métriques s’offrent à nous :
— en régression : moyenne des erreurs absolues, moyenne des erreurs quadratiques, etc
— classification : nombre d’erreurs, précision, sensibilité, etc.
V Tests efficaces
Les tests d’un programme peuvent s’avérer coûteux en terme de complexité algorithmiques. Par exemple,
prenons un algorithme A de produit de matrice . À chaque exécution de notre algorithmes sur les entrées A
et B, il faut vérifier que A(A, B) = AB. Il faut donc réaliser un produit de matrice avec un algorithme dont
on est déjà sûr. Ainsi, à chaque test, on réalise un O(n3 ) opérations élémentaires en plus de l’exécution de
l’algorithme. Il existe des manières plus efficaces pour réaliser ce test.
Développement 25. Vérification du produit de matrice efficace.
27 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
28 © 2022 M. Marin
Leçon 4
Note pour la défense du plan : L’idée de la leçon est de partir des algorithmes. On explique les
algorithmes et on remarque quels sont nos besoins au niveau des structures de données (quelles sont les
opérations fréquentes, qu’est ce qui pourrait être coûteux) et à partir de cela on propose de nouvelles
structures.
I Introduction
A Motivation
Dans cette leçon nous apprenons à concevoir, utiliser et analyser des structures de données avancées
pour développer des algorithmes efficaces.
Définition 4.1 Une structure de donnée abstraite est une liste d’opérations que l’on peut effectuer
sur un ensemble de données.
Exemple 4.1 La structure de donnée de pile FIFO (First-In-First-Out) est définie par les opérations
empiler, dépiler et est vide où :
— empiler prend en entrée une pile P et un élément x et empile l’élément x sur la pile P.
— depile prend en entrée une pile non vide P et enlève le dernier élément x empilé sur P et le renvoie.
— est vide prend en entrée une pile P et renvoie un booléen : Vrai si la pile est vide et Faux sinon.
Exercice 4.1 Donner une spécification possible pour la structure de donnée abstraite de file FILO (First-
In-Last-Out) et dictionnaire.
Remarque 4.1 La spécification que la pile ne doit pas être vide pour pouvoir dépiler est appelée une pré-
condition. Le comportement qu’adopte l’opération dépile dans le cas de la pile vide n’est pas spécifié.
29
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Définition 4.2 Une implémentation d’une structure de donnée abstraite est la spécification
technique qui définit comment les données sont stockées dans la machine et qui fournit l’algorithme
pour chacune des opérations définies par la structure abstraite.
On parlera simplement de structure de données. On peut alors définir la complexité en temps et en
espace des opérations dans le pire cas et en moyenne ainsi que l’espace occupé par une instance de la
structure.
Exemple 4.2 La structure de donnée abstraite de pile peut être implémentée par une liste chaı̂née. On
donne la complexité des opérations ainsi que l’espace de stockage utilisé par la structure.
Exercice 4.2 Donner une implémentation de la structure abstraite de file qui utilise deux listes chaı̂nées
ainsi que la complexité des opérations dans le pire cas et l’espace utilisé.
On préférera une implémentation avec des listes d’adjacence si on fait de nombreux appels à Voisins(G,
u).
Cas de l’algorithme de Floyd Warshall : On préférera une implémentation avec des matrices d’adja-
cence, structure que l’algorithme utilise naturellement.
Exercice 4.3 Pour tout graphe orienté à n sommets, on définit un puits comme étant un sommet de degré
entrant égal à n − 1 et de degré sortant nul. En admettant que le graphe est représenté par sa matrice
d’adjacence, montrer que l’on peut déterminer si le graphe admet un puits en O(n).
Remarque 4.2 Ce résultat est surprenant car, dès que l’on représente les graphes par matrice d’adjacence,
les algorithmes ont tendance à avoir une complexité au moins O(n2 ).
D Les dictionnaires
Soit U un ensemble d’éléments appelés valeurs, et K un ensemble d’éléments appelés clés. Un dic-
tionnaire sert à stocker des couples (k, x) avec k ∈ K, x ∈ U .
30 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
B-Arbres. Un B-arbre est une implémentation d’un dictionnaire sous la forme d’un arbre équilibré. Son
utilisation est très pertinente dans le cadre d’une hiérarchie mémoire.
Développement 3. B-arbres.
retourner distance
L’algorithme de Dijkstra sélectionne itérativement et de façon gloutonne les sommets les plus proches
de s qui sont accessibles par le sommet courant. Il utilise un front de sommets éligibles au cours de son
exécution.
Remarque 4.3 On ne s’intéresse pas ici à la correction de l’algorithme mais aux structures de données à
utiliser.
Besoin de deux structures de données : une pour le graphe et une seconde que l’on appelle file de
priorité.
Nous allons évidemment utiliser une implémentation du graphe avec des listes d’adjacence car on a
besoin de parcourir les sommets adjacents.
31 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
B La structure de donnée
Définition 4.5 Une file de priorité est une structure qui permet d’organiser un ensemble d’éléments
munis de clés, l’ensemble de clés étant ordonné. Une file de priorité Q est définie pour tout élément
x associé à une clef k par les opérations Inserer(Q, x, k), Extraire min(Q), Est vide(Q) et
Réduire clé(Q, x, k).
C Implémentations
Implémentation de file de priorité avec un tableau Dans un tableau on stocke dans la case i la clé
de l’élément i.
Exercice 4.4 Donner une implémentation des opérations qui définissent une file de priorité ainsi que leurs
complexités dans le pire cas.
On remarque que l’implémentation a des performances faibles notamment pour la fonction Extraire min(Q).
Définition 4.6 Un tas-min (resp. max) est un arbre binaire complet gauche dont les noeuds sont
étiquetés par des éléments munis de clés qui vérifie la propriété du tas-min : la clé d’un nœud est
inférieure aux clés de ses enfants.
Exemple 4.3 On donne un tas-max pour la liste [4, 1, 3, 2, 16, 9, 10, 14, 8, 7]. On donne un tas-min avec
ces éléments ainsi que plusieurs exemples d’arbres qui ne sont pas des tas.
On utilise un tableau indexé de 1 à n pour stocker un tas de n éléments. On accède aux enfants d’un
noeud d’indice i en prennant les éléments d’indice 2i et 2i + 1 et on accède au parent en prenant l’élément
d’indice bi/2c.
Exemple 4.4
1. [16, 14, 10, 8, 7, 9, 3, 2, 4, 1] vérifie la structure de tas-max ;
2. On montre qu’il s’agit du parcours en largeur de l’arbre.
3. [16, 4, 10, 14, 7, 9, 3, 2, 8, 1] ne vérifie pas la structure de tas-max ;
4. Un tableau trié par ordre croissant (resp. décroissant) vérifie la structure de tas-min ([Link]).
Proposition 4.2 Avec une implémentation de tas-min pour la file de priorité et de liste d’adjacence
pour le graphe, l’algorithme de Dijkstra est en O((|V | + |E|) log(|V |)).
32 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
A Définition
Parfois une simple analyse de la complexité dans le pire cas n’est pas représentative du nombre
d’opérations effectuées effectuées par un algorithme. On introduit alors la complexité amortie. Elle permet
de majorer le coût total d’une séquence d’opérations et d’associer a chaque opération un coût amorti.
Il existe trois méthodes majeures qui permettent de déterminer une complexité amortie.
Méthode de l’agrégat
Définition 4.7 On considère une suite de n opérations licite sur une structure de donnée. Soit T (n)
le coût total de ces n opérations. Le coût amorti de chaque opération est alors T (n)/n.
Exemple 4.5 Supposons que l’on effectue une suite de n−1 opérations de coût constant puis une opération
de coût O(n). Le coût amorti de chacune de ces opérations est O(1).
Exemple 4.6 On donne la complexité amortie pour n opérations enfiler(F, x) et n opérations defiler(F)
sur une file implémentée avec deux listes chaı̂nées en O(1).
Exercice 4.5 On suppose que l’on dispose d’un graphe orienté pondéré G = (V, E, w) où w : E →
{0, ..., c} où c ∈ N. Montrer que l’on peut implanter Dijkstra en complexité amortie O(|V | + |E|) pour ce
genre de graphe.
Méthode du comptable A chaque opération i dans la séquence, on associe un coût fictif fi qui définira
le coût amorti de l’opération. Chaque opération a aussi son coût réel ci . Au cours de l’exécution, les
opérations pour lesquelles fi > ci nous font gagner du crédit et les opérations pour lesquelles fi ≤ ci
coûtent du crédit.
Les coûts fictifs que l’on a associés aux opérations sont valides si Σni=0 fi − ci ≥ 0.
Si fi est constant en fonction de n alors le coût amorti est constant, sinon, il est en O(fi ).
Remarque 4.4 Dans cette méthode, on fait payer le coût des opérations dont le coût réel est élevé aux
opérations dont le coût réel est plus faible.
Exemple 4.7 On donne le coût amorti de n insertions dans un tableau dynamique. Application au coût
d’une insertion dans un tas.
Soit :
Φ(Dn ) ≥ 0
Exemple 4.8 On fait l’analyse amortie d’une pile implémentée par une liste chaı̂née à laquelle on ajoute
l’opération Multi Depiler.
33 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Remarque 4.5 On ré-utilise une structure que l’on a déjà vu dans le passé, la file de priorité pour stocker
les arêtes dont les étiquettes sont leur poids.
Il nous faut une manière efficace de déterminer si l’ajout d’une arête à la forêt T crée un cycle ou non.
Pour cela nous allons introduire une nouvelle structure de données : unir et trouver.
Exemple 4.9 En partant d’un ensemble simple {3, 9, 18, 33, 208} on crée la structure d’unir et trouver et
on effectue quelques opérations unir et trouver.
On donne l’exemple de l’algorithme de détection de composantes connexes dans un graphe.
Remarque 4.6 Même dans des textes écrits en français, il ne sera pas rare de lire Union-Find plutôt que
unir et trouver. En anglais on parle aussi de structure d’ensemble disjoints.
Premier cas : chaque élément pointe directement sur son représentant On obtient une implémentation
avec unir en O(n) dans le pire cas et trouver en O(1).
Second cas : lors de l’union, on ne modifie pas le représentant On obtient une implémentation avec
unir en O(1) et trouver en O(n) dans le pire cas.
On voit apparaı̂tre une structure arborescente.
34 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Troisième cas : chaque élément pointe directement sur son représentant et lors de l’union
on fait pointer le plus petit ensemble sur le reste On obtient une implémentation avec un coût
amorti pour m opérations Créer ensemble(E, x), Unir(E, i, j) et Trouver(E, x) dont n opérations
Créer ensemble(E, x) en O(m + n log(n)).
On ajoute des heuristiques pour améliorer la complexité des opérations : l’union par rang et la com-
pression de chemins.[Dasgupta et al., 2008, p. 132]
Proposition 4.3 Pour m opérations trouver sur une structure à n éléments, on obtient un coût amorti
en O((m + n) log∗ (n)).
Remarque 4.7 Soit α la fonction inverse d’Ackermann, en pratique, la complexité amortie est même en
O((m + n)α(n)) pour m opérations unir et trouver.
35 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
36 © 2022 M. Marin
Leçon 5
I Piles et files
Motivation. Les piles et les files sont parmi les structures les plus naturelles et les plus faciles à manipuler.
Elles apparaissent dans de nombreux algorithmes, et forment ainsi les structures de données les plus basiques.
Définition 5.1 (Structure abstraite de pile) Un pile P est une structure de données abstraites
ayant les trois opérations :
— Empiler(P,x) : empile x au sommet de la pile P .
— Dépiler(P) : dépile P et renvoie l’élément dépilé.
— Est Vide(P) : renvoie vrai si la pile P est vide, faux sinon
On peut aussi ajouter d’autres opérations, comme par exemple Sommet(P) qui renvoie le sommet de
la pile sans le dépiler.
Définition 5.2 (Structure abstraite de pile) Un file F est une structure de données abstraites
ayant les trois opérations :
— Enfiler(F,x) : enfile x à la queue de la file F .
37
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
II Implémentations
Plusieurs implémentations pour les piles et les files sont possibles et plus ou moins judicieuses selon le
contexte.
Implémentations d’une pile. On présente ici deux implémentations des piles : l’une contiguë et l’autre
chaı̂née.
Contiguë Les éléments de la pile sont rangés dans un tableau T et l’on conserve aussi l’indice de sommet
de pile dans une variable som. Pour dépiler, on renvoie T[som] et le sommet de pile devient som-1.
L’empilement se fait de manière similaire. Il existe deux variantes de cette implémentation, la première
qui renvoie une erreur lorsque le tableau est plein, l’autre qui gère les indices de manière circulaire
(via des modulos).
Chaı̂née Les éléments de la pile sont chaı̂nés entre eux, et le sommet d’une pile non vide est le premier
de la liste ; la pile vide est représentée par NIL.
Implémentation d’une file. Comme pour les piles, on présente ici deux implémentations des files, l’une
contiguë et l’autre chaı̂née.
Contiguë Les éléments de la file sont rangés dans un tableau, et on conserve les indices i et j (i < j) de
début et fin de file.
Chaı̂née Dans le cas d’une représentation chaı̂née, soit on a deux pointeurs, t^ ete et queue, vers le premier
et dernier élément de la file ; soit on récupère le premier élément via le pointeur du dernier élément
(comme sur la figure).
38 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Remarque 5.1 Le parcours en profondeur s’écrit plus élégamment en récursif. L’utilisation d’une pile
permet souvent de dérécursifier certains algorithmes.
Calcul de l’enveloppe convexe. Étant donné un ensemble Q de points sur le plan, un problème classique
consiste à trouver l’enveloppe convexe EC(Q), c’est-à-dire le plus petit polygone convexe qui contient
l’ensemble des points de Q. Un algorithme classique pour calculer cette enveloppe convexe est le balayage
de Graham, qui utilise une pile de manière astucieuse.
Développement 1. Présentation du balayage de Graham et correction.
Algorithme de remplacement de page. Dans le cadre d’une hierarchie mémoire (avec une petite
mémoire rapide et une grande mémoire lente), il faut un algorithme de remplacement de page. Deux des
algorithmes les plus connus sont FIFO et LRU (Least-Recently-Used). La première consiste à remplacer la
page en mémoire cache qui est rentrée en première, tandis que la deuxième consiste à remplacer la page
qui a été utilisée le moins récemment.
Développement 21. Analyse des performances des algorithmes onlines de remplacement de pages.
Remarque 5.2 L’algorithme FIFO s’implémente directement via une file, ce qui n’est pas le cas de LRU
qui nécessite une valeur de priorité pour chaque élément. On aura alors besoin des files de priorités.
retourner distance
39 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
L’algorithme de Dijkstra sélectionne itérativement et de façon gloutonne les sommets les plus proches
de s qui sont accessibles par le sommet courant. Il utilise un front de sommets éligibles au cours de son
exécution.
Remarque 5.3 On ne s’intéresse pas ici à la correction de l’algorithme mais aux structures de données à
utiliser.
Besoin de deux structures de données : une pour le graphe et une seconde que l’on appelle file de
priorité.
Nous allons évidemment utiliser une implémentation du graphe avec des listes d’adjacence car on a
besoin de parcourir les sommets adjacents.
B La structure de donnée
Définition 5.3 Une file de priorité est une structure qui permet d’organiser un ensemble d’éléments
munis de clés, l’ensemble de clés étant ordonné. Une file de priorité Q est définie pour tout élément
x associé à une clé k par les opérations Inserer(Q, x, k), Extraire min(Q), Est vide(Q) et
Réduire clé(Q, x, k).
Définition 5.4 Un tas-min (resp. max) est un arbre binaire complet gauche dont les noeuds sont
étiquetés par des éléments munis de clés qui vérifie la propriété du tas-min : la clé d’un nœud est
inférieure aux clés de ses enfants.
Exemple 5.1 On donne un tas-max pour la liste [4, 1, 3, 2, 16, 9, 10, 14, 8, 7]. On donne un tas-min avec
ces éléments ainsi que plusieurs exemples d’arbres qui ne sont pas des tas.
On utilise un tableau indexé de 1 à n pour stocker un tas de n éléments. On accède aux enfants d’un
noeud d’indice i en prennant les éléments d’indice 2i et 2i + 1 et on accède au parent en prenant l’élément
d’indice bi/2c.
Exemple 5.2
1. [16, 14, 10, 8, 7, 9, 3, 2, 4, 1] vérifie la structure de tas-max ;
2. On montre qu’il s’agit du parcours en largeur de l’arbre.
3. [16, 4, 10, 14, 7, 9, 3, 2, 8, 1] ne vérifie pas la structure de tas-max ;
4. Un tableau trié par ordre croissant (resp. décroissant) vérifie la structure de tas-min ([Link]).
Proposition 5.2 Avec une implémentation de tas-min pour la file de priorité et de liste d’adjacence
pour le graphe, l’algorithme de Dijkstra est en O((|V | + |E|) log(|V |)).
40 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
— L’algorithme de Prim ;
— L’ordonnancement des taches avec priorité (attention, c’est pas toujours des tas, par exemple l’or-
donnanceur linux utilise des arbres rouge noir)
Méthode de l’agrégat. Dans cette méthode, on montre que pour tout n, une suite de n opérations prend
le temps total T (n) dans le cas le plus défavorable. Ainsi, le coût amortie d’une opération sera T (n)/n.
On considère n opérations sur notre pile, chacune de ces opérations pouvant être soit DEPILER, EMPILER
ou MULTIDEP.
— Via une analyse dans le pire des cas, chaque opération coûte au pire O(n).
— Via la méthode de l’agrégat, on obtient une complexité amortie en O(1).
Méthode du comptable. Dans cette méthode, on affecte des coût différents à des opérations différentes
(un crédit). Le coût attribué à cette opération est alors son coût amorti.
Remarque 5.4 Ici, la valeur de T (n) ne change pas par rapport à la méthode de l’agrégat, mais on a
plus de précision sur le coût amorti de chaque opération.
Ainsi, en notant ci le coût de la ième opération, et cˆi son coût amorti, il faut que :
n
X n
X
cˆi ≥ ci
i=1 i=1
Méthode du potentiel. Le but est ici de représenter le travail prépayé comme une énergie potentielle .
On note Di notre structure après i opérations. On appelle fonction potentiel Φ : {Di } → R, tel que
Φ(Di ) est le potentiel de la structure Di . Le coût amorti est alors :
cˆi = ci + Φ(Di ) − Φ(Di−1 )
Pn
Proposition 5.3 i=1 cˆi est un majorant du coût réel total si, et seulement si, Φ(Dn ) ≥ Φ(D0 ).
En revenant à notre pile, on pose Φ(Pi ) = |Pi |, c’est-à-dire la hauteur de la pile. On retombe alors sur
les coûts amorties de la méthode comptable.
41 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Dijkstra en temps linéaire. On regarde ici une application de l’analyse amortie aux files de priorités.
Exercice 5.2 Montrer que, étant donné un graphe pondéré G = (V, E, w) où w : E → {0, ..., K} avec
K une constante, on peut résoudre le problème des plus courts chemins depuis un sommet en temps
O(|V | + |E|).
42 © 2022 M. Marin
Leçon 6
Introduction
Cette leçon présente deux structures de données abstraites fondamentales : les ensembles et les diction-
naires. Ces deux structures sont très fréquentes, pratiques à utiliser et faciles à implémenter. On étudie de
près les dictionnaires, en donnant 3 familles d’implémentations (liste chainée, table de hachage et arbre),
et l’on montre comment adapter facilement ces méthodes aux ensembles.
On appelle également parfois les dictionnaires des tableaux associatifs. Certains langages de program-
mation utilisent également la terminologie map. Dans la suite, on se restreint au cas où K = N, ce qui
n’est pas contraignant, car en pratique on manipule des objets dans un ensemble discret, codés en binaire.
Définition 6.2 (Ensemble) Soit U un ensemble d’éléments. Un ensemble S est une structure de
donnée abstraite défini par les opérations suivantes :
— Inserer(S,x) : insère x dans S.
— Recherche(S,x) : renvoie vrai si, et seulement si x est dans S.
— Supprime(S,x) supprime x de S s’il est présent.
43
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
On remarque la similarité entre les ensembles et les dictionnaires. Nous allons supposer que tous les
éléments x ∈ U ont un attribut de clé primaire [Link], tel que deux éléments de U distincts auront des clés
distinctes. Alors, on peut implémenter directement un ensemble à l’aide d’un dictionnaire.
Exercice 6.1 Donner une implémentation des ensembles utilisant les dictionnaires.
Cette hypothèse sur les éléments de U n’est pas si contraignante, car en pratique on considère toujours
des éléments ayant une clé identifiable. En Python, la fonction id renvoie pour chaque objet une valeur
distincte et constante tout au long de l’exécution du programme, et peut donc simuler une clé. En C,
l’adresse mémoire peut directement servir de clé, car deux objets du même type n’auront jamais la même
adresse.
II Premières implémentations
Nous allons étudier quelques manières d’implémenter les dictionnaires. Dans chaque version, on s’intéresse
à la complexité des trois primitives. Plus précisément, on regarde la complexité dans le pire cas, et la com-
plexité en moyenne. On distingue également les complexités des recherches fructueuses et infructueuses.
Nous verrons au fur et à mesure le modèle aléatoire considéré.
Listes chainées Voyons une implémentation naı̈ve des dictionnaires, par liste chaı̂née. Dans cette implémentation,
un dictionnaire D est une liste chaı̂née de couples (k, x).
— Insertion : on insère les nouveaux couples en tête de liste.
— Recherche : on recherche linéairement dans la liste.
— Suppression : on recherche linéairement, puis on supprime le maillon, en redirigeant les pointeurs si
besoin.
Proposition 6.1 Dans une implémentation d’un dictionnaire via une liste chaı̂née :
— l’insertion se fait en O(1) opérations élémentaires ;
— la recherche et la suppression se fait en O(n) opérations élémentaires où n est le nombre de
clés dans le dictionnaire, dans le pire des cas et en moyenne.
Tableaux à adressage direct. On suppose maintenant que les clés sont non seulement des entiers mais
sont en plus bornées : K = [0, m−1] pour un certain m ∈ N. Alors, on peut implémenter notre dictionnaire
directement avec un tableau T de taille m :
— Insertion : Pour insérer le couple (k, x), on effectue l’opération T [k] ← x
— Recherche : Pour rechercher la valeur de k, on renvoie T [k], si cette case est non vide.
— Suppression : Pour supprimer la valeur de k, on vide T [k].
Toutes ces opérations sont en O(1) ! En revanche, il faut allouer au démarrage un espace mémoire
proportionnel à la taille de l’univers des clés, y compris lorsque l’on utilise très peu de clés.
Exemple 6.1 On souhaite stocker des informations concernant les variables lors d’une étape de compila-
tion. En autorisant les 26 lettres minuscules et majuscules, et en bornant la taille du nom des variables par
40, notre tableau doit contenir au moins 2640 cases : ce n’est pas réalisable !
44 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Définition 6.3 (Arbre binaire de recherche) Soit A un arbre binaire enraciné, étiqueté par une
fonction e : A → U . On dit que A est un arbre binaire de recherche (ou ABR) si pour tout nœud
n, en notant g et d ses fils gauche et droit (s’ils existent), on a e(g) < e(n) < e(d).
De tels ABR ne peuvent que stocker des clés, mais on modifie facilement la structure pour pouvoir
rajouter des données satellites sur les nœuds, et donc implémenter les dictionnaires. A nouveau, cette
structure nécessite que l’ensemble où les clés vivent soit muni d’un ordre total.
Plusieurs ABR distincts peuvent représenter le même dictionnaire.
Exemple 6.2 Deux ABR représentants le même dictionnaire.
10 8
5 15 5 12
3 8 12 3 10 15
Suppression d’un élément. La suppression est un peu plus subtil. Lorsque le nœud est une feuille ou si
le nœud n’a qu’un enfant, alors la transformation est simple. Par contre, si le nœud possède deux enfants,
alors il faut retirer le minimum du sous-arbre droit (ou le maximum du sous-arbre gauche) pour le remplacer.
Exercice 6.3 Écrire une fonction extraire min : tree -> (tree*int) qui, étant donné un ABR, re-
tourne le couple correspondant au minimum et à l’arbre obtenu en le supprimant. En déduire une fonction
supprime : tree -> int -> tree.
— Arbre parfaitement équilibré : soit A un ABR contenant n valeurs, tel que chaque feuille est à la
même hauteur h. Alors, h = O(log(n)), et donc l’insertion, la recherche se font en (O(log(n)).
— Arbre linéaire : Soit A un ABR contenant n valeurs, tel qu’aucun nœud n’a de fils gauche. Alors, sa
hauteur est h = n : L’insertion et la recherche se font en O(n).
Complexité. Cet exemple souligne un problème majeur de l’implémentation par ABR : l’ordre d’insertion
des éléments détermine le squelette de l’arbre. La hauteur peut alors être linéaire.
B Arbres équilibrés.
Arbres AVL. Une solution est de rééquilibrer l’arbre de recherche au fil des insertions, et de garantir à
chaque instant une hauteur h = O(log(n)). Une telle solution a été notamment implémenté dans les AVL
(Adelson-Velsky et Landis) dont les mécanismes de rééquilibrages se basent sur des opérations de rotation
d’arbres. Un tel arbre est dit automatiquement équilibré.
45 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Exemple 6.3
y x
Rotation droite
x C A y
Rotation gauche
A B B C
B-arbres. Dans certains cas, le côté binaire des arbres de recherches peuvent poser problème. En effet,
regardons le cas d’une hiérarchie mémoire où les données sont stockés dans une mémoire lente (comme le
disque par exemple). Au lieu de rapatrier un nœud après l’autre, il peut être plus intéressant de créer de
plus gros nœuds .
Développement 3. B-arbres.
IV Tables de hachage
Dans la méthode arborescente vu précédemment, il y a un ordre sur les clés, et la place de la clé dépend
du rang de sa clé par rapport aux clés des autres éléments. Cependant, dans les méthodes de hachage la
place d’un élément est calculée uniquement à partir de sa clé.
Exemple 6.4 Si K = N et qu’on veut utiliser un tableau de taille m << |K| fixée, on peut discriminer
k ∈ K par (k mod m).
L’objet fondamental des tables de hachage est la fonction de hachage, qui transforme la clé en une
adresse dans une zone de mémoire contiguë ou un indice de tableau. Les cases de ce petit tableau sont
appelées alvéoles.
Définition 6.4 (Fonction de hachage) Soit K un ensemble de clés, et m ∈ N avec m << |U |. Une
fonction de hachage est une fonction h : K → [0, m − 1].
Donc, lorsque l’on souhaite stocker le couple (k, x), au lieu de stocker x dans la case k, on calcule
h(k), et l’on stocke x dans cette case. Comme h n’est pas injective (< |K|), il existe des valeurs k 6= k 0
telles que h(k) = h(k 0 ). On appelle collision un tel couple, et on dit qu’il y a collision dans une table de
hachage lorsque l’on insère une valeur dont la clé a le même hash qu’une clé déjà présente dans la table.
Exemple 6.5 Si K = Σ≤m−1 un ensemble de chaı̂nes de caractères de taille au plus m − 1 sur un alphabet
Σ. Une fonction de hachage possible consiste à associer à chaque mot sa taille. Il y a une collision pour
chaque mot de même longueur.
Choix de la fonction de hachage. Dans le premier exemple, la fonction de hachage est un reste modulo
m. Lorsque l’on prend m = 2p une puissance de 2, le hash d’une clé est constitué des p premier bits de
cette clé. En pratique, il peut être souhaitable que la fonction de hachage ne reflète pas la structure des
clés, et donc le choix de m est important.
En particulier, il est souhaitable le hachage soit uniforme. Lorsque
1
∀x ∈ U, ∀i ∈ {0, ..., m − 1}, P r[h(x) = i] =
m
on parle de hachage uniforme simple.
Il y a deux manières de gérer les collisions :
— par chaı̂nage : chaque case du tableau contient une liste chaı̂née, et donc si deux clés on le même
hash, elles se cumulent dans la même liste chaı̂née.
— par adressage ouvert : lorsque l’alvéole est déjà prise, on en cherche une autre.
46 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Analyse de complexité. Quelle est l’efficacité du hachage avec chaı̂nage ? En particulier, combien de
temps faut-il pour rechercher un élément dont on connaı̂t la clé ?
Étant donnée une table de hachage T à m alvéoles qui stocke n éléments, on définit pour T le facteur
de remplissage α par n/m, c’est-à-dire le nombre moyen d’éléments stockés dans une chaı̂ne. Notre analyse
se fera en fonction de α.
Théorème 6.1 Dans une table de hachage pour laquelle les collisions sont résolues par chaı̂nage,
une recherche infructueuse prend un temps moyen Θ(1 + α), sous l’hypothèse d’un hachage uniforme
simple.
Théorème 6.2 Dans une table de hachage pour laquelle les collisions sont résolues par chaı̂nage,
une recherche réussie prend en moyenne un temps Θ(1 + α), sous l’hypothèse d’un hachage uniforme
simple.
V Ensemble de mots
Les tables de hachage et les ABR permettent de stocker des données très générales. En contrepartie,
la structure des données que l’on cherche à stocker n’est pas exploitée. Dans certains cas, cette structure
peut mener à des implémentations encore plus efficaces que les tables de hachage. Intéressons nous au
problème suivant.
On cherche à stocker un ensemble de mots sur l’alphabet Σ, de façon à pouvoir ajouter, chercher
et supprimer des mots de cet ensemble. On souhaite également stocker, pour chaque mot, des données
satellites (par exemple : une définition pour les mots du dictionnaire d’une langue).
Les arbres préfixes (ou tries) sont une excellente manière d’implémenter de tels ensembles. Voyons sur
un exemple comment ces arbres fonctionnent :
Exemple 6.6 On considère l’arbre préfixe pour l’ensemble des mots a, aa, ab et bb.
aa
a a
a b ab
b
b b bb
Les arbres préfixes sont utilisés par exemple pour les algorithmes de prédiction de texte. Ils ont l’avantage
d’être très efficaces P
en espace et en temps : pour stocker des mots w1 , . . . , wn , un arbre préfixe utilise un
espace mémoire O( |wi |), et la recherche ou l’insertion d’un mot w se font en O(|w|). En particulier,
c’est indépendant du nombre d’éléments présents !
47 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
48 © 2022 M. Marin
Leçon 7
I Terminologie
Définition 7.1 (Chemin) Dans un graphe orienté G (resp. non orienté), on appelle chemin de lon-
gueur λ, une suite de (λ + 1) sommets (s0 , s1 , ..., sλ ) tel que : pour tout 0 ≤ i ≤ λ − 1, (si , si+1 ) est
un arc (resp. une arête) de G.
Remarque 7.1 Par convention, on dit qu’il y a un chemin de longueur 0 de tout sommet vers lui-même.
Dans un graphe non-orienté, les chemins sont aussi appelés chaı̂nes.
Définition 7.2 (Chemin élémentaire) Un chemin est dit élémentaire s’il ne contient pas plusieurs
fois le même sommet.
Exercice 7.1 Montrer que deux chemins élémentaires de longueur maximale dans un graphe connexe G
ont un sommet en commun.
Définition 7.3 (Circuit/Cycle) Dans un graphe orienté (resp. non orienté), un chemin (s0 , ..., sλ )
dont les λ arc (resp. arêtes) sont distincts deux à deux et tels que s0 = sλ , est un circuit (resp. un
cycle).
Exercice 7.2 On dit qu’un graphe non orienté est Eulérien s’il existe un cycle passant par chaque arête
exactement une fois. Montrer qu’un graphe est eulérien si et seulement si tous ses sommets sont de degré
pair.
II Accessibilité
49
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Définition 7.4 (Accessibilité) Étant donné un graphe G (orienté ou non) et deux sommets s et t,
on dit que t est accessible depuis s s’il existe un chemin allant de s à t dans G.
A Tri topologique
Définition 7.5 (Tri topologique) Étant donné un graphe orienté acyclique G = (V, E) avec V =
{v1 , ..., vn }, un tri topologique de V est une permutation σ : [n] → [n] tel que pour tout (ui , uj ) ∈ E,
σ(i) < σ(j).
Exemple 7.1 On souhaite exécuter 5 tâches t1 , ..., t5 sur un unique processeur. Cependant ces tâches
suivent le graphe de dépendance suivant :
1 2
Un tri topologique de ce graphe est alors (1, 2, 5, 4, 3), et on peut exécuter les tâches dans cet ordre
sans créer de problème.
Théorème 7.1 L’algorithme qui, étant donné un graphe orienté acyclique, réalise un parcours en
profondeur où la fonction de Post-traitement(u) ajoute u à une liste et retourne cette liste réalise un
tri topologique.
Définition 7.6 (Connexité) Un graphe orienté est dit fortement connexe si pour tout couple de
sommets distincts (u, v), u est accessible depuis v et v depuis u.
Un graphe non orienté est dit connexe si pour toute pair de sommets {u, v}, u est accessible
depuis v.
À partir du parcours en profondeur, on peut aussi calculer les composantes connexes d’un graphe non
orienté facilement. En effet, on a exactement une composante connexe par appel à la fonction Visiter-PP.
50 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Définition 7.7 (Composante fortement connexe (CFC)) On appelle composante fortement connexe
d’un graphe orienté un sous-graphe fortement connexe maximal, c’est-à-dire un sous-graphe fortement
connexe qui n’est pas strictement contenu dans un autre sous-graphe fortement connexe.
Exercice 7.3 Montrer que le graphe des composantes fortement connexes, obtenu en remplaçant chaque
CFC par un unique sommet, est un graphe orienté acyclique.
Le calcul des composantes fortement connexes est plus compliqué qu’un simple parcours.
Théorème 7.2 Étant donné un graphe orienté G = (V, E), on peut calculer les composantes forte-
ment de connexes en temps linéaire, O(|V | + |E|).
Développement 14. Ces algorithmes permettent de résoudre le problème 2-SAT en temps linéaire.
Définition 7.9 (Graphe pondéré) Un graphe pondéré est un graphe G = (V, E) muni d’une fonc-
tion de poids w : E → R. On peut étendre w à V 2 en posant w(u, v) = +∞ lorsque (u, v) ∈ / E.
Le poids d’un chemin est alors défini comme la somme des poids des arêtes qui le compose.
Définition 7.10 (Distance dans un graphe pondéré) Étant donné un graphe pondéré (G, w) dans
circuit de poids strictement négatif, on définit la distance de u à v par :
51 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
A Algorithme de Floyd-Warshall
On essaye ici de trouver les plus courts chemins pour toutes les paires de sommets. On supposera
G = (V, E, w) avec V = {0, ..., n − 1}, et si ij ∈
/ E, alors on pose w(ij) = +∞.
(k)
Cet algorithme utilise la programmation dynamique : en notant dij la distance du plus court chemin
allant de i à j passant par les k premiers sommets, on a
( (k)
dij = wij si k = 0
(k) (k−1) (k−1) (k−1)
dij = min(dij , dik + dkj ) si k ≥ 1
L’algorithme de Floyd-Warshall implémente directement cette idée avec une complexité temporelle
O(|V |3 ).
Exercice 7.4 (Plus grande bande passante) On définit la bande passante d’un chemin comme le plus
petit poids d’une arête sur ce chemin. Proposer un algorithme qui, pour toute paire de sommet, détermine
la plus grande bande passante d’un chemin entre ces deux sommets.
B Algorithme de Dijkstra
On essaye ici, étant donné une source s, de trouver l’ensemble des plus courts chemin de s aux autres
sommets.
retourner distance
Théorème 7.5 L’algorithme de Dijkstra réalise au plus |V | appels à Insérer et ExtraireMin, et |E|
appels à DiminuerPriorité.
Implémentation. Une structure de file de priorité doit être implémenter pour l’algorithme de Dijkstra.
Par exemple, on utilisant un tas-min, on obtient les opération élémentaires en O(log n) où n est le nombre
d’éléments dans le tas. Ainsi, la complexité de Dijsktra est en O((|V | + |E|) log |V |).
Exercice 7.5 En supposant que la fonction de poids vérifie w : E → {0, ..., K}, montrer que l’on peut
implémenter l’algorithme de Dijkstra en temps linéaire.
52 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Définition 7.11 (Problème du voyageur de commerce (TSP)) Étant donné un graphe pondéré
complet G = (V, E), une fonctionP de coût w : E → N, existe-t-il un cycle C passant par chaque
sommet une et une seule fois avec e∈C w(e) ≤ k ?
Théorème 7.7 Pour tout λ ≥ 1, il n’existe pas de λ-approximation de TSP, à moins que P = NP.
On peut tout de même restreindre le problème au cas où la fonction de coût w vérifie l’inégalité
triangulaire. On obtient le problème TSP − EUCLIDE.
B Chemin disjoints
On considère le problème d’optimisation suivant, consistant à réserver des chemins disjoints dans un
graphe pour satisfaire des requêtes.
Ce problème s’avère être NP-complet aussi, mais on peut trouver des approximations.
Théorème 7.9 Il existe une (n − 1)-approximation pour le problème MEDP, où n est le nombre de
sommets du graphe en entrée.
53 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
√
Théorème 7.10 Il existe une d m e-approximation pour le problème MEDP, où m est le nombre
d’arêtes du graphe en entrée.
1
On peut aussi montrer que l’algorithme précédent est le meilleur possible : il n’existe aucune m 2 − -
approximation ( > 0) à moins que P = NP.
54 © 2022 M. Marin
Leçon 8
Spécification.
Définition 8.1 (Problème de tri) Étant donné un tableau de n éléments T = [e1 , ..., en ] où chaque
élément ei est muni d’une clé c(ei ) ∈ U où U est totalement ordonné, le résultat du problème du tri
est une permutation de L, noté L0 = [e01 , ..., e0n ] où pour tout 1 ≤ i < j ≤ n, c(e0i ) ≤ c(e0j ).
Exemple 8.1 On cherche à trier les mots [aaa,bb,aaaa,c] où la clé de chaque mot est sa taille. La solution
au problème du tri est alors la liste [c,bb,aaa,aaaa].
Stabilité. Dans le cas où les éléments de la liste à trier ont une même clé, la solution au problème n’est
pas unique. On dit qu’une méthode de tri est stable si en cas d’égalité de clés, les éléments triés conserve
l’ordre de départ.
Organisation de la mémoire. Si la liste à trier ne tient pas en mémoire, on est amené à utiliser des
techniques particulières, dites tris externes. À l’inverse, si on peut tout faire en mémoire centrale, on
réalise un tri interne. On dit qu’un tri est en place si on ne réalise pas de copie de la liste à trier.
Convention. Pour faciliter la compréhension, on se contente dans cette leçon de trier des listes d’entiers,
et on se concentrera sur les tris internes.
55
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Exercice 8.1 En se rappelant que le nombre de comparaisons nécessaires pour récupérer le minimum
d’une liste k éléments est k − 1, montrer que TriSélection réalise exactement n(n−1)
2 comparaisons et n − 1
échanges.
Tri à bulles. Voici ici l’exemple d’un algorithme peu efficace mais simple à implémenter. L’idée est de
parcourir la liste par la fin, et à chaque fois qu’on rencontre deux éléments successifs non triés, on les
échange.
Exercice 8.2 Montrer que le nombre de comparaisons de TriBulles est le même que celui de TriSélection.
En déduire une borne sur le nombre d’échanges.
Théorème 8.1 En supposant que la liste L est une permutation de {1, ..., n} tiré uniformément, le
nombre moyen d’échange de TriBulles(L) est un O(n2 ).
Exercice 8.3 (Tri d’une liste en Ocaml) Écrire un fonction OCaml insert : int -> int list ->
int list qui insère un élément dans une liste triée, et une fonction triInsertion : int list -> int
list réalisant un tri insertion d’une liste d’entier.
56 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Théorème 8.2 (Correction et complexité du tri insertion) Étant donné un tableau T à n éléments,
la procédure TriInsertion(T, n − 1) trie T en réalisant dans le pire cas n(n−1)
2 − 1 comparaisons.
Théorème 8.3 (Complexité en moyenne du tri insertion) Étant donné un tableau à n éléments,
2
l’algorithme de tri par insertion réalise en moyenne n +3n
4 − 1 comparaisons.
Remarque 8.1 Au lieu de compter le nombre de comparaisons, on pourrait compter le nombre de trans-
fert (un transfert est une opération de la forme T [...] ← ...). Ce nombre reste quadratique en la taille de
la liste.
Le tri par insertions séquentielles n’utilise pas les spécificités des tableaux, et peut donc être implémenter
pour trier une liste comme sur l’exercice précédent.
Puisque l’accès à un élément d’un tableau est en coût constant, on peut rechercher la place de l’élément
à insérer en réalisant un nombre logarithmique de comparaisons via dichotomie. On n’améliore pas le nombre
de transfert mais le nombre de comparaisons devient un O(n log n).
A Tri fusion
L’algorithme du tri par fusion suit fidèlement la méthodologie diviser-pour-régner. Intuitivement, il
agit de la manière suivante :
Diviser Diviser la suite de n éléments à trier en deux sous-suites de n/2 éléments chacune.
Régner : trier les deux sous-suites de manière récursive en utilisant le tri par fusion.
Combiner : Fusionner les deux sous-suites triées pour produire la réponse triée.
La récursivité s’arrête quand la séquence à trier est de longueur 1.
Exercice 8.4 Écrire trois fonctions OCaml permettant l’implémentation du tri fusion de deux listes d’en-
tiers : split : int list -> (int list * int list), merge : int list -> int list -> int list
et merge sort : int list -> int list.
Théorème 8.4 L’algorithme de tri par fusion trie une séquence d’entiers en O(n log n).
B Tri rapide
L’idée est de partager la liste en deux sous-listes, où tous les éléments de la première sont plus petits
que ceux de la seconde. On trie alors récursivement ces deux sous-listes. Pour séparer les deux listes, on
choisit un pivot.
Comment choisir le pivot ? L’idéal serait de prendre la médiane. Cependant, ce serait trop coûteux en
temps. Plusieurs moyens de choisir le pivot existe. On en verra deux : on prend le premier élément de la liste
ou un élément au hasard. D’autres choix sont possibles, comme la médiane des trois premiers éléments.
57 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Algorithme de tri rapide. On se donne tout d’abord une procédure Partition(T,p,r) qui effectue la
partition du sous-tableau T [p...r] et retourne l’indice q du pivot.
Partition du tableau. Le but est ici de partitionner le sous-tableau T [p...r] en choisissant pour pivot
T [r].
Théorème 8.5 La procédure TriRapide(T, 0, |T | − 1) trie le tableau T . Son temps d’exécution est
dans le cas le plus défavorable un O(n2 ).
Remarque 8.2
1. Même si sa complexité est moins bonne que le tri-fusion, le tri rapide est en place. Il est donc plus
utilisé en pratique.
2. Via une analyse probabiliste, on peut montrer que la complexité moyenne du tri rapide est un
O(n log n). Cette preuve est très similaire à la preuve de l’espérance du nombre de comparaison du
tri rapide randomisé.
58 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Théorème 8.6 L’espérance du nombre de comparaisons du tri rapide randomisé d’un ensemble à n
éléments est au plus 2nHn où Hn est le terme général de la série harmonique.
Théorème 8.7 La complexité, en nombre de comparaisons, aussi bien moyenne qu’au pire, de tout
algorithme de la classe TC est d’ordre supérieur ou égal à n log2 n.
Remarque 8.3 Tous les algorithmes ne sont pas de la classe TC. Certains algorithmes utilisent des
propriétés sur les objets à trier, comme le tri par paquet qui fonctionne sur des nombres réels appartenant
à un intervalle borné fixé à l’avance. Cet algorithme est en moyenne linéaire.
Moyenne géométrique. Étant donné deux ensembles de réels A et B chacun de taille n, on cherche à
ordonner A et B de tel manière à maximiser
Yn
abi i
i=1
L’algorithme glouton qui prend les maximums de A et B, les associe ensemble et recommence est
optimal. Il suffit alors de trier A et B, ce qui se fait en O(n log n)
Problème du gymnase. Le but est d’organiser plusieurs évènements dans un gymnase sans que ceux-ci
se chevauchent. On cherche à placer un maximum d’évènements, chacun étant caractérisé par une heure
de début di et de fin fi . L’algorithme glouton qui trie les évènements par heure de fin croissante, et place
les évènements de manière gloutonne est alors optimal.
B Somme de flottants
Un nombre flottant est une paire d’entiers (m, d) représentant le nombre m × bd où b vaut 2 ou 10.
Dans tous les environnements de programmation, m et d sont limités : chaque opération arithmétique peut
alors avoir une erreur d’arrondi.
59 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
On introduit alors une valeur < 1 appelée précision machine afin de borner les erreurs en fonction
de . Par exemple, pour l’addition de deux nombres flottants w et y, on peut écrire
avec |δxy | ≤ . On considère l’algorithme de somme d’une séquence (x1 , ..., xn ) donné ci-dessous :
Or, si on considère que est assez petit tel que pour tout flottant x, 2 x est négligeable, alors on peut
estimer une borne supérieure en pour la différence dn = |FloatSum(x1 , ..., xn ) − (x1 + ... + xn )| :
n
X
en = (n − i + 1)xi
i=1
Pour justifier cette formule, on peut montrer par récurrence que pour tout n il existe un flottant x tel que
dn ≤ en + 2 x.
Théorème 8.8 La séquence de flottants x1 , ..., xn minimise en lorsque les xi sont triés par ordre
décroissant.
Un pré-traitement qui trie en O(n log n) les n nombres flottants permet alors de calculer la somme en
minimisant l’erreur en .
C Balayage de Graham
Étant donné un ensemble de points sur le plan, on cherche une enveloppe convexe de ces points. Une
solution, appelée balayage de Graham, cherche le point p0 d’ordonnée et d’abscisse minimales, puis trie
les sommets par angle polaire respectivement à p0 . Ce pré-traitement nous permet de calculer l’enveloppe
convexe de proche en proche.
60 © 2022 M. Marin
Leçon 9
I Introduction
A Motivation
B Définitions de base
Définition 9.1
Un alphabet Σ est un ensemble fini. Un mot sur Σ est une séquence finie de caractères de Σ. Σ∗
est l’ensemble des mots sur Σ. Le mot vide est noté .
— y est préfixe d’un mot x s’il existe z tel que x = yz.
— z est un suffixe d’un mot x s’il existe y tel que x = yz
— w est un facteur d’un mot x s’il existe y et z tels que x = ywz.
Si x est de taille n, pour tous 0 < i < j ≤ n on notera x[i...j] = xi xi+1 . . . xj . Si j < i, on a
x[i..j] = .
61
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Définition 9.2 (Arbre des préfixes) Etant donné un ensemble de mots X, l’arbre des préfixe P
de cet ensemble est défini comme suit :
— L’ensemble des sommets de P est l’ensemble des préfixes des mots de X ;
— Il existe une arête du préfixe de p à p0 dans P si p0 = p · a avec a ∈ Σ.
Proposition 9.1 Un arbre préfixe est une implémentation d’un ensemble. Pour un mot de taille n, la
recherche, l’insertion et la suppression se font en O(n). La création est en O(Σx∈X |x|).
Exemple 9.1 On crée l’arbre des préfixes associé à l’ensemble {tarte, tartine, pates, patates, tarot, tri}.
Application 9.1 En traitement du langage naturel : autocomplétion, détection de lexiques dans un texte ;
Algorithmique : trier rapidement un ensemble de chaines de caractères.
B Arbres suffixes
Définition 9.3 (Arbre des suffixes) Un arbre des suffixe d’un mot x est l’arbre préfixe de l’en-
semble des suffixes de x.
Définition 9.4 (Arbre des suffixes généralisés) Un arbre suffixe généralisé (ASG) d’un ensemble
de mots X est l’arbre préfixe de l’ensemble des suffixes des mots de X.
Proposition 9.2 Un ASG d’un ensemble X se construit naı̈vement en O(Σx∈X |x|2 ) dans le pire cas.
Application 9.2 — Trouver les occurences d’un ou plusieurs motifs dans un ensemble de mots ;
— Trouver la plus longue sous chaı̂ne commune à un ensemble de mots en temps linéaire en la somme
des mots, ce qui améliore une approche naı̈ve par programmation dynamique.
C Compression
On dispose d’un texte t et on souhaite lui donner une représentation en mémoire de taille faible.
Définition 9.5 (Codes) Un codage pour un alphabet Σ est une fonction injective h : Σ → {0, 1}∗ .
Pour un caractère c, h(c) est le mot de code de c. On étend h à Σ+ en posant h(x) = h(x1 ) . . . h(xm )
pour tout mot x ∈ Σ+ de taille m.
62 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Exemple 9.3 Exemple de codage à longueur variable et préfixe pour Σ = {a, b, c, d, e, f } est la fonction
χ définie par χ(a) = 0, χ(b) = 101, χ(c) = 100, χ(d) = 111, χ(e) = 1101 et χ(f ) = 1100.
Codage de Huffman
Idée 9.1 Pour le codage : étant donné un texte t, on souhaite le représenter avec le moins de bits possibles
à l’aide d’un code préfixe de longueur variable.
Pour l’algorithme : construire de façon gloutonne un arbre dont les feuilles sont les éléments de Σ et dont
le chemin de la racine à une feuille donne le mot de code du caractère (enfant gauche → 0 et enfant droit
→ 1).
Algorithme de Huffman
Algorithme 9.1 : Algorithme de Huffman
Données : Un texte t.
Résultat : Un arbre qui représente le code de Huffman de t.
T = {(c, f req(c, t)), c ∈ Σ} ;
# T contient un ensemble d’arbres que nous allons fusionner itérativement
tant que |T | > 1 faire
choisir t1 , t2 ∈ T de fréquence minimale ;
Remplacer t1 et t2 dans T par un éléménent t3 de fréquence f (t3) = f (t1 ) + f (t2 ) ;
retourner T[0]
Proposition 9.3 L’algorithme de Huffman produit un codage préfixe de longueur variable en temps
O(|Σ| log(|Σ|) + t) en utilisant un tas.
Théorème 9.1 Soit h le code de Huffman d’un texte t et l un autre codage des caractères non
ambigü. Alors, |h(t)| ≤ |l(t)|.
Remarque 9.3 En particulier, cela signifie que h effectue bien une compression efficace.
Application 9.3 Est utilisé à certaines étapes de la compression dans les formats JPEG, MP3 et zip par
exemple.
63 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Algorithme de Lempel, Ziv, Welch On part d’un dictionnaire qui à chaque caractère associe un entier
encodé sur un nombre fixe de bits (ex : ASCII) et on l’étend dynamiquement aux chaı̂nes de caractères lors
de la lecture de texte.
1. on lit les symboles jusqu’à former un mot qui n’est pas dans le dictionnaire ;
2. on l’ajoute au dictionnaire avec le prochain code libre (257 en premier) ;
3. on écrit le code correspondant au mot sans le dernier caractère ;
4. on recommence en partant du dernier caractère.
Exemple 9.4 Faire trouver l’algorithme de compression et décompression sur un texte exemple : taratata.
Proposition 9.5 On construit le dictionnaire dynamiquement de telle sorte qu’il n’est pas nécessaire
au décodage. Il est même reconstruit dynamiquement au décodage.
Définition 9.6 Un mot s de taille k est une sous-séquence de x de taille m s’il existe une famille
strictement croissante d’indices {i1 , · · · , ik } telle que pour tout j, xij = sj .
La problème : étant donnés x et y deux mots sur un alphabet Σ, donner une PLSC de x et y.
Résolution du problème : on utilise un algorithme de programmation dynamique.
B Distance d’édition
Définition 9.7 On définit les opérations de substitution, insertion et suppression dans une chaı̂ne de
caractères comme opération d’édition. La distance de Levenshtein entre deux mots est le nombre
minimal d’opérations d’édition nécessaires pour passer de l’une à l’autre.
Remarque 9.5 Dans la section suivante, on aborde la recherche de motif. On cherche ainsi sous-chaı̂ne
de notre texte à distance nulle de notre motif. Si on allège cette contrainte (en bornant la distance par
exemple), on peut faire de la recherche de motif approchée .
Étant donné un texte T et un motif M , le but est ici de déterminer si M est un facteur de T , autrement
dit si M apparaı̂t dans T . Si besoin, on pourra aussi calculer le nombre d’occurrences de M dans T .
Dans toute la partie on aura |M | = k et |T | = n.
64 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Exemple 9.7 Avec T = aabababababbbababab et M = abab, les occurrences sont {2, 4, 6, 8, 13, 15}.
Dans les algorithmes que l’on va étudier, on observera une fenêtre de taille m du texte.
B Algorithme naı̈f
Proposition 9.6 Dans le pire cas, la complexité temporelle est O(mn). Pour un texte aléatoire,
l’algorithme est en O(2n) en moyenne.
Remarque 9.6 Même si cette complexité linéaire peut sembler satisfaisante, les textes sont souvent de
très grande taille. On va donc essayer d’améliorer la complexité moyenne de cet algorithme.
L’idée est de lire de gauche à droite le texte T et M de droite à gauche ce qui nous permet de sauter
des parties entières des mots. Trois cas peuvent se produire :
— tous les caractères sont égaux, on a trouvé une occurrence et on décale la fenêtre au maximum ;
— si on trouve une différence à la lettre ti et que ti ∈
/ M , on décale la fenêtre à l’indice i + 1.
— si on trouve un différence à la lettre ti avec ti ∈ M , on décale la fenêtre pour aligner la première
occurrence de ti dans M à l’indice i.
Algorithme 9.2 : Algorithme de Horspool
Données : Un texte t et un motif x.
Résultat : L’ensemble des occurences de x dans t.
pour j allant de 1 à n − m + 1 faire
si t[j..j + m]! = x alors
Décaler x pour aligner t[j + m] et l’occurence de t[j + m] la plus à droite dans x ;
sinon
Ajouter j aux occurences de x dans t et décaler x à la deuxième occurence la plus à droite
de t[j + m] dans x ;
Remarque 9.7 En pratique, l’algorithme est très rapide (sous linéaire en moyenne) et très utilisé.
On peut construire un automate qui reconnaı̂t le langage le langage Σ∗ M . Ainsi, on simule A sur
l’entrée T .
Développement 8. Construction de l’automate des motifs.
65 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
E Généralisations du problème
Recherche d’un ensemble fini de motifs dans un texte. Soit X un ensemble de motifs et t un texte.
L’algorithme d’Aho-Corasick qui reprend l’idée de trie et d’automate des occurrences permet de résoudre
ce problème en temps O(m + n) où m = Σx∈X |x| et O(m) en espace.
Appartenance d’un mot au langage d’une expression régulière. Le problème de recherche d’un motif
x dans un texte t revient à décider si t ∈ Σ∗ xΣ∗ . On peut généraliser au problème suivant : étant donné une
expression régulière e et un mot t, est-ce-que t ∈ L(e) ? Pour cela, on crée un automate fini déterministe
à partir de l’expression régulière.
Application 9.4 Ce problème est le problème de base de l’analyse lexicale, qui est centrale en compilation.
Appartenance d’un mot au langage d’une grammaire. De la même manière, on peut se demander si
un mot peut être engendré par une grammaire. L’algorithme Cocke-Younger-Kasami (CYK) permet de
résoudre ce problème dans le cadre des grammaires sous forme normale de Chomsky.
Application 9.5 Ce problème est le problème de base de l’analyse syntaxique, qui est centrale en compi-
lation.
66 © 2022 M. Marin
Leçon 10
I Motivation et définitions
De manière abstraite, un arbre est une structure de donnée qui stocke des éléments de manière
hiérarchique. À part l’élément du haut, chaque élément a un parent et possiblement des enfants. L’élément
du haut est appelé racine et on représente un arbre du haut vers le bas (contrairement aux arbres en bio-
logie).
Exemple 10.1 (Arborescence des fichiers) Un système de fichier peut être représenté par un arbre, où
les enfants d’un fichiers est l’ensemble des fichiers qu’il contient.
Exemple 10.2 (Expressions arithmétiques) Une expression arithmétique, comme par exemple (2 + 3) ×
(5 + 7), se représente naturellement par un arbre. Les parenthèses servent juste à la hiérarchie.
A Définitions
Définition 10.1 (Arbre binaire) Un arbre binaire est soit vide (noté Nil), soit de la forme B =
(u, B1 , B2 ), où B1 et B2 sont des arbres binaires disjoints et u est un nœud appelé racine de B.
Remarque 10.1
1. Cette définition n’est pas symétrique, l’arbre (u, (u1 , Nil, Nil), (u2 , Nil, Nil)) n’est pas la même chose
que (u, (u2 , Nil, Nil), (u1 , Nil, Nil)).
2. B1 (resp. B2 ) est appelé sous-arbre gauche (resp. droit) de B = (u, B1 , B2 ). La racine de B1
(resp. B2 ) est appelé fils gauche (resp. droite).
3. Un nœud racine d’un arbre de la forme (u, Nil, Nil) et appelé feuille.
Définition 10.2 (Arbre) Un arbre est soit l’arbre vide Nil, soit A = (u, A1 , ..., Ap ) où p ≥ 1,
A1 , ..., Ap sont des arbres et u est un nœud.
67
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Si à chaque nœud d’un arbre on associe une étiquette (un entier par exemple), on parle d’arbre étiqueté.
Définition 10.3 (Taille) La taille t(A) d’un arbre binaire A est le nombre de nœud qui le compose.
On peut la définir inductivement :
— t(Nil) = 0,
— t(u, A1 , A2 ) = 1 + t(A1 ) + t(A2 ).
Définition 10.4 (Hauteur) La hauteur h(A) d’un arbre A est la taille d’un plus long chemin de la
racine jusqu’à une feuille. On peut la définir de manière inductive :
— h(Nil) = 0,
— h(u, A1 , A2 ) = 1 + max(h(A1 , A2 ).
Exercice 10.2 Écrire deux fonctions taille : binaryTree -> int et hauteur : binaryTree -> int.
Exercice 10.3 (Lemme de König) Tout arbre infini à branchement fini contient un chemin infini.
blog2 nc ≤ h ≤ n − 1
1 2n
Proposition 10.1 Le nombre d’arbres binaires de taille n est bn = n+1 n .
Exercice 10.4 (Arbres filiformes) Un arbre filiforme est une généralisation des peignes a au moins un
enfant qui est une feuille. Combien existe-t-il d’arbres filiformes à n nœuds ?
II Représentation et parcours
A Représentation des arbres binaires
Utilisation des pointeurs. À chaque nœud, on associe deux pointeurs, l’un vers le sous-arbre gauche,
l’un vers le sous-arbre droit. L’arbre est déterminé par l’adresse de sa racine.
68 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Utilisation de tableaux. Lorsqu’on ne peut pas allouer dynamiquement de la mémoire, on peut implémenter
l’arbre binaire via un tableau à trois colonnes (noeud, fils droit et fils gauche). Cette structure permet no-
tamment de stocker un arbre dans une base de donnée relationnelle.
Représentation des arbres binaires parfaits. Si un tel arbre contient n nœuds, il peut être représenté
par un tableau de taille n indicé de 1 à n. Ainsi, si un nœud est à la case 1 ≤ i ≤ n, ses enfants seront
aux cases 2i et 2i + 1.
Théorème 10.1 Il existe une bijection entre les arbres généraux ayant n + 1 nœuds et les arbres
binaires à n nœuds.
Parcours en profondeur. L’idée est d’aller le plus loin possible et de revenir en arrière lorsqu’on rencontre
une feuille.
Parcours en largeur. Un parcours en largeur correspond à un ordre hiérarchique de l’arbre. Cet algorithme
est naturellement itératif et peut s’implanter via une file.
69 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
L’idée générale est de stocker les couples (k, x) dans un arbre, de telle sorte qu’il soit facile de retrouver
un élément en partant de la racine.
Définition 10.5 (Arbre binaire de recherche) Soit A un arbre binaire enraciné, étiqueté par une
fonction e : A → U . On dit que A est un arbre binaire de recherche (ou ABR) si pour tout nœud
n, en notant g et d ses fils gauche et droit (s’ils existent), on a e(g) < e(n) < e(d).
De tels ABR ne peuvent que stocker des clés, mais on modifie facilement la structure pour pouvoir
rajouter des données satellites sur les nœuds, et donc implémenter les dictionnaires. Cette structure nécessite
que l’ensemble où les clés vivent soit muni d’un ordre total.
Exercice 10.5 Définir un type tree en OCaml où chaque nœud contient un entier. Écrire les fonctions
recherche : tree -> bool et insert : tree -> int -> tree.
Suppression d’un élément. La suppression est un peu plus subtile. Lorsque le nœud est une feuille
ou si le nœud n’a qu’un enfant, alors la transformation est simple. Par contre, si le nœud possède deux
enfants, alors il faut retirer le minimum du sous-arbre droit (ou le maximum du sous-arbre gauche) pour le
remplacer.
Exercice 10.6 Écrire une fonction extraire min : tree -> (tree*int) qui, étant donné un ABR,
retourne le couple correspondant au minimum et à l’arbre obtenu en le supprimant. En déduire une fonction
supprime : tree -> int -> tree.
— Arbre parfaitement équilibré : soit A un ABR contenant n valeurs, tel que chaque feuille est à la
même hauteur h. Alors, h = O(log(n)), et donc l’insertion, la recherche se font en (O(log(n)).
— Arbre linéaire : Soit A un ABR contenant n valeurs, tel qu’aucun nœud n’a de fils gauche. Alors, sa
hauteur est h = n : L’insertion et la recherche se font en O(n).
Complexité. Cet exemple souligne un problème majeur de l’implémentation par ABR : l’ordre d’insertion
des éléments détermine le squelette de l’arbre, et on peut obtenir un peigne.
B Arbres équilibrés.
Arbres AVL. Une solution est de rééquilibrer l’arbre de recherche au fil des insertions, et de garantir à
chaque instant une hauteur h = O(log(n)). Une telle solution a été notamment implémenté dans les AVL
(Adelson-Velsky et Landis) dont les mécanismes de rééquilibrages se basent sur des opérations de rotation
d’arbres. Un tel arbre est dit automatiquement équilibré.
Exemple 10.4
70 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
y x
Rotation droite
x C A y
Rotation gauche
A B B C
B-arbres. Dans certains cas, le côté binaire des arbres de recherches peuvent poser problème. En effet,
regardons le cas d’une hiérarchie mémoire où les données sont stockés dans une mémoire lente (comme le
disque par exemple). Au lieu de rapatrier un nœud après l’autre, il peut être plus intéressant de créer de
plus gros nœuds .
Développement 3 : B-arbre
Définition 10.6 (Arbre) On appelle arbre un graphe non orienté, connexe et acyclique.
Proposition 10.3 Soit G = (V, E) un graphe non orienté, ayant n sommets. Les propriétés suivantes
sont équivalentes :
1. G est un graphe connexe sans cycle,
2. G est connexe avec (n − 1) arêtes,
3. G est acyclique avec (n − 1) arêtes.
Étant donné un graphe G = (V, E) connexe, on appelle arbre couvrant de G tout sous-graphe
connexe et acyclique de G. Lorsque le graphe est pondéré, un problème classique est la recherche d’un
arbre couvrant de poids minimum. On étudie deux algorithmes permettant de résoudre ce problème :
celui de Kruskal et celui de Prim.
Besoin de deux structures de données : une pour le graphe et une seconde que l’on appelle file de
priorité.
71 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
La structure de donnée.
Définition 10.7 Une file de priorité est une structure qui permet d’organiser un ensemble d’éléments
munis de clés, l’ensemble de clés étant ordonné. Une file de priorité Q est définie pour tout élément
x associé à une clé k par les opérations Inserer(Q, x, k), Extraire min(Q), Est vide(Q) et
Réduire clé(Q, x, k).
Une première implémentation des files de priorités utilise simplement un tableau. On présente une
manière plus efficace : un tas.
Définition 10.8 Un tas-min (resp. max) est un arbre binaire complet gauche vérifiant la propriété
du tas-min (resp. max) : la clé d’un nœud est inférieur à la clé de ses enfants.
Exemple 10.5 On donne un tas-max pour la liste [4, 1, 3, 2, 16, 9, 10, 14, 8, 7]. On donne un tas-min
avec ces éléments ainsi que plusieurs exemples d’arbres qui ne sont pas des tas.
Comme on l’a vu précédemment, un arbre binaire complet gauche peut facilement être représenté via
un tableau. Si le tableau est trié, alors le tas correspondant vérifie la propriété du tas-min.
On fournit l’implémentation de Inserer(Q, x, k), Extraire min(Q) et Réduire clé(Q, x, k).
On montre qu’elles se font en O(log(n)).
Complexité de Prim.
Proposition 10.4 Avec une implémentation de tas-min pour la file de priorité et de liste d’adjacence
pour le graphe, l’algorithme de Prim est en O(|E| + |V | log(|V |)).
Développement 4. Autre application du tas : création d’un tas en O(n) et tri par tas
72 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Remarque 10.2 On ré-utilise une structure que l’on a déjà vu dans le passé, la file de priorité pour stocker
les arêtes dont les étiquettes sont leur poids.
Il nous faut une manière efficace de déterminer si l’ajout d’une arête à la forêt T crée un cycle ou non.
Pour cela nous allons introduire une nouvelle structure de données : unir et trouver.
Exemple 10.6 En partant d’un ensemble simple {3, 9, 18, 33, 208} on crée la structure d’unir et trouver
et on effectue quelques opérations unir et trouver.
On donne l’exemple de l’algorithme de détection de composantes connexes dans un graphe.
Remarque 10.3 Même dans des textes écrits en français, il ne sera pas rare de lire Union-Find plutôt
que unir et trouver. En anglais on parle aussi de structure d’ensemble disjoints.
Théorème 10.4 (Complexité) L’algorithme de Kruskal utilise |V | fois CréerEnsemble, 2|E| fois
Trouver et |V | − 1 fois Union.
Implémentation avec des forêts Un ensemble est représenté par un arbre, chaque nœud est un élément
et il pointe vers son parent. La racine de l’arbre est le représentant de l’ensemble.
Optimisation 1 : Union par rang. On associe à chaque racine un rang, correspondant à la hauteur
de son arbre. En s’assurant que lors d’une union, on augmente au maximum la hauteur de 1, on peut avoir
une borne sur la hauteur maximal d’un arbre et donc les opérations Unir et Trouver en O(log n).
Théorème 10.5 Avec une implémentation de la structure Unir et trouver par une forêt avec union
par rang, l’algorithme de Kruskal s’exécute en temps O(|V | + |E|) log n).
Remarque 10.4 On atteint le temps de tri des arêtes qui est incompressible. Par contre, si on suppose
que les arêtes sont déjà triées, on peut encore améliorer la complexité.
73 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
74 © 2022 M. Marin
Leçon 11
Dans le cadre de cette leçon, on s’intéresse à la recherche d’une solution à un problème en relâchant
certaines contraintes. Dans la première partie, on autorise notre algorithme à avoir une partie aléatoire,
pouvant causer un temps d’exécution ou un résultat aléatoire. Dans un second temps, on s’autorisera à
renvoyer un résultat différent du résultat optimal, tant qu’il reste assez proche.
I Algorithmes probabilistes
A Algorithmes déterministes/probabilistes
Définition 11.1 (Algorithme déterministe) Un algorithme déterministe est un algorithme qui, étant
donné une entrée x, produit toujours la même exécution.
Définition 11.2 (Algorithme probabiliste) Un algorithme probabiliste est un algorithme qui, étant
donné une entrée x, produit une sortie qui dépend non seulement de x mais aussi de valeur obtenue
via un générateur de nombre aléatoire.
Remarque 11.2 Le sens de déterministe ci-dessus n’est pas le même que celui utilisé pour les machines
de Turing. En effet, une machine de Turing non-déterministe renvoie toujours la même valeur sur une
même entrée.
B Exemples
On commence ici par donner deux exemples : le tri rapide randomisé, et un algorithme probabiliste pour
trouver une coupe minimale.
75
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Tri rapide randomisé Dans cette version du tri rapide, on choisit le pivot de manière aléatoire.
Remarque 11.3 Cet algorithme renvoie toujours un tableau trié, mais son temps d’exécution dépend des
choix aléatoires du pivot. On parle d’algorithme du type Las Vegas.
Théorème 11.1 L’espérance du nombre de comparaisons du tri rapide randomisé d’un ensemble à n
éléments est au plus 2nHn où Hn est le terme général de la série harmonique.
Définition 11.3 Étant donné un graphe G = (S, A) connexe, une coupe de G est un ensemble
d’arêtes A0 ⊂ A tel que G0 (V, A0 ) n’est pas connexe.
L’algorithme randomisé pour obtenir une coupe minimum consiste à contracter des arêtes prises
aléatoirement jusqu’à obtenir un graphe à deux sommets. On utilise pour cela une généralisation des
graphes : les multigraphes. Son temps d’exécution est toujours un O(|S|) puisqu’on contracte exactement
|S| − 1 arêtes, mais on ne retourne pas forcément une coupe minimum. On appelle cela un algorithme du
type Monte Carlo.
Théorème 11.2 L’algorithme randomisé décrit ci-dessus retourne un coupe minimale avec une pro-
babilité ≥ |S|2 2 .
Définition 11.4 (Algorithme Monte Carlo) Un algorithme de Monte Carlo pour un problème P
est un algorithme probabiliste A tel que pour toute instance I de P :
1. A(I) est une solution erronée avec une certaine probabilité,
2. le temps d’exécution de A sur I peut être borné indépendamment des choix aléatoires de A.
Remarque 11.4 Pour les problèmes de décision, il y a deux types d’algorithmes Monte Carlo : ceux à
erreur unilatéral (one-sided error) et à erreur bilatéral (two-sided error).
76 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Exemple 11.1 (Hachage) Algorithme Monte Carlo pour le problème de décision : existe-t-il x ∈ X tel
que f (x) = y0 , où X est un grand ensemble, f : X → R une fonction de hachage et y0 ∈ R.
Proposition 11.1 (Amplification) Soient 0 < 2 < 1 < 1. S’il existe un algorithme Monte Carlo
pour un problème P ayant une probabilité d’erreur 1 , alors on peut construire un algorithme Monte
Carlo pour le problème P ayant une probabilité d’erreur 2 .
Définition 11.5 (Algorithme Las Vegas) Un algorithme du type Las Vegas pour un problème P
est un algorithme probabiliste A tel que pour toute instance I de P :
1. A(I) est une solution correcte à l’instance I,
2. le temps d’exécution de A sur I est une variable aléatoire.
Exemple 11.2 (Hachage) Algorithme Las Vegas pour le problème de l’exemple précédent.
Exercice 11.1 (De Monte Carlo à Las Vegas) Étant donné un algorithme A du type Monte Carlo pour
un problème P ayant une probabilité d’erreur , construire un algorithme Las Vegas pour P. Que vaut
l’espérance de son temps d’exécution ?
II Algorithmes d’approximation
Lorsqu’on étudie un problème d’optimisation, il n’est pas toujours concevable de trouver une solution
optimale en temps polynomiale. On utilisera alors des algorithmes d’approximations qui permettent de
donner une solution proche de l’optimale tout en s’exécutant en temps polynomiale.
A Définitions
On s’intéresse ici exclusivement à des problèmes d’optimisation, dont on rappelle la définition.
C C∗
max , ≤λ
C∗ C
où C = c(i, A(i)) et C ∗ = c(i, s∗ ) avec s∗ une solution optimale pour l’instance i.
77 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
B Exemples
Couverture de sommets (Vertex cover).
Algorithme 11.2 : Glouton-VC
Données : Un graphe G = (V, E).
Résultat : Un couverture des sommets de G, noté S.
S ← ∅;
tant que ∃(u, v) ∈ E, u, v ∈ / S faire
choisir (u, v) ∈ E telle que u, v ∈
/S
S ← S ∪ {u, v}
Théorème 11.3 L’algorithme Glouton-VC est une 2-approximation pour le problème de couverture
de sommet.
C Non-approximabilité
De la même manière que l’on peut montrer que des problèmes sont trop durs pour être résolus par
des algorithmes en temps polynomial, on peut montrer que certains problèmes ne peuvent même pas être
approximés. L’exemple le plus connu est le problème du voyageur de commerce.
Théorème 11.5 Pour tout λ ≥ 1, il n’existe aucune λ-approximation pour le problème du voyageur
de commerce à moins que P = NP.
Remarque 11.5 La méthode générale pour prouver qu’il n’existe pas d’algorithme d’approximation est
souvent le même que pour le théorème précédent : on suppose qu’un tel algorithme existe et on construit
un algorithme polynomial qui résout un problème NP-complet.
Exercice 11.2 Montrer que si on suppose que la fonction de coût satisfait l’inégalité triangulaire , on peut
trouver une 2-approximation du problème du voyageur de commerce.
78 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Définition 11.9 (MAX-3-CNF) Étant donné n variables x1 , x2 , ..., xn et une formule sous forme
normale conjonctive à m clauses contenant chacune 3 littéraux, maximiser le nombre de clauses
satisfaite avec une assignation.
Théorème 11.6 Soit une instance de MAX-3-CNF à n variables x1 , x2 , ..., xn et m clauses ; alors,
l’algorithme randomisé qui affecte indépendamment à chaque variable la valeur 1 avec une probabilité
1/2 et la valeur 0 sinon est une 8/7-approximation randomisée.
79 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
80 © 2022 M. Marin
Leçon 12
Introduction
Face à un problème algorithmique, plusieurs choix de stratégies algorithmiques sont possibles. L’objectif
de cette leçon est de donner un large éventail des différents paradigmes et leurs applications.
I Algorithmes gloutons
A Principe
Définition 12.1 Un algorithme glouton est un algorithme qui, à chaque pas de calcul, fait un choix
localement optimal.
Exemple 12.1 On considère un gymnase dans lequel on souhaite organiser n événements (si , ei ) (chaque
événement est caractérisé par une date de début et une date de fin). Le but est de maximiser le nombre
d’événements qui auront bien lieu dans le gymnase.
— choix 1 : l’événement qui minimise la durée (ei − si ) ;
— choix 2 : l’événement qui a commence le plus tôt (qui minimise si ) ;
— choix 3 : l’événement qui termine le plus tôt (qui minimise ei ).
Chacun des choix mène à un algorithme glouton différent.
Exercice 12.1 Montrer que les algorithmes gloutons faisant le choix 1 et 2 ne sont pas optimaux.
Exemple 12.2 Dans le problème du gymnase, l’algorithme respectant toujours le choix 3 est optimal.
Qn bi
Exercice 12.2 Étant donné A = (ai )1≤i≤n et B = (bi )1≤i≤n , donner un algorithme qui maximise i=1 ai .
81
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Arbres couvrants.
P Étant donné un graphe G = (V, E, w), on cherche un arbre couvrant T ⊂ E qui
maximise e∈T w(e).
Algorithme de Kruskal :
• on trie les arêtes par poids croissant ;
• pour chaque arête, la prendre si elle ne forme pas de cycle avec l’arbre en cours de construction.
Remarque 12.1 Cette proposition est directe si on montre que les forêts d’un graphe ont une structure
de matroı̈de.
D Algorithme online
Certaines fois, la stratégie gloutonne est la seule possible puisque les données arrivent de manière
indépendante. On peut donner plusieurs exemples :
— les algorithmes de remplacement de pages : FIFO ou LRU ;
— un algorithme d’ordonnancement qui exécute les tâches les plus tôt possible ;
— en apprentissage machine, l’algorithme 1-NN renvoie la valeur associée au plus proche voisin de la
donnée reçue.
Définition 12.2 Un algorithme diviser pour régner résout le problème sur une instance de taille n
en utilisant les résolutions sur des instances indépendantes plus petites (n/2 puis n/4 par exemple),
et en fusionnant les résultats.
n/2 n/2
Exemple 12.3 Le tri-fusion est un algorithme diviser pour régner, permettant de trier n éléments en
O(n log n) comparaisons.
Pour évaluer la complexité d’un tel algorithme, le théorème suivant est fondamental.
82 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Théorème 12.1 (Master’s theorem) Soient a, b ≥ 1, et T une suite vérifiant T (n) = aT (n/b) +
cnα .
— Si a > bα , alors T (n) = Θ(nlogb (a) ) ;
— Si a = bα , alors T (n) = Θ(nα log(n)) ;
— Si a < bα , alors T (n) = Θ(nα ).
Exemple 12.4
1. Exponentiation rapide :
xn/2 × xn/2
n si n est pair
x = bn/2c bn/2c
x ×x × x sinon
Remarque 12.2 Certaines de ces opérations efficaces, comme l’addition binaire rapide, peuvent directe-
ment être implantées en machine.
C Recherche efficace
La dichotomie est l’exemple le plus simple de la stratégie diviser pour régner. Les arbres binaires de
recherches (ABR) utilisent ce principe au sein de leurs structures de donnée.
Exercice 12.3 On considère une matrice M dans laquelle les coefficients sont triés par lignes et par
colonnes. Donner un algorithme qui recherche un élément x dans M . Donner sa complexité.
Exercice 12.4 On considère n points y1 , ..., yn . Donner un algorithme qui retourne argmax1≤i<j≤n (yj −yi ).
n
Exemple 12.5 Calcul de k .
83 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Remarque 12.3 Pour éviter de calculer à nouveau les solutions intermédiaires, on peut les stocker. On
parle de mémoı̈sation.
En général,
Exercice 12.5 On définit la bande passante d’un chemin comme le minimum des poids des arêtes du
ce chemin. Donner un algorithme qui, étant donné un graphe orienté pondéré, calcule la bande passante
maximale entre deux sommets pour toute paire de sommets.
Commet mesurer la similarité entre deux chaı̂nes de caractères ? Cette problématique trouve son
sens en bio-informatique ainsi qu’en traitement de texte.
Définition 12.4 Étant donné w = w1 ...wn , une sous-séquence de w est un mot w0 = wi1 ...wik
avec 1 ≤ i1 < ... < ik ≤ n.
Proposition 12.2 Étant donné deux mots x et y, on peut trouver une plus longue sous-séquence
commune en O(|x|.|y|) opérations élémentaires.
Distance d’édition. La distance d’édition entre deux mots mesure le nombre d’opérations élémentaires
(insertion, suppression, substitution) permettant de passer d’une chaı̂ne à une autre.
Développement 7. Calcul de la distance d’édition.
Comment savoir si un mot est engendré par une grammaire ? Cette problématique se pose en
analyse syntaxique.
84 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Définition 12.6 (Problème du mot) Étant donné une grammaire G = (Σ, V, S, R) et un mot w ∈
Σ∗ , est-ce que w ∈ L(G) ?
L’algorithme de Cocke-Younger-Kasami (CYK) résout ce problème en temps cubique pour des gram-
maires sous forme normale de Chomsky. Pour cela, il faut poser Eij = {A ∈ V |A ⇒∗ wi ...wj }.
Définition 12.7 Un algorithme de retour sur trace parcourt l’ensemble des solutions à un problème
que l’on peut représenter sous la forme d’un arbre.
L’algorithme peut ne pas parcourir tout l’arbre en s’arrêtant sur une branche si la solution en cours de
construction n’est pas valide.
Application. Cette méthode est très utile lorsqu’on recherche une solution exacte à un problème tout en
évitant une recherche par force brute.
Exemple 12.6 Algorithme de résolution d’un sudoku par retour sur trace.
Définition 12.8 (Maximum Independent Set) Étant donné un graphe G = (V, E), un ensemble
V 0 ∈ V est dit indépendant si pour tout i, j ∈ V 0 , on a (i, j) ∈
/ E. Le problème du Maximum
Independant Set consiste à trouver un ensemble indépendant d’un graphe de cardinal maximum.
L’algorithme de retour sur trace représente l’ensemble des solutions sous la forme d’un arbre, où chaque
nœud de niveau i représente le choix pour le sommet i (est-ce qu’on le place ou non dans notre ensemble).
Dans le pire des cas, l’arbre obtenu est de taille 2|V | .
Théorème 12.2 L’algorithme de retour sur trace pour le problème de K-coloration s’exécute en O(1)
opérations élémentaires en moyenne.
85 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
86 © 2022 M. Marin
Leçon 13
Introduction. Le but de cette leçon est de présenter l’ordonnancement de tâches d’un point de vue
algorithmique tout d’abord. La question de l’ordonnancement permettront d’introduire la gestion de la
concurrence et de la synchronisation. On finira par aborder l’ordonnancement au sein d’un système d’ex-
ploitation.
A Un premier problème
On considère un gymnase dans lequel on souhaite organiser n événements (si , ei ) (chaque événement
est caractérisé par une date de début et une date de fin). Le but est de maximiser le nombre d’événements
qui auront bien lieu dans le gymnase.
— choix 1 : l’événement qui minimise la durée (ei − si ) ;
— choix 2 : l’événement qui a commence le plus tôt (qui minimise si ) ;
— choix 3 : l’événement qui termine le plus tôt (qui minimise ei ).
Chacun des choix mène à un algorithme glouton différent.
Exercice 13.1 Montrer que les algorithmes gloutons faisant le choix 1 et 2 ne sont pas optimaux.
Définition 13.1 Soient n tâches T = {T1 , ...Tn } caractérisées par leurs temps d’exécution w1 , ..., wn .
Un ordonnancement de T est une fonction
σ :T →N×P
où P est un ensemble de processeurs. Pour tout T ∈ T , si σ(T ) = (t, p), alors la tâche T sera exécutée
à l’instant t sur le processeur p.
87
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
On considère ici des processeurs identiques et on cherche à minimiser le maximum des temps d’exécution
sur l’ensemble des processeurs. De plus, deux tâches sont dites indépendantes si elles peuvent être exécutées
dans n’importe quel ordre.
Malgré le théorème précédent, on peut trouver des algorithmes efficaces pour résoudre ce problème.
Développement 20. Algorithme d’approximation pour INDEP(p).
Exemple 13.1
1 2
3 4 5 P2 2 4
P1 1 3 5 6
6
Comme on peut s’en douter, la généralisation du problème précédent aux tâches dépendantes res-
tent NP-complet. Il existe cependant des heuristiques permettant d’obtenir de bons résultats, comme la
méthode du chemin critique.
Exercice 13.3 Donner une borne inférieur sur le temps d’exécution optimal d’un ordonnancement en
fonction de la topologie du graphe de tâche.
Exercice 13.4 Montrer que l’algorithme online qui exécute les tâches sur un unique processeur dans leur
ordre d’arrivé minimise le maximum des temps de réponse de ces tâches.
Une autre simplification concerne la capacité de nos tâches à s’exécuter en parallèle. Si celles-ci de-
mandent l’accès à une ressource commune, des problème de synchronisation apparaissent.
88 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
II Concurrence et synchronisation
A Processus et Threads
Définition 13.4 Un processus est une abstraction par le système d’un programme en cours d’exécution.
Un thread ou fil d’exécution d’un processus est une exécution (état des registres et de la pile)
pour un processus.
Remarque 13.1 Il existe de nombreux processus dont on n’a pas conscience, qui tourne continuellement
en fond. Pour l’instant, on pourra considérer que tous les processus s’exécute en parallèle.
Implémentation. Chaque processus dispose de son propre état des registres de l’UC, de son propre espace
d’adressage et de ses propres fichiers ouverts. Ces informations sont gardés en mémoire dans la table des
processus. De la même manière, les informations relatives à un thread sont stockées dans une table des
threads.
Remarque 13.2 L’espace d’adressage entre threads d’un même processus est le même, ils partagent les
variables globales. Les piles et états des registres sont cependant distincts.
Définition 13.5 Une condition de concurrence est une situation où deux threads ou processus
exécutent une zone du code d’écriture ou de lecture dans sur une mémoire partagée et dont le résultat
dépend de l’ordre d’exécution des instructions. Une telle zone de code est appelée section critique.
Si plusieurs threads n’entrent jamais dans leur section critique en même temps, on dit qu’il y a
exclusion mutuelle.
Exemple 13.3 Dans le programme précédent où plusieurs threads se comptent, la section critique corres-
pond à la ligne d’incrémentation du compteur. On peut alors identifier la condition de concurrence.
89 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Ces deux solutions présentent un problème majeur : il s’agit d’attente active. Le processeur exécute du
code qui ne fait rien, et donc de la ressource est perdue.
Définition 13.6 Un sémaphore est une variable qui stocke un entier qui représente le nombre de res-
sources d’un type disponible. Deux opérations sont possibles sur un tel entier : le prendre (décrémenter)
s’il est non nul (sinon, on bloque tant qu’il est nul) ou le libérer de manière réciproque. Ces opérations
sont atomiques.
Un mutex est un sémaphore binaire.
Pour implémenter les sémaphores et mutex dans le système, on utilise des interruptions. Lorsqu’un
thread bloque, il est placé dans un état bloqué special. L’ordonnanceur (cf partie suivante) n’essaye plus
de l’exécuter tant qu’il n’a pas récupérer l’information disant que le mutex ou sémaphore a été libéré. La
librairie pthread permet d’utiliser des mutex, et la librairie semaphore les sémaphores.
Stratégie de la tâche la première arrivée. Une tâche est le temps entre deux Entrées/Sorties pour un
thread. Cette stratégie consiste à exécuter les tâches dans leur ordre d’arrivé. On peut montrer un certain
résultat d’optimalité pour cette stratégie (cf exercice 13.4).
Stratégie de la tâche la plus courte en premier. cette stratégie minimise le temps de réponse réponse
mais, pour l’appliquer, il faut connaı̂tre le temps des tâches. En général ce n’est pas le cas, on utilise
l’estimation par le calcul du vieillissement.
On étudie maintenant des stratégies préemptives, c’est-à-dire le système peut arrêter un thread à
tout moment et le relancer au même endroit.
Stratégie du tourniquet (Round-Robin). On définit un quantum de temps sur lequel les threads sont
s’exécuter avant d’être préempté. On utilise alors une structure de file pour savoir quel thread exécuter.
La taille du quantum est souvent un compromis entre le coût de changement de contexte et du temps de
réponse.
90 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Ordonnancement par priorités. On a n groupes de priorités, on exécute les threads de priorité maximale
d’abord selon une stratégie du tourniquet, puis, on passe à la suivante, etc. On peut alors avoir des priorités
statistiques ou dynamiques qui dépendent du temps des tâches et la durée d’attente par exemple.
91 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
92 © 2022 M. Marin
Leçon 14
I Processus et Threads
Motivation Si on veut exécuter plusieurs programmes en même temps, on a besoin d’une notion de
parallélisme, ou de pseudo-parallélisme.
A Les processus
Définition 14.1 Un processus est une abstraction par le système d’un programme en cours d’exécution.
Remarque 14.1 Il existe de nombreux processus dont on n’a pas conscience, qui tourne continuellement
en fond. Pour l’instant, on pourra considérer que tous les processus s’exécute en parallèle.
Implémentation. Chaque processus dispose de son propre état des registres de l’UC, de son propre espace
d’adressage et de ses propres fichiers ouverts. Ces informations sont gardés en mémoire dans la table des
processus.
Manipulation. On peut manipuler ces processus via des appels sytèmes comme fork, execve, exit,
waitpid, kill, pipe, mmap, etc.
B Les threads
Il peut arriver au sein d’une même application de vouloir exécuter plusieurs tâches en parallèle.
Définition 14.2 Un thread ou fil d’exécution d’un processus est une exécution (état des registres
et de la pile) pour un processus.
93
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Remarque 14.2 L’espace d’adressage entre threads d’un même processus est le même, ils partagent les
variables globales. Les piles et états des registres sont cependant distincts.
Implémentation. Comme pour les processus, les informations relatives à un thread sont stockées dans
une table des threads.
Définition 14.3 Une condition de concurrence est une situation où deux threads ou processus
exécutent une zone du code d’écriture ou de lecture dans sur une mémoire partagée et dont le résultat
dépend de l’ordre d’exécution des instructions. Une telle zone de code est appelée section critique.
Si plusieurs threads n’entrent jamais dans leur section critique en même temps, on dit qu’il y a
exclusion mutuelle.
Exemple 14.2 Dans le programme précédent où plusieurs threads se comptent, la section critique corres-
pond à la ligne d’incrémentation du compteur. On peut alors identifier la condition de concurrence.
Ces deux solutions présentent un problème majeur : il s’agit d’attente active. Le processeur exécute du
code qui ne fait rien, et donc de la ressource est perdue.
Définition 14.4 Un sémaphore est une variable qui stocke un entier qui représente le nombre de res-
sources d’un type disponible. Deux opérations sont possibles sur un tel entier : le prendre (décrémenter)
s’il est non nul (sinon, on bloque tant qu’il est nul) ou le libérer de manière réciproque. Ces opérations
sont atomiques.
Un mutex est un sémaphore binaire.
94 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Implémentation en système. On utilise des interruptions. Lorsqu’un thread bloque, il est placé dans
un état bloqué special. L’ordonnanceur (cf partie suivante) n’essaye plus de l’exécuter tant qu’il n’a pas
récupérer l’information disant que le mutex ou sémaphore a été libéré.
Manipulation. La librairie pthread permet d’utiliser des mutex, et la librairie semaphore les sémaphores.
D Exercices
Exercice 14.2
1. Implémentation d’un produit de matrice en parallèle.
2. Implémentation d’un système où un thread lit des requêtes dans un fichier et les fait exécuter par
d’autres threads.
3. Implémentation de la solution de Dijkstra au problème du dı̂ner des philosophes.
4. Résolution du problème des Rédacteurs-Lecteurs.
Remarque 14.3 Les stratégies offline sont étudiées dans d’autres cadres.
On commence par étudier des stratégies non préemptives, c’est-à-dire que le système ne peut pas
arrêter un thread pendant son exécution.
Stratégie de la tâche la première arrivée. Une tâche est le temps entre deux Entrées/Sorties pour un
thread. Cette stratégie consiste à exécuter les tâches dans leur ordre d’arrivé. On peut montrer un certain
résultat d’optimalité pour cette stratégie.
Développement 27. Étude d’un algorithme d’ordonnancement online.
Stratégie de la tâche la plus courte en premier. cette stratégie minimise le temps de réponse réponse
mais, pour l’appliquer, il faut connaı̂tre le temps des tâches. En général ce n’est pas le cas, on utilise
l’estimation par le calcul du vieillissement.
On étudie maintenant des stratégies préemptives, c’est-à-dire le système peut arrêter un thread à
tout moment et le relancer au même endroit.
Stratégie du tourniquet (Round-Robin). On définit un quantum de temps sur lequel les threads sont
s’exécuter avant d’être préempté. On utilise alors une structure de file pour savoir quel thread exécuter.
La taille du quantum est souvent un compromis entre le coût de changement de contexte et du temps de
réponse.
Ordonnancement par priorités. On a n groupes de priorités, on exécute les threads de priorité maximale
d’abord selon une stratégie du tourniquet, puis, on passe à la suivante, etc. On peut alors avoir des priorités
statistiques ou dynamiques qui dépendent du temps des tâches et la durée d’attente par exemple.
95 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
IV Le coût du parallélisme
Loi d’Amdall. Si un programme séquentiel s’exécute en temps T = (1 − p)T + pT où (1 − p) est la
proportion de temps qui ne peut pas être parallélisé (E/S par exemple). On a alors, en utilisant s thread,
un temps d’exécution Ts qui vérifie
T
Ts ≥ (1 − p)T + p
s
Tout n’est pas parallélisable et paralléliser ne divise pas le temps d’exécution par le nombre de threads.
Coût des changements de contexte. Les changements de contexte sont coûteux, il n’est pas souhaitable
qu’une part importante du temps y soit affectée.
Risques d’interblocages. Plus il y a de fils d’exécutions concurrents voulant accéder à des ressources
communes, plus il y a de risques d’interblocages. Un interblocage est une situation durant laquelle plusieurs
fils d’exécution ont besoin d’une ressource détenue par une autre : ils sont alors tous en attente. Dans un
système classique, il est très difficile de les éviter et d’en sortir ; la plupart du temps, une politique de
l’autruche est appliquée.
Programmation et preuves. La programmation avec plusieurs fils d’exécution est évidemment plus
complexe qu’une manière plus classique. De la même manière, montrer la correction du programme est
plus difficile.
96 © 2022 M. Marin
Leçon 15
I Introduction
Un ordinateur contient plusieurs niveaux de mémoires : de la mémoire cache rapide proche du processeur,
de la mémoire RAM moyennement rapide et de la mémoire morte d’accès lent pour des raisons économiques.
Lorsqu’un processus (programme en cours d’exécution) demande une donnée, comment faire en sorte
qu’elle arrive au processeur de manière efficace ?
Définition 15.1 (Localité temporelle) Lorsqu’un processus utilise une donnée, il a tendance à la
ré-utiliser peu de temps après.
Définition 15.2 (Localité spatiale) Lorsqu’un processus utilise une donnée, il a tendance à aussi
utiliser d’autres données proches en mémoire de celle-ci.
On organise alors la mémoire d’un ordinateur en hiérarchie mémoire. Une telle organisation consiste
en un empilement de mémoires de différentes tailles et vitesses (qu’on numérote de 1 à n).
Plus une donnée est utilisée souvent, plus elle est proche du processeur (utilisation de la localité
temporelle).
Lorsqu’un processus demande une donnée x :
— (hit) si la donnée x est présente au niveau k , alors la donnée est transférée au niveau k − 1 par bloc
(utilisation de la localité spatiale),
97
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
CPU
temps d’accès,
taille prix
niveau 1
niveau 2
...
niveau n
Entre la mémoire principale et le processeur, les niveaux de mémoires sont appelées caches. Même si une
hiérarchie mémoire peut contenir plusieurs niveaux, les données sont copiées seulement entre deux niveaux
à la fois, on se concentrera sur seulement deux niveaux, l’un rapide et l’autre lente.
Définitions. L’unité d’information minimale pouvant être présente ou non dans une hiérarchie mémoire
à deux niveaux est appelée ligne de cache. On appelle taux de défaut mémoire la fraction de mémoire
accédée qui n’est pas trouvée en mémoire rapide. Le coût de l’échec est le temps nécessaire pour remplacer
une ligne en mémoire rapide par une ligne en mémoire lente.
Les programmes favorisent une partie de leurs adresses à tout instant par localité temporelle
(tendance à réutiliser une donnée récente) et par localité spatiale (tendance à utiliser une donnée
proche déjà utilisée). La hiérarchie mémoire utilise à son avantage la localité temporelle en
gardant proche du processeur les données récemment utilisées ; et de la localité spatiale en
mouvant les données par blocs contiguës.
Structure de données. Il est important pour le programmeur de connaı̂tre l’importance de cette hiérarchie
mémoire dans son choix de structures de données par exemple.
Développement 3. Les B-arbres sont un exemple de structure de donnée prenant en compte cette
hiérarchie.
Définition 15.3 Un cache à acces direct est un cache pour lequel chaque bloc de la mémoire est
associé à une unique adresse dans le cache.
98 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
En général, si on essaye de placer une ligne x dans un cache à N blocs, on la placera à l’adresse x
mod N .
Puisque cette fonction n’est pas bijective, pour savoir si un mot à une certaine adresse du cache est
le bon, on ajoute des étiquettes (tags). De plus, on ajoute un bit de validité permettant de savoir si
l’information présente en cache est correcte ou non (au démarrage par exemple).
Exercice 15.1 Combien de bits sont nécessaires pour un cache à accès direct pour une mémoire de 16KiO
de données et des mots blocs de 4 mots, chaque adresse étant sur 64bits.
Remarque 15.1 La taille d’un ligne de cache est une métrique importante. Une grande taille exploite
mieux la localité spatiale, mais augmente le nombre de cache miss.
On considère le cas où le processeur exécute une instruction de stockage. Si on écrit seulement dans
le cache, la mémoire n’est plus cohérente avec le cache. Une première solution consiste à toujours écrire
dans le cache et dans la mémoire : on parle de politique write-through.
Cette politique cause évidemment des problèmes de performance. Une autre politique, le write-back,
consiste à écrire en mémoire la ligne seulement lorsqu’elle est remplacée. Cette politique est plus efficace,
mais plus dure à implanter.
Dans cette partie, on donne des métriques permettant de mesurer la performance d’un cache.
— Temps CPU : temps de calcul du CPU, mesuré en cycles d’horloges ou en seconde ;
— TH : période de l’horloge (seconde par cycle) ;
— Cycles Exécution : nombre de cycle d’exécution du CPU ;
— Cycles Mémoire : nombre de cycle où le CPU attend de la mémoire (principalement dû aux caches
miss).
On a alors
On s’intéresse maintenant à la valeur Cycles Mémoire, que l’on veut minimiser. Ce paramètre dépend
de deux paramètres : le nombre de cache miss et la pénalité associé à chaque cache miss.
Pour réduire le nombre de cache miss, on peut revenir sur l’idée d’un cache à accès direct.
Définition 15.4 Un cache pleinement associatif est un cache dans lequel chaque ligne peut être
placée n’importe où dans le cache.
Il existe aussi un entre deux : les caches associatifs par paquet. De nouvelles problématiques apparaissent
avec ce type de cache. En effet, dans un tel cache, toutes les entrées du cache doivent être parcourues
pour trouver une ligne. Pour améliorer la performance, cette recherche est parallélisée, mais cela augmente
le coût matériel.
99 © 2022 M. Marin
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Choix de la ligne à remplacer. Lors d’un cache miss, cette fois on peut choisir la ligne de cache à
évincer. Il existe plusieurs stratégies :
— FIFO : on retire la ligne de cache qui est rentrée en premier en mémoire principale ;
— LRU (Least Recently Used) : on retire la ligne qui a été utilisé la moins récemment .
On parle d’algorithme de pagination.
Développement 21. Comment mesurer la performance de tel algorithme online ?
IV Mémoire virtuelle
On reprend les concepts vu précédemment, avec cette fois pour mémoire rapide la mémoire principale,
et pour mémoire lente la mémoire morte.
A Principes
Lorsqu’il y a plusieurs processus (programme en cours d’exécution), on aimerait que chacun ait l’im-
pression de s’exécuter tout seul de manière sécurisé (protection), et que chacun ait accès à l’entièreté de
la mémoire (espace).
Ces problématiques ont motivé la mémoire virtuelle, qui est une technique utilisant la mémoire prin-
cipale comme un cache pour la mémoire secondaire (ou mémoire morte).
Protection. Dans l’idéal, chaque processus a son propre espace d’adressage, et la mémoire virtuelle
est chargé de traduire l’adresse du processus à une adresse physique dans la mémoire principale. Cette
traduction assure de la protection.
Espace. Historiquement, lorsqu’un programme devenait trop grand pour tenir en mémoire centrale, c’était
au programmeur de le diviser en tâches indépendantes pour le faire rentrer. La mémoire virtuelle permet
d’alléger ce fardeau et de gérer les deux niveaux de hiérarchie mémoire : la mémoire principale (aussi appelée
mémoire physique pour séparer de la mémoire virtuelle) et la mémoire secondaire.
B Adresses et page
On appelle page un bloc mémoire de la mémoire virtuelle, et un page fault l’évènement qui a lieu
lorsque le processeur veut accéder à une page non présente en mémoire principale.
Adresse virtuelle et adresse physique. Avec la mémoire virtuelle, le processeur produit une adresse
virtuelle, qui est traduite (via une combinaison de hardware et de software) en une adresse physique
utilisable pour accéder à la mémoire principale. On appelle ce mécanisme la traduction d’adresse.
Remarque 15.2 La partie matérielle chargé de ces traductions (entre autres) est l’unité de gestion de
mémoire (MMU). La partie logicielle est le gestionnaire de mémoire, faisant partie du système d’exploita-
tion.
Dans la mémoire virtuelle, l’adresse est divisé en un numéro de page virtuel ainsi qu’un offset. Le
nombre de bits dans l’offset détermine la taille de la page.
traduction
27 26 25 … 13 12 11 10 9 8 … 3210
numéro de page physique offset
Puisqu’un page fault a un coût énorme, on cherche à optimiser le placement des pages. En particulier, en
cas de page fault, la page virtuelle peut être associée à n’importe quelle adresse physique : c’est exactement
le même principe qu’un cache associatif.
Définition 15.6 (Table des pages) Table contenant la traduction des adresses virtuelles vers les
adresses physiques dans un système de mémoire virtuelle. Cette table, stockée en mémoire principale,
est le plus souvent indexés par les adresses virtuelles et contient les adresses physiques, ainsi qu’un bit
de validité pour chaque page.
0
1
1
0
1
Mémoire
0 secondaire
Pour indiquer la localisation de cette table des pages en mémoire, le hardware contient un registre
spécial, appelé registre de la table des pages.
Remarque 15.3 La table des pages, avec le compteur ordinal et les registres définissent le contexte du
processus. Lorsque l’OS décide de donner la main à un autre processus, il doit changer le contexte et
change notamment la valeur du registre de la table des pages. L’OS est aussi chargé de mettre à jour
cette table si besoin.
Remarque 15.4 Pour chaque processus, il existe un espace en mémoire secondaire, appelé mémoire
swap, permettant de garder les pages virtuelles qui ne sont pas en mémoire principale.
Remplacement de pages. Lorsqu’il y a un page fault, c’est-à-dire que le processus essaye d’accéder
à une adresse virtuelle dont le bit de validité est 0, l’OS prend la main (via exception). L’OS va alors
chercher la page dans le prochain niveau de la hiérarchie (mémoire secondaire), et décide de placer la page
en mémoire principale. Le problème est alors le même que pour un cache miss dans un cache pleinement
associatif : des algorithmes du type LRU peuvent être utilisés.
I Circuits séquentiels
Avec des portes logiques, on peut construire des circuits combinatoires permettant de calculer une
fonction booléenne. On va maintenant inclure des mémoires dans ces circuits.
Définition 16.1 (Logique séquentielle) Un circuit séquentiel est en groupe d’éléments logiques
contenant de la mémoire. Ainsi, la fonction calculée par un tel système dépend des entrées mais aussi
de l’état actuel de sa mémoire.
A Horloge
Avant de discuter des circuits séquentiels, il est utile de discuter la notion d’horloge. On parle alors
d’élément à état pour un élément contenant de la mémoire.
Pourquoi avoir une horloge ? Une horloge est nécessaire lorsque un système contient des états devant
être mis à jour.
Définition 16.2 (Horloge) Une horloge est un signal libre ayant une période (et donc une fréquence)
fixée. Une période d’horloge est appelée cycle d’horloge.
front descendant
front montant
période
L’état d’un élément à état est mis à jour au front montant de l’horloge, et lorsqu’un système contient
une unique horloge, on dit qu’il est synchrone.
103
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Stabilité. Entre deux front montants d’horloge, un élément à état peut être mis à jour et une contrainte
apparaı̂t : à chaque front montant, tout les éléments à états doivent être valide et stable. Cela pose une
contrainte sur la fréquence de l’horloge.
élément élément
circuit
à état à état élément
combinatoire circuit
1 2 à état
combinatoire
1
L’entrée d’un circuit combinatoire via d’un élément On peut mettre à jour un élément à état
en état et la sortie est écrite dans un élément à via un circuit combinatoire en un seul
mémoire. Les états sont mis à jours aux fronts mon- cycle d’horloge.
tants.
Registres et verrous. Les registres (ou flip-flop, ou bascule) et les verrous dont les éléments mémoires
les plus simples. Dans les deux éléments, la sortie est égal à la valeur stocké dans l’état, la différence entre
les deux venant du moment où l’état est mis à jour.
Dans un registre, le changement d’état est déclenché par l’horloge tandis que dans un verrou, il est
déclenché par l’entrée.
On utilisera ici principalement une bascule D, que l’on peut construire via deux verrous D.
C
Q
D
D D Q D Q Q
Lorsque l’horloge est basse, le premier Verrou Verrou
verrou est passant et le second bloquant. D D
C C Q Q
Au front montant, le second devient pas-
sant et le premier bloque, on garde l’état
D jusqu’au prochain front montant.
C
Exercice 16.1 Dessiner les chronogrammes des éléments ci-dessus, en supposant que tous les états soient
initialement à 0.
Exercice 16.2 Construire un verrou à partir d’un multiplexer 2 :1. Avec deux tels verrous, construire un
registre.
Mémoire adressable (RAM). Une mémoire adressable est composé d’un ensemble de registres qui
peuvent être lus ou écris via une adresse d’entrée.
k m
Addr out
m
in Exemple d’interface d’une RAM recevant une adresse
RAM sur k bits, une donnée d’entrée sur m bits, un bit
Write-Enable ainsi que l’horloge, et renvoyant une
WE donnée sur m bits.
C
Exercice 16.3 Implanter l’interface ci-dessus en utilisant 2k × m bascules D, ainsi qu’un demultiplexer
k : 2k , ainsi qu’un multiplexer 2k : m.
Remarque 16.1 La mémoire adressable construite ci-dessus est statique, contrairement à une mémoire
dynamique (qui n’utilise pas de flip-flop) qui a besoin d’être rafraı̂chit périodiquement. On fait alors la
distinction entre une SRAM (plus rapide, plus chère) et une DRAM
De la même manière qu’une fonction booléenne est l’objet théorique associé aux circuits combinatoire,
il est possible de donner un penchant similaire pour les circuits séquentiels. L’équivalent théorique de ces
derniers sont les automates finis avec sortie, tels que les machines de Moore ou de Mealy.
II Hiérarchie mémoire
Un ordinateur contient plusieurs niveaux de mémoires : de la mémoire cache rapide proche du processeur,
de la mémoire RAM moyennement rapide et de la mémoire morte lente d’accès, principalement dû à des
raisons économiques.
Lorsqu’un processus (programme en cours d’exécution) demande une donnée, comment faire en sorte
qu’elle arrive au processeur de manière efficace ?
Définition 16.3 (Localité temporelle) Lorsqu’un processus utilise une donnée, il a tendance à la
ré-utiliser pas longtemps après.
Définition 16.4 (Localité spatiale) Lorsqu’un processus utilise une donnée, il a tendance à aussi
utiliser d’autres données proche en mémoire de celle-ci.
CPU
temps d’accès,
taille prix
niveau 1
niveau 2
...
niveau n
Plus une donnée est utilisée souvent, plus elle est proche du processeur (utilisation de la localité
temporelle).
Lorsqu’un processus demande une donnée x :
— si la donnée x est présente au niveau k (hit), alors la donnée est transférée au niveau k − 1 par bloc
(utilisation de la localité spatiale),
— sinon (miss), la donnée est demandée au niveau k + 1.
Entre la mémoire principale et le processeur, les niveaux de mémoires sont appelées caches.
Structure de données. Il est important pour le programmeur de connaı̂tre l’importance de cette hiérarchie
mémoire dans son choix de structures de données par exemple.
Développement 3. Les B-arbres sont un exemple de structure de donnée prenant en compte cette
hiérarchie.
Protection. Dans l’idéal, chaque processus a son propre espace d’adressage, et la mémoire virtuelle
est chargé de traduire l’adresse du processus à une adresse physique dans la mémoire principale. Cette
traduction assure de la protection.
Espace. Historiquement, lorsqu’un programme devenait trop grand pour tenir en mémoire centrale, c’était
au programmeur de le diviser en tâches indépendantes pour le faire rentrer. La mémoire virtuelle permet
d’alléger ce fardeau et de gérer les deux niveaux de hiérarchie mémoire : la mémoire principale (aussi appelée
mémoire physique pour séparer de la mémoire virtuelle) et la mémoire secondaire.
B Adresses et page
On appelle page un block mémoire de la mémoire virtuelle, et un page fault l’évènement qui a lieu
lorsque le processeur veut accéder à une page non présente en mémoire principale.
Adresse virtuelle et adresse physique. Avec la mémoire virtuelle, le processeur produit une adresse
virtuelle, qui est traduite (via une combinaison de hardware et de software) en une adresse physique
utilisable pour accéder à la mémoire principale. On appelle ce mécanisme la traduction d’adresse.
Remarque 16.2 La partie matérielle chargé de ces traductions (entre autres) est l’unité de gestion de
mémoire (MMU). La partie logicielle est le gestionnaire de mémoire, faisant partie du système d’exploita-
tion.
Dans la mémoire virtuelle, l’adresse est divisé en un numéro de page virtuel ainsi qu’un offset. Le
nombre de bits dans l’offset détermine la taille de la page.
traduction
27 26 25 … 13 12 11 10 9 8 … 3210
numéro de page physique offset
Remarque 16.3 De nos jours, les deux niveaux de mémoires utilisent des DRAMs et de la mémoire flash
pour les appareil personnels, et des DRAMs et des disques magnétiques pour les serveurs.
Relocalisation. L’espace d’adressage d’un processus est principalement constitué d’un pile (pour l’exécution)
et d’un tas (pour la mémoire dynamique). La mémoire virtuelle permet de ne pas à avoir chercher un espace
suffisant et contiguë en mémoire principale, mais seulement de chercher un nombre suffisant de pages.
Puisqu’un page fault a un coût énorme, on cherche à optimiser le placement des pages. En particulier,
en cas de page fault, la page virtuelle peut être associée à n’importe quelle adresse physique.
Le placement et le remplacement des pages en mémoire physique est alors assuré par le système
d’exploitation. Même si un traitement logiciel peut paraı̂tre ici très coûteux, il reste négligeable face au
coût d’un page fault.
Définition 16.6 (Table des pages) Table contenant la traduction des adresses virtuelles vers les
adresses physiques dans un système de mémoire virtuelle. Cette table, stockée en mémoire principale,
est le plus souvent indexés par les adresses virtuelles et contient les adresses physiques, ainsi qu’un bit
de validité pour chaque page.
0
1
1
0
1
Mémoire
0 secondaire
Pour indiquer la localisation de cette table des pages en mémoire, le hardware contient un registre
spécial, appelé registre de la table des pages.
Remarque 16.4 La table des pages, avec le compteur ordinal et les registres définissent le contexte du
processus. Lorsque l’OS décide de donner la main à un autre processus, il doit changer le contexte et
change notamment la valeur du registre de la table des pages. L’OS est aussi chargé de mettre à jour
cette table si besoin.
Remarque 16.5 Pour chaque processus, il existe un espace en mémoire secondaire, appelé mémoire
swap, permettant de garder les pages virtuelles qui ne sont pas en mémoire principale.
Remplacement de pages. Lorsqu’il y a un page fault, c’est-à-dire que le processus essaye d’accéder
à une adresse virtuelle dont le bit de validité est 0, l’OS prend la main (via exception). L’OS va alors
chercher la page dans le prochain niveau de la hiérarchie (mémoire secondaire), et décide de placer la page
en mémoire principale.
Si la mémoire principale est déjà pleine, il faut choisir une page à remplacer. Il existe plusieurs stratégies :
— FIFO : on retire la page qui est rentré en premier en mémoire principale ;
— LRU (Least Recently Used) : on retire la page qui a été utilisé la moins récemment .
Développement 21. Comment mesurer la performance de tel algorithme online ?
I Introduction au parallélisme
A Un premier modèle simple
On présente ici un modèle plus sophistiqué qu’une architecture de Von Neuman : le modèle de la
mémoire partagée entre plusieurs processeurs.
P1
P2 Mémoire
P3
B Processus et threads
Définition 17.1 Un processus est un programme en cours d’exécution, pouvant lui-même contenir
plusieurs fils d’exécution appelés threads.
Définition 17.2 Chaque processus contient son propre espace d’adressage virtuel, un identifiant, un
ensemble de fichiers ouverts et d’autres informations, toutes stockées dans une structure de donnée
en mémoire appelée bloc de contrôle des processus.
Deux threads d’un même processus partagent leur espace d’adressage. Ils ont cependant un pointeur
de pile différent.
109
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Remarque 17.1
— Dans un système d’exploitation, on isole les processus à l’aide de la mémoire virtuelle pour des
raisons de sécurité. Ce n’est pas le cas des threads car ils coopèrent entre eux.
— Les processus et threads ne servent pas qu’au parallélisme pur : ils permettent aussi une illusion du
parallélisme.
Définition 17.3 Une condition de concurrence est une situation où deux threads ou processus
exécutent une zone du code d’écriture ou de lecture dans sur une mémoire partagée et dont le résultat
dépend de l’ordre d’exécution des instructions. Une telle zone de code est appelée section critique.
Si plusieurs threads n’entrent jamais dans leur section critique en même temps, on dit qu’il y a
exclusion mutuelle.
Exemple 17.1 Dans le programme précédent où plusieurs threads se comptent, la section critique corres-
pond à la ligne d’incrémentation du compteur. On peut alors identifier la condition de concurrence.
Ces deux solutions présentent un problème majeur : il s’agit d’attente active. Le processeur exécute du
code qui ne fait rien, et donc de la ressource est perdue.
Définition 17.4 Un sémaphore est une variable qui stocke un entier qui représente le nombre de res-
sources d’un type disponible. Deux opérations sont possibles sur un tel entier : le prendre (décrémenter)
s’il est non nul (sinon, on bloque tant qu’il est nul) ou le libérer de manière réciproque. Ces opérations
sont atomiques.
Un mutex est un sémaphore binaire.
Implémentation en système. On utilise des interruptions. Lorsqu’un thread bloque, il est placé dans
un état bloqué special. L’ordonnanceur (cf partie suivante) n’essaye plus de l’exécuter tant qu’il n’a pas
récupérer l’information disant que le mutex ou sémaphore a été libéré.
Exemple 17.2 On pourra illustrer l’utilisation des mutex avec le problème du dı̂ner des philosophes.
Définition 17.5 Les processus et threads peuvent être dans plusieurs états dont bloqué, en attente
et en exécution. C’est l’ordonnanceur qui s’occupe de changer les états.
Stratégies. L’ordonnanceur peut avoir différentes stratégies : par exemple en exécutant le premier pro-
cessus disponible, ou le plus court, ou en exécutant chaque processus les uns après les autres durant un
certain quantum de temps (Round-Robin).
Développement 27. Étude d’un algorithme d’ordonnancement online.
P1
P2 Mémoire
P3
E/S
A Notion d’interblocage
Définition 17.6 Un ensemble de processus est en interblocage si chaque processus attend un évènement
que seul un autre processus de l’ensemble peut provoquer.
Exemple 17.3 Supposons que deux processus veulent souhaitent enregistrer un document numérisé sur
un scanner. Le processus A demande la permission pour utiliser le scanner et on lui accorde. Le processus
B, codé différemment, demande lui en premier la permission au graveur CD, et on lui accorde. Les deux
processus se sont alors bloqués l’un l’autre.
Remarque 17.2
1. Un interblocage peut arriver même avec un seul thread : s’il essaye de verrouiller un mutex qu’il a
déjà verrouillé avant.
2. Un interblocage peut donc arriver même sans Entrées/Sorties avec le modèle précédent.
Théorème 17.1 (Conditions d’interblocage) Pour qu’il y ait interblocage, les quatre conditions
suivantes doivent être vérifiées :
1. Condition d’exclusion mutuelle : chaque ressource est soit attribuée à un seul processus, soit
disponible,
2. Condition de détention et d’attente : les processus ayant déjà obtenu des ressources peuvent
en demander de nouvelles.
3. Pas de réquisition : les ressources déjà détenues ne peuvent être retirées de force à un processus.
Elles doivent être explicitement libérées par le processus qui la détient.
4. Condition d’attente circulaire : il doit y avoir un cycle d’au moins deux processus, chacun
attendant une ressource détenue par un autre processus du cycle.
Remarque 17.3 La première solution peut paraı̂tre bancale, mais elle reste possible si les interblocages
sont peu fréquents et sans grande répercussion.
En pratique, les méthodes comme l’algorithme du banquier de Dijkstra demandent beaucoup trop
d’informations et sont trop contraignantes pour être implémenter. Il faut adopter des bonnes pratiques de
conception et accepter le risque d’interblocage en général.
L1
P1 L2
L3
L1
P2 L2 Mémoire
L1
P3 L2
E/S
L’intégration de la hiérarchie dans notre modèle posent de nouveaux problèmes de cohérences. Quand
un processus écrit dans un cache L1 ou L2, si la même ligne est présente dans le cache d’un autre processus,
il faudra mettre en place un mécanisme de synchronisation.
Remarque 17.4 On s’intéresse ici à une vision horizontale de la cohérence des caches. Un autre problème
de cohérence, lui verticale dans la hiérarchie mémoire, apparaı̂t lorsqu’on écrit dans un cache.
Le protocole MESI. Chaque ligne dispose d’un état qui est modifié par le contrôleur de cache selon la
situation :
— Modified : la ligne a été modifié et n’est disponible que dans ce cache ;
— Exclusive : la ligne dans le cache est la même qu’en mémoire principale mais n’est que dans ce cache ;
— Shared : ligne non modifiée et partagée ;
— Invalid : la ligne a été invalidée, elle entraı̂ne un cache miss.
Motivations et problématiques liées au stockage Dès lors qu’il y a des données, des problèmes de
stockage apparaissent :
1. Stockage persistant et non volatile. On souhaite sauvegarder des données sur le long terme et qui
restent en mémoire après extinction de l’ordinateur.
2. Efficacités. Les accès au disque sont très fréquents (∼ 10Go d’écritures par jour sur le disque pour
un usage moyen d’un ordinateur) : ils doivent être efficaces.
3. Données massives, stockage distribué et données partagées.
4. Questions de sécurité.
Disque SSD. Il est plus cher que les disques dures mais aussi beaucoup plus rapide. Il est constitué de
circuits et n’a pas de partie mécanique. Les accès directs sont rapides et ont tous la même durée. Cependant
les SSD ont un nombre d’écriture par cellule limité (typiquement d’une centaine à quelques milliers de To).
Remarque 18.1 Le temps d’accès disque est très élevé par rapport au temps de calcul de l’UC.
Les bandes magnétiques. Elles sont très peu chères mais très lentes. Elles servent uniquement à de
l’archivage.
La technique RAID. On utilise plusieurs disques en parallèle avec un certain niveau de redondance (six
niveaux). Cela permet la récupération de données en cas de panne d’un des disques.
115
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Encryption. Sans rentrer dans la théorie, on peut souvent avoir besoin d’encrypter un fichier. La com-
mande gpg permet cela.
Allocation par listes chaı̂nées. Si un fichier nécessite n blocs, on lui alloue n blocs quelconques dans le
disque et chaque bloc pointe sur le prochain. On résout le problème de fragmentation et on peut facilement
modifier la taille d’un fichier. Il faut trouver une manière efficace de faire un accès direct à un fichier :
1. Un bloc par maillon, cette solution est peu efficace.
2. Table d’allocation des fichiers (FAT). Elle associe à un bloc l’indice du bloc suivant et est stockée en
mémoire centrale. Cette solution est trop coûteuse pour de grands disques.
3. Les i-nodes : structure de donnée qui contient les blocs utilisés. Il s’agit d’un tableau d’indices des
blocs. Il y a un nombre fini de blocs pour un fichier, donc une taille maximale. On ne garde en
mémoire que les i-nodes des fichiers utilisés.
Remarque 18.2 La plupart de ces optimisations ont été réalisées quand le disque magnétique était le
seul moyen de stockage massif largement utilisé. Elles ne s’appliquent pas toutes aux SSD. En particulier,
certaines sont même à éviter comme la défragmentation.
Définition 18.1 Une base de donnée est un moyen de stocker des informations de manière structurée,
de façon à pouvoir y accéder, les modifier, et les gérer, facilement à l’aide de requêtes plus ou moins
complexes, souvent formulées dans un langage tel que le SQL.
Les systèmes de gestion de base de données (SGBD) font partie des produits logiciels les plus
vendus. En effet, les bases de données sont omniprésentes dans le monde moderne : ressources humaines,
gestion de stock, de commandes en lignes, etc... Face à la multitude de situations dans lesquelles on utilise
des BDD, et face à la multitude de manières de concevoir des bases répondant à un même problème, il est
nécessaire de développer des méthodes systématiques permettant de concevoir des bases vérifiant certaines
propriétés garantissant un bon fonctionnement.
Lien avec le système de gestion de fichier. Un système de fichiers est un logiciel qui gère et organise
les fichiers sur un support de stockage, tandis que le SGBD est un logiciel utilisé pour accéder, créer et
gérer des bases de données. Une différence majeure est qu’un SGBD fournit un mécanisme de récupération
après incident, ce qui n’est pas le cas pour un système de fichier.
Modèle relationnel. Dans ce modèle, une base de données est un ensemble de tables, représentant des
objets ainsi que leurs relations.
Exemple 18.1 Voici un exemple de deux tables, ou relations, appelés FILMS et SÉANCES.
Titre Réalisateur Acteur
Titanic J. Cameron L. Di Caprio
FILMS Terminator J. Cameron A. Schwarzenegger
Inception C. Nolan L. Di Caprio
Une merveilleuse histoire du temps J. Marsh E. Redmayne
Cinéma Horaire Titre
Le grand club 18h Titanic
SÉANCES Le Palace 18h30 Terminator
Le Palace 20h Inception
La Plage 19h Inception
En pratique. On peut interagir avec une base de données, pour lire des données qui vérifient certaines
conditions. Dans les SGBD, cela se fait à l’aide de requêtes dans un langage de programmation spécial, le
plus répandu étant SQL.
Structure de donnée. En pratique, une base de donnée est stockée sur un disque externe, et les opérations
les plus coûteuses en temps sont alors la lecture et l’écriture sur le disque. Ainsi que le SGBD optimise
l’accès aux tuples d’une base de données, il est judicieux de recourir à une structure de donnée arborescente
avec une ramification importante. C’est typiquement ce qu’offre la structure de B-arbres.
Développement 3. Présentation des B-arbres et complexité de l’algorithme de recherche.
Définition 18.2 Soit R = {t1 , ..., tn } une table (t1 , ..., tn sont les lignes).
— Une superclé est un sous ensemble d’attributs I tel que si t1 , t2 ∈ T , alors t1 .I 6= t2 .I.
— Une clé primaire est une superclé minimale pour l’inclusion.
Les clés primaires sont un moyen utile d’éviter les doublons : lors de l’insertion d’un nouvel enregistre-
ment, un système de gestion peut vérifier que cela ne crée pas de doublon au niveau de la clé primaire de
la table.
Clé étrangère. On peut ajouter une nouvelle contrainte à notre base de donnée. Si une de ses tables
contient un certain attribut, on peut forcer ces valeurs à apparaı̂tre dans l’autre table.
Définition 18.3 Soient R1 , R2 deux tables sur les schémas R1 [U1 ] et R2 [U2 ], tels que R1 possède
parmi ses attributs une clé primaire pour R2 . Cet attribut ou ensemble d’attributs est appelé clé
étrangère de T1 référençant T2 .
Les clés étrangères permettent de faire le lien entre différentes relations, et garantit l’intégrité référentielle
du schéma. Cela signifie que lorsque l’on insère un enregistrement x dans R1 , on doit vérifier qu’il existe
dans R2 un enregistrement dont la valeur pour la clé primaire correspond à la valeur de x pour la clé
étrangère .
Exemple 18.2 Pour la table FILMS, l’attribut Titre est une clé primaire, et c’est une clé étrangère de
FILMS référençant la table SÉANCES.
La projection La projection d’une table en vue de l’obtention d’un ensemble de colonnes de cette table
se fait via l’ordre SELECT.
Exemple 18.3 Si on veut récupéré la table des titres et des noms des réalisateurs, on utilisera la requête
suivante
SELECT t i t r e , r é a l i s a t e u r FROM F i l m s
Restriction-Sélection. On peut faire des restrictions (ou sélections, mais le nom est trompeur) à l’aide
du mot-clé WHERE. Là où les projections permettent de sélectionner des colonnes , les restrictions permettent
de sélectionner des lignes de la table.
Jointures. Les jointures en SQL permettent d’associer plusieurs tables via une même requête, en asso-
ciant certains attributs de chaque table.
Une jointure est donc un sous-ensemble du produit cartésien entre deux tables, obtenue via la requête
SELECT * FROM table1,table2,... ;.
Remarque 18.3 Dès qu’on combine plusieurs tables, on peut utiliser le nom des tables comme préfixe
pour sélectionner un attribut spécifique. Par exemple, on pourra avoir une commande de la forme SELECT
[Link], [Link] FROM table1,table2 ;.
On peut alors utiliser le mot-cl” JOIN pour resteindre ce produit cartésien selon une condition. La
syntaxe est alors SELECT ... FROM table1 JOIN table2 ON condition ;.
Exemple 18.5 On veut l’ensemble des séances avec le réalisateur et l’acteur principal en plus pour chaque
films.
SELECT ∗
FROM Sé a n c e s
JOIN F i l m s
ON Sé a n c e s . t i t r e = F i l m s . t i t r e ;
I Introduction
L’objectif de cette leçon est de donner poser les bases de la logique booléenne et des circuits booléens
permettant de comprendre un cours d’architecture des ordinateurs.
Cette leçon permet notamment de faire des ponts avec d’autres domaines, notamment la théorie
des graphes. Il s’agit aussi d’une application concrète de la logique booléenne vue une première fois en
mathématiques.
Les blocs logiques sont catégorisés en deux types : ceux qui ne contiennent pas de mémoire et ceux qui
en possèdent. Un bloc sans mémoire est dit combinatoire et la sortie d’un tel bloc dépend seulement des
entrées. À l’inverse, dans un bloc avec mémoire, la sortie dépend de l’entrée et du contenu de la mémoire,
appelé état du bloc logique. On parlera principalement ici de logique combinatoire et on abordera à la
fin la logique séquentiel.
Comme les blocs combinatoires ne contiennent pas de mémoire, ils peuvent être représentés par une
fonction au sens mathématique.
Définition 19.1 Une fonction booléenne à n entrées et m sorties est une fonction f : {0, 1}n →
{0, 1}m .
Une représentation possible pour une telle fonction est une table de vérité. Étant donné une fonction
booléenne à n entrées, sa table de vérités aura alors 2n lignes.
Exemple 19.1 On considère une fonction booléenne à trois entrées A, B et C et trois sorties D, E et F .
On la définit comme suit : D vaut 1 si au moins une des trois entrées vaut 1, E vaut 1 si exactement deux
entrées valent 1 et F vaut 1 si les trois entrées valent 1. La table contient 23 = 8 lignes.
121
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
A B C D E F
0 0 0 0 0 0
0 0 1 1 0 0
0 1 0 1 0 0
0 1 1 1 1 0
1 0 0 1 0 0
1 0 1 1 1 0
1 1 0 1 1 0
1 1 1 1 1 1
n
Proposition 19.1 Il existe 2m2 fonctions booléenne à n entrées et m sorties.
B Algèbre booléenne
Une autre approche pour définir une fonction logique est d’utiliser des équations logiques, via l’algèbre
booléenne. En algèbre booléenne, toutes les variables sont soit 0, soit 1, et de manière classique, il y a
trois opérations :
— l’opération OU, noté + ou ∧ : le résultat de A + B vaut 1 si et seulement si au moins une des deux
variables vaut 1 ;
— l’opération ET, noté . ou ∨ : le résultat de A.B vaut 1 si et seulement si les deux variables valent 1 ;
— l’opération unaire NON, noté . . . ou ¬ : le résultat de A vaut 1 si et seulement si A vaut 0.
Il y a plusieurs lois qui permettent de manipuler ces équations :
— identité : A + 0 = A et 0 + A = A ;
— absorption : A + 1 = 1 et A.0 = 0 ;
— inverse : A + A = 1 et A.A = 0 ;
— commutativité : A + B = B + A et A.B = B.A ;
— associativité : A + (B + C) = (A + B) + C et A.(B.C) = (A.B).C ;
— distributivité : A.(B + C) = (A.B) + (A.C) et A + (B.C) = (A + B).(A + C)
Théorème 19.1 Toute fonction booléenne f : {0, 1}n → {0, 1} à n entrée et une sortie peut s’écrire
comme une équation booléenne.
Exemple 19.2 La fonction f : {0, 1}3 → {0, 1} valant 1 si et seulement si exactement deux de ses entrées
valent 1 peut se réécrire :
Remarque 19.1 Il est possible d’étendre nos opérations en en rajoutant, comme par exemple l’opération
OU EXCLUSIF (ou XOR).
C Portes logiques
Définition 19.2 Une porte logique est un dispositif (un composant électronique par exemple) qui
implémente une fonction logique basique, comme le ET ou le OU.
La plupart du temps, au lieu de dessiner explicitement un inverseur, on se contente d’ajouter une petite
bulle sur une entrée ou une sortie.
Un circuit logique se présente comme un circuit électronique avec des entrées, des portes et une ou
plusieurs sorties. Pour des soucis de stabilité, il ne doit pas y avoir de boucle.
Remarque 19.2 Cette dernière condition nous dit que si on ignore la spécificité de chaque porte, le circuit
est un fait un graphe orienté acyclique. Nous détaillerons cette formalisation dans la dernière partie.
Exemple 19.3 (Un premier circuit logique) On revient à notre exemple précédent, le circuit suivant
implémente f .
A Décodeurs
Le premier bloc logique que nous allons construire est le décodeur. Le décodeur le plus commun possède
n bits d’entrées et 2n sorties : les n bits d’entrée peuvent être vu comme une adresse écrite en binaire, et
la sortie vaut 1 pour cette adresse et 0 ailleurs.
Exemple 19.4 (Un décodeur 2 bits et sa table de vérité)
Out0
In0 In0 In1 Out0 Out1 Out2 Out3
Out1 0 0 1 0 0 0
Décodeur 0 1 0 1 0 0
Out2
In1 1 0 0 0 1 0
Out3 1 1 0 0 0 1
B Multiplexeurs
Une fonction booléenne souvent utilisé est le multiplexeur, aussi appelé un sélecteur. Regardons le
multiplexeur à 2 bits ci-dessous : ils possèdent 3 entrées, deux valeurs et un bit de contrôle. Ce dernier
bit permet de choisir l’entrée qui choisit comme sortie.
De manière générale, un multiplexeur permet d’aiguiller une entrée parmi 2n vers un bit de sortie à
l’aide de n bits de contrôles (eux aussi en entrée).
De manière général, un multiplexeur peut être découper en trois parties :
1. un décodeur qui génère 2n signaux à partir des n bits de contrôle, chacun indiquant une valeur
différente d’entrée ;
2. un tableau de 2n portes ET, chacun combinant une des entrées avec un bit venant du décodeur ;
3. une grande porte OU qui combine les sorties des portes ET.
Le principal intérêt des circuits logiques est la possibilité de réaliser des opérations logiques et arithmétiques
sur des vecteurs de bits. On commence ici par l’opération la plus simple : l’addition.
Voici l’interface d’un additionneur 1 bit, qui additionne deux bits a et b avec une retenue entrante c,
et renvoie un bit de sortie ainsi qu’une retenue sortante c0 .
a b
c’ ADD c
Exercice 19.5 Donner la table de vérité de l’additionneur un bit, ainsi qu’un circuit qui l’implémente.
On peut alors construire un additionneur n bits en juxtaposant n additionneurs un bit. Le circuit obtenu
est appelé full-adder.
Le principal problème de ce circuit est le temps de propagation de la retenue. Ce temps est proportionnel
au chemin critique du circuit : c’est le plus long chemin dans le circuit. Dans le cas du full-adder, ce chemin
est de longueur O(n).
Additionneur rapide. Il est possible de construire un additionneur plus rapide en propageant la retenue
de manière astucieuse.
Développement 22. Construction d’un additionneur rapide.
Les circuits combinatoires peuvent aussi permettre de construire des mémoires à lecture seule. Une
ROM est appelée mémoire parce que certains endroits peuvent être lus, mais ces endroits sont fixés (le
plus souvent à la fabrication). Ainsi, une ROM reçoit en entrée une adresse et retourne une certaine sortie
selon l’adresse reçue.
Remarque 19.3 Il s’agit ni plus ni moins que l’implémentation d’une fonction logique classique. La seule
différence est conceptuelle, puisque ici l’entrée est en fait une adresse.
IV Formalisation
On présente ici une formalisation de la notion de circuit [Perifel, 2014], permettant de travailler plus
théoriquement sur ceux-ci via la théorie des graphes. Ces définitions trouvent aussi leur place en théorie de
la complexité, où les circuits booléennes forment un modèle de calcul équivalent aux machines de Turing.
A Définitions
L’idée consiste à voir un circuit comme un graphe orienté qui, pour des raisons de stabilité, ne contient
pas de circuit.
Définition 19.3 (Circuit booléen) Un circuit booléen est un graphe orienté acyclique (et connexe)
tel que :
— les sommets de degré entrant nul sont appelés entrées et sont étiquetés par le nom d’une
variable xi , ou par la constante 0 ou 1 ;
— les sommets de degré entrant 1 sont appelés portes de négations et sont étiquetés par ¬ ;
— kes autres sommets sont soit des portes de conjonction, soit des portes de disjonction,
respectivement étiquetés par ∧ et ∨ ;
— les sommets de degré sortant nul sont appelés sorties ;
— les voisins entrants d’une porte sont appelés arguments.
Sauf mention du contraire, nos circuits auront une sortie unique et les portes ∨ et ∧ auront un degré
entrant égal à 2.
Enfin si C est un circuit, on appellera sous-circuit de C issu d’une porte γ le circuit dont la sortie
est γ et dont les sommets sont tous les prédécesseurs de γ dans G.
Un circuit booléen calcule une fonction booléenne, définie de manière naturelle comme suit :
Définition 19.4 Soit C un circuit booléen sur les variables x1 , ..., xn . La fonction calculée par une
porte γ de C est la fonction fγ : {0, 1}n → {0, 1} définie récursivement comme suit :
— si γ est une entrée étiqueté par une variable xi , alors fγ (x1 , ..., xn ) = xi ;
— si γ est une entrée étiquetée par une constante c ∈ {0, 1}, alors fγ (x1 , ..., xn ) = c ;
— si γ est une porte ¬ dont l’argument est la porte γ 0 , alors fγ = ¬fγ ;
Vk
— si γ est une porte ∧ (resp. ∨) dont les arguments sont les portes γ1 , ..., γk , alors fγ = i=1 fγi
(resp. fγ = ki=1 fγi )
W
Si le circuit C a k portes de sorties s1 , ..., sm , la fonction calculée par C est alors la fonction
f : {0, 1}n → {0, 1}m dont la ième composante est la fonction fsi .
B Mesures
On peut maintenant définir deux caractéristiques importantes des circuits :
Lemme 19.1 Soit C un circuit de profondeur p ayant une unique sortie. Si les portes ont un degré
entrant majoré par k (pour un entier k ≥ 2), alors p < |C| ≤ k p+1 − 1.
Lemme 19.2 Soit t et k des entiers. Si les portes ont un degré entrant majoré par k, alors il y a au
plus 3t t(k+1)t circuits de taille ≤ t.
Exercice 19.6 (Circuits inverseurs) On appelle circuit n-inverseur un circuit ayant n entrées booléennes
b1 , ....bn (et éventuellement deux entrées V et F, et dont les n sorties sont ¬b1 ,...., ¬bn .
1. Montrer qu’un circuit 1-inverseur contient au moins une porte NON.
2. Montrer qu’un circuit 2 ou 3-inverseur contient au moins deux portes NON.
3. Montrer qu’un circuit n-inverseur contient au moins blog2 (n)c + 1 portes NON.
Calcul de la profondeur. Le calcul de la profondeur d’un graphe orienté acyclique nous permet de
calculer la longueur du chemin critique d’un circuit booléen. Ce calcul est possible en temps linéaire en la
taille du graphe.
Développement 23. Calcul de la profondeur d’un DAG.
I Introduction
A Motivation du cours
Ce cours permet de comprendre le fonctionnement de l’objet ubiquitaire qu’est l’ordinateur. En plus de
la compréhension de l’informatique de bas niveau (proche de la machine), ce cours permet notamment de
motiver certains aspects plus théoriques de l’informatique.
Explosion des performances Depuis les premiers ordinateurs, on a pu observer une augmentation ex-
ponentielle des puissances de calcul, de la taille des mémoire, etc. La loi de Moore (1965) affirme que les
ressources des circuits intégrés doublent tous les 18-24 mois.
Remarque 20.1 Actuellement, la loi de Moore a atteint ses limites physiques. On tente de la conserver
par l’ajout du parallélisme qui permet d’augmenter les performances.
Où on en est actuellement De nos jours, les ordinateurs sont omniprésents (explosion du nombre
d’ordinateurs existants) : pc, tablettes, smartphones, ordinateurs de bord dans les voitures, objets connectés
(iot), serveurs et super calculateurs. Par exemple, plusieurs milliards d’objets sont connectés à Internet.
127
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Aspects sociétaux De grands enjeux politiques et économiques se jouent dans l’industrie et la recherche
de l’informatique. L’écologie est aussi à prendre en compte, autant dans la conception (on parle d’éco-
conception) que dans la prédiction de la consommation (effet rebond).
La mémoire est adressable. C’est-à-dire qu’elle est séparée en cases qui contiennent un nombre fixe de
bits (un octet typiquement). Et chaque case a une adresse.
Le processeur accède à la mémoire par blocs de 8, 16, 32 ou 64 bits appelés mots mémoire (de nos
jours, les architectures 64 bits sont les plus courantes). Pour cet accès, le processeur utilise un registre
appelé compteur ordinal( Program Counter ou PC) qui contient une adresse en mémoire.
Cycle de von Neumann :
1. Lire le mot mémoire qui commence à l’adresse PC ;
2. Interpréter ce mot mémoire comme une instruction et l’exécuter ;
3. Augmenter le PC pour passer à l’instruction suivant et recommencer.
Programme utilisateur
Langage assembleur
Jeu d’instruction
Portes logiques
Niveau d’abstraction
et le logiciel. Il définit entre autres l’ensemble des instructions valides, ce qu’elles font, leur donne une
mnémonique (une représentation textuelle qui facilite l’utilisation pour les humains), le type des données
supportées, le nombre de registres, leur utilisation ainsi que les drapeaux.
Le langage assembleur est un langage de programmation défini par l’ensemble des mnémoniques
étendu notamment pour permettre l’écriture de fonctions.
Exemple 20.1 On observe certains de ces éléments sur le jeu d’instruction RISC-V 32 bits (RV32I).
On montre notamment les instructions simples comme add rs1 rs2 rd, and rs1 rs2 rd et lw rd
rs1(offset).
Remarque 20.2 L’ISA contient d’autres informations que nous ignorons par simplicité et pour ne pas
alourdir le propos.
Chacun des types d’instructions a un format de code machine spécifique, séparé en champs. L’un d’eux
est l’opcode qui permet de retrouver le type d’instruction.
Exercice 20.1 Quelques exercices :
1. À l’aide d’une spécification détaillée de l’ISA RV32I, effectuer quelques traductions entre assembleur
et langage machine.
2. Détailler l’exécution (contenu des registres et PC) pour un programme simple écrit en assembleur.
3. “Traduction” d’un programme simple écrit en C vers l’assembleur : initiation à la compilation.
Lien avec la théorie des graphes. Un circuit booléen peut être vu comme un graphe orienté acyclique.
Le délai su circuit est alors lié au chemin critique dans ce graphe.
Développement 23. Recherche de chemin critique dans un circuit booléen.
Exemple 20.2 Donner un cas où le circuit ne fonctionne pas comme prévu à cause de problèmes de
synchronicité.
On synchronise alors les registres à l’aide d’une horloge (signla en créneau de période fixe en général)
commune qui arrive à tous les registres de façon synchrone.
Exemple 20.3 Détail des états d’un circuit en utilisant un chronogramme.
Remarque 20.3 On évite de mettre des portes logiques sur le signal d’horloge.
Une machine de Moore est une abstraction des circuits séquentiels. Elle est définie par un ensemble
d’entrée E, de sortie S, des états Q (abstraction de l’état des registres), une fonction de transition T :
Q × E− > Q (abstraction d’un circuit booléen) et d’une fonction de sortie F : Q− > S (abstraction d’un
autre circuit booléen). On dit que c’est un automate.
Exemple 20.5 Construction l’automate pour le circuit du compteur.
Classification de Flynn : SISD (processeur RV32I), SIMD (processeurs vectoriels), MIMD (ordinateur
multi-processeur) et MISD (n’existe pas).
Proposition 20.1 Si on découpe le traitement d’une instruction en k étapes, on peut alors espérer
que pour un processeur pipeliné, le temps d’exécution pour un grand nombre d’instructions soit divisé
par k.
Remarque 20.4 En pratique, du aux dépendances, il faut parfois faire une pause dans le pipeline. On
appelle cela buller.
Exemple 20.8 Donner le diagramme d’exécution d’un programme assembleur avec dépendances de données
et de contrôle. On pourra aborder la prédiction de branchement.
I Introduction
Cette leçon a pour but de traiter la transmission automatique d’informations numériques. La transmis-
sion de ce type de données est omniprésente dans la technologie, et spécialement dans les télécommunications.
Il est donc nécessaire de s’appuyer sur de bonnes bases théoriques pour que cette transmission soit fiable,
ce dernier terme se voyant donner plusieurs significations.
En particulier, cette leçon se focalisera sur deux aspects de la transmission de données : le codage et
le routage.
PERTURBATION
RÉSEAU
SOURCE DESTINATION
Codage Décodage
Routage
La communication d’une information commence par sa formulation par un émetteur, se poursuit par
un transit par un canal et se termine par la reconstitution du message par le destinataire. L’information
doit être reçue par son destinataire dans son intégralité, en sécurité, et le plus rapidement possible. La
première partie s’intéressera à la formulation et la reconstitution d’un message, tandis que la seconde à la
transmission.
133
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
II Codage
On s’intéresse ici à la formulation d’une information par l’émetteur ainsi qu’à sa reconstitution par le
destinataire. On s’intéresse à trois problématiques majeures :
— l’efficacité de la transmission : compression des données ;
— la sécurité de l’information : cryptage, authentification ;
— l’intégrité du message : correction d’erreurs.
Pour formaliser les messages sources et les codes, on définit le langage dans lequel ils sont exprimés.
On appelle alphabet un ensemble fini Σ d’éléments appelés caractères. Une suite de caractère de Σ est
appelée chaı̂ne ou mot, et on note Σ∗ l’ensemble des chaı̂nes de Σ. Comme alphabet du code et l’alphabet
de la source peuvent différer, on parle d’alphabet source et d’alphabet de code.
Exemple 21.1 (Image) Supposons qu’un scanner nous retourne une suite de pixel et qu’on veuille les
coder en suite de bits, l’alphabet source est {, } et l’alphabet de code est {0, 1}.
A Compression du message
Un premier exemple de compression. On reprend l’exemple de notre image. Par exemple, dans de
nombreux cas (pour le scan d’une feuille blanche par exemple), il arrive très souvent que les bords soient
blancs et que le message par exemple 1025 pixels blancs. Au lieu de transmettre directement le message
comprenant 1025 fois 0, on peut transmettre [1025]2 0, ce qui s’écrit en binaire 10000000001 0.
Codage de Huffman. De manière plus formelle, on peut définir un code comme une fonction. En
effet, un code binaire est une fonction injective c : Σ → {0, 1}∗ , qui peut être étendue en une fonction
c : Σ∗ → {0, 1}∗ par concaténation. On présente ici le codage de Huffman, qui repose notamment sur
l’utilisation d’arbres binaires.
Développement 30. Présentation du codage de Huffman permettant de compresser des données, et preuve
de son optimalité.
À partir de maintenant, le message transmis est un donc une suite de bits représentant une information.
B Détection d’erreur
La fiabilité d’un canal est loin d’être parfaite. Par exemple, les canaux téléphoniques ont un taux d’erreur
variant de 10−4 à 10−7 , ce qui est loin d’être négligeable pour des longs messages. Il existe des moyens de
détecter et même de corriger des erreurs de transmissions lorsqu’on reçoit un message. Ils sont tous basés
sur le même principe : ajouter de l’information pour vérifier la cohérence du message reçu.
Un codage par blocs de taille k consiste à découper le message m ∈ {0, 1}∗ en blocs Mi de k symboles,
en traitant chaque bloc l’un après l’autre.
Afin de se protéger contre des erreurs en les détectant voire en les corrigeant de manière automatique,
les méthodes de codage par blocs ajoutent de la redondance aux k symboles d’information du bloc initial.
Lorsqu’on reçoit au bout du canal un mot de code qui n’est pas dans CΦ , il y a eu une erreur. Il y a
deux cas :
— on corrige directement le mot via les bits supplémentaires ;
— le récepteur demande de ré-envoyer le message.
Un exemple
P simple de détection par parité. On code le mot m = s1 ...sk par Φ(m) = s1 ...sk ck+1 où
ck+1 = ( ki=1 si ) mod 2. Autrement dit, ck+1 = 0 si et seulement si m contient un nombre pair de 1. On
peut alors faire un contrôle de parité qui permet de détecter une erreur si un nombre impair de bits ont
changé.
C Le chiffrement
Supposons maintenant, avant d’avoir compressé le message et apporté un format qui permette la
détection d’erreurs, que nous voulions garder le message secret pour toute personne excepté son destinataire.
Le canal téléphonique, comme la plupart des canaux, ne permet pas de garder le secret en lui-même. Tout
message qui y transite peut être facilement lu par un tiers.
Codage de César. Pour illustrer le codage de César, on suppose que l’alphabet source est l’alphabet latin,
et on numérote les lettres de 0 à 25. En supposant que l’émetteur et le destinataire ont choisi discrètement
un entier K, appelé clé, alors chaque lettre x est codé par fK (x) = x + K mod 25. De manière plus
générale, si l’alphabet source contient n caractères, un codage de César est une fonction fK : x 7→ x + K
mod n.
Exemple 21.2 Pour l’alphabet latin et K + 3, le mot INFORMATIQUE est codé en LQIRUPDWLTXH.
D’autres systèmes cryptographiques plus élaborées existent, comme le chiffrement affine (fa,b (x) =
a.x + b mod n) ou encore les chiffrements par substitution où chaque lettre par un symbole d’un autre
alphabet.
D Décodage
On présente maintenant l’opération inverse du codage : le décodage. Le message arrive, on peut vérifier
s’il contient des erreurs et éventuellement les corriger et ensuite le décompresser. Il se peut alors que le
message soit crypté.
Déchiffrement. Si on est bien le destinataire, alors on connaı̂t un moyen pour retrouver le message initial,
on parle de déchiffrement. Dans le cas du codage de César, pour décoder un message y, il suffit de prendre
y − K mod n.
L’attaque. Pour qui ne possède pas la clef ou la méthode de chiffrement, on parle d’attaque sur le code,
ou de cassage du code. La discipline qui consiste à inventer des méthodes d’attaque pour briser les codes
existants ou pour construire des codes plus résistants est la cryptanalyse. Pour le codage de César, un
algorithme d’attaque consiste à tester l’ensemble des décalages possibles.
Exercice 21.1 Donner un algorithme permettant de casser un chiffrement affine. Donner sa complexité.
Remarque 21.1 Dans certains cas, il se peut que lors de la phase de décompression, il y ait des pertes
de données. On utilise souvent des codes qui permettent un certain niveau de perte, quand on estime que
l’information importante n’est pas altérée. C’est souvent le cas pour les informations audiovisuelles.
III Routage
En pratique, un canal ne va pas directement d’un point A à un point B, mais il fait partie d’un réseau.
De manière théorique, un réseau est un graphe pondéré G = (V, E, w) où les sommets sont soit des
hôtes (émetteurs ou destinataires) ou des intermédiaires appelés routeurs et les arêtes symbolisent une
communication possible entre les sommets. Un algorithme de routage est un algorithme de décision à
l’origine du choix du chemin que vont emprunter les données dans un réseau.
Pour assurer l’efficacité, on cherche donc à transmettre les données de manière rapide. Cela revient la
plupart du temps à trouver un plus court chemin.
Définition 21.2 (Graphe pondéré) Un graphe pondéré est un graphe G = (V, E) muni d’une fonc-
tion de poids w : E → R. On peut étendre w à V 2 en posant w(u, v) = +∞ lorsque (u, v) ∈ / E.
Le poids d’un chemin est alors défini comme la somme des poids des arêtes qui le compose.
Définition 21.3 (Distance dans un graphe pondéré) Étant donné un graphe pondéré (G, w) dans
circuit de poids strictement négatif, on définit la distance de u à v par :
A Algorithme de Floyd-Warshall
On essaye ici de trouver les plus courts chemins pour toutes les paires de sommets. On supposera
G = (V, E, w) avec V = {0, ..., n − 1}, et si ij ∈
/ E, alors on pose w(ij) = +∞.
(k)
Cet algorithme utilise la programmation dynamique : en notant dij la distance du plus court chemin
allant de i à j passant par les k premiers sommets, on a
( (k)
dij = wij si k = 0
(k) (k−1) (k−1) (k−1)
dij = min(dij , dik + dkj ) si k ≥ 1
L’algorithme de Floyd-Warshall implémente directement cette idée avec une complexité temporelle
O(|V |3 ).
B Algorithme de Bellman-Ford
On essaye ici, étant donné une source s, de trouver l’ensemble des plus courts chemin de s aux autres
sommets.
Théorème 21.1 L’algorithme de Bellman-Ford est correct et s’exécute en temps O(n × m) où n est
le nombre de sommets et m le nombre d’arcs.
Théorème 21.2 Il existe une (n − 1)-approximation pour le problème MEDP, où n est le nombre
de sommets du graphe en entrée.
√
Théorème 21.3 Il existe une d m e-approximation pour le problème MEDP, où m est le nombre
d’arêtes du graphe en entrée.
D Algorithmes distribués
Les algorithmes précédents nécessitent de connaı̂tre la topologie du graphe en entier. Ce n’est pas
toujours le cas, et cette topologie peut être variable (en cas de panne d’un routeur par exemple). En
pratique, les algorithmes utilisés sont donc distribués.
Routage par vecteur de distance. Pour cet algorithme, chaque routeur maintient une table lui donnant
les distances les plus courtes pour chaque destination, et aussi le prochain lien à suivre pour chaque chemin.
Les tables sont mis à jour en échangeant des informations avec ses voisins. Cet algorithme est aussi appelé
l’algorithme de Bellman-Ford distribué. Il s’agissait de l’algorithme de routage de ARPANET, et est aussi
utilisé par l’Internet sous le nom de RIP.
Routage par état fini. Dans cet algorithme, chaque routeur doit :
1. Découvrir ses voisins et connaı̂tre leurs adresses ;
2. Initialiser les distances (ou coût) avec ses voisins ;
3. Construire un paquet contenant tout ce qu’il vient d’apprendre ;
Introduction
Définition 22.1 Une base de donnée est un moyen de stocker des informations de manière structurée,
de façon à pouvoir y accéder, les modifier, et les gérer, facilement à l’aide de requêtes plus ou moins
complexes, souvent formulées dans un langage tel que le SQL.
Les systèmes de gestion de base de données (SGBD) font partie des produits logiciels les plus
vendus. En effet, les bases de données sont omniprésentes dans le monde moderne : ressources humaines,
gestion de stock, de commandes en lignes, etc... Face à la multitude de situations dans lesquelles on utilise
des BDD, et face à la multitude de manières de concevoir des bases répondant à un même problème, il est
nécessaire de développer des méthodes systématiques permettant de concevoir des bases vérifiant certaines
propriétés garantissant un bon fonctionnement.
I Modèle relationnel
Exemple 22.1 Voici un exemple de deux tables, ou relations, appelés FILMS et SÉANCES.
Titre Réalisateur Acteur
Titanic J. Cameron L. Di Caprio
FILMS Terminator J. Cameron A. Schwarzenegger
Inception C. Nolan L. Di Caprio
Une merveilleuse histoire du temps J. Marsh E. Redmayne
Cinéma Horaire Titre
Le grand club 18h Titanic
SÉANCES Le Palace 18h30 Terminator
Le Palace 20h Inception
La Plage 19h Inception
A Domaine et signature
Domaine. On se donne un domaine D, c’est-à-dire un ensemble qui contient tous les éléments pouvant
apparaı̂tre dans un table. Pour l’exemple précédent, on aurait pu prendre l’ensemble des chaı̂ne de caractère.
139
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Signature. À chaque table on peut faire correspondre un ensemble d’attributs. Par exemple, les attributs
de la table FILMS sont :
On appelle alors schéma de relation la spécification d’une table et de ses attributs, et une signature
est un ensemble de schémas de relation.
B Tuple
De manière informelle, un tuple correspond à une ligne de la table.
Définition 22.2 Soit R[U ] un schéma de relation. Un tuple sur R[U ] est une fonction t : U → D.
On notera le tuple par :
u = hU1 : t(U1 ), ..., U2 : t(U2 )i
où U = {U1 , ..., Un }. On autorisera à noter u = ht(U1 ), ..., t(Un )i s’il n’y a aucune ambiguı̈té.
On remarque que ce tuple n’apparaı̂t pas dans la table FILMS, mais cela reste un tuple pour le schéma
correspondant.
C Base de données
Définition 22.3 (Table) Un ensemble fini de tuples sur un schéma de relation R[U ] s’appelle une
table (ou relation) de R[U ]. S’il n’y a pas d’ambiguı̈té, on peut la noter R.
La définition classique d’une base de donnée est alors simplement un ensemble de tables. C’est là que
la vision logique intervient, puisqu’on peut voir une base de données comme un modèle.
Définition 22.4 (Base de donnée) Soit S une signature. une base de donnée est un modèle M =
(D, (dM )d∈D , (RM )R[U ]∈S ) tel que :
— pour tout d ∈ D, dM = d ;
— pour tout schéma de relation R[U ] ∈ S, RM est une table de R[U ].
En pratique. On peut interagir avec une base de données, pour lire des données qui vérifient certaines
conditions. Dans les SGBD, cela se fait à l’aide de requêtes, dans un langage de programmation spécial, le
plus répandu de ces langages est SQL.
Notons qu’aujourd’hui on observe un changement de paradigme vers des bases de données qui utilisent
des graphes plutôt que des relations. Par exemple, le système Neo4j qui utilise le langage Cypher, permettant
de faire des requêtes sur des graphes.
Il y a une infinité de manières de représenter ce problème sous forme relationnelle. Certaines peuvent
présenter des problèmes de redondance, ou d’intégrité. Comment trouver une bonne manière de représenter
ces données ?
Définition 22.5 Soit R = {t1 , ..., tn } une table sur le schéma de relation R[U ].
— Une superclé est un sous ensemble d’attributs I ⊆ U tel que si t1 , t2 ∈ T , alors t1 .I 6= t2 .I.
— Une clé primaire est une superclé minimale pour l’inclusion.
Les clés primaires sont un moyen utile d’éviter les doublons : lors de l’insertion d’un nouvel enregistre-
ment, un système de gestion peut vérifier que cela ne crée pas de doublon au niveau de la clé primaire de
la table.
Clé étrangère. On peut ajouter une nouvelle contrainte à notre base de donnée. Si une de ses tables
contient un certain attribut, on peut forcer ces valeurs à apparaı̂tre dans l’autre table.
Définition 22.6 Soient R1 , R2 deux tables sur les schémas R1 [U1 ] et R2 [U2 ], tels que R1 possède
parmi ses attributs une clé primaire pour R2 . Cet attribut ou ensemble d’attributs est appelé clé
étrangère de T1 référençant T2 .
Les clés étrangères permettent de faire le lien entre différentes relations, et garantit l’intégrité référentielle
du schéma. Cela signifie que lorsque l’on insère un enregistrement x dans R1 , on doit vérifier qu’il existe
dans R2 un enregistrement dont la valeur pour la clé primaire correspond à la valeur de x pour la clé
étrangère .
Exemple 22.5 Pour la table FILMS, l’attribut Titre est une clé primaire, et c’est une clé étrangère de
FILMS référençant la table SÉANCES.
B Modèle Entité-Association
Le modèle entité-association est un modèle conceptuel permettant de visualiser des données structurées.
Lors de la conception d’une base de donnée, on commence généralement par représenter les données dans
ce modèle. Puis, on peut passer du modèle EA au modèle relationnel, en utilisant des clés primaires et
étrangères qui correspondront aux contraintes réelles du problème perçues par le concepteur de la base.
Définition 22.7 — Une entité est un objet possédant un nom et des attributs.
— Une association est un lien entre deux entités, reflétant un lien conceptuel. On représente une
association par une ligne tracée entre deux entités, avec le nom de l’association indiqué.
— Une association possède deux arités, chacune contenant deux nombres, qui correspondent res-
pectivement au maximum et au minimum d’entités avec lesquelles une entité peut être en lien
via l’association.
— Si une entité peut être entièrement déterminé par un sous-ensemble de ses attributs, on souligne
ces attributs pour le mettre en évidence.
C Structure de donnée.
En pratique, une base de donnée est stockée sur un disque externe, et les opérations les plus coûteuses
en temps sont alors la lecture et l’écriture sur le disque. Ainsi que le SGBD optimise l’accès aux tuples d’une
base de données, il est judicieux de recourir à une structure de donnée arborescente avec une ramification
importante. C’est typiquement ce qu’offre la structure de B-arbres.
Développement 3. Présentation des B-arbres et complexité de l’algorithme de recherche.
Langage impératif. Dans un tel langage, l’utilisateur décrit une séquence d’opérations pour calculer le
résultat voulu.
Langage fonctionnel. Dans un tel langage ; le calcul est exprimé par l’évaluation d’une fonction pouvant
opéré sur les les données ou sur le résultat d’un autre appel de fonction.
Langage déclaratif. Dans un tel langage, l’utilisateur décrit les informations désirées sans donner une
séquence spécifique d’opération ou une fonction. L’information est le plus souvent décrite en utilisant une
forme de logique mathématique. C’est le système de gestion de base de donnée (SGBD) qui gère comment
obtenir cette information.
A Opérations et requêtes
On présente ici d’abord les opérations de l’algèbre relationnelle informellement :
— la sélection : permet d’extraire certaine ligne via une formule booléenne ;
— la projection : permet d’extraire certaines colonnes ;
— les opérations ensemblistes : produit cartésien, union et différence.
Définition 22.8 (Requête de l’algèbre relationnelle) On définit les requêtes de l’algèbre relation-
nelle par induction :
— le tuple hj : d rangle ;
— toute table R ;
— σb (q) avec b de forme j1 = j2 ou j1 = a ;
— πj1 ,...,jk (q) ;
— q × q0 ;
— q ∪ q0 ;
— q\q 0
sont des requêtes, où q et q 0 sont des requêtes, j1 , ..., jk des attributs et a une constante. Pour l’union
et la différence, q et q 0 doivent avoir les mêmes attributs.
Exemple 22.8 La requête Quels films ont-été réalisés par C. Nolan ? , on écrit :
B Sémantique
Définition 22.9 Étant donné une base de donnée M et une requête q de l’algèbre relationnelle, on
définit la sémantique de q (ou la réponse) par induction sur q :
— Jhj : diKM = {hj : di} ;
— JRKM = RM ;
— J
sigmab (q)KM = {u ∈ JqKM |b(u) est vrai} ;
— Jπj1 ,...,jk (q)KM = {huj1 , ...., ujk i|u ∈ JqKM }
— Jq × q 0 KM = {hu, u0 i|u ∈ JqKM , u ∈ Jq 0 KM }
— Jq ∗ q 0 KM = JqKM ∗ Jq 0 KM pour ∗ ∈ {∪, \}.
Titre
Inception
Requête satisfaisable. Une requête q est alors dit satisfaisable s’il existe une base de donnée M pour
laquelle JqKM soit non vide.
Algèbre SPC. On définit l’algèbre SPC comme l’algèbre relationnelle restreinte aux requêtes ne contenant
ni union, ni différence.
IV Calcul relationnel
La logique du premier ordre appliquée aux bases de données s’appelle le calcul relationnel. On écrit
une requête comme une formule, où les variables libres sont les éléments à trouver. Comme on nomme les
attributs, une formule atomique est de la forme R(u) où R[U ] est un schéma de relation et u : U → D ∪ X
où D est un domaine et X l’ensemble des variables.
Définition 22.10 (Sémantique d’une requête du calcul relationnel) La sémantique d’une requête
ϕ sur une base de donnée M, noté JϕKM , est définit inductivement sur les formules :
— JR(x1 , ..., xn )KM = RM ;
— J(x = y)KM = {hd, di|d ∈ D} ;
— J(x = a)KM = {hai} ;
— Jψ(x1 , ..., xn ) ∧ θ(y1 , ..., yn )KM = Jψ(x1 , ..., xn )KM o
n(i,j)|xi =yj Jθ(y1 , ..., yn )KM ;
— J¬ψ(x1 , ...., xn )KM = (DM )n \Jψ(x1 , ...., xn )KM ;
— J∃ψ(x1 , ...., xn )KM = π6=i Jψ(x1 , ...., xn )KM ;
où o
n et la jointure définit ci-dessous, et π6=i est la projection sur toutes les composantes différentes
de i.
Cette sémantique est cohérente avec la sémantique classique en logique du premier ordre.
Exercice 22.1 Déduire du théorème précédent que pour toute structure M et toute formule close ϕ, on
a M |= ϕ si, et seulement si, JϕKM 6= ∅.
V Indépendance du domaine
Domaine. Le domaine peut clairement être infini (comme l’ensemble des chaı̂nes de caractères). Deux
problèmes peuvent alors apparaı̂tre :
1. la réponse à une requête peut être infini ;
2. la réponse peut dépendre du domaine.
Ces problèmes apparaissent notamment avec la négation, le disjonction et le quantificateur universel. On
va donc se tourner vers un modèle plus faible du calcul relationnel, mais qui permet d’éviter ces problèmes.
Indépendance du domaine. Une requête ϕ est indépendante du modèle si, pour toutes bases M et M0
0
qui ne diffèrent que de leur domaine, on a JϕKM = JϕKM .
0 0
Exemple 22.12 On prend DM = {1} et DM = {1, 2}, et on considère la table RM = RmathcalM =
{h1, 1i}. Il suffit alors de prendre la requête ∀y, R(x, y) qui s’interprète comme un élément de la première
colonne en relation avec tous les éléments du domaine.
Domaine actif. Pour éviter les réponses infini, on se restreint au domaine actif. Le domaine actif de
M (resp. ϕ) est l’ensemble des éléments de D qui sont aussi dans M (resp. ϕ).
Définition 22.11 Soit S une signature. Une formule du calcul conjonctif est une requête ne contenant
que les connecteurs ∧ et ∃, et où on interdit les formules (x = y) avec x et y libres n’apparaissant
dans aucune autre relation R.
Exemple 22.13 Les formules R(x, y) ∧ (x = a) ou encore ∃yR(x, y, z) ∧ (x = y) sont des formules du
calcul conjonctif, mais pas R(x, y) ∧ (z = x) car z n’apparaı̂t que dans l’égalité.
Théorème 22.2 Les requêtes du calcul conjonctif sont finies (leurs réponses sur toute abse de donnée
est un ensemble fini), indépendantes du domaine et satisfaisables.
VI Théorème de Codd
Edgar Frank Codd est l’inventeur du concept de base de données relationnelle, et à ce titre, lauréat du
prix Turing en 1981. On lui doit notamment l’équivalence entre les deux langages de requêtes vu.
Théorème 22.3 (Théorème de Codd conjonctif) Il y a équivalence entre les requête du calcul
conjonctif et les requêtes satisfaisables de l’algèbre SPC.
Théorème 22.4 (Théorème de Codd) Restreint au domaine actif, le calcul relationnel et l’algèbre
relationnelle sont équivalents.
I Le langage SQL
Les serveurs de données relationnels présentent aujourd’hui une interface externe sous forme d’un lan-
gage de recherche et de mis à jour, permettant de spécifier les ensembles de données en fonction de certaines
valeurs. Les opérations directement utilisables par les usagers sont en général celles des langages dits as-
sertionnels, comme le SQL ou Structured Query Langage. Aujourd’hui, le langage SQL est normalisé
(par l’organisme ANSI) et constitue le standard d’accès aux bases de données relationnelles. De manière
générale, SQL comme les autres langages qui ont été proposés comportent quatre opérations de bases : le
recherche, l’insertion, la suppression et la modification.
II Recherche de données
On utilisera l’exemple des tables suivantes.
Exemple 23.1 Voici un exemple de trois tables, ou relations, appelés Films et Séances.
titre réalisateur acteur
Titanic J. Cameron L. Di Caprio
Films Terminator J. Cameron A. Schwarzenegger
Inception C. Nolan L. Di Caprio
Une merveilleuse histoire du temps J. Marsh E. Redmayne
cinéma horaire titre prix
Le grand club 18h Titanic 8
Séances Le Palace 18h30 Terminator 9
Le Palace 20h Inception 9
La Plage 19h Inception 7
A La projection
La projection d’une table en vue de l’obtention d’un ensemble de colonnes de cette table se fait via
l’ordre SELECT.
Exemple 23.2 Si on veut récupéré la table des titres et des noms des réalisateurs, on utilisera la requête
suivante
SELECT t i t r e , r é a l i s a t e u r FROM F i l m s
147
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Remarque 23.1 Lorsqu’on veut sélectionner toutes les colonnes, on peut remplacer l’ensemble des noms
par le caractère *.
Avec une simple requête SELECT ... FROM ..., on n’élimine pas les doublons. Le mot clé DISTINCT
permet de les supprimer.
Exemple 23.3 On veut la table des noms des réalisateurs sans doublons, on utilisera alors la requête :
SELECT DISTINCT r é a l i s a t e u r FROM F i l m s
B Restrictions-Sélection
On peut faire des restriction (ou sélection, mais le nom est trompeur) à l’aide du mot-clé WHERE. Là
où les projections permettent de sélectionner des colonnes , les restrictions permettent de sélectionner des
lignes de la table.
On peut alors faire des restrictions plus complexes à l’aide des comparateurs arithmétiques <,>, <=,
>=, =, ! = et des opérateurs logiques AND, OR et NOT.
Il existe encore d’autres prédicats permettant des requêtes encore plus complexe :
— IN peut être utilisé dans des conditions de la forme expression IN (valeur 1,...,valeur n) ou
encore expression IN (sous requ^ ete) ;
— BETWEEN peut être utilisé dans des conditions de la forme expression BETWEEN valeur 1 AND
valeur 2 ;
— LIKE permet de faire un filtrage, voir exemple ci-dessous
Exemple 23.5 On veut récupérer la table des films ayant un titre commençant par ’T’, on utilise la requête
suivante :
SELECT ∗ FROM F i l m s
WHERE t i t r e LIKE ’T%’ ;
Remarque 23.2 Le caractère ’%’ signifie ici n’importe quelle chaı̂ne de caractère , on pourrait aussi
utiliser le caractère ’ ’ qui signifie n’importe quel caractère , mais on contraint alors la taille de la
chaı̂ne de caractère.
Exemple 23.6 On veut trier les séances par prix croissant, et en cas d’égalité par ordre alphabétique du
titre du film :
SELECT ∗ FROM Sé a n c e s
ORDER BY p r i x ASC , t i t r e ASC ;
Remarque 23.3 L’ordre des éléments après un ORDER BY compte. Dans l’exemple ci-dessus, on trie
d’abord les éléments par prix croissant et c’est seulement en cas d’égalité qu’on trie par nom croissant.
Il est parfois utile de ne pas récupérer tous les résultats, mais seulement les n premiers. On utilisera
pour cela le mot-clé LIMIT. Si on veut décaler les le résultat (par exemple si on ne veut pas récupérer les
m premiers), on utilisera le mot-clé OFFSET
D Jointures
Les jointures en SQL permettent d’associer plusieurs tables via une même requête, en associant certains
attributs de chaque table.
Une jointure est donc un sous-ensemble du produit cartésien entre deux tables, obtenue via la requête
SELECT * FROM table1,table2,... ;.
Remarque 23.4 Dès qu’on combine plusieurs tables, on peut utiliser le nom des tables comme préfixe
pour sélectionner un attribut spécifique. Par exemple, on pourra avoir une commande de la forme SELECT
[Link], [Link] FROM table1,table2 ;.
On peut alors utiliser le mot-cl” JOIN pour resteindre ce produit cartésien selon une condition. La
syntaxe est alors SELECT ... FROM table1 JOIN table2 ON condition ;.
Exemple 23.7 On veut l’ensemble des séances avec le réalisateur et l’acteur principal en plus pour chaque
films.
SELECT ∗
FROM Sé a n c e s
JOIN F i l m s
ON Sé a n c e s . t i t r e = F i l m s . t i t r e ;
Il est aussi possible de réaliser ce que l’on appelle des jointures externes, permettant de conserver tous
les éléments d’une table même s’il aurait été éliminer par une jointure classique.
Exemple 23.8 Si on remplace le JOIN par un LEFT JOIN dans la commande précédente, on aura toutes
les séances, même si on ne dispose pas des informations concernant ce film dans la table Films.
Exemple 23.9 Par exemple, si on veut compter le prix moyen des séances pour le film Titanic, on utilisera
SELECT AVG( p r i x )
FROM Sé a n c e s
WHERE t i t r e = ” T i t a n i c ” ;
Ces fonctions peuvent être utilisées dans la clause SELECT, et sont aussi utilisables pour effectuer des
calculs d’agrégats.
B Agrégats
Un agrégat est un partitionnement horizontal d’une table en sous-table en fonction des valeurs d’un
ou plusieurs attributs de partitionnement, suivi de l’application d’une fonction de calculs à chaque attribut
des sous-tables tables obtenues.
En SQL, le partitionnement s’exprime via la clause GROUPE BY <spécifications colonnes>.
La clause HAVING permet quant à elle d’exprimer une restriction sur les lignes de la table obtenue avec
les fonctions de calculs. Ainsi,
— les tests sur les colonnes simples s’opèrent dans la clause WHERE ;
— les tests sur les fonctions de regroupement se font dans la clause HAVING.
C Exercice
On résout ici un problème à l’aide de requêtes avancées, sur une base de données sur des entreprise.
L’idée est notamment la syntaxe WITH ... AS ..., permettant ainsi de simplifier certaines requêtes.
Développement 19. Résolution d’un exercice avancé de SQL.
A Création de tables
SQL permet la créer des relations sous forme de table via la requête CREATE TABLE nom table
(élément1, élément2,...), où chaque éléments est de la forme nom element Complement où le
complément peut donner des informations, sur le type notamment.
Remarque 23.5 À la création d’une table, on peut ajouter des contraintes d’intégrité, par exemple en
spécifiant l’unicité de l’attribut (syntaxe UNIQUE ou PRIMARY KEY) ou le fait que la donnée ne peut être
manquante (syntaxe NOT NULL).
Remarque 23.6 À la place d’une table, on peut créer une vue via la commande CREATE VIEW. Celle-ci
sont juste des tables d’affichage et ne peuvent pas être mis à jour.
Indexation. Il est possible d’organiser nos données via une indexation sur une ou plusieurs colonnes,
permettant ainsi d’accélérer certaines requêtes. Cela fonctionne via la syntaxe CREATE INDEX index nom
ON table (colonne1, colonne2);. Une structure de donnée classique en indexation est un B-arbre.
Développement 3. Présentation des B-arbres.
Introduction
Définition 24.1 (Apprentissage machine, Tom Mitchell, 1997) On dit qu’un programme apprend
via l’entraı̂nement E pour un ensemble de tâches T et une mesure de performance P si sa performance
sur T mesuré par P s’améliore après E.
I Apprentissage supervisé
On définit formellement un problème d’apprentissage supervisé.
Données :
— Domaine : ensemble quelconque X , souvent muni d’une distance d ;
— Ensemble d’arrivée : ensemble quelconque Y ;
— Ensemble d’entraı̂nement : S = {(x1 , y1 ), ...(xn , yn )} une suite finie de X × Y.
Sortie : Une fonction h : X → H.
Exemple 24.1 (Problème de météo) On considère un problème où, en fonction de la température et de
l’humidité, on essaye de savoir s’il y a du soleil : On a alors :
— X = R2 (température × humidité) ;
— Y = {soleil, gris} ;
— S est un ensemble de mesures météorologiques de la forme ((T, H), ∆) où T et H sont respectivement
une température et une mesure d’humidité, et ∆ est choisi en fonction s’il y a des nuages.
153
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
A Algorithmes de régression
Dans cette partie, on suppose d = 1. Notre ensemble d’apprentissage est donc un ensemble de points
sur un plan.
Régression linéaire univariée. L’idée derrière cette méthode est simple, on cherche à trouver la droite
la plus proche de nos points.
On pose ici une fonction hypothèse h : x 7→ θ0 + θ1 x où θ0 et θ1 sont des paramètres à optimiser,
de telle manière à ce que h(xi ) soit proche de yi pour tout 1 ≤ i ≤ n. Pour cela, on cherche à minimiser
la fonction :
n
1 X
J(θ0 , θ1 ) = (h(xi ) − yi )2
2n
i=1
On peut alors utiliser des algorithmes d’optimisation pour minimiser cette fonction (qui est convexe par
ailleurs), comme la descente de gradient.
Remarque 24.1 On peut généraliser cette méthode pour d quelconque, on parle alors de régression
linéaire multivariée.
Dans certains cas, notre modèle n’est pas assez précis, on parle de sous-apprentissage (voir figure
ci-dessous).
Régression polynomiale. On va ici présenter une méthode similaire à la précédente, sauf que notre
fonction hypothèse sera une fonction polynomiale. La fonction hypothèse est donc de la forme :
k
X
h : x 7→ ai xi
i=0
0
20
0 2 4 6 8 10 0 2 4 6 8 10 0 2 4 6 8 10
Remarque 24.2 On remarque qu’il faut trouver un compromis entre l’erreur d’apprentissage (erreur du
modèle sur l’ensemble d’entraı̂nement) et l’erreur de prévision. On parle de compromis biais-variance.
Remarque 24.3 Lorsque Y est continue, on peut, à la place de retourner la majorité, retourner la
moyenne, voir la moyenne pondéré par l’inverse de la distance.
C Arbres de décision
Un arbre de décision est un prédicteur h : X → Y qui prédit la valeur de x en traversant un arbre de
la racine jusqu’à une feuille.
Exemple 24.2 (Le fruit est-il mûre ?) Un arbre de décision pourrait suivre le raisonnement suivant : si
la couleur est vive, le fruit est mûre. Sinon, le fruit est mûre si, et seulement s’il est mou.
Pour construire un arbre de décision avec notre jeu d’entraı̂nement, l’idée la plus simple est que chaque
nœud soit un seuil pour une des caractéristiques.
On présente ici un algorithme récursif classique, appelé ID3, permettant de construire un classifieur
dans le cas où X = {0, 1}d et Y = {0, 1}.
Comment choisir la fonction de gain ? On veut une fonction qui permet de mesurer à quelle point
notre coupe enlève du désordre, on peut prendre la fonction de Gini ou l’entropie.
Remarque 24.4 Ici on a un long temps d’apprentissage mais la prédiction est rapide (à l’inverse des k
plus proches voisins). Il est aussi facilement interprétable.
Test du meilleur
modèle
Itération : sélection du
meilleur modèle
Approche empirique
Définition 24.2 (Matrice de confusion) Matrice M de taille N ×N où N est le nombre de [Link]
colonnes sont les prédictions et les lignes la réalité. Une bonne matrice de confusion donne des gros
nombres sur la diagonal et peu ailleurs.
On déduit de cette matrice la précision, qui est le nombre de bonnes prédictions sur le nombre totale.
Approche théorique : apprentissage statistique. On peut mettre un modèle probabiliste sur nos
données : les données (xi , yi ) sont des réalisations de variables aléatoires (Xi , Yi )1≤i≤n et les (Xi , Yi )
sont iid selon la loi (X, Y ). On s’intéresse surtout à la probabilité PY |X .
On ne connaı̂t pas la loi P(X,Y ) , car sinon on aurait un prédicteur simple, le prédicteur de Bayes :
h∗ : x 7→ argmaxy P (Y = y|X = x)
qui, étant donné un point dans l’espace, retourne le y qui maximise la probabilité d’avoir y sachant qu’on
a la donnée x. On peut effectivement montrer que c’est le meilleur prédicteur.
On a alors :
L’objectif est souvent de construire un classifieur pas trop loin de celui de Bayes, permettant ainsi
d’étudier la performance de notre algorithme.
Théorème 24.1 Soit X = [0, 1]d , Y = {0, 1} et D = (X, Y ) une distribution sur X × Y tel que la
fonction de probabilité conditionnelle associée est c-lipschitzienne. Soit hs le classifieur obtenue via
l’algorithme 1-NN sur un ensemble de tirages S ∼ (X, Y )n . Alors,
√ 1
ES∼Dn [lP (hs )] ≤ 2lP (h∗ ) + 4c dn− d+1
On initialise l’algorithme avec un cluster par point. À chaque itération, on calcul la distance minimal
entre deux clusters et on les fusionne.
Pour calculer la distance entre deux clusters, on a plusieurs choix comme le minimum entre deux points
du cluster, la distance moyenne ou la distance maximale.
Fonction objectif. Pour évaluer la performance d’une partition C1 , ..., Ck , on pose Gk (C1 , ..., Ck ) la
somme des distances quadratiques de chaque point à son centroı̈de correspondant.
Remarque 24.5 Une version amélioré consiste à initialiser en prenant au hasard un point, puis un autre
point loin premier, le troisième loin des deux premiers etc.
Convergence. Le lemme suivant nous assure la terminaison mais ne nous dit rien sur la vitesse de
convergence.
Lemme 24.1 À chaque itération de l’algorithme des k-Moyennes, la fonction objectif diminue.
Enfin, l’algorithme peut converger vers un point qui n’est même pas un minimum local de la fonction
objectif.
C Minimisation du diamètre
On considère un problème similaire au précédent, le but étant cette fois de minimiser le maximum des
diamètre d’un groupe. Étant donné C ⊂ S, on définit le diamètre de C par :
δ(C) = max
0
d(x, x0 )
x,x ∈C
max δ(Ci )
1≤i≤k
I Motivation
Compilation. Un compilateur est un programme qui transforme un code source en un code objet.
Généralement, le code source est écrit dans un langage de programmation haut niveau facilement compréhensible
par l’humain. Le code objet est généralement écrit en langage de plus bas niveau, par exemple un langage
assembleur ou langage machine, afin de créer un programme exécutable par une machine.
Généralement, une chaı̂ne de compilation se divise en trois partie :
1. le front-end (ou phase frontale), permettant de transformer le code source en pseudo-code ;
2. l’optimiseur de pseudo-code ;
3. le back-end (ou phase finale), permettant de transformer le pseudo-code en code objet.
On s’intéresse ici seulement au début de la phase frontale.
II Analyse lexicale
Principe. L’analyse lexicale est la conversion d’un texte en une liste de tokens (symboles) et elle fait
partie de la première phase de la chaı̂ne de compilation. Un programme réalisant une analyse lexicale est
appelé un analyseur lexical (ou lexer ).
Exemple. Un analyseur syntaxique peut recevoir en entrée la phrase Alfred Aho et Jeffrey Ullman ont
reçu le prix Turing en 2020 , il retourne la liste des mots [Alfred, Aho, et, Jeffrey, Ullman, ont, reçu, le,
prix, Turing, en, 2020].
Expressions régulières. On fixe un alphabet Σ, qui est juste un ensemble de symboles, appelés lettres.
159
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Remarque 25.1 On pourra confondre une expression rationnelle e et son langage associé L(e).
Exemple 25.2 L’ensemble des identifiants de la forme id.N où N est un nombre quelconque est le langage
décrit par l’expression régulière id.(1|2|...|9)∗ . Ces expressions régulières sont très utiles pour décrire
et trouver des chaı̂nes vérifiant un pattern.
Exercice 25.1 Donner une expression régulière décrivant l’ensemble des nombres flottants.
Règle. À chaque lexème, on ajoute un type. Par exemple, pour l’entrée k := 5 + 2*i , le lexème k
est du type identifiant et 5 du type entier.
Définition 25.2 (Règle de l’analyse lexicale) Soit Σ un alphabet et T l’ensemble de type des
lexèmes. Une règle d’analyse lexicale est la donnée :
— d’une expression régulière e sur Σ, dont le langage associé ,ne contient pas le mot vide ;
— d’un type de lexème t ∈ T .
On note cette règle e → t.
B Automates finis
L’idée est maintenant de vérifier automatiquement si un mot est dans le langage d’une expression
régulière. On utilise pour cela un automate fini.
Définitions.
Exemple 25.3 Un automate M1 = (Q, Σ, δ, q1 , F ) où Q = {q1 , q2 , q3 }, Σ = {0, 1}, F = {Q2 }, et δ décrit
par le tableau ci-dessous.
0 1 0 1
q1 q1 q2 1 0
q2 q3 q2 q1 q2 q3
debut
q3 q2 q2 0,1
Étant donné un automate fini M = (Q, Σ, σ, q0 , F ) et w = w1 ...wn un mot sur Σ, on dit que M
accepte w s’il existe une suite d’états r0 , ..., rn vérifiant :
— q0 = r0 ;
— δ(ri , wi+1 ) = ri+1 pour i = 0, ..., n − 1 et
— rn ∈ F .
Étant donné un langage L ⊂ Σ∗ , on dit que M reconnaı̂t L si L = {w|M accepte w}.
Expression régulière vers automate. On montre maintenant que pour toute expression régulière, son
langage associé est reconnu par un automate finis.
Théorème 25.1 Pour toute expression régulière e, il existe un automate fini M tel que L(e) = L(M ).
Remarque 25.2 Le sens réciproque de ce théorème est vrai : c’est le théorème de Kleene.
Représentation d’un automate en machine. Il est très facile de simuler un automate sur un ordinateur.
Il suffit de stocker la fonction de transition dans un tableau de dimension |Q| × |Σ|.
Théorème 25.2 Dans le pire des cas, l’analyse lexicale est quadratique en la taille du mot d’entrée.
Exercice 25.2 On considère les règle suivante a → lettre-a et a∗ b → lettre-a-itérée-b. Compter le nombre
de lectures pour analyser le texte an .
D Applications
Coloration. Un analyseur lexical n’est pas utilisé qu’en compilation. Un éditeur de texte qui met des
couleur à votre programme fait de la coloration syntaxique et utilise pour cela un analyseur lexical.
Erreurs. Ce même éditeur peut aussi détecter une erreur simple de syntaxe s’il ne détecte pas un lexème
par exemple. De plus, en décorant un peu plus les lexèmes avec par exemple la ligne, sa valeur, etc, on
peut afficher une erreur plus lisible pour le programmeur.
Remarque 25.3 En cas d’erreur, notre logiciel peut calculer la distance d’édition du mot non reconnu
avec certains lexèmes, afin de proposer une correction à l’utilisateur.
A Grammaires hors-contexte
Les langages de programmations ont souvent une structure naturellement récursive. Par exemple, si b
est un booléen, P1 et P2 deux programmes, alors if b then P1 else P2 est aussi un programme. Pour
travailler sur la structure de tels langages, on introduit la notion de grammaire.
Définition 25.4 Une grammaire hors contexte est un quadruplet (V, Σ, R, S) où :
— V est un ensemble fini appelé non terminaux ;
— Σ est un ensemble fini, disjoint de V , appelé terminaux ;
— R est un ensemble fini de règle, chacune étant une variable et un mot sur les variables et
terminaux ;
— S ∈ R est l’axiome.
Dans le cas d’une analyse syntaxique, les terminaux correspondent aux lexèmes.
Exemple 25.5 On considère la grammaire suivante :
S → S+S
S → c
où le seul non-terminal est S, et le seul terminal est c.
Dérivation. On peut définir une dérivation d’un mot comme une application successive de règles. De
manière plus formel, si u, v et w sont des mots contenant des terminaux et non-terminaux, et A → x est
une règle, on dit que uAv donne uwv, et on note uAv ⇒ uwv.
Ainsi, on dit que v dérive de u, noté u ⇒∗ v, si u = v ou il existe une suite u1 , ..., uk pour k ≥ 0 avec
u ⇒ u1 ⇒ u2 ... ⇒ uk ⇒ v
On appelle longueur de dérivation de u à v le nombre de règle appliquée, ici k + 1.
Le langage de la grammaire est {w ∈ Σ∗ |W ⇒∗ w}.
On parle de dérivation leftmost (resp. rightmost) lorsqu’à chaque règle appliquée, c’est le non-terminal
le plus à gauche (resp. droite) qui est impliqué.
Ambiguı̈té d’une grammaire. Lorsque pour un même mot reconnu, il existe plusieurs dérivations dis-
tinctes, on dit que la grammaire est ambiguë.
Analyse descendante. Un algorithme d’analyse descendante tente, à partir d’une liste de lexèmes, de
construire une dérivation depuis l’axiome est de descendre jusqu’à la phrase.
Analyse ascendante. Un algorithme d’analyse ascendante tente, à partir d’une liste de lexèmes, de
construire une dérivation à l’envers depuis la liste et de remonter jusque l’axiome.
Idée de l’algorithme. Le but est d’explorer l’ensemble des dérivations jusqu’à obtenir un mot composé
uniquement de terminaux, et le comparer au mot en entrée. Puisqu’il y a possiblement des branche infini,
on utilise la borne suivante sur la taille des dérivations.
Théorème 25.3 Soit G une grammaire et m ∈ Σ∗ . Les deux assertions suivantes sont équivalentes :
— m ∈ L(G)
— il existe une dérivation reconnaissant m dont la longueur est inférieure à a|m|×r
où a est le nombre maximum de symbole à droite d’une règle, et r le nombre de non-terminaux.
La complexité exponentielle rend cet algorithme inutilisable en pratique, on va donc plutôt chercher
d’autres méthodes.
Signification de LL(1). Left to right - Leftmost derivation et le 1 signifie qu’on lit seulement le caractère
courant pour trouver la dérivation.
Premier. Étant donné une grammaire G et un mot w composé de terminaux et non-terminaux, on définit
un ensemble premier(w) correspondant à l’ensemble des lettres pouvant commencé un mot de terminaux
u et tel que v ⇒∗ u.
où :
premier0 (w) = {a ∈ Σ|w ⇒∗ aw0 , w0 ∈ (V ∪ Σ)∗ }
Développement 12. Calcul de premier. On donne une définition axiomatique de premier et un algorithme
de calcul par saturation.
Suivant. Pour tout non terminal N , on définit suivant(N ) comme l’ensemble des terminaux pouvant
apparaı̂tre après N dans une dérivation depuis S.
Calcul de suivant. De la même manière, on peut définir de manière axiomatique suivant et le calculer
par saturation.
B Grammaire LL(1)
Définition 25.7 (Grammaire LL(1)) Une grammaire G est dite LL(1) si, et seulement si, pour tout
non-terminal N , en notant
N → w1 , ..., N → wn
Exemple 25.6 La grammaire S → +SS|c est LL(1), mais pas la grammaire S → SS + |c ne l’est pas.
Ainsi, on en déduit un algorithme permettant de savoir si un mot m est engendré par une grammaire
G qui est LL(1) linéaire en la taille du mot.
Introduction
L’objectif de cette leçon est de formaliser la notion de complexité, c’est à dire l’idée que certains
problèmes sont plus durs que d’autres. Par exemple, il est facile d’écrire un programme qui, étant donné
un graphe, décide si le graphe est connexe, et qui termine en un temps raisonnable. On ne connaı̂t pas
d’algorithme polynomial déterminant si un graphe est k-colorable, mais il est facile de vérifier si une
k-coloration donnée est valide. Les notions de décision et de vérification sont au centre de l’étude des
problèmes en informatique. Nous pouvons ainsi définir certaines classes de problèmes : ceux qui peuvent
être décidés en un certain temps, ceux dont on peut vérifier une potentielle solution en un certain temps.
I Problèmes de décision
Exemple 26.1 — Premier : E = N, P = {p|p premier} est le problème de savoir si un nombre donné
est premier ou non.
— Connexe : E = {G|G graphe fini}, P = {G|G connexe} est le problème de savoir si un graphe
donné est connexe ou non.
— Clique : E = {(G, k)|G graphe fini, k ∈ N}, P = {(G, k)|G graphe fini ayant une clique de taille k}
est le problème de savoir si un graphe G contient une clique de taille k.
Encodage Pour raisonner sur les problèmes avec un point de vue informatique, il faut refléter le fait que
l’on travaille non pas sur des entiers, des graphes, mais sur des bits. Lorsqu’un problème traite d’objets
complexes, il faut représenter, encoder ces objets sur des bits. Comme on peut facilement encoder n’importe
quel alphabet fini sur des bits, on s’autorise à encoder des instances de problèmes sur un alphabet fini fixé
Σ, qui peut être {0, 1}, {0, 1} avec un symbole additionnel de séparation, etc... Un problème peut alors
être vu comme le langage des mots qui correspondent au codage des instances positives.
167
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Exemple 26.2 Par exemple, un nombre entier n peut être encodé avec son écriture binaire, ou en unaire,
c’est à dire avec une suite 1...1
|{z}.
n fois
Un graphe G peut être encodé sur l’alphabet {0, 1, •} comme n • M où n est le nombre de sommets de
G et M une suite de n × n booléens formant la matrice d’adjacence de G.
Définition 26.2 (Taille d’une instance) On définit la taille d’une instance I comme sa longueur
dans un encodage choisi. On la note |I|.
Dans le cas des entiers, remarquons que le premier encodage prend une taille log(n), et le deuxième une
taille n. Dans la suite, la métrique de performance des algorithmes permettant de résoudre un problème sera
le nombre d’opérations élémentaires effectuées en fonction de la taille de l’instance. Le choix de l’encodage
n’est donc pas trivial : pour les entiers, on a une différence d’échelle exponentielle sur la taille selon le
codage !
Exercice 26.1 Donner la taille de l’encodage par matrice d’adjacence d’un graphe à n sommets. Donner
un encodage des graphes de taille log(n) + log(m) + 2 ∗ m ∗ log(n).
On considère donc dans la suite que les problèmes sont des langages sur l’alphabet Σ. Un programme
répondant à un problème de décision est donc similaire à un automate fini : il prend en entrée un mot
sur l’alphabet Σ et accepte ou rejette ce mot. La différence est qu’un automate fini a une expressivité
limitée : le modèle mathématique d’une machine correspondant à la puissance de calcul des ordinateurs
réels s’appelle la Machine de Turing.
II Complexité
La mesure de complexité d’un problème va être le temps que met un algorithme efficace à le résoudre.
A Classe P
Définition 26.4 La classe P est la classe des problèmes décidables en temps polynomial :
+∞
[
P= DTIME(nk )
k=0
La classe P représente les problèmes considérés comme faciles en informatique, car décidables avec un
programme qui peut tourner en pratique sur un ordinateur. En théorie cette classe contient des problèmes
qui sont décidables avec des algorithmes en O(n1000 ), et donc irréalisables, mais la plupart des problèmes
réels rencontrés étant dans P sont décidables en O(n5 ) au maximum.
Il existe certains problèmes dont on ne sait pas s’ils sont dans P ou pas. L’exemple canonique d’un tel
problème est SAT : Étant donné une formule booléenne ϕ sous forme normale conjonctive, existe-t-il
une valuation σ telle que σ(ϕ) = 1
2 − SAT. On restreint le problème précédent aux formules booléennes telle que chaque clause ne contient
que 2 littéraux : on appelle ce problème 2 − SAT.
B Certificat
Bien qu’on ne sache pas résoudre SAT en temps polynomial, on peut vérifier une solution à une
instance de SAT en temps polynomial : étant donné une valuation σ, il est facile de déterminer σ(ϕ). On
souhaite formaliser cette notion de vérification de solution.
Définition 26.5 (NP) Un problème L sur l’alphabet Σ est dans NP s’il existe un polynôme p(n) et
un problème L0 ∈ P sur un alphabet Σ0 contenant Σ tel que pour tout x ∈ Σ∗ :
x ∈ L ⇐⇒ ∃u ∈ Σ0<p(|x|) , xu ∈ L0
Lorsque x ∈ L et u est tel que xu ∈ L0 , on dit que u est un certificat pour x.
En d’autres termes, un problème est dans NP s’il existe un protocole polynomial permettant de décider
une instance si l’on lui donne un indice, sous la forme du certificat. Notons que tout problème dans P est
aussi dans NP : il suffit de prendre L0 = L dans la définition précédente. En revanche, personne n’a pu
prouver ou infirmer que P = NP, c’est même un des problèmes du prix du millénaire. Il se pourrait même
que cette question soit indécidable dans le modèle mathématique actuel !
Exemple 26.4 Souvent, le certificat prend la forme d’une solution au problème. Considérons le problème
SAT défini plus haut. Un bon candidat pour un certificat d’une instance positive ϕ est une valuation σ
telle que σ(ϕ) = 1. Un tel certificat est bien encodable en taille polynomiale en |ϕ|, et donc SAT est bien
dans NP.
III NP-complétude
Nous avons donc vu qu’il existe des problèmes faciles à vérifier, les problèmes NP. On veut s’intéresser
aux problèmes les plus durs de la classe NP. En effet, nous allons voir que certains problèmes sont NP-
complets, ce qui signifie que s’ils sont dans P , alors P = NP. Autrement dit, ils sont au moins aussi durs
que tous les problèmes NP.
A Réduction polynomiale
Remarquons que la relation ≤p est transitive (exercice : le montrer. Indice : une composition de polynôme
est un polynôme), et donc si un problème B est dans P et se réduit à un problème B 0 , alors B 0 est aussi
dans P.
Supposons maintenant que B est NP-complet et se réduit à un problème B 0 . Alors B 0 est aussi NP-
complet. Le problème est donc d’exhiber un premier problème NP-complet, à partir duquel faire des
réductions.
Cette preuve utilise les Machines de Turing, l’idée étant d’encoder le comportement d’une telle machine
dans une formule booléenne.
Définition 26.7 (3-SAT) Étant donné une formule booléenne φ sous forme normale conjonctive
telle que chaque clause contienne 3 littéraux, existe-t-il une valuation v telle que v(φ) soit vrai ?
Définition 26.9P(2-PARTITION)
P Étant donné n entiers a1 , ..., an , existe-t-il un sous-ensemble
I ⊂ [n] tel que i∈I ai = i∈I
/ ai .
Lorsqu’on étudie un problème d’optimisation, il n’est pas toujours concevable de trouver une solution
optimale en temps polynomiale. On utilisera alors des algorithmes d’approximations qui permettent de
donner une solution proche de l’optimale tout en s’exécutant en temps polynomiale.
C Définitions
On s’intéresse ici exclusivement à des problèmes d’optimisations, dont on rappelle la définition.
C C∗
max , ≤λ
C∗ C
où C = c(i, A(i)) et C ∗ = c(i, s∗ ) avec s∗ une solution optimale pour l’instance i.
D Exemples
Couverture de sommets (Vertex cover). Le problème VERTEX − COVER défini précédemment
peut être réécrit sous la forme d’un problème d’optimisation : le but est alors de trouver une couverture de
sommet minimum.
Algorithme 26.1 : Glouton-VC
Données : Un graphe G = (V, E).
Résultat : Un couverture des sommets de G, noté S.
S ← ∅;
tant que ∃(u, v) ∈ E, u, v ∈ / S faire
choisir (u, v) ∈ E telle que u, v ∈
/S
S ← S ∪ {u, v}
Théorème 26.4 L’algorithme Glouton-VC est une 2-approximation pour le problème de couverture
de sommet.
E Non-approximabilité
De la même manière que l’on peut montrer que des problèmes sont trop durs pour être résolu par
des algorithmes en temps polynomiale, on peut montrer que certain problème ne peuvent même pas être
approximer. L’exemple le plus connu est le problème du voyageur de commerce.
Théorème 26.5 Pour tout λ ≥ 1, il n’existe aucune λ-approximation pour le problème du voyageur
de commerce à moins que P = NP.
Remarque 26.1 La méthode général pour prouver qu’il n’existe pas d’algorithme d’approximation est
souvent le même que pour le théorème précédent : on suppose qu’un tel algorithme existe et on construit
un algorithme polynomial qui résout un problème NP-complet.
Exercice 26.2 Montrer que si on suppose que la fonction de coût satisfait l’inégalité triangulaire , on peut
trouver une 2-approximation du problème du voyageur de commerce.
Définition 26.12 (MAX-3-CNF) Étant donné n variables x1 , x2 , ..., xn et une formule sous forme
normale conjonctive à m clauses contenant chacune 3 littéraux, maximiser le nombre de clauses
satisfaite avec une assignation.
Théorème 26.6 Soit une instance de MAX-3-CNF à n variables x1 , x2 , ..., xn et m clauses ; alors,
l’algorithme randomisé qui affecte indépendamment à chaque variable la valeur 1 avec une probabilité
1/2 et la valeur 0 sinon est une 8/7-approximation randomisée.
On présente ici un nouveau modèle de calcul, proposé par Alan Turing en 1936, appelé machine de
Turing. Ces machines sont similaires à un automate finis, mais avec une quantité illimité de mémoire.
Ainsi, une machine de Turing peut autant faire que n’importe quel ordinateur, et pourtant il existe certains
problème qu’elles ne peuvent pas résoudre.
Configuration. Lorsqu’une machine calcule, elle change d’état, de contenue du ruban ainsi que la position
de la tête. Ces trois éléments forment la configuration de la machine. De manière générale, on représente
une configuration par un triplet uqv où q est l’état courant, uv est la chaı̂ne sur le ruban et la tête est
positionné sur le premier caractère de v. On appelle configuration initiale (resp. acceptante / rejetante)
toute configuration dont l’état est initiale (resp. acceptant/rejettant).
Calcul. On peut alors formaliser la notion de calcul. On appelle pas de calcul le passage d’une configuration
C1 à une configuration C2 . Un calcul est une suite de pas de calcul partant d’une configuration initiale à
une configuration finale.
Remarque 27.1 On peut définir les machines de Turing avec de nombreuses variations :
— possibilité pour la tête de lecture de rester sur place ;
— utilisation de multiples rubans infini ou bi-infini ;
173
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
— restriction de Σ à |Σ| = 2 ;
— non déterminisme dans les transitions.
Ces variantes restent équivalentes au modèle proposé.
Étant donné une machine de Turing M et un mot w, on dit que M accepte (resp. rejette) w s’il
existe un calcul acceptant de M sur l’entrée w.
Langage accepté, décidable. Après avoir définit une notion de calcul, on peut maintenant s’intéresser
à la décidabilité.
Définition 27.2 (Mot et langage accepté) L’ensemble des mots acceptés par une machine de Tu-
ring M , noté L(M ), est appelé langage accepté par M .
Un langage L est reconnaissable par machine de Turing s’il existe une machine M telle que
L(M ) = L.
Lorsqu’on lance une machine de Turing, il y a trois possibilités : le mot est accepté, rejeté ou la machine
boucle (ne termine pas). Lorsqu’une machine ne boucle jamais, on dit qu’elle décide L(M ) : elle accepte
w si w ∈ L(M ) et rejette sinon.
Définition 27.3 (Langage décidable) Un langage L est dit décidable s’il existe une machine M
qui décide L.
Exemple de machine de Turing. Les machines de Turing peuvent être représentées via un graphe orienté
de la même manière que les automates finis. On peut aussi, de manière moins formelle, décrire le fonction-
n
nement de la machine. Considérons le langage L = {02 |n ≥ 0} sur l’alphabet Σ = {0}. La machine M
décrite ci-dessous décide L :
M = ” Sur l’entrée w :
1. Traverser le ruban de gauche à droite en rayant un 0 sur deux.
2. Si, à l’étape 1, le ruban contient un unique 0, accepter.
3. Si, à l’étape 1, le ruban contient plus qu’un seul 0 et que ce nombre est impair, rejeter.
4. Remettre la tête de lecture sur le bord gauche du ruban.
5. Revenir à l’étape 1.
Des problèmes décidables. On rappelle qu’un problème est la donnée de la représentation des éléments
d’en ensemble au plus dénombrable et d’une question sur ces éléments. Par exemple, le problème qui consiste
à déterminer si un entier en base 10 est premier n’est pas le même lorsque les entiers sont donnés via leur
décomposition en facteurs premiers. Un problème P = (I, Q) (où I est l’ensemble des instances et Q une
proposition sur I) peut se voir comme le langage L(P) = {hxi ∈ I|Q(x)}. On dira alors que le problème P
est décidable lorsque L(P) l’est. On pourra alors confondre problème et langage dans les sections suivantes.
Problème indécidable. Par argument diagonale, on peut montrer l’existence d’un problème indécidable.
Remarque 27.2 Cette preuve peut aussi se faire par un argument de dénombrement. En effet, il existe
un nombre indénombrable de langages alors qu’il y a un nombre dénombrable de machines de Turing.
Remarque 27.3 Les machines RAM peuvent être vues comme des ordinateurs à mémoire infinie ; tandis
que les fonctions récursives comme des fonctions écrites dans un langage fonctionnel.
Tous ces modèles définissent une notion de calcul différente de celles proposée par les machine de
Turing, mais ne change pas la notion de décidabilité. Autrement dit, un langage est décidable par machine
de Turing si, et seulement si, il est calculable par une fonction récursive.
Ces remarques ont mené à la thèse de Church : la notion de calculabilité définie par les machines de
Turing correspond exactement à la notion de calcul naturel. Autrement dit, tout ce qui est calculable par
un système physique est calculable par une machine de Turing.
Remarque 27.4 Cette thèse n’est pas démontrable dans le cadre de l’informatique. Elle nécessite d’abor-
der la notion de calcul naturel ou de système physique .
Fonction calculable. On dira qu’une fonction f : Σ∗ → Σ∗ est calculable s’il existe une machine de
Turing M qui, sur chaque entrée w, s’arrête avec seulement f (w) sur son ruban. Par exemple, toutes les
fonctions arithmétiques usuels sont calculables.
Réduction. Les fonctions calculables peuvent alors être des transformations de code de machines.
w ∈ A ⇔ f (w) ∈ B
Machine universelle. On appelle machine universelle une machine de Turing pouvant simuler n’importe
quelle machine de Turing sur n’importe quelle entrée.
Définition 27.7 (POST) Étant donné une séquence de couple de mots (a1 , b1 ), ..., (an , bn ) sur un
alphabet Σ, existe-t-il un mot w ∈ Σ∗ tel qu’il existe une suite d’indice i1 , ..., ik vérifiant w =
ai1 ...aik = bi1 ...bik .
Ce problème est très utile pour montrer des résultats sur les grammaires et langages algébriques ;
D Problème de logique
La théorie de la calculabilité trouve une application importante en logique mathématiques. On dira
qu’une théorie logique est décidable si l’ensemble des formules closes et vraies est décidable.
Arithmétique de Peano. L’arithmétique de Peano est la théorie au premier ordre des entiers munis de
l’addition et de la multiplication.
Théorème 27.6 Un langage est reconnu par une machine de Turing si, et seulement si, il est énuméré
par un énumérateur.
Un langage est alors dit co-récursivement énumérable si son complémentaire est récursivement
énumérable.
Exercice 27.4 Montrer qu’un langage est décidable si, et seulement si, il est récursivement énumérable et
co-récursivement énumérable.
B Théorème de Rice
Cette notion permet d’introduire un théorème très puissant sur l’indécidabilité de certains langages : le
théorème de Rice. De manière informelle, il peut s’interpréter comme l’indécidabilité de la correction des
programmes.
I Formules
A Syntaxe de la logique propositionnelle
Le langage de la logique propositionnelle repose sur la notion de variable propositionnelle, que l’on note
souvent p, q, x, y, z et qui est juste un ensemble de symbole que l’on notera X.
On appelle connecteur logique les symboles permettant de connecter les formules, comme ¬, ∧, ∨ et
→.
Exemple 28.1 Pour X = {p, q, r}, les mots (p → (¬q ∧ r)) et ((p → ¬q) ∧ r sont des formules.
Arbre de syntaxe. Via la définition récursive, une formule peut être représenté par un arbre. Cette
bijection entre les arbres syntaxiques et les formules est justifiée par le théorème de lecture unique.
Théorème 28.1 (Théorème de lecture unique) Soit φ une formule. Un seul des cas suivant est
possible :
— φ est une variable propositionnelle ;
— il existe une unique constante c ∈ {>, ⊥} telle que ϕ = c ;
— il existe une unique formule ψ telle que ϕ = ¬ψ ;
179
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
— il existe une unique de formule (ψ1 , ψ2 ) et un unique opérateur o ∈ {∧, ∨, →} tels que ϕ =
(ψ1 o ψ2 ) .
Taille, hauteur et sous-formule. La taille |ϕ| et la hauteur h(ϕ) d’une formule sϕ e définisse comme la
taille et la hauteur de l’arbre syntaxique unique associée. Une sous-formule SF(ϕ) est la formule associée
à un sous-arbre de l’arbre syntaxique.
Exemple 28.2 Pour ϕ = (p → (¬q ∧ r)), on a |φ| = 6, h(ϕ) = 3 et SF(ϕ) = {ϕ, p, q, r, (¬q ∧ r), ¬q}.
Valuation. Étant donné un ensemble de variable propositionnelle X, une valuation sur X est une fonction
v : X → {0, 1}.
Valeur de vérité d’une formule. On peut étendre le domaine de définition d’une valuation au formule
et ainsi définir la valeur de vérité de cette formule.
Définition 28.2 Soit ϕ une formule sur un ensemble de variables X et v : X → {0, 1} une valuation.
On définit l’évaluation de ϕ pour v par :
— v(x) = v(x) pour x ∈ X ;
— v(>) = 1 et v(⊥) = 0 ;
— v(¬ψ) = 1 ssi v(ψ) = 0 ;
— v(ψ1 ∧ ψ2 ) = 1 ssi v(ψ1 ) = 1 et v(ψ2 ) = 1 ;
— v(ψ1 ∨ψ2 ) = 1 ssi v(ψ1 ) = 1 ou v(ψ2 ) = 1 ; v(ψ1 → ψ2 ) = 1 ssi v(ψ1 ) = 1 implique v(ψ2 ) = 1 ;
Si v(ϕ) = 1, on note v |= ϕ : v satisfait ϕ .
Table de vérités. Une représentation de la sémantique d’une formule est possible par table de vérité.
Dans une telle table, chaque ligne correspond à une valuation et la dernière colonne à la valeur de vérité
pour cette valuation.
Satisfiabilité,tautologie.
Définition 28.3
Une formule ϕ est satisfiable s’il existe une valuation v telle que v |= ϕ.
Une formule ϕ est une tautologie (ou est valide) si pour toute valuation v, on a v |= ϕ. On note
alors |= ϕ.
Une formule ϕ est un contradictoire (ou est insatisfiable) si pour toute valuation v, on a v 6|= ϕ.
Proposition 28.1 La formule ϕ est une tautologie si, et seulement si, ¬ϕ est contradictoire.
Remarque 28.1 Pour toute formule φ, on a |= ϕ ∨ ¬ϕ (tiers-exclu) et |= ¬(ϕ ∧ ¬ϕ) (non contradiction).
C Équivalence et conséquence
Équivalence. La sémantique des formules nous permet de définir une notion d’équivalence sur les for-
mules.
Définition 28.5 Soit ϕ et ψ deux formules. On dit que ψ est conséquence logique de ϕ, noté ϕ |= ψ
si pour toute valuation v, si v(φ) = 1 alors v(ψ) = 1.
II Fragments syntaxiques
A Système complet de connecteur
On parle de système de connecteurs pour parler d’un ensemble de connecteur logiques, comme
{¬, ∧, ∨, →}.
Définition 28.6 Un système de connecteurs est complet si toute formule est équivalente à une
formule ne s’écrivant qu’avec les connecteurs de ce système.
Application aux circuits booléens. Un circuit logique peut être défini comme un graphe orienté acy-
clique, ayant des noeuds spéciaux appelés entrées et sorties et dont les noeuds intermédiaires sont des
connecteurs logiques. Le théorème précédent nous affirme que tout circuit peut être réalisé seulement avec
des portes NAND.
B Formes normales
Définition 28.7 On appelle littéral toute variable propositionnelle ou négation de variable proposi-
tionnelle.
Forme normale conjonctive. On définit une clause comme une disjonction de littéraux. Ainsi, une
formule sous forme normale conjonctive est une conjonction de clauses.
Définition 28.8 Une formule ϕ est sous forme normale conjonctive s’il existe des littéraux lij pour
1 ≤ i ≤ n et 1 ≤ j ≤ m tels que :
n _
^ m
ϕ= lij
i=1 j=1
Proposition 28.5 Toute formule ϕ est équivalente à une autre formule sous forme normale conjonc-
tive.
La preuve de la proposition précédente nous donne un algorithme permettant de mettre une formule
sous forme normale conjonctive, l’idée étant pour chaque sous-formule, d’envoyer vers l’avant les ∧.
Forme normal disjonctive. Il existe une autre forme normale, duale à la première.
Définition 28.9 Une formule ϕ est sous forme normale conjonctive s’il existe des littéraux lij pour
1 ≤ i ≤ n et 1 ≤ j ≤ m tels que :
n ^
_ m
ϕ= lij
i=1 j=1
Proposition 28.6 Toute formule ϕ est équivalente à une autre formule sous forme normale disjonctive.
La preuve de la proposition précédente nous donne un algorithme permettant de mettre une formule
sous forme normale conjonctive, l’idée étant de dresser la table de vérité, puis d’en déduire directement la
formule.
Exercice 28.3 Donner un moyen effectif de passer d’une forme normale à une autre.
Exercice 28.5 Montrer qu’il existe une formule φG,k telle que φG, est satisfiable si, et seulement si, G est
k-coloriable.
On se ramène alors à un problème de satisfiabilité d’une formule. On peut faire de même avec de
nombreux problèmes ou jeu, comme le sudoku par exemple.
Problème SAT.
Définition 28.10 SAT Étant donné une formule ϕ sous forme normale conjonctive, ϕ est-elle satis-
fiable ?
Remarque 28.3 On peut supposer ϕ qui n’est pas sous forme normale, cela ne change pas les énoncés
suivants.
Une première restriction. On peut déjà se demander si une restriction des formules aux clauses à
seulement 3 littéraux (le nouveau problème est appelé 3-SAT) nous permet de changer la complexité du
problème. La réponse est négative.
B Problème 2-SAT
On décide ici de restreindre un peu plus le problème.
C MAX − SAT
On peut aussi définir un problème d’optimisation, le but étant cette fois de maximiser le nombre de
clause valide.
Définition 28.12 (MAX − SAT) Étant donné une formule ϕ sous forme normale conjonctive avec
les clauses c1 , ..., cm , trouver
max |{1 ≤ i ≤ m|v(ci ) = 1}
v
Le problème précédent est évidemment NP-complet. Cependant, il existe des résultats intéressant sur
le nombre de résultat et les algorithmes probabilistes.
Théorème 28.4 Étant donné m clauses c1 , ..., cm ayant k1 , ..., km littéraux respectivement. En notant
k = min ki , il existe une valuation qui satisfait au moins
m
X
(1 − 2−ki ) ≥ m(1 − 2−k )
i=1
clauses.
La preuve du problème précédent nous donne aussi un algorithme d’approximation probabiliste pour le
problème MAX − SAT.
IV Compacité
On termine ici par un résultat de compacité, permettant de déduire des résultats sur un ensemble infini
à partir de ses sous-ensemble finis. On suppose ici l’ensemble des variables propositionnelles dénombrable.
Définition 28.13 Un ensemble de formule est satisfiable s’il existe une valuation qui satisfait toutes
ses formules.
Un ensemble de formule est dit finiment satisfiable si toute ses parties finies sont satisfiables.
Proposition 28.9 Un graphe G est k-coloriable si, et seulement si, tous ses graphes finis le sont.
La théorie de la calculabilité commence par une question : qu’est-ce qu’un ordinateur ? Puisque les
ordinateurs réels sont très compliqués, on utilise un modèle de calcul qui est simplement un ordinateur
idéalisé bien défini mathématiquement. On commence ici avec le modèle le plus simple : l’automate fini.
I Automates finis
A Un premier exemple
L’idée d’un automate fini est de représenter un ordinateur ayant une quantité très limité de mémoire.
On peut donner l’exemple d’un tel logiciel avec un simple mécanisme de gestion d’ouverture de porte
automatique.
PERSONNE INTERIEUR,EXTERIEUR
PERSONNE
Capteur Capteur
Intérieur Extérieur
fermée ouverte
porte INTERIEUR,EXTERIEUR
187
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Exemple 29.2 Un automate M1 = (Q, Σ, δ, q1 , F ) où Q = {q1 , q2 , q3 }, Σ = {0, 1}, F = {Q2 }, et δ décrit
par le tableau ci-dessous.
0 1 0 1
q1 q1 q2 1 0
q2 q3 q2 q1 q2 q3
début
q3 q2 q2 0,1
Étant donné un automate fini M = (Q, Σ, σ, q0 , F ) et w = w1 ...wn un mot sur Σ, on dit que M
accepte w s’il existe une suite d’états r0 , ..., rn vérifiant :
— q0 = r0 ;
— δ(ri , wi+1 ) = ri+1 pour i = 0, ..., n − 1 et
— rn ∈ F .
Étant donné un langage L ⊂ Σ∗ , on dit que M reconnaı̂t L si L = {w|M accepte w}.
Définition 29.2 (Langage rationnel) Un langage est dit rationnel s’il existe un automate fini M
qui le reconnaı̂t.
Exemple 29.3 On revient à notre exemple M1 . Ici, on a L(M1 ) = {w ∈ {0, 1}∗ |w contient au moins un 1 et un nombre
Exemple 29.4 (Un automate plus compliqué) On construit un automate M2 qui, sur l’alphabet Σ =
{0, 1, 2, R}, qui étant donné un mot de Σ∗ accepte si et seulement si la somme des éléments après le
dernier R (reset) vaut 0 modulo 3.
0
q1
2, R 2
1 1
début q0 q2
1, R
0, R 2 0
C Automate minimal
Le but est ici de construire un automate ayant le moins d’état possible.
q ∼A q 0 ⇔ {w ∈ Σ∗ : δ ∗ (q, w) ∈ F } = {w ∈ Σ∗ : δ ∗ (q 0 , w) ∈ F }
Autrement dit, q et q 0 ne sont pas équivalents si et seulement si on peut les distinguer par un mot
w, c’est-à-dire si w est accepté depuis seulement un des deux.
Exercice 29.1
1. Montrer que l’équivalence de Nérode est bien une relation d’équivalence.
Proposition 29.1 Parmi tous les automates déterministes complets reconnaissant L(A)n l’automate
A est l’unique (à renommage des états près) automate ayant le plus petit nombre d’états.
Les classes d’équivalences de ∼A peuvent être déterminées effectivement en temps polynomial via l’al-
gorithme de Moore ou de Hopcroft. on peut alors calculer en temps polynomial l’automate minimal d’un
automate déterministe complet donné.
On peut aussi qualifier l’équivalence de Nérode via les résiduels mais cette fois sur les mots et non les
états. Le résiduel du langage L par rapport au mot u est l’ensemble u−1 L = {v ∈ Σ∗ |uv ∈ L}. On note
alors x ≡M y si, et seulement si x−1 L = y −1 L.
Théorème 29.1 (de Myhill-Nérode) Un langage est rationnel si, et seulement si, il admet un nombre
finis de résiduels.
Les automates finis peuvent avoir des applications en algorithmique de texte. Étant donné un texte T
de longueur n et un motif M de longueur m, l’algorithme naı̈f décidant si M est une sous-chaı̂ne de T est
un O(nm). On peut mieux faire avec des automates.
Développement 8. recherche de motif à l’aide de l’automate des occurrences.
II Non déterminisme
Jusqu’à présent, chaque étape de calcul dépend uniquement de l’étape précédente. Dans une machine
non-déterministe, plusieurs choix peuvent exister.
Exemple 29.5 (automate non déterministe et d’un arbre de calcul) On définit un automate non-déterministe
N1 qu’on simule sur l’entrée 0101.
début q0
début q0 0, 1 0
1 q0
1 1.
q1 1
q0 q2
0, q1
0
q2 q0
0
1 1.
1 1 q2
q0 q2
q3 q1
0,1 1
q3
Définition 29.5 Un automate fini non déterministe est un quintuplet (Q, Σ, I, F, δ) où
— Q est un ensemble fini d’états ;
— Σ est un ensemble fini appelé alphabet ;
— δ : Q × Σ → P(Q) est la fonction de transition où Σ = Σ ∪ {} ;
— q0 ∈ Q est l’état initial ;
— F ⊂ Q est l’ensemble des états acceptants.
Exercice 29.2 Donner un automate non-déterministe et déterministe qui reconnaı̂t le langage sur l’alpha-
bet unaire {1} des mots ayant soit un nombre de 0 qui est un multiple de 2 ou 3.
Théorème 29.2 (Équivalence) Tout automate fini non déterministe est équivalent à un automate
fini déterministe.
Remarque 29.1 La preuve du théorème précédent nous donne un algorithme de déterminisation d’un
automate non déterministe.
B Stabilité
Définition 29.6 (Opérations rationnelles) Soient A et B deux langages. Les opérations rationnelles
sont :
— l’union : A ∪ B = {w|w ∈ A ∧ w ∈ B} ;
— la concaténation : A.B = {xy|x ∈ A ∨ y ∈ B} ;
— l’étoile : A∗ = k≥0 Ak .
S
Proposition 29.2 La classe des langages rationnels est stable par opération rationnelle, par intersec-
tion et par passage au complémentaire.
Remarque 29.2 Les langages rationnels ne sont pas stables par inclusion (an bn ⊂ Σ∗ )
A Définition
Remarque 29.3 On pourra confondre une expression rationnelle e et son langage associé L(e).
Exemple 29.7 L’ensemble des identifiants de la forme id.N où N est un nombre quelconque est le langage
décrit par l’expression rationnelle id.(1|2|...|9)*. Ces expressions rationnelles sont très utiles pour
décrire et trouver des chaı̂nes vérifiant un pattern.
Théorème 29.3 (Kleene 1956) Un langage est rationnel si et seulement s’il est décrit par une ex-
pression rationnelle.
Exemple 29.8 La preuve de ce théorème nous fournit des algorithmes permettant de passer d’une ex-
pression rationnelle à un automate (méthode de Thompson) et vice-versa (algorithme de McNaughton et
Yamada)
Remarque 29.4 Pour passer d’un automate à une expression rationnelle, on peut utiliser une méthode
similaire à l’élimination de Gauss en mathématiques. Il faudrait pour cela un moyen de résoudre une
équation entre langages, via le lemme d’Arden.
IV Un modèle de calcul
A Des langages non rationnels ?
Proposition 29.3 (Lemme de l’étoile) Pour tout langage rationnel L, il existe un entier n tel que
pour tout w ∈ L vérifiant |w| ≥ n, w peut s’écrire w = xyz avec :
1. pour tout i ≥ 0, xy i z ∈ L ;
2. |y| > 0 ;
3. |xy| ≤ n.
Exemple 29.9
— L = {an bn , n ∈ N} n’est pas rationnel car il ne vérifie pas le lemme précédent.
— L() l’ensemble des mots bien parenthésés non plus.
B Hiérarchie de Chomsky
Les automates finis ne sont pas le seul modèle de calcul. Les automates finis et les langages rationnels
forment ensemble le premier niveau de la hiérarchie de Chomsky.
Langages décidables
-
Machine de Turing
Langages algébriques
-
Automates à pile
Langages rationnels
-
Automates finis
La notion de décidabilité utilise les Machine de Turing, permettant de donner une définition formelle à la
notion d’algorithme. On peut simplement définir la décidabilité d’un langage par l’existence d’un algorithme
permettant de décider l’appartenance d’un mot à ce langage.
Théorème 29.4 (Presburger, 1929) La théorie du premier ordre des entiers munis de l’addition est
décidable.
Développements
193
Développement 1
Balayage de Graham. On considère un ensemble de points Q, dont on veut calculer l’enveloppe convexe
EC(Q).Le balayage de Graham résout le problème de l’enveloppe convexe en gérant une pile S de points
candidats. Chaque point de l’ensemble Q est empilé une fois, et les sommets qui ne sont pas dans EC(Q)
finissent par être dépilés.
On utilisera deux autres opérations sur la pile :
— Sommet(S) : retourne le sommet de la pile sans changer son contenu ;
— Sous-Sommet(S) : retourne l’élément juste en dessous du sommet de la pile, sans changer son
contenu.
À la fin de l’exécution, S contiendra, du bas vers le haut, les sommets de EC(Q) dans l’ordre trigo-
nométrique.
Algorithme. On suppose que l’on dispose d’une fonction Tri-Polaire(Q, p0 ) qui trie les éléments de
Q par angle polaire respectivement à p0 , dans le sens trigonométrique. Si deux éléments ont le même angle,
on garde seulement celui qui est le plus loin de p0 .
Théorème 1.1 Si la procédure BalayageGraham est exécutée sur un ensemble Q de points tel que
|Q| ≥ 3, alors à la fin de la procédure, la pile S contient du bas vers le haut, les sommets de EC(Q)
dans le sens trigonométrique.
195
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Au début de chaque itération du Pour, la pile S contient, du bas vers le haut, les sommets de
EC(Qi−1 ) pris dans l’ordre trigonométrique.
Initialisation Pour i = 3, on a Qi−1 = {p0 , p1 , p2 }. L’enveloppe convexe d’un triangle est lui-même et S
contient p0 , p1 , p2 .
Conservation Au début de l’itération i, le sommet de la pile est pi−1 . Soit pj le sommet de la pile après
l’exécution du Tant que juste avant d’empiler pi . En notant Pi l’ensemble des sommets dépilés lors
de la boucle Tant que de l’itération i, on veut montrer que :
Soit pt un sommet dépilé, et soit pr le sommet qui était juste en dessous de pt . On sait deux choses :
— l’angle polaire de pt (respectivement à p0 ) est supérieur à celui de pr et inférieur à pi ;
Complexité. Le balayage de Graham réalise O(n log n) opérations élémentaires où n est le nombre de
points. La recherche de p0 est linéaire, et le tri se fait en O(n log n). Le traitement suivant se fait alors en
O(n) en complexité amortie. En effet, chaque sommet est empilé exactement une fois et chaque sommet
ne peut être dépilé qu’une fois. Au total, on réalise exactement m fois Empiler et au plus m − 2 fois
Dépiler (car p0 , p1 et pm ne sont jamais dépilés). Puisque m ≤ n, on réalise tout le traitement en O(n)
opérations.
Ce développement consiste à donner une brève introduction à la théorie des matroı̈des. On montre que
l’algorithme glouton est optimal pour ces structures, s’intégrant ainsi dans la leçon 1 et 12. La principale
application de cette théorie étant la preuve de l’optimalité de l’algorithme de Kruskal, ce développement
peut aussi illustrer la leçon 10.
Exemple de matroı̈de. On peut montrer que l’ensemble des forêts d’un graphe est un matroı̈de.
Démonstration. Un ensemble X ∈ I est indépendant si, et seulement si, X est une forêt du graphe G.
1. Un sous-ensemble d’une forêt est encore une forêt. En effet, si X ⊂ I et il existe Y ⊂ X contenant
un cycle, alors X contient aussi ce cycle, ce qui est absurde.
2. Soient A et B deux forêts de G avec |A| < |B|. Chaque sommet de G est contenu dans un arbre de
A (si le sommet est isolé, alors il est dans un arbre à un sommet). Ainsi, A contient |V | − |A| arbres
(resp. |V | − |B| pour B). En effet, dès qu’on ajoute une arête, on fusionne deux arbres et le nombre
total d’arbres diminue de un.
Ainsi, B contient moins d’arbres que A, et donc il existe un arbre T de B qui n’est pas inclus dans
un arbre de A (si ce n’était pas le cas, on aurait |V | − |B| ≥ |V | − |A| car chaque arbre de B est
dans un arbre de A). Autrement dit, il existe deux sommets u, v ∈ V tels que u et v sont dans T
mais pas dans le même arbre de A. Il existe donc une arête (x, y) sur le chemin de T allant de u à
v qui n’est pas dans A. On a alors A ∪ {(x, y)} qui est toujours une forêt.
199
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Lemme 2.1 Tous les ensembles indépendants maximaux d’un matroı̈de ont le même cardinal.
Démonstration. Par l’absurde, on utilise la propriété d’échange et on contredit la maximalité du plus petit
des deux ensembles indépendants.
Démonstration.
Soit X = {x1 , ..., xm } la solution renvoyée par l’algorithme glouton. On a alors w(x1 ) ≥ ... ≥ w(xm ).
Soit Y = {y1 , ..., ym } une solution optimale avec w(y1 ) ≥ ... ≥ w(ym ). On montre que pour tout
1 ≤ i ≤ m, w(xi ) ≥ w(yi ). L’optimalité de la solution en découle directement.
Si ce n’était pas le cas, prenons k le plus petit indice tel que w(xk ) < w(yk ). On remarque que
k > 1 car {x1 } est le singleton indépendant de poids maximum. On regarde alors A = {x1 , ..., xk−1 } et
B = {y1 , ..., yk }. Puisque |B| = |A| + 1, on peut appliquer la propriété d’échange et il existe 1 ≤ i ≤ k tel
que A ∪ {yi } est indépendant et yi ∈ / A. On a alors w(yi ) ≥ w(yk ) > w(xk ). L’algorithme glouton aurait
alors choisit yi avant xk (il suffit de prendre le sous-ensemble de A ∪ {yi } des éléments ayant un poids
inférieur à w(yj ), qui est indépendant par hérédité).
Optimalité de l’algorithme de Kruskal. Le théorème précédent nous permet de montrer que l’algorithme
de Kruskal qui construit un arbre couvrant renvoie bien un arbre optimal. En effet, les arêtes sont triées
par poids croissant et on choisit de manière gloutonne la prochaine arête qui ne crée pas de cycle.
B-arbres
Motivation et exemple. Dans le cas d’une grande quantité d’information, il est parfois nécessaire d’uti-
liser des supports de stockage dont le temps de réponse pour une lecture est élevé (disque dur, ...). Pour
implanter une structure de dictionnaire dans ce cas, on préférera contracter notre ABR afin de faire moins
d’appels à la mémoire lente.
10 20
2 5 7 15 25 28 42
Définition.
Définition 3.1 (B-arbre) Soit un entier t ≥ 2. On appelle B-arbre d’ordre t un arbre vérifiant les
invariants suivants :
— Chaque nœud x contient les attribut suivants :
1. le booléen [Link] (VRAI si X est une feuille, FAUX sinon) ;
2. le nombre x.n de clés dans ce nœud ;
3. le tableau trié [Link]́ de taille x.n contenant les clés de ce noeud (numéroté de 1 à x.n) ;
4. le tableau [Link] de taille x.n + 1 contenant l’ensemble des enfants du nœud x (numéroté
de 0 à x.n).
201
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
— pour tout nœud x, toute clé ki apparaissant dans le fils [Link] , on a ki−1 ≤ [Link]́i ≤ ki
(1 ≤ i ≤ n).
— toutes les feuilles ont la même profondeur h ;
— pour tout noeud x non racine, on a t − 1 ≤ x.n ≤ 2t − 1 ;
— la racine x0 vérifie 1 ≤ x0 .n ≤ 2t − 1.
Ici, au vu de son utilisation, on suppose le B-arbre stocké dans une mémoire auxiliaire d’accès lent, on
va donc compter notre complexité en fonction du nombre de lecture et d’écriture dans cette mémoire. On
considère deux fonctions LIRE et ECRIRE permettant d’accéder à notre mémoire auxiliaire.
Recherche dans un tel arbre. La fonction de recherche dans un B-arbres ressemble beaucoup à la
recherche dans un ABR, sauf que dans chaque nœud, il faut chercher le bon intervalle, et que la complexité
sera évaluée en fonction d’appel à la fonction LIRE. On suppose que notre racine est en mémoire principale,
il n’y a donc pas besoin de faire de appel à LIRE.
Démonstration. Par définition, T contient au moins une clé à la racine, et donc sa racine possède au moins
2 enfants. On a donc :
— au moins 1 nœud à la profondeur 0 ;
— au moins 2 nœuds à la profondeur 1 ;
— au moins 2t nœuds à la profondeur 2...
On montre alors par récurrence immédiate qu’on a au moins 2ti−1 nœuds à la profondeur i. Puisque chaque
nœud contient au moins (t − 1) clés, on a :
h h
X
i−1 t −1
n ≥ 1 + (t − 1) 2t = 1 + 2(t − 1) = 2th − 1
t−1
i=1
Étant donné un B-arbre T de racine x0 , la fonction Recherche(x,c) fait h appels à LIRE dans le pire
des cas, que l’on peut borner via le théorème ci-dessus.
Insertion dans un B-arbre. Ici les choses se compliquent puisque l’insertion est moins évidente. Il y a
des situations qui peuvent poser problème, par exemple si l’arbre est déjà plein.
Ce développement présente un algorithme permettant de construire un tas en temps linéaire, ainsi que
le tri par tas. Un tas permet notamment d’implémenter une file de priorité, illustrant ainsi les leçons 4
et 5. De plus, ce développement illustre l’utilisation des arbres pour réaliser des structures de données
arborescentes et s’insère donc naturellement dans la leçon 10.
Tas-max. La structure de tas max est une structure de données qu’on peut représenter par un arbre
binaire complet gauche, où chaque étiquette d’un nœud est plus grande que celle de ses descendants. Un
tas peut être implanter par un tableau à n éléments numérotés de 1 à n, où le fils gauche (resp. droit)
d’un élément i ∈ {0, ..., n − 1} est 2i (resp. 2i + 1).
Tri par tas. L’idée du tri par tas est assez élémentaire une fois qu’on a la structure de donnée : étant
donné les n éléments à trier, on construit un tas les contenant puis on extrait n fois le minimum du tas.
Étant donné un tableau A (représentant un arbre binaire) et un indice i, on note Ti l’arbre correspondant
enraciné au noeud i. L’algorithme suivant, Entasser-Max, reçoit un tableau A et un indice i tel que les
arbres enracinés en 2i et 2i + 1 soient des tas, et modifie A afin que Ti soit un tas.
On déduit de cet algorithme une procédure permettant de transformer un tableau en tas-max en temps
linéaire. Enfin, avec ce tas, on en déduit une procédure permettant de réaliser un tri en place.
205
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Analyse. Le seul point complexe est la correction de la procédure Entasser-Max. Les autres corrections
en découle presque directement.
Lemme 4.1 Soit 1 ≤ i ≤ n, si T2i et T2i+1 sont des tas , alors après Entasser-Max(A,i), Ti est
un tas. La procédure s’exécute en temps O(h(i)), où h(i) est la hauteur de Ti .
Démonstration. On note Ch la complexité dans le pire des cas de l’algorithme Tasser pour un nœud i dont
l’arbre enraciné en i est de hauteur h. On a alors dans le pire cas Ch = O(1) + Ch−1 et donc Ch = O(h).
Prouvons maintenant la correction. On montre par induction structurelle sur les arbres binaires que
l’arbre obtenu vérifie la propriété du tas-max.
Si Ti est une feuille, alors Entasser-Max(A,i) ne modifie pas Ti qui est déjà un tas. Sinon, on fait
une disjonction de cas selon la valeur de plus grand :
— si max=i, alors l’arbre enraciné en i est un tas ;
— si max=g, alors on a si d < n A[g] ≥ A[i] et A[g] ≥ A[d]. On échange alors dans A les nœuds i et g et
la propriété du tas-max est vérifiée localement. Puisque Tg était bien un tas par hypothèse, ses enfants
le sont aussi et donc ont peut appliquer l’hypothèse d’induction sur g et après Entasser-Max(A,g),
Tg est aussi un tas.
— si max=d, on raisonne de la même manière.
Démonstration.
Complexité : On note C(n) la complexité dans le pire des cas de l’appel Construire(A) où n =
[Link]. Uun tas à n éléments a une taille bornée par blog2 nc et le nombre de nœud ayant la hauteur h est
au plus dn/2h+1 e. Puisque notre fonction appelle la fonction Tasser pour tout nœud ayant une hauteur
strictement positive, on a :
blog2 nc l blog2 nc
X n m X n
C(n) = O(h) = O h
2h+1 2h
h=1 h=1
Or, on peut calculer la deuxième somme en dérivant la série géométrique de raison x et en appliquant la
formule en 1/2. On obtient alors
X h 1/2
= =2
2h (1 − 1/2)2
h≥0
Démonstration.
Complexité : D’après le lemme 4.2, l’appel Construire(T ,n) transforme T en un tas en temps
O(log n) sans prendre de place supplémentaire. De plus, l’algorithme fait n − 1 appels échanges et appels
à la fonction Tasser qui s’exécute dans le pire des cas en O(log n). Donc l’algorithme s’exécute en
O(n + n log n) = O(n log n).
Correction : On pose l’invariant suivant : à chaque itération, le sous-tableau A[i + 1...n] contient
les n − i − 1 plus grands éléments de T triées, et A[1...i] est un tas max .
— Avant de rentrer dans la boucle, on a i = n et donc A[n + 1, n] est vide et A[1...n] est effectivement
un tas d’après le lemme 4.2.
— On suppose que pour i > 0, on a A[i + 1...n] trié et qui contient les plus grands éléments et A[1...i]
est un tas. On a alors A[1] le plus grand élément de A[1...i], et donc après échange, A[i...n] est
encore trié et contient les n − i plus grands éléments de A. Ensuite, d’après le lemme 4.1, après
l’appel Entasser-Max(A,0), on a A[1...i − 1] qui est un tas.
À la fin de la boucle, on a alors i = 0 et donc A[1...n] qui est trié.
Ce développement étudie la complexité moyenne de la recherche dans une table de hachage où les
collisions sont gérées par chaı̂nage. Il trouve ainsi sa place dans la leçon 6, et peut servir d’illustration dans
la leçon 4. Ce développement étant un peu cours, il pourra être complété par une simple analyse du pire
des cas au début, ainsi qu’avec des dessins au tableau afin d’illustrer les démonstrations.
Introduction. On considère une table de hachage où les collisions sont résolues par chaı̂nage, et on se
place dans l’hypothèse de hachage uniforme simple : chaque élément a la même chance d’être haché
vers l’une des alvéoles indépendamment des endroits où les autres éléments sont allés.
Pour une table de hachage à m alvéoles et n éléments, on note α = n/m le facteur de remplissage.
Théorème 5.1 Dans une table de hachage pour laquelle les collisions sont résolues par chaı̂nage,
une recherche infructueuse prend un temps moyen Θ(1 + α), sous l’hypothèse d’un hachage simple
uniforme.
Démonstration. On note Rech− (T, k) la variable aléatoire qui compte le nombre de comparaisons dans
une recherche infructueuse d’un élément k dans un tableau T à n éléments.
m
X
E(Rech− (T, k)) = P(h(k) = i)λi
i=1
Maintenant, on note Rech− (n, m) le nombre de comparaison dans un tableau déjà rempli avec n
éléments k1 , ..., kn . Par hypothèse de hachage uniforme simple, le tableau est tiré uniformément parmi
l’ensemble des tableaux à m alvéoles et n éléments. Ainsi, par linéarité de l’espérance,
1 X
E(Rech− (n, m)) = E(Rech− (T, k))
mn
T
209
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Théorème 5.2 Dans une table de hachage pour laquelle les collisions sont résolues par chaı̂nage, une
recherche fructueuse prend un temps moyen Θ(1+α), sous l’hypothèse d’un hachage simple uniforme.
Démonstration. L’idée est de prendre aléatoirement un élément qui est déjà dans le tableau, et compter
le nombre de comparaisons nécessaires. Le nombre d’éléments examinés pour un élément x est 1 plus le
nombre d’éléments suivants qui ont été hachés au même endroit.
On suppose que l’on a inséré les éléments x1 , ..., xn dans la table de hachage, de clés respectives
k1 , ..., kn . On note Rech+ (xi ) le nombre de comparaisons nécessaires pour trouver xi . De plus, on note
Xij = 1{h(ki ) = h(kj )}. Par hypothèse de hachage simple uniforme, E(Xij ) = 1/m. Ainsi,
n
1X
E(Rech+ (n, m)) = E(Rech+ (xi ))
n
i=1
n n
1X X
= E 1+ Xij
n
i=1 j=i+1
n n
1 X X
= 1 + E(Xij )
n
i=1 j=i+1
1 X 1
=1+
n m
1≤i<j≤n
1 n(n − 1)
=1+
n 2m
α 1
=1+ −
2 2m
Ce développement a pour but d’étudier la complexité moyenne du tri rapide randomisé, et s’intègre
ainsi naturellement dans la leçon 8. Puisque cet algorithme est probabiliste et utilise le paradigme diviser
pour régner , il peut aussi servir d’illustration dans les leçons 11 et12.
Le but est de montrer le théorème suivant, qui analyse le temps d’exécution moyen du tri rapide
randomisé (pour le pseudo-code, voir 8.6).
Théorème 6.1 L’espérance du nombre de comparaisons du tri rapide randomisé d’un ensemble à n
éléments est au plus 2nHn où Hn est le terme général de la série harmonique.
Démonstration. Soit T un tableau à n éléments. Pour 1 ≤ i ≤ n, on note ti le i-ème plus petit élément de
T . Ainsi, t1 = min T et tn = max T . On pose Xij la variable aléatoire valant 1 si ti et tj sont comparés
au cours de l’exécution, et 0 sinon.
On remarque alors que deux éléments ti et tj sont comparés au plus une fois. En effet, si ti et tj sont
comparés , l’un des deux était un pivot et n’est comparé avec plus personne ensuite.
P
Ainsi, le nombre total de comparaisons est N = 1≤i<j≤n Xij . Par linéarité de l’espérance, on a alors
X
E(N ) = E(Xij )
1≤i<j≤n
211
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
2
Lemme 6.1 Pour tout 1 ≤ i < j ≤ n, on a pij = j−i+1 .
On peut voir l’exécution du tri rapide comme un arbre binaire, avec comme nœuds les pivots successifs.
On note alors π la permutation de T obtenu en faisant un parcours en largeur de l’arbre obtenu. L’arbre ci-
dessous représente l’exécution du tri rapide sur un tableau à 9 éléments. On a ici π = (4, 8, 2, 5, 1, 9, 6, 3, 7).
t5
t3 t8
t1 t4 t7 t9
t2 t6
Or, on sait que Hn ∼ ln n + o(1), le temps d’exécution de l’algorithme est O(n log n).
Distance d’édition
Ce développement présente la distance d’édition entre deux chaı̂nes de caractères, ainsi qu’un algorithme
de programmation dynamique permettant de la calculer, s’intégrant ainsi dans les leçons 9 et 12.
Définitions. Il y a plusieurs définition de distance possible entre deux chaı̂nes de caractères. On va définir
la distance d’édition, ou distance de Levenshtein, entre deux chaı̂nes de caractères x et y en considérant
les opérations suivantes :
— la substitution d’une lettre de x à une position donnée par une lettre de y ;
— la suppression d’une lettre de x à une position donnée ;
— l’insertion d’une lettre de y dans x à une position donnée.
Pour chaque opération, on définit un coût. Soit a, b ∈ Σ, on a :
— sub(a, b) : coût pour substituer b par a ;
— sup(a) : coût pour supprimer la lettre a ;
— ins(b) : coût pour insérer la lettre b.
Définition 7.1 Étant donné deux chaı̂nes de caractères x et y, la distance d’édition entre x et y, noté
lev(x, y) est défini par :
lev(x, y) = min{coût de σ : σ ∈ Tx,y }
où Tx,y est l’ensemble des séquences d’opérations qui transforme x en y, le coût d’une séquence étant
la somme des coût de chaque opération.
Remarque 7.1
1. La distance de Hamming est un cas particulier de la distance d’édition, où on considère seulement
l’opération de substitution (on peut mettre un coût valant +∞ pour ces deux opérations).
2. Ici, on se contentera de prendre les coût sup et ins unitaire, et sub(a, b) = 1{a 6= b}.
Exemple 7.1 (ADN) On considère deux mots sur l’alphabet {A, T, C, G}, x = AAT GC et y = CAGC.
On a lev(x, y) = 2 puisque qu’il suffit de substituer le premier A de x à C, et supprimer le T .
213
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
pour i ∈ {−1, 0, ..., m − 1} et j ∈ {−1, 0, ..., n − 1}. Pour calculer T [i, j] on utilisera la proposition
suivante :
T [−1, −1] = 0
T [i, −1] = T [i − 1, −1] + supp(x[i])
T [−1, j] j − 1] + ins(y[j])
= T [−1,
T [i − 1, j − 1] + sub(x[i], y[j])
T [i, j] = min T [i − 1, j] + sup(x[i])
T [i, j − 1] + ins(y[j])
retourner T [m − 1, n − 1]
Exemple 7.2 On revient à notre exemple précédent en prenant chaque coût égal à 1 :
A A T G C
2 1 2 3 4 5
C 1 2 3 4 5 5
A 2 1 2 2 3 4
G 3 2 2 3 2 3
C 4 3 3 3 3 2
Correction de l’algorithme. On utilisera ce lemme qu’on utilisera pour montrer la proposition précédente.
Démonstration. La séquence d’opérations qui transforme ua en peut être réarranger de telle sorte à finir
par la suppression de la lettre a (commutativité de la suppression). Le reste de la séquence transforme u
en . Ainsi, on a :
Le second point se fait de la même manière. Pour le troisième point, on fait une distinction de cas selon
l’opération d’édition.
Démonstration de la proposition 7.1. C’est une conséquence directe du fait que lev(, ) = 0 et du lemme
précédent en prenant a = x[i], b = y[j], u = x[0...i − 1] et v = [y....j − 1].
Corollaire 7.1 L’algorithme DynamicLev retourne lev(x, y) sur l’entrée (x, y).
Complexité.
Proposition 7.2 L’algorithme DynamicLev, sur une entrée (x, y), s’exécute en temps O(|x| × |y|) et
en espace O(min(|x|, |y|)).
Démonstration. La complexité en temps s’obtient directement via la double boucle. Pour la complexité en
espace, il suffit de remarquer que seulement deux lignes (ou deux colonnes) sont nécessaires simultanément.
σ(u) = max{i : Mi @ u}
Construction d’un automate. Soit A = (Q, Σ, I, F, δ) l’automate déterministe complet défini par :
— Q = {0, ..., k} ;
— I = {0} ;
— F = {k} ;
— pour tout q ∈ Q et a ∈ Σ, δ(q, a) = σ(Mq a).
On va alors montrer que le langage reconnu par notre automate est Σ∗ M , ce qui nous permettra de
l’utiliser afin de résoudre le problème initial. On montrera ensuite que cet automate est minimal, et qu’il
n’y a donc pas besoin de le minimiser avant de traiter le texte.
Un premier lemme. Ce premier lemme, manipulant préfixe et suffixe, nous sera utile pour montrer la
proposition précédente.
217
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
La démonstration de ce lemme est difficile à faire en direct au tableau. Il sera judicieux de justifier la
démonstration avec des dessins, quitte à le faire manière un peu moins formel pour gagner en clarté.
Démonstration.
1. Si u @ v, alors tout suffixe de u est suffixe de v. Ainsi, on a {i : Mi @ u} ⊂ {i : Mi @ v}, et donc
σ(u) ≤ σ(v).
2. Si σ(ua) = 0, alors puisque σ(u) ≥ 0, l’inégalité est vérifiée. Sinon, on a σ(ua)−1 ≥ 0. On remarque
alors que Mσ(ua)−1 @ u, et ainsi, σ(ua) − 1 ≤ σ(u).
3. On montre le résultat par double inégalité.
— Par définition Mσ(u) @ u et donc Mσ(u) a @ ua et donc d’après le point 1, σ(Mσ(u)a ) ≤ σ(ua).
— Pour l’autre inégalité, on observe que Mσ(u) a et Mσ(ua) sont deux suffixes de ua. Ainsi, le
plus court des deux mots est suffixe de l’autre. D’après le point 2, on a |Mσ(ua) | = σ(ua) ≤
σ(u) + 1 = |Mσ(u) a|. Ainsi, Mσ(ua) @ Mσ(u) a, d’où σ(ua) = σ(Mσ(ua) ) ≤ σ(Mσ(u) a) d’après
le point 1.
Nous pouvons maintenant revenir au langage reconnu par l’automate défini ci-dessus.
Démonstration. On procède par récurrence sur la longueur l de u.
• Si l = 0, alors u = et δ ∗ (0, ) = 0 = σ().
• Supposons que l > 0 et que pour tout mot v ∈ Σl−1 vérifie δ ∗ (0, v) = σ(v). Soit u ∈ Σl , qu’on écrit
v = ua avec |u| = l − 1 et a ∈ Σ. En appliquant l’hypothèse de récurrence :
et donc L(A) = Σ∗ M .
Lemme 8.2 Pour i, j ∈ Q tels que i > j, le mot mi+1 ...mk est accepté par A à partir de i mais pas
de j. Ainsi, A est minimal.
Démonstration. On note Ni = mi+1 ...mk le suffixe de M de taille k−i. Par construction, on a δ ∗ (0, Mi ) = i,
d’où
δ ∗ (i, Ni ) = δ ∗ (0, Mi Ni ) = δ ∗ (0, M ) = k
Ainsi, Ni est accepté à partir de i dans A.
Pour j < i, alors |Mj Ni | = j + k − i < k = |M |. Ainsi, M n’est pas un suffixe de Mj Ni et donc
Dans ce développement, on étudie un problème de routage qui s’avère être NP-complet, et on propose
d’en étudier un algorithme d’approximation. Dans ce cadre, ce développement rentre dans la leçon 11, mais
aussi dans la leçon 26 si celle-ci traite des algorithmes d’approximations. Puisque le problème étudié concerne
du routage, il s’intègre notamment dans la leçon 21 si un point de vue théorique est abordé.
Enfin, d’une manière plus large, ce problème consiste à trouver des chemins disjoints dans un graphe,
donnant une application aux algorithmes de plus courts chemins présentés en leçon 7.
Introduction. Le routage consiste à envoyer un paquet d’une source à une destination dans un réseau.
Plus formellement, on représente ce réseau comme un graphe orienté G = (V, E), et on définit un ensemble
de requêtes R ⊂ V × V que l’on essaye de satisfaire.
Dans ce problème, tous les paquets d’une même requête passent par le même chemin dans le réseau. De
plus, deux chemins de deux requêtes différentes ne peuvent pas partager une arête. Autrement dit, chaque
requête réserve un chemin qu’elle seule peut utiliser. On veut alors maximiser le nombre de requêtes
satisfaites.
Algorithme. On présente ici un algorithme glouton plutôt naı̈f, nommé Shorts-Requests-First ou SRF.
L’idée est de router les requêtes courtes d’abord, c’est-à-dire celles dont le plus court chemin est minimal
221
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
√
Théorème 9.2 L’algorithme 9.1 a un ratio d’approximation de d me pour le problème MEDP sur
un graphe avec m arêtes, et cette borne est atteinte.
On remarque alors que m = |E| = q 2 . L’ensemble des requêtes est défini par :
c’est-à-dire l’ensemble des couples (début,fin) des q + 1 chemins du graphe G. Puisque tous les chemins
ont la même longueur q, l’algorithme SRF va choisir la requête R0 = (u10 , u1q ) en premier. Or chaque
requête suivante partage une arête avec le seul chemin possible pour satisfaire cette requête, donc |A| = 1.
√
Or, la solution optimale consiste à choisir les q autres requêtes, donc |A∗ | = q = d me.
Cas général :
Étant donné une instance de MEDP (G, R) où G a m arrêtes, on note A∗ = {ri∗ } une solution
optimale. On note p∗i le chemin associé à ri∗ ∈ A∗ .
L’idée consiste à procéder par stratégie adverse : on lance l’algorithme et en parallèle, on supprime des
requêtes de A∗ .
On lance SRF sur (G, R). À chaque itération, SRF sélectionne une requête r ∈ R de chemin π.
— Si r ∈ A∗ , on supprime r de A∗ .
— Pour tout r∗ ∈ A∗ telle que π ∗ ∩ π 6= ∅, on supprime r∗ de A∗ .
Lemme 9.1 Pour montrer que |A∗ | ≤ λ|A| pour une certaine constante λ, il suffit de montrer que
lorsque l’algorithme SRF accepte une requête, pas plus de λ requêtes sont supprimées de A∗ .
Démonstration. L’algorithme accepte |A| requêtes (autant d’itérations), et à la fin, A∗ est vide.
√
Le ratio d m e du théorème 9.2 est optimal, on ne peut pas faire mieux à moins que P = NP.
Ce développement présente une introduction à la méthode probabiliste. Cette méthode permet très
souvent de donner des algorithmes d’approximations probabilistes et s’insère donc dans la leçon 11. Si les
algorithmes d’approximations sont mentionnés dans la leçon 26, ce développement permet d’illustrer une
manière d’obtenir des certificats d’existence de solution à des problèmes d’optimisation.
Méthode probabiliste. La méthode probabiliste est un moyen de montrer l’existence d’objets par un
simple résonnement de dénombrement. Si la probabilité d’obtenir un objet ayant certaine propriété est
strictement positive, alors un tel objet existe. Par exemple, si la probabilité de tirer au sort un ticket
gagnant est non nulle, alors il y a au moins un ticket gagnant.
Cette méthode est non-constructiviste en générale, on montre juste l’existence sans vraiment donner
l’objet. Mais le plus souvent, une preuve utilisant la méthode probabiliste est équivalente à un algorithme
probabiliste permettant de construire cet objet.
Lemme 10.1 On considère un univers Ω et X une variable aléatoire sur Ω. Si E(X) = µ, alors
P (X ≥ µ) ≥ 0 et P (X ≤ µ) > 0.
Ainsi, il existe au moins une réalisation de X supérieure ou égal à µ, et au moins une réalisation de X
inférieure ou égal à µ.
Application : MAX-SAT.
Théorème 10.1 Étant donné un ensemble de m clauses, on note ki le nombre de littéraux dans la
ième clause pour 1 ≤ i ≤ m. Soit k = min1≤i≤m ki . Il existe une distribution de valeurs de vérités
qui satisfait au moins
Xm
(1 − 2−ki ) ≥ m(1 − 2−k )
i=1
clauses.
Démonstration. Pour chaque variable, on tire au sort sa valeur de vérité. En notant Xi pour 1 ≤ i ≤ m
la variable aléatoire qui vaut 1 la ième clause est satisfaite et 0 sinon, on a PP(Xi = 0) = 2−ki par
indépendance des choix. En notant N le nombre de clause satisfaite, on a N = ni=1 Xi et par linéarité
225
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
de l’espérance :
n
X m
X
E(N ) = E(Xi ) = (1 − 2−ki )
i=1 i=1
Ainsi, la preuve ci-dessus peut être transformer en un algorithme qui assigne à chaque variable une
valeur de vérité au hasard. Son temps d’exécution est un O(m), et il renvoie une distribution qui satisfait
en espérance m(1 − 2−k ) clauses. Cet algorithme est donc une 1 − 2−k -approximation randomisé.
Théorème 10.2 Soit G = (S, A) un graphe connexe à n sommets et m ≥ n/2 arêtes. Alors G
contient un ensemble indépendant ayant au moins n2 /4m sommets.
Démonstration. Soit d = 2m/n ≥ 1 le degré moyen d’un sommet dans G. On considère l’algorithme
probabiliste suivant :
Remarque : l’algorithme se passe en deux phases, une phase d’échantillonnage, et une phase de modifica-
tion. On parle de méthode Sample and Modify.
Soit X le nombre de sommets qui survivent à la première étape de l’algorithme. X suit une loi binomiale
de paramètre n et 1/d. Ainsi, E(X) = n/d.
Soit Y le nombre d’arêtes qui survivent à la seconde étape. Chaque arête survit si, et seulement si ses
deux sommets adjacents survivent, donc Y est la somme de m variables de Bernoulli de paramètre (1/d)2 .
Ainsi,
1 nd n
E(Y ) = m = 2 =
d 2d 2d
La seconde étape retire toutes les arêtes restantes et donc au plus Y sommets. Ainsi, lorsque l’algorithme
termine, le nombre de sommets restants est X − Y , et
n n n
E(X − Y ) = − =
d 2d 2d
Ce qui conclut notre preuve.
L’algorithme ci-dessus nous donne donc une max(n/4m, 4m/n) approximation randomisée, ainsi qu’une
borne inférieure sur le cardinal du plus grand ensemble indépendant, noté α.
Algorithme CYK
Introduction. L’algorithme CYK permet de résoudre le problème du mot pour les grammaires algébriques.
On dit qu’une grammaire G = (V, Σ, R, S) est sous forme normale de Chomsky si elle ne contient que des
règles de la forme A → a (avec a ∈ Σ et A ∈ V ) ou A → A1 A2 (A1 , A2 ∈ V ).
Définition 11.1 (Problème de mot) Étant donné un mot w = w1 ...wn et une grammaire G sous
forme normale de Chomsky, a-t-on w ∈ L(G) ?
Montrons l’inclusion indirecte. Soit A ∈ V tel qu’il existe k tel que i ≤ k ≤ j − 1, B ∈ Ei,k et
C ∈ Ek+1,j avec A → BC ∈ R. On a alors B →∗ wi ....wk , et C →∗ wk+1 ...wj . Ainsi,
A → BC →∗ wi ....wk C →∗ wi ....wj
227
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
et donc A ∈ Ei,j .
Réciproquement, soit A ∈ Ei,j , c’est-à-dire A →∗ wi ...wj . On a au moins deux lettres dans le mot
wi ...wj , la première règle appliquée est donc de la forme A → BC. On utilise alors l’arbre de dérivation de
A →∗ wi ...wj dans lequel A est la racine et a deux enfants, B et C. Pour exhiber k, il suffit de prendre
le nombre l de feuilles dans l’arbre enraciner en B, et on pose k = i + l − 1. Ainsi, on a B →∗ wi ...wk et
C →∗ wk+1 ...wj . Cela conclut la preuve.
Algorithme. L’algorithme va donc d’abord calculer les Ai,i et remonter jusqu’à l’ensemble E1,n en cal-
culant diagonale par diagonale.
retourner S ∈ E1,n ;
Commentaires. Il faut d’abord transformer une grammaire pour la mettre sous forme normale de Chom-
sky. Cette transformation peut faire exploser la taille de la grammaire. Il faut aussi vérifier qu’on a bien
∈/ L(G), ce qui peut se faire par saturation.
où :
premier0 (w) = {a ∈ Σ|w ⇒∗ aw0 , w0 ∈ (V ∪ Σ)∗ }
Définition axiomatique. On considère une vision ensembliste des fonctions. On veut montrer que la
fonction premier est la plus petite partie P de (V ∪ Σ)∗ × (Σ ∪ {0 }) telle que
1. a ∈ P (a) ;
2. 0 ∈ P () ;
3. si N → w1 ...wn , alors P (w1 ....wn ) ⊂ P (N ) ;
4. si N → , alors 0 ∈ P (N ) ;
5. (P (w1 )\{0 }) ⊂ P (w1 ....wn ) ;
6. si 0 ∈ P (w1 ), alors P (w2 ...wn ) ⊂ P (w1 ....wn )
Démonstration.
1. Pour a ∈ Σ, on a a ⇒∗ a et donc a ∈ premier(a).
2. De même, ⇒∗ , et donc 0 ∈ premier() ;
3. Si N → w1 ...wn . Soit a ∈ premier(w1 ...wn ).
Si a = 0 , alors w1 ...wn ⇒∗ , et donc par transitivité N ⇒∗ . Ainsi, 0 ∈ premier(N ).
Si a 6= 0 , on a w1 ...wn ⇒ aw0 , et donc encore par transitivité, N ⇒∗ aw0 , et finalement a ∈
premier(N ).
229
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Lemme 12.2 Soit P la plus petite partie vérifiant les axiomes 1 à 6. Montrer que pour tout n ∈ N,
pour tout a ∈ Σ, pour tout w, w0 ∈ (Σ ∪ V )∗ , on a
— si w ⇒n aw0 , alors a ∈ P (w) ;
— si w ⇒n , alors 0 ∈ P (w).
Théorème 12.1 premier est la plus petite partie P vérifiant les propriétés 1 à 6.
Démonstration. Soit P la plus petite partie vérifiant les axiomes 1 à 6. Puisque premier vérifie ces axiomes
d’après le lemme 12.1, on a P ⊂ premier. Le lemme 12.2 nous permet de montrer l’autre inclusion. En
effet, si a ∈ premier(w), il existe w0 ∈ (Σ ∪ V )∗ tel que w ⇒∗ aw0 (avec possiblement a = 0 et donc
w0 = ). Par le lemme 12.2, on a directement a ∈ P (w), et donc pour tout w, on a premier(w) ⊂ P (w).
Finalement, premier = P .
Algorithme. Cette définition axiomatique nous donne un moyen de calculer premier par saturation via
l’algorithme 12.1.
Or par HR, on a a ∈ P (xi ) ⊂ P (xi ...xk ) par 5. En combinant, on a a ∈ P (x1 ...xk ), puis
a ∈ P (N ) par 3, et finalement par 5, on a a ∈ P (N w2 ...wm ), c’est-à-dire a ∈ P (w).
— Si w ⇒n , on recommence avec w qui commence par un non terminal.
Voyageur de commerce
Définition 13.1 (Voyageur de commerce (TSP)) Étant donné un graphe complet G = (V, E) et
une fonction de coût w : EP→ N et nu entier k, existe-t-il un cycle C passant par chaque sommet
une et une seule fois, avec e∈C w(e) ≤ k ?
Démonstration. TSP est trivialement dans NP, un certificat étant la donnée du cycle. Il suffit alors de
vérifier si ce cycle passe bien une et une seule fois par chaque sommet et si la somme des poids est bien
inférieure à k.
Pour montrer que TSP est NP-dure, on réduit HC (circuit hamiltonien) à TSP.
Soit G = (V, E) une instance de HC. On construit alors une instance de TSP de la manière suivante :
— on pose G0 = (V, E 0 ) le graphe complet contenant les mêmes sommets que G ;
— on pose w : E → {0, 1} où w(e) = 1{e ∈
/ E}, c’est-à-dire toute les arêtes de G ont un poids de 0,
et 1 sinon.
— on prend k = 0.
Cette nouvelle instance de TSP a bien une taille polynomiale en la taille de l’instance de départ.
Si G admet un cycle hamiltonien C. On montre que C est une solution de TSP. En effet, P C passe par
chaque sommet une et une seule fois, et puisqu’il ne passe que par
P des arêtes de E, on a e∈C w(e) = 0.
Réciproquement, si TSP admet une solution C vérifiant e∈C w(e) = 0, alors pour tout e ∈ C,
w(e) = 0 et donc e ∈ E. Ainsi, C est une solution de HC.
233
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Théorème 13.2 Pour tout λ ≥ 1, il n’existe pas de λ-approximation de TSP, à moins que P = NP.
Démonstration. Soit λ ≥ 1. On procède par l’absurde, on suppose qu’il existe une λ-approximation pour
TSP, et on montre que l’on peut résoudre HC avec cet algorithme.
Soit G = (V, E) une instance de HC. On transforme cette instance en une instance de TSP :
— G0 = (V, E 0 ) le graphe complet ayant les mêmes sommets que G.
— w : E 0 → N où
1 si e ∈ E
w(e) =
λn + 1 sinon
On note Copt la solution optimale de TSP pour ce problème, et on note copt le poids du cycle associé.
De même, on pose Calgo la solution retournée par notre algorithme d’approximation, et calgo le poids du
cycle. Par hypothèse, on a :
calgo ≤ λ × copt
On considère deux cas :
— Si calgo ≥ λn + 1, alors on a copt > n. On en déduit que G n’admet pas de cycle hamiltonien, car si
c’était le cas, ce cycle ne passerait que par des arêtes de poids 1 et aurait donc un poids total de n.
— Si calgo < λn + 1, alors on remarque que Calgo ne passe que par des arêtes de E, puisque s’il passait
par une seule arête hors de E, son poids dépasserait λn + 1. La solution calgo est donc un cycle
hamiltonien de G.
Ainsi, le résultat de notre algorithme nous permet de résoudre le problème HC, ce qui conclut la preuve.
Cas euclidien. On termine par une restriction du problème du voyageur de commerce, nous permettant
de trouver un algorithme d’approximation. On définit TSP − EUCLIDIEN le problème TSP dans lequel
la fonction de poids w vérifie l’inégalité triangulaire :
Démonstration. Soit G = (V, E) et w une instance de TSP − EUCLIDIEN. On note copt le coût optimal
pour le problème. On note w(T ) la somme des poids des arêtes de l’arbre couvrant de poids minimal retenu
par l’algorithme.
En prenant le cycle optimal, si on enlève une arête, on obtient un arbre. Ainsi, on a copt ≥ w(T ).
On considère maintenant la solution Calgo de poids calgo retournée par l’algorithme. On note O l’ordre
du parcours et ainsi, C est obtenu en gardant seulement la première occurrence de chaque sommet dans
O.
Remarque 1 : dans l’ordre O, on rencontre exactement 2 fois chaque arête de T .
Remarque 2 : Calgo est obtenu en supprimant de O des sommets. Or, en supprimant un sommet de O,
on n’augmente pas le poids du chemin associé : (x, y, z) (x, z), on a w(x, y) + w(x, z) ≥ w(x, z).
Ainsi, en notant CO le chemin associé à O, on a calgo = w(Calgo ) ≤ w(CO ) par la remarque 2. Or
w(C0 ) = 2 × w(T ) par la remarque 1. Finalement, en combinant les inégalités,
calgo ≤ 2copt
Ce développement propose de montrer que le problème 2-SAT est décidable en temps linéaire. Pour
cela, on montre que résoudre ce problème revient à déterminer les composantes fortement connexes d’un
certain graphe. Ainsi, ce développement s’intègre dans la leçon 26 comme illustration non évidente d’un
problème P. De plus, il s’insère naturellement dans la leçon 28 pour illustrer un problème de satisfiabilité.
Enfin, ce développement illustre une application non tivial des algorithmes permettant de déterminer les
composantes fortement connexes d’un graphe, tels que Tarjan ou Kosaraju, et s’intègre alors parfaitement
dans la leçon 7.
Remarque 14.1 On remarque que la taille du graphe est linéaire en la taille de la formule.
237
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Théorème 14.2 F est satisfiable si, et seulement si aucune composante fortement connexe de G ne
contient à la fois une variable x et son complément x.
Lemme 14.1 Soit d une valuation. On a d(F ) = 0 si, et seulement s’il existe un chemin l1 ...lk dans
G avec d(l1 ) = 1 mais d(lk ) = 0.
Démonstration. Si d(F ) = 0, alors il existe une clause C = u ∨ v telle que d(C) = 0. On prend alors le
chemin uv, et on a d(u) = 1 et d(v) = 0.
Réciproquement, on suppose d(F ) = 1. Montrons tout d’abord que si uv ∈ E et d(u) = 1, alors
d(v) = 1. En effet, si uv ∈ E, alors on a dans F la clause u ∨ v ou la clause v ∨ u (donc la même clause).
On a directement d(v) = 1.
Ainsi, s’il existait un chemin l1 ...lk dans G avec d(l1 ) = 1, alors on a immédiatement d(l2 ) = ... =
d(lk ) = 1.
Démonstration du sens direct du théorème 14.2. Par contraposée, on suppose qu’il existe x et x dans la
même composante fortement connexe. Soit d une valuation, montrons d(F ) = 0. Il y a deux cas :
— si d(x) = 1, alors puisqu’il existe un chemin x x et d(x) = 0, on conclut par le lemme 14.1 que
d(F ) = 0 ;
— si d(x) = 0, alors on a d(x) = 1 et il existe un chemin x x dans G. On conclut de la même
manière.
Ainsi, F n’est pas satisfiable.
Lemme 14.2 Pour toute composante fortement connexe C de G, il existe une composante fortement
connexe C tel que u ∈ C ⇔ u ∈ C.
On peut maintenant construire la valuation. On considère le graphe des composantes fortement connexes
de G. Il s’agit d’un DAG et on peut alors prendre un tri-topologique de ce graphe C1 ....Cn . On construit
la valuation d de la manière suivante : pour i = 1...n, si Ci n’a toujours pas été traitée, alors on pose
d(Ci ) = 1 et d(Ci ) = 0.
La fonction d est bien définie par hypothèse. Par l’absurde, si d(F ) = 0, alors il existe un chemin l1 ....lk
dans G avec d(l1 ) = 1 mais d(lk ) = 0. En particulier, il existe une arête ll0 sur le chemin avec d(l) = 1
et d(l0 ) = 0. On a l et l0 dans deux CFC différentes C et C 0 . Puisque d(C) = 1, C est après dans le tri
topologique (C < C). Puisque d(C 0 ) = 0, C 0 a été traité avant et donc C 0 < C 0 . Enfin, puisqu’on a une
arête entre C et C 0 , on a C < C 0 , et donc :
Le théorème 14.1 est alors un corollaire du théorème 14.2 puisqu’il suffit de calculer les composantes
fortement connexes en temps linéaire (via Tarjan ou Kosaraju), et de vérifier linéairement si une variable
et sa négation sont dans la même CFC.
Lemme 15.1 (Lemme de König) Tout arbre infini à branchement fini admet une branche infinie.
Démonstration. Soit T un arbre enraciné ayant une infinité de nœuds et tel que chaque nœud interne a un
degré fini. On construit une suite (xn )n∈N telle que pour tout n ∈ N, Txn est infini à branchement fini.
— On prend x0 la racine de T , et la propriété est vérifiée au rang 0.
— On suppose qu’on dispose déjà de x0 ...xn . Par hypothèse de récurrence sur n, le sous-arbre de Txn
est infini à branchement fini. On note f1 , ..., fk les enfants de P
cet arbre. Par l’absurde, si tous les
(Tfi ) sont finis, chacun contenant ni nœuds, alors Txn contient ki=1 ni noeuds, ce qui est absurde.
Donc il existe 1 ≤ i ≤ k tel que Tfi soit infini à branchement fini, on pose xn+1 = fi .
On a ainsi construit par récurrence un chemin infini dans T .
Un théorème de compacité.
Rappel :
1. F est satisfiable s’il existe une valuation d : V → {0, 1} telle que pour toute formule F ∈ F,
d(F ) = 1.
2. F est finiment satisfiable si toute partie finie de F est satisfiable.
Démonstration. Le sens direct est trivial. Étant donné une valuation d : F → {0, 1} telle que d(F) = 1 et
un sous-ensemble G ⊂ F fini, on a toujours d(G) = 1.
241
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Réciproquement, on va construire un arbre afin d’appliquer le lemme de König. Soit (φn )n∈N une
énumération de F, et (vn )n∈N une énumération de V . On construit un arbre T de racine r de la manière
suivante :
— pour n ∈ N, les sommets de T de hauteur n + 1 sont étiquetés par vn ou vn ;
— pour un sommet x de T de hauteur n, alors y ∈ {vn , vn } est un enfant de x si, et seulement s’il
existe une valuation satisfaisant x, y, φ0 , ..., φn et tous les ancêtres de x.
Montrons que T est infini à branchement fini. Chaque nœud a au plus deux enfants, donc T est à
branchement fini. Il reste à montrer que T est infini, et pour cela, on montre que pour tout n > 0, T
a au moins un nœud à la profondeur n. Soit n > 0, par hypothèse {φ0 , ..., φn−1 } est satisfiable par une
valuation d. Ainsi, il existe au moins une branche de longueur n dans T en prenant pour 1 ≤ i ≤ n, xi si
d(xi ) = 1 et xi sinon.
Ainsi, par application du lemme, il existe une branche infini (xi )i>0 dans T . On pose alors d(vi ) = 1 si
xi = vi et d(vi ) = 0 si xi = xi . Cette valuation satisfait alors toute formule de F.
Théorème 15.2 Soit K > 0 et G un graphe. G est K-coloriable si, et seulement si, tous ses graphes
finis le sont.
L’idée ici n’est pas de faire une preuve très détaillée du théorème. On pourra se contenter de donner
les points clés de la preuve, l’idée principale étant de montrer une application du théorème de compacité.
Démonstration. Le sens direct est trivial, puisqu’il suffit de prendre une restriction du K-coloriage de G
pour colorier un de ses sous-graphes.
Réciproquement, on définit pour tout graphe H = (V, E) une formule ΦH satisfiable si, et seulement si
H est K-coloriable. L’idée est de considérer l’ensemble des variables (xv,l )v∈V,l∈J1;KK . On pourra prendre :
^ _ ^ ^ ^
ΦH = xv,l ∧ ¬(xv,l ∧ xv,l0 ) ∧ ¬(xu,l ∧ xv,l )
v∈V 1≤l≤K 1≤l<l0 ≤K uv∈E 1≤l≤K
Soit H l’ensemble des sous-graphes finis de G. On pose F = (ΦH )H∈H et on remarque que F
est finiment satisfiable. En effet, si on prend un sous-ensemble fini de F, l’union des sous-graphes finis
correspondants est encore un sous-graphe fini de G et est donc K-coloriable. Ainsi, il existe une valuation
satisfaisant ce sous-ensemble. Par le théorème de compacité, F est satisfiable par une valuation d de
laquelle on peut extraire un K-coloriage de G.
Théorème de Myhill-Nérode
u−1 L = {v ∈ Σ∗ |uv ∈ L}
Théorème 16.1 Un langage est reconnaissable si, et seulement s’il n’admet qu’un nombre fini de
résiduel.
Puisqu’il y a un nombre fini d’état, on obtient alors un nombre fini de résiduels (c’est-à-dire pour tout mot
u ∈ Σ∗ , u−1 L ∈ {Lq }q∈Q ).
Sens indirect. Soit Q l’ensemble des résiduels de L. On note q0 = L = −1 L, et F l’ensemble des
résiduels contenant le mot vide. On pose A = (Σ, Q, q0 , F, δ) avec :
δ(u−1 L, a) = (ua)−1 L
243
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Proposition 16.1 Un automate minimal reconnaissant L contient autant d’état que L admet de
résiduels.
Démonstration. Étant donné un automate reconnaissant L, on peut associer à chaque état un résiduel via
une fonction injective d’après la preuve du sens direct. Cet automate a donc plus d’états que L admet de
résiduels. Enfin, d’après le sens indirect, cette borne est atteinte.
Lemme 16.1 (de non pompage) Soit L un langage sur un alphabet à une lettre Σ = {a}.
L est rationnel si, et seulement si, pour tout v ∈ Σ∗ , il existe des entiers m > n > 0 vérifiant
(v m )−1 L = (v n )−1 L.
Démonstration. Montrons le sens direct. Si L est rationnel, alors il admet un nombre fini de résiduels. Ainsi,
pour tout v ∈ Σ∗ , d’après le principe des tiroirs, il existe m > n > 0 tels que (v m )−1 L = (v n )−1 L.
Montrons le sens indirect. En prenant v = a, notons m > n > 0 tel que (am )−1 L = (am )−1 L.
Montrons, par récurrence forte sur k, que :
Fonction objectif des k-moyennes. Cette fonction objectif mesure la distance quadratique de chaque
point de S au centroı̈de de son cluster. Étant donné un cluster Ci , son centroı̈de est défini par :
X
µi (Ci ) = argminµ∈X d(x, µ)2
x∈Ci
Problème. Il est cependant complexe de trouver le minimum de cette fonction. En revanche, l’algorithme
suivant est souvent utilisé. On considère la distance euclidienne k.k.
245
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Lemme 17.1 La fonction objectif des k-moyennes décroı̂t strictement à chaque itération de l’algo-
rithme ci-dessus.
Démonstration. On pose G(C1 , ..., Ck ) la fonction objectif, que l’on peut réécrire :
k X
X
G(C1 , ..., Ck ) = min kx − µi k2 (17.1)
µ1 ,...,µn ∈X
i=1 x∈Ci
1 P
On pose µ(Ci ) = |Ci | x∈Ci x. On remarque :
X
µ(Ci ) = argminµ∈X kx − µk2 (17.2)
x∈Ci
(Pour le montrer, il suffit de remarquer que la fonction est strictement convexe, et on regarde quand le
gradient s’annule).
On peut alors réécrire :
k X
X
G(C1 , ..., Ck ) = kx − µ(Ci )k2
i=1 c∈Ci
(t−1) (t−1)
On considère la mise à jour à l’itération t. On note C1 ,...,Ck la partition précédente, et soit
(t−1) (t−1) (t) (t)
µi = µi (Ci ), et soit C1 ,...,Ck la nouvelle partition à l’itération t. En utilisant l’équation 17.1
k
(t) (t) (t−1) 2
X X
G(C1 , ..., Ck ) ≤ kx − µi k (17.3)
i=1 x∈C (t)
i
On utilise l’équation 17.2 pour le membre droite, et l’équation 17.3 puis 17.2 sur le membre gauche,
on a :
(t) (t) (t−1) (t−1)
G(C1 , ..., Ck ) ≤ G(C1 , ..., Ck )
max δ(Ci )
1≤i≤k
On veut montrer que l’algorithme ci-dessus est une 2-approximation. Pour cela, il faut tout d’abord
montrer qu’il retourne bien une partition de S.
Démonstration. Par l’absurde, on suppose qu’il existe un point q qui n’est pas retenu par l’algorithme. On
remarque alors que tous les Ci sont non vides (et donc les pi sont bien définis). Ainsi, pour tout 1 ≤ i ≤ k,
247
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
on a d(pi , q) > ∆∗ , et de plus, pour tout 1 ≤ i < j ≤ k, on a d(pi , pj ) > ∆∗ . Ainsi, {p1 , ..., pk , q} est
un sous-ensemble de S contenant k + 1 points tous distants d’au moins strictement ∆∗ . Un tel ensemble
ne peut pas être partitionner en k clusters de diamètre au plus ∆∗ , donc S non plus. Or, S peut être
partitionné de la sorte par hypothèse, c’est absurde.
Démonstration. Par le lemme précédent, l’algorithme retourne bien une partition C1 , ..., Ck de l’ensemble
S en entrée. Soit 1 ≤ i ≤ k et x, x0 ∈ Ci , on a :
Cas général. On ne suppose plus connaı̂tre ∆∗ . On considère alors l’algorithme qui sélectionne les centres
successivement en prenant le point le plus loin des précédents, puis qui assigne chaque point au centre le
plus proche.
Remarque 18.1 Dans l’algorithme, on considère une notion de distance à un ensemble d(x, E) où x ∈ S
et E ⊂ S. Plusieurs définitions sont possibles, mais on utilisera ici
Démonstration. On compare les exécutions des algorithmes 18.1 et 18.2. Si, lors de l’exécution du premier,
les centres sélectionnés par le second sont disponibles, alors la proposition précédente S
est correcte.
i−1
On suppose que ce n’est pas le cas, c’est-à-dire qu’il existe 1 < i ≤ k avec qi ∈ j=1 Ci et pj = qj
∗
pour 1 ≤ j < i. Ainsi, il existe un indice 1 < l < i avec qi ∈ Cl , et donc d(qi , pl ) ≤ ∆ . Or, l’algorithme
18.2 choisit le point le plus loin pour nouveau centre. Autrement dit,
Ainsi, pour tout x ∈ S, on a d(x, {q1 , ..., qi−1 ) ≤ d(qi , {q1 , ..., qi−1 ) ≤ d(qi , ql ) ≤ ∆∗ . Donc tous les points
de S sont à une distance au plus ∆∗ des centres déjà sélectionnés. Avec un raisonnement similaire que
pour l’algorithme précédent, on a bien une 2-approximation dans ce cas.
Ce développement est la présentation et la correction d’un exercice avancé de SQL. Il permet notam-
ment de présenter la clause WITH ... AS ... qui simplifie certaines requêtes. Il s’intègre parfaitement
dans les leçons 23 et 18.
Exercice 19.1 On considère une base de donnée sur des entreprises, ainsi qu’une série de requêtes sur
cette base.
La base de données contient les tables suivantes :
— Employes(id, nom, rue, ville)
— Travail(id, nom compagnie, salaire)
— Compagnie(nom compagnie, ville)
Donner une expression SQL pour chacune des requêtes suivantes :
1. Trouver l’identifiant, le nom de la ville de résidence de chaque employé travaillant pour Banque
Centrale .
2. Trouver l’identifiant, le nom et la ville de résidence de chaque employé travaillant pour Banque
Centrale et gagnant plus de 10 000 €.
3. Trouver l’identifiant de chaque employé qui gagne plus que n’importe quel employé de la Banque
Locale .
4. On suppose que des compagnies peuvent se situer dans plusieurs villes. Trouver le nom de chacune
des compagnies qui ne sont situées que dans des villes où la Banque Locale est implantée.
Pour les questions suivantes, on utilisera la clause WITH, qui s’utilise de la manière suivante WITH
nom table(att 1,...,att n) AS (sous-requ^ ete) requ^ete et qui permet de renommer la table ob-
tenue via la sous-requête.
5. Trouver le nom de la (ou les) compagnie(s) qui a(ont) le plus d’employés.
6. Trouver le nom de chaque compagnie où, en moyenne, un employé gagne plus qu’un salarié de
Banque Centrale .
Solution.
1.
SELECT e . i d , e . v i l l e
FROM Employes a s e
JOIN T r a v a i l a s t on e . i d=t . i d
WHERE t . nom compagnie = ” Banque C e n t r a l e ”
249
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
2.
SELECT e . i d , e . v i l l e
FROM Employes a s e
JOIN T r a v a i l a s t on e . i d=t . i d
WHERE t . nom compagnie = ” Banque C e n t r a l e ” AND t . s a l a i r e >= 10000
3.
SELECT e . i d
FROM Employes AS e
JOIN T r a v a i l AS t on e . i d=t . i d
WHERE t . s a l a i r e >= (
SELECT MAX( s a l a i r e )
FROM T r a v a i l
WHERE nom compagnie = ” Banque L o c a l e ”
)
4.
SELECT nom compagnie
FROM Compagnie
EXCEPT
5.
WITH NbEmployes ( nom compagnie , n ) AS (
SELECT nom compagnie , COUNT( i d ) AS n
FROM T r a v a i l
GROUP BY nom compagnie
)
6.
WITH t r a v a i l s a l a i r e ( nom compagnie , s a l a i r e m o y e n ) AS (
SELECT nom compagnie , AVG( s a l a i r e )
FROM T r a v a i l
GROUP BY nom compagnie
)
FROM t r a v a i l s a l a i r e
WHERE nom compagnie = ” K i n d e r ”
)
soit minimal.
P2 2 P2 3
P1 1 3 P1 1 2
On considère l’algorithme glouton-online qui, pour toute tâche T1 , ..., Tn , l’affecte au processeur le
plus libre. Sur l’exemple précédent, l’ordonnancement obtenu via glouton-online correspond au schéma de
gauche. On remarque directement que cet algorithme n’est pas optimal.
253
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Pn
Lemme 20.1 τ ∗ ≥ 1
p i=1 w(Ti )
Dé[Link] note τ1 , ..., τp les temps d’exécutions sur chaque processeur, et on a alors S :=
P n p
i=1 w(Ti ) = k=1 τk .
On sait que τ ∗ = max1≤k≤p τk . Ainsi,
Xp
S≤ τ∗
k=1
La lemme en découle directement.
p
X
S= τi
i=1
p
X
= τ1 + τi
i=2
≥ τ1 + (p − 1)τ0
= pτ1 − (p − 1)w(Tj )
= pτ − (p − 1)w(Tj )
La solution gloutonne qui prend la grande tâche en dernier va alors remplir les p processeurs de manière
équilibrée avec les p(p − 1) premières tâches. Ainsi, lorsqu’on affecte la dernière tâche, tous les processeurs
sont remplis avec p − 1 tâches et ainsi τ = p − 1 + p = 2p − 1. Finalement,
τ 2p − 1 1
∗
= =2−
τ p p
On pourra mentionner le fait que l’algorithme glouton offline, qui trie les tâches avant de les exécuter
n’est pas optimal non plus, mais donne un meilleur facteur d’approximation.
Problème de pagination online. On considère une mémoire à deux niveaux : un cache (ou mémoire
rapide) de taille k, et une mémoire principale (ou mémoire lente) pouvant potentiellement garder une
infinité d’éléments en mémoire (on peut aussi fixer sa taille très grande devant k).
Un algorithme de pagination décide quel élément garder en mémoire cache à tout instant. On a une
séquence de requêtes ρ = (ρ1 , ..., ρN ), chacun spécifiant un élément mémoire ;
— si l’élément ρi est dans le cache, on a un cache hit ;
— si l’élément ρi n’est pas dans le cache, on a un cache miss.
Remarque 21.1 Un algorithme online n’a pas accès aux requêtes futures, c’est la différence avec un
algorithme offline qui connaı̂t à l’avance toutes les requêtes.
257
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Remarque 21.2 L’algorithme MIN a connaissance de la totalité de ρ, il supprime du cache l’élément qui
apparaı̂t le plus longtemps après dans ρ. La preuve d’optimalité est non trivial.
Remarque 21.3 Sans perdre de généralité, on considère que le cache est toujours vide au départ (les k
premières requêtes entraı̂nent toujours un cache miss).
Comment mesurer la performance d’un tel algorithme. Une première idée consiste à faire comme
pour la complexité : étudier le pire cas. Étant donné un algorithme A, on définie la performance dans le
pire cas P C(A) comme :
P C(A) = max fA (ρ)
ρ∈RN
Cette mesure n’est pas très utile, comme le montre le théorème suivant.
Théorème 21.1 On considère qu’il n’y a que k + 1 éléments mémoires et on considère des séquences
de requêtes de taille N . Pour tout algorithme déterministe de pagination A,on a P C(A) = N .
Démonstration. Soit ρ = (ρ1 , ..., ρk ) une séquence de départ avec les ρi distincts deux à deux. La mémoire
cache est alors remplie. On demande le k + 1-ème élément mémoire ρk+1 , et A supprime un élément que
l’on nomme ρk+2 . On demande ensuite ρk+2 qui n’est plus dans le cache, et on recommence jusqu’à ρN .
La séquence se construit alors par induction sur N .
Démonstration du théorème 21.2. Soit ρ = (ρ1 , ...., ρN ). On partitionne ρ en phases de la manière suivante :
P0 est la phase vide, et pour i > 0, Pi est la séquence maximale suivant Pi−1 contenant au plus k requêtes
distinctes.
On numérote les phases pour 0 ≤ i ≤ m, et on montre que pour tout i, fA (Pi ) ≤ k pour A ∈
{LRU, F IF O}.
Par l’absurde, si fA (Pi ) > k, alors il existe un élément mémoire α qui provoque un cache miss par
deux fois dans la phase Pi . On montre que cela est impossible via les algorithmes LRU et FIFO. Entre les
deux moments où α provoque un cache miss, les requêtes concernent au plus les k − 1 autres éléments
mémoires de la phase Pi .
— Cas LRU : au moment de l’éviction de α (entre les deux caches miss), α avait la dernière utilisation
la plus tôt. Ainsi, les k − 1 autres éléments du caches ont eu une requête entre le premier cache miss
de α et son éviction. Puisque lors de son éviction, la requête concernait un élément non présent en
cache, on obtient k + 1 éléments mémoires distincts demandés durant Pi .
— Cas FIFO : même raisonnement. En effet, lors de l’éviction de α, les k − 1 autres éléments sont en
haut de la file, α en bas et on insère un k + 1-ème élément dans la file, ce qui est absurde.
Ainsi, on a
m
X
fA (ρ) = fA (Pi ) ≤ mk
i=1
Maintenant, montrons que fo (ρ) ≥ m − 1. Soit 0 < i < m, on pose q la première requête de Pi , et q 0
la première requête de Pi+1 . On considère la séquence qui démarre juste après q et termine juste après q 0 .
On montre que MIN fait au moins une erreur sur cette séquence. En effet, q est chargé en mémoire cache
au début, et cette séquence contient au moins k éléments distincts (q 0 et Pi sans q). Ainsi, au moins un
de ces éléments ne sera pas dans le cache. Ainsi, on obtient au moins (m − 1) cache miss.
Finalement,
fA (ρ) − kfo (ρ) ≤ mk − k(m − 1) = k
Ce qui conclut la preuve.
Pour rendre la preuve plus claire, il sera judicieux de faire un schéma temporel des différentes requêtes.
De plus, il s’avère que la preuve est beaucoup plus claire pour FIFO que LRU.
Pratique vs Théorie. En pratique, on sait bien que LRU marche mieux que FIFO, alors que théoriquement
leurs performances sont similaires. Ce modèle de performance est donc critiquable. Pour distinguer théoriquement
FIFO et LRU, il faut alors notamment limiter la capacité de l’adversaire, et prendre en compte les principes
de localités.
Introduction. Un additionneur n bits est un circuit combinatoire : il peut être écrit sous le forme d’une
formule logique. L’additionneur classique (full-adder ) présente un problème majeur : pour calculer le i-ème
bit du résultat, on doit attendre la retenue sortante ci . Dans ce circuit, le chemin critique est alors de taille
O(n).
si = ai bi ci + ai bi ci + ai bi ci + ai bi ci (22.1)
ci+1 = ai bi + ai ci + bi ci (22.2)
ci+1 = gi + pi ci , gi = ai bi , pi = ai + bi (22.3)
— si gi est vraie, alors ci+1 est vraie, et donc une retenue est générée ;
— si pi est vraie, alors si ci est vraie, alors on a ci+1 qui est vraie, la retenue a été propagée.
En itérant, on a alors :
CLA. L’additionneur qui calcule les retenues en utilisant l’équation 22.4 s’appelle additionneur à rete-
nue anticipée, ou CLA pour carry-Lookahead adder. Malheureusement, cette formule n’est pas utilisable
directement telle quelle (il fait faire un OR à n entrée et un AND à n entrées, et des fils très longs).
On va donc travailler par bloc :
— Pij : une retenue est propagée de i à j.
— Gij : une retenue est générée entre i et j ;
261
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
En notant cv et cr les longueurs des chemins critiques dans dans les blocs vert et rouge, on remarque
que pour n bits, la longueur du chemin critique lCLA (n) vérifie l’inéquation :
Introduction. La recherche d’un chemin critique dans un circuit booléen revient au calcul de la profondeur
d’un DAG (graphe orienté acyclique). Un tel algorithme peut trouver application dans le cadre d’un logiciel
de dessin de circuit booléen, permettant ainsi de connaı̂tre la longueur du chemin critique et d’identifier de
tels chemins.
L’idée de l’algorithme proposé ici est de simplifier et modifier un algorithme classique : l’algorithme des
plus courts chemins dans un DAG pondéré.
Algorithme. On présente ici un algorithme linéaire qui calcule la profondeur d’un DAG, ainsi que sa
correction totale. Il repose notamment sur un tri topologique du graphe.
Démonstration. Étant donné un DAG G = (V, E), son tri topologique se réalise en temps O(|V | + |E|),
donc linéaire en la taille du graphe. La première boucle s’exécute en |V | opérations élémentaires. Ensuite,
en notant pour d(u) le degré entrant d’un sommet u ∈ V , l’exécution de la seconde boucle réalise
n
X
(d(vi ) − 1) + 1 = |E|
i=1
263
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Démonstration. On montre l’invariant de boucle suivant (pour la seconde boucle) : À la fin de chaque
itération, dist[vj ] (1 ≤ j ≤ i) contient la longueur du plus long chemin terminant en vj .
Initialisation À la première itération, on a dist[v1 ] = max ∅ = 0. En effet, puisque G est un DAG, v1 est
une source et n’a pas d’antécédent dans le graphe.
Conservation On suppose l’invariant vrai à l’itération i − 1, montrons qu’il l’est à l’itération i. Pour
1 ≤ j ≤ i − 1, par hypothèse, dist[vj ] est déjà la longueur du plus chemin terminant en vj . Montrons
qu’un plus long chemin terminant en vi est de longueur dist[vi ] = maxuvi ∈E (dist[u] + 1).
Soit µ un plus long chemin terminant en vi . Par tri topologique de G, on peut écrire µ = vi1 ...vik
avec k ≤ i la longueur du chemin et vik = vi . On remarque alors que µ0 = vi1 ...vik−1 est un plus long
chemin terminant en vik−1 par maximalité de µ. Par hypothèse, on a alors k − 1 = dist[vik−1 ]. Ainsi,
maxuvi ∈E (dist[u] + 1) ≥ dist[vik−1 ] + 1 = k. Par l’absurde, si dist[vi ] > k, il existe un arc uvi ∈ E
tel que dist[u] ≥ k. Par l’invariant, il existe un plus long chemin terminant en u de longueur ≥ k, et
on peut donc construire un chemin terminant en vi de longueur > k, ce qui contredit la maximalité
de µ.
Ainsi, en sortie de boucle, dist[u] contient la longueur des plus longs chemins terminant en u ∈ V .
Finalement, maxu∈V dist[u] est bien la profondeur du DAG.
Conclusion. Cet algorithme nous permet de calculer la profondeur du graphe. À partir du tableau obtenu,
on peut aussi retourner l’ensemble des chemins critiques. Pour cela, il suffit pour chaque sommet qui
maximise dist[u], de remonter dans le DAG par les ancêtres v de u tels que dist[v] = dist[u] − 1 et ainsi
de suite. Dans le cadre d’un logiciel, cette modification permettrait à l’utilisateur de colorier les chemins
critiques sur son circuit.
Validation croisée
Ce développement présente la méthode de la validation croisée, ainsi qu’un exemple où celle-ci échoue.
Cette méthode est particulièrement utilisée en apprentissage machine, illustrant ainsi la leçon 24. De plus,
cette méthode permet de tester un modèle sur des données de manière astucieuse, et illustre la leçon 3 si
ce genre de tests sont abordés.
Objectif. Pour valider un modèle, la méthode classique consiste à réserver une partie des données seule-
ment pour cette partie. Cependant, lorsque les données sont rares, on ne veut pas en gaspiller. La méthode
de validation croisée (k-fold) permet d’estimer la performance de notre modèle sans perdre trop de données.
Principe. Dans une validation croisée k-fold, on sépare notre jeu d’entraı̂nement (les 80% restant après
le retrait des données servant au test) en k sous-ensembles de taille n/k. Pour chaque sous-ensemble,
on entraı̂ne notre algorithme sur les données restantes et vérifie sur celui-ci. Enfin, on estime l’erreur en
prenant la moyenne de toutes les erreurs.
Remarque 24.1 On rappelle que l’erreur est une mesure définie avec le problème. Ainsi, étant donné un
ensemble de test S ∈ (X × Y)n , et une fonction h : X → Y, on notera l’erreur LS (h). Si Y est discret :
1
LS (h) = |{1 ≤ i ≤ n|h(xi ) 6= yi }|
n
et si Y est continue, on peut prendre :
n
1X
LS (h) = (h(xi ) − yi )2
n
i=1
Sélection de modèle. La validation croisée est principalement utilisée pour de la validation de modèle.
On a donc un algorithme A, et un ensemble de paramètres Θ. L’algorithme de sélection est alors l’algorithme
46 ci-dessous.
265
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
θ∗ ← argminθ [erreur(θ)] ;
retourner A(S; θ∗
Exemple. Si on considère l’algorithme des k voisins, on peut prendre Θ = {1, ..., n} où n est le nombre
de points. Ce choix n’est pas bon en pratique à cause de la complexité.
Considérons une implémentation des k voisins naı̈ve : on calcule toutes les distances en n opérations
élémentaires, et on cherche les K minimums avec un tableau auxiliaire de taille k. On récupère alors les
k minimums en parcourant les n − k sommets restants et en retirant le maximum du tableau. On obtient
une complexité au plus (n − k)k.
Ainsi, en notant CVC-kNN la complexité de la validation croisée appliquée à l’algorithme des k voisins,
on a :
Xn
CVC-kNN ≤ k i(n − i) + n = O(kn3 )
i=1
Il faut donc faire un choix raisonnable de Θ, puisqu’en général prendre trop de voisins mène à un
sous-apprentissage.
Efficacité. La validation croisée fonctionne bien en pratique. Mais cela peut échouer, comme le montre
le cas suivant, qui est exemple un peu artificiel.
On suppose qu’on dispose d’un jeu de données S ∈ (X × Y)n . Pour tout y ∈ Y, l’étiquette est choisi
uniformément et aléatoirement parmi {0, 1}. On considère un algorithme d’apprentissage A qui retourne
le classifieur constant h : x 7→ 1 si la parité des étiquettes sur l’ensemble d’entraı̂nement vaut 1, et le
classifieur nul sinon.
La différence entre l’erreur estimée par la validation croisée n-fold (FOO) et la vraie erreur
est toujours 1/2.
En effet, la vraie erreur est ES (LS (h)) = n/2 car h est constante égale à 1 ou 0 avec probabilité 1/2.
Soit S = (x1 , y1 ), ..., (xn , yn ) un jeu de test et h = A(S). On note Si = S\{(xi , yi )} et hi = A(Si ).
On note n1 (resp. n0 ) le nombre de 1 dans S, et sans perdre de généralité, on suppose n1 impair.
Soit 1 ≤ i ≤ n.
— Si yi = 1, alors Si contient un nombre pair de 1, et donc hi = 0. On a alors LSi (hi ) = 1.
— Si yi = 0, alors Si contient un nombre impair de 1, et donc hi = 1, et LSi (hi ) = 1.
On raisonne de même si n1 est pair. Finalement,
n
!
1X
ES LSi (hi ) =1
n
i=1
Introduction. On considère ici un exemple où utiliser de l’aléatoire permet de vérifier une égalité plus
rapidement que les algorithmes déterministes. L’idée est de vérifier si un algorithme réalise bien le produit
de matrice, sans pour autant utiliser un algorithme déterministe réalisant ce calcul.
On se concentrera ici sur des matrices d’entiers modulo 2, avec A et B deux matrices, et C la matrice
à tester.
Lemme 25.1 Choisir r = (r1 , r2 , ..., rn ) ∈ {0, 1}n aléatoirement et uniformément est équivalent à
choisir chaque ri uniformément et indépendamment dans {0, 1}.
Démonstration. Si chaque ri est tiré uniformément et indépendamment, chacun des 2n vecteurs de {0, 1}n
a une probabilité 2−n d’être tiré.
et de manière équivalente : Pn
j=2 d1j rj
r1 = − (25.1)
d11
Maintenant, on utilise le lemme précédent. Au lieu de considérer qu’on tire uniformément r dans {0, 1}n ,
on suppose que l’on tire chaque ri indépendamment et uniformément dans {0, 1}, de rn jusqu’à r1 . En
267
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
particulier, on peut supposer que l’on choisit (r2 , ..., rn ) uniformément dans {0, 1}n−1 et r1 uniformément
dans {0, 1}. Ces deux tirages sont réalisés de manière indépendante.
D’après la formule des probabilités totales :
X
P(ABr = Cr) = P(ABr = Cr ∩ (r2 , ..., rn ) = (x2 , ..., xn ))
(x2 ,...,xn )∈{0,1}n−1
Pn !
j=2 d1j rj
X
≤ P r1 = − ∩ (r2 , ..., rn ) = (x2 , ..., xn )
d11
(x2 ,...,xn )∈{0,1}n−1
Pn !
j=2 d1j rj
X
= P r1 = − .P ((r2 , ..., rn ) = (x2 , ..., xn ))
d11
(x2 ,...,xn )∈{0,1}n−1
X 1
= .P ((r2 , ..., rn ) = (x2 , ..., xn ))
2
(x2 ,...,xn )∈{0,1}n−1
1
=
2
Pour améliorer la probabilité d’erreur du théorème précédent, il suffit de faire tourner plusieurs tests.
Si l’algorithme trouve un r tel que ABr 6= Cr, alors l’algorithme retourne correctement AB 6= C. Si on
trouve ABr = Cr pour k tests, alors on retourne AB = C, ce qui est vrai avec une probabilité ≥ 1 − 2−k .
La complexité de ce test est alors en O(kn2 ).
Algorithme de Peterson
Introduction. Lorsqu’il y a une condition de concurrence, on veut que nos différents processus soient
en exclusion mutuelle, c’est-à-dire qu’aucun des processus ne rentre dans leur section critique en même
temps. Après une proposition non satisfaisante de Dekker (présentée par Dijkstra), Peterson a proposé une
méthode élégante pour résoudre ce problème avec deux processus.
#d e f i n e FALSE 0
#d e f i n e TRUE 1
#d e f i n e N 2 / ∗ nombre de p r o c e s s u s ∗ /
int turn ;
i n t i n t e r e s t e d [ N ] ; / ∗ à q u i l e t o u r ? ∗ /
/ ∗ on i n i t i a l i s e l e s v a l e u r s à 0 ( FALSE ) ∗ /
v o i d e n t e r r e g i o n ( i n t p r o c e s s ) / ∗ l e p r o c e s s u s e s t 0 ou 1 ∗ /
{
i n t o t h e r ; /∗ nombre de l ’ a u t r e p r o c e s s u s ∗/
o t h e r = 1 − p r o c e s s ; /∗ l ’ oppos é du p r o c e s s u s ∗/
i n t e r e s t e d [ p r o c e s s ] = TRUE ; /∗ on e s t i n t é r e s s é ∗/
t u r n = o t h e r ; /∗ on i n i t i a l i s e l e d r a p e a u ∗/
/∗ a t t e n t e ∗/
w h i l e ( t u r n == o t h e r && i n t e r e s t e d [ o t h e r ] == TRUE) ;
}
269
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Exclusion mutuelle.
Proposition 26.1 L’algorithme de Peterson vérifie la propriété d’exclusion mutuelle, c’est-à-dire deux
processus ne rentrent jamais dans leurs sections critiques en même temps.
Démonstration. On raisonne par l’absurde. Supposons que le processus 0 soit dans sa section critique, et le
processus 1 accède à la sienne. Ainsi, enter region(0) a terminé (le processus 0 a réussi à rentrer dans
sa section critique) et seulement après l’appel enter region(1) a terminé.
Code couleur :
turn=0 ou interested[1]=False (0 entre en section critique)
enter_region(0)
enter_region(1)
turn=1 ou interested[0]=False (1 entre en section critique)
2ème cas :
1er cas :
interested[0]=True interested[0]=True
turn=1 turn=1
turn=0 interested[1]=False
turn=0 interested[1]=True
Pas de modification de
turn ni de interested turn = 0 Pas de modification de
turn ni de interested
turn=1 ou interested[0]=False ABSURDE turn=1 ou interested[0]=False ABSURDE
Exclusion mutuelle.
Démonstration. On raisonne par l’absurde, si les deux appels sont bloqués dans la boucle while, on a à la
fois turn == 1 et turn == 0 .
Absence de famine.
Proposition 26.3 L’algorithme de Peterson assure l’absence de famine, c’est-à-dire que tout processus
qui demande à rentrer en section critique y accède en temps fini, à condition que chaque processus
reste un temps fini dans sa section critique.
Démonstration. Par l’absurde, si le processus 0 lance son appel enter region(0) et reste bloque indéfiniment
dans sa boucle while à partir de l’instant t0 . Donc pour tout temps t ≥ t0 , on a turn == 0 interested[1]
== TRUE. Par la deuxième contrainte, on sait que le processus 1 a appelé entre region(1). De plus, le
processus 1 ne reste pas bloqué, puisque sinon il y aurait interblocage. Ainsi, 1 entre en section critique
et en sort en temps fini, il appelle alors leave region(1). À la fin de cet appel, on a intersted[1] ==
FALSE. Il y a alors deux cas :
— si le processus 0 reprend la main, alors on a une absurdité puisque intersted[1] == FALSE ;
— si le processus 1 garde et appelle à nouveau enter region(1), alors il reste cette fois bloqué dans
le while puisqu’il est le dernier à avoir modifié turn. Ainsi, lorsque le processus 0 reprend la main,
il est débloqué, ce qui est absurde aussi.
Proposition 26.4 L’algorithme de Peterson assure une attente bornée, c’est-à-dire qu’un nombre
borné de processeur peuvent passer avant lui lorsqu’il demande à entrer dans sa section critique.
Ce développement présente un simple algorithme d’ordonnancement online qui s’avère être optimal
lorsqu’il s’agit de minimiser le temps de réponse maximal. Il s’insère alors naturellement dans la leçon
13 ainsi que dans toutes les leçons dans lesquelles l’ordonnanceur d’un système d’exploitation est abordé,
comme les leçons 14 et 17. Enfin, puisqu’il s’agit d’un algorithme glouton, il pourra illustrer la leçon 12.
Ordonnancement online. On considère ici une stratégie simple d’ordonnancement online sur un unique
processeur. Ainsi, le système reçoit les tâches au fur et à mesure et n’a pas connaissance de celles-ci à
l’avance. L’idée est alors simplement d’exécuter les tâches dans leur ordre d’arrivé et montrer un certain
résultat d’optimalité.
Remarque 27.1 La date d’exécution (et donc le temps de réponse aussi) dépend de la stratégie adoptée
par l’ordonnanceur.
T1 T2
d1 d2 f1 f2
T2 T1
d1 d2 f2 f1
Supposons que l’on veuille minimiser le temps de réponse maximal. On considère la stratégie Premier
Arrivé Premier Servi (PAPS) qui exécute chaque tâche dans leur ordre d’arrivé.
273
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Démonstration. Le but est d’appliquer une propriété d’échange. On considère un ordonnancement optimal
S, différent de la stratégie PAPS. Ainsi, il existe deux tâches Ti et Tj telles que :
— Ti est exécutée juste après Tj (donc fj < fi ) ;
— alors que Ti était disponible avant Tj (di < dj )
Regardons ainsi les temps de réponse dans S. On a Ri = fi − di > fj − dj = Rj .
Ri
Rj
S Tj Ti
di dj fj fi
Ri '
R j'
S’ Ti Tj
di dj fi' f j'
On remarque que les seuls temps de réponses changés sont ceux de Ti et Tj . Or, on a
0
Ri = fi0 − di < fi − di = Ri
Rj0 = fj0 − dj < fi − dj < fi − di = Ri
Ainsi, max(Ri0 , Rj0 ) < Ri ≤ max(Rj , Ri ). On en déduit alors que S 0 est optimal. En appliquant successi-
vement cette propriété d’échange, on peut alors se ramener à PAPS, montrant ainsi que cette stratégie est
optimale.
Pn
Remarque 27.2 Si on veut minimiser cette fois la somme des temps de réponses i=1 Ri , PAPS n’est
plus optimale.
Ce développement propose d’étudier des résultats de décidabilité et d’indicibilité autour des langages
rationnels, permettant d’illustrer de manière intéressante la hiérarchie de Chomsky. Il s’insère naturellement
dans la leçon 27 ainsi que dans la leçon 29 si les concepts de décidabilité sont abordés.
Introduction. On considère trois problèmes autours des langages rationnels. Deux d’entre eux sont
décidables, et l’autre est indécidable.
Démonstration. Un automate fini déterministe accepte au moins un mot si, et seulement si, il peut atteindre
un état final depuis l’état initial en suivant la fonction de transition.
275
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Remarque 28.1 On dit que L(C) est la différence symétrique de L(A) et L(B).
Montrons que L(A) = L(B) si, et seulement si, L(C) = ∅. Si L(A) = L(B) = X, alors L(C) =
(X ∩X)∪(X ∩X) = ∅∪∅ = ∅. Réciproquement, si L(C) = ∅, on a L(A)∩L(B) = ∅ et (L(A)∩L(B)) = ∅.
Soit x ∈ L(A), alors par l’absurde, si x ∈/ L(B), alors x ∈ L(B) et donc x ∈ L(A) ∩ L(B), ce qui est
absurde. Ainsi, on a L(A) ⊂ L(B) et de la même manière L(B) ⊂ L(A) ce qui conclut la preuve.
On peut alors construire C via A et B en utilisant les résultats de clôtures sur les langages rationnels.
En effet, ces constructions sont des algorithmes pouvant être implantées via des machines de Turing. On
en déduit la machine F qui décide EQAF D :
Démonstration. On procède par réduction depuis le problème de l’acceptation ATM . On suppose que RTM
est décidable par une machine de Turing R et on l’utilise pour construire une machine de Turing S qui
décide ATM .
Idée. S reçoit en entrée hM, wi où M est une machine de Turing et w un mot, et va modifier M en une
nouvelle machine M2 dont le langage est rationnel si, et seulement si M accepte w. Pour cela, M2 va
reconnaı̂tre automatique tous les mots de {0n 1n |n ≥ 0}, mais si en plus M accepte w, alors M2 accepte
tous les autres mots.
Construction de S.
Si M accepte w, alors on remarque que L(M2 ) = Σ∗ , car M2 accepte sur toute entrée x. Ainsi, L(M2 )
est rationnel, R accepte sur l’entrée hM2 i et finalement S accepte.
Si M n’accepte pas w, on remarque que L(M2 ) = {0n 1n |n ≥ 0} qui n’est pas rationnel. Ainsi R
rejette sur l’entrée hM2 i et donc S rejette.
Finalement, S décide ATM , et ainsi RTM est indécidable.
Remarque 28.2 Ce résultat nous montre qu’il n’est pas possible de décider si, lors de notre choix d’une
machine de Turing comme modèle de calcul, on aurait pu simplement choisir un automate.
Remarque 28.4 Le fait que {0n 1n [n ≥ 0} n’est pas rationnel découle du lemme de l’étoile, dont on
rappelle l’énoncé ci-dessous.
Lemme 28.1 (de l’étoile) Soit L un langage rationnel. Il existe un entier p tel que pour tout mot
w de L tel que |w| ≥ p possède une factorisation w = xyz vérifiant :
1. pour tout i ≥ 0, xy i z ∈ L ;
2. |y| > 0 ;
3. |xy| ≤ p.
Théorème de Rice
Ce développement présente le théorème de Rice avec quelques applications simples du théorème. Puisque
ce résultat permet de montrer l’indécidabilité de certains langages, il s’insère naturellement dans la leçon
27. De manière indirecte, ce théorème donne un résultat d’indécidabilité sur la correction des programmes
et peut donc illustrer la leçon 1.
De manière plus formelle, on a P ⊂ {L(M )|M est une machine de Turing}, et on définit LP le langage
des machines de Turing vérifiant P : LP ⊂ {< M > |L(M ) ∈ P }. P est non triviale s’il existe M1 et M2
avec < M1 >∈ LP et < M2 ∈ / LP .
Exemple 29.1 On peut prendre P la propriété : le langage est non vide , et alors LP = {< M >
|L(M ) 6= ∅}.
Mw = Sur l’entrée u :
1. Si M accepte w, simuler M0 sur u et retourner le résultat.
2. Sinon, rejeter.
On veut montrer :
w ∈ L(M ) ⇔< Mw >∈ LP
Si M accepte w, alors L(Mw ) = L(M0 ) et donc < Mw >∈ LP . Réciproquement, si M n’accepte pas
w. Il y a deux cas :
— Si M ne s’arrête pas sur w, alors M ne s’arrête jamais et donc L(M ) = ∅
— Si M rejette w, Mw rejette tous les mots u, d’où L(Mw ) = ∅
Ainsi, < Mw >∈ / LP puisque ∅ ∈
/ LP .
Or, la fonction f qui à < M, w > associe < Mw > est calculable, et donc L∈ se réduit à LP . Ainsi,
LP est indécidable.
279
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Attention, le théorème de Rice n’est pas directement applicable ici car la propriété ne porte pas sur les
langages récursivement énumérables (ici sur deux langages récursivement énumérables).
On montre la proposition suivante par réduction de L∅ à L6= .
Démonstration. On fixe une machine M∅ qui ignore son entrée et rejette directement. On a alors L(M∅ ) = ∅.
On pose g la fonction qui à < M > associe < M, M∅ > (qui est calculable), et on a
Codage de Huffman
Ce développement présente le codage de Huffman qui permet de compresser un texte. Il s’agit d’un
exemple classique d’algorithme glouton optimal sur les textes, illustrant ainsi les leçons 12 et 9. Cette
méthode servant à compresser un texte, ce développement s’insère aussi dans les leçons traitant du stockage
et de l’échange de données, notamment les leçons 18 et 21. Enfin, ce codage utilise les arbres binaires de
manière astucieuse, puisque chaque arbre binaire représente un code préfixe, illustrant ainsi la leçon 10. Il est
à noter que pour certaines de ces leçons, un développement similaire mais moins théorique sur l’algorithme
LZW pourrait être plus judicieux.
Définitions. On rappelle ici quelques définitions. On considère un alphabet Σ ayant au moins deux
caractères.
Définition 30.1 (Code binaire) On appelle code binaire toute fonction injective c : Σ → {0, 1}∗ .
L’ensemble c(Σ) est appelé ensemble des mots de code. On peut étendre c à Σ∗ par concaténation.
Un code est dit préfixe si aucun mot de code n’est préfixe d’un l’autre.
Proposition 30.1 L’opération de décodage a une unique solution pour un code préfixe.
On donne ici seulement une justification de la proposition ci-dessus. En effet, en lisant un mot w ∈
{0, 1}∗ , si on trouve un mot de code, alors c’est le seul possible puisqu’il n’est préfixe d’aucun autre.
Code préfixe et arbre binaire. Tout code binaire préfixe peut être représenté par un arbre binaire dont
les feuilles sont les lettres de l’alphabet Σ. Un 0 est associé à chaque branche gauche et un 1 à chaque
branche droite, et pour obtenir le mot de code associé à une lettre il suffit de considérer le chemin de la
racine à la feuille associée. Par exemple, si l’on considère le code c sur Σ = {α, β, γ} défini par c(α) = 0,
c(β) = 10 et c(γ) = 11, l’arbre correspondant est le suivant :
0 1
α
0 1
β γ
281
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
Codage binaire optimal. On considère un texte w où chaque lettre a ∈ Σ apparaı̂t avec une fréquence
f (a). Étant donné un code préfixe, représenté par un arbre T , on associe un coût
X
B(T ) = f (a) × lT (a)
a∈Σ
où lT (a) est la taille du mot de code de a. Si f (a) est exactement le nombre d’occurrences de a dans le
texte w, alors B(T ) est exactement le nombre de bits utilisés pour encoder le texte. Un code préfixe T est
dit optimal pour un texte si, pour ce texte, B(T ) est minimum.
Lemme 30.1 Pour tout code binaire préfixe optimal, il existe un arbre binaire correspondant ayant
|Σ| feuilles et |Σ| − 1 noeuds internes.
Démonstration. Un code préfixe optimal peut être représenté par un arbre binaire où chaque noeud interne
a deux enfants. En effet, si un tel code est représenté par un arbre où un nœud interne n’a qu’un seul
enfant, alors on peut supprimer ce noeud et remonter l’enfant. On obtient alors un arbre T 0 représentant
un code préfixe avec B(T 0 ) < B(T ), ce qui est absurde.
Un tel arbre possède |Σ| feuilles car il code |Σ| lettres. Si on prend un point de vue de théorie des
graphes, chaque noeud interne possède un degré 3 sauf la racine qui est de degré 2. En notant N le nombre
de noeud interne et F le nombre de feuilles, on a :
2(N + F − 1) = 3(N − 1) + 2 + F
Lemme 30.2 Il existe un code préfixe optimal tel que les deux lettres de plus basses fréquences sont
jumelles dans l’arbre (leurs mots de code ont la même taille mais ne diffèrent que par le dernier bit).
Démonstration. On considère un code préfixe optimal T et on considère deux lettres de plus basses
fréquences x et y. Soient a et b deux lettres qui sont jumelles dans T et à la profondeur maximale
dans T . On échange x avec a et y avec b, on obtient un nouvel arbre T 0 . Pour montrer que T 0 est optimal,
on montre que B(T 0 ) ≤ B(T ). On a
B(T ) − B(T 0 ) = f (a)(lT (a) − lT 0 (a)) + f (b)(lT (b) − lT 0 (b)) + f (x)(lT (x) − lT 0 (x)) + f (y)(lT (y) − lT 0 (y))
Or, on a lT 0 (a) = lT (x) et lT 0 (b) = lT (y), lT (a) = lT (b) et f (a), f (b) ≤ f (x), f (y), et sans perdre de
généralité on peut supposer f (a) ≤ f (b). Ainsi,
B(T ) − B(T 0 ) ≥ f (a) [lT (a) − lT (x) + lT (a) − lT (y)) + lT (x) − lT (a) + lT (y) − lT (a)] ≥ 0
Ainsi, T 0 correspond à un code préfixe où les deux lettres de plus basses fréquences sont jumelles à une
profondeur maximale dans l’arbre.
Lemme 30.3 Étant donné deux lettres x et y de plus basses fréquences, on considère Σ0 = Σ\{x, y}∪
{z} où z est une nouvelle lettre de fréquence f (z) = f (x) + f (y).
Si T 0 est un code optimal pour Σ0 , alors l’arbre T obtenu en remplaçant z par un noeud interne
et deux feuilles x et y est optimal pour Σ.
Démonstration. Soit T 00 un arbre optimal pour Σ, d’après le lemme précédent on peut considérer que x et
y sont jumelles à profondeur maximale dans T 00 . En remplaçant x et y ainsi que leur parent commun par
une unique feuille z, on obtient un code préfixe T 000 pour Σ0 .
Or, en notant l = lT 00 (x) = lT 00 (y),
De manière similaire, on a B(T 0 ) = B(T ) − f (x) − f (y). Or, T 0 est optimal pour Σ0 , d’où B(T 0 ) ≤
B(T 000 ) et donc B(T ) ≤ B(T 00 ). Ainsi, T est optimal pour Σ.
Construction du codage de Huffman. On donne ici un algorithme qui, étant donné un ensemble de
fréquence par lettre, retourne un code optimal appelé codage de Huffman. Elle utilise une file de priorité
et qui construit un arbre.
La complexité d’une itération de l’algorithme est un O(log(n)), et donc l’arbre est construit en
O(n log(n)).
Exemple. En prenant un texte sur Σ = {a, b, c, d, e} où f (a) = 18, f (b) = 15, f (c) = 10, f (d) = 8 et
f (e) = 5 et en appliquant l’algorithme, on obtient l’arbre suivant :
55
0 1
33 23
0 1 0 1
a, 18 b, 15 c, 10 13
0 1
d, 8 e, 5
[Albert, 1998] Albert, L. (1998). Cours et exercices d’informatique : classes préparatoires, 1er et 2nd cycles
universitaires. Passeport pour l’informatique. Vuibert.
[Arora and Barak, 2006] Arora, S. and Barak, B. (2006). Computational Complexity : A Modern Approach.
Cambridge University Press.
[Beauquier et al., 2005] Beauquier, D., Berstel, J., and Chrétienne, P. (2005). Éléments d’algorithmique.
Masson, Version.
[Belghiti et al., 2016] Belghiti, I., Mansuy, R., and Vie, J. (2016). Les clefs pour l’info. Im-et-Ker. Calvage
et Mounet.
[Benoit et al., 2013] Benoit, A., Robert, Y., and Vivien, F. (2013). A Guide to Algorithm Design : Para-
digms, Methods, and Complexity Analysis. Applied Algorithms and Data Structures series. Chapman &
Hall/CRC.
[Biernat et al., 2015] Biernat, E., Lutz, M., and LeCun, Y. (2015). Data science : fondamentaux et études
de cas : Machine learning avec Python et R. Eyrolles.
[Carton and Perrin, 2008] Carton, O. and Perrin, D. (2008). Langages formels, calculabilité et complexité.
Vuibert.
[Casanova et al., 2008] Casanova, H., Legrand, A., and Robert, Y. (2008). Parallel Algorithms. CRC Press.
[Cormen et al., 2009] Cormen, T. H., Leiserson, C. E., Rivest, R. L., and Stein, C. (2009). Introduction
to Algorithms, Third Edition. The MIT Press, 3rd edition.
[Crochemore and Rytter, 1994] Crochemore, M. and Rytter, W. (1994). Text algorithms. Maxime Cro-
chemore.
[Dasgupta et al., 2008] Dasgupta, S., Papadimitriou, C. H., and Vazirani, U. V. (2008). Algorithms.
McGraw-Hill.
[Dumas et al., 2007a] Dumas, J.-G., Roch, J.-L., Tannier, E., and Varrette, S. (2007a). Théorie des Codes :
compression, cryptage, correction. Sciences Sup. Dunod.
[Dumas et al., 2007b] Dumas, J.-G., Roch, J.-L., Tannier, E., and Varrette, S. (2007b). Théorie des Codes :
compression, cryptage, correction. Sciences Sup. Dunod.
[Gaudel et al., 1990] Gaudel, M.-C., Soria, M., and Froidevaux, C. (1990). Types de données et algo-
rithmes. Ediscience.
[Goodrich and Tamassia, 2014] Goodrich, M. T. and Tamassia, R. (2014). Algorithm Design and Appli-
cations. Wiley.
[Gusfield et al., 1997] Gusfield, D., Press, C. U., and Fund, J. D. . P. S. H. L. (1997). Algorithms on Strings,
Trees, and Sequences : Computer Science and Computational Biology. EBL-Schweitzer. Cambridge
University Press.
[Hennessy and Patterson, 2012] Hennessy, J. L. and Patterson, D. A. (2012). Computer Architecture : A
Quantitative Approach. Morgan Kaufmann, Amsterdam, 5 edition.
[Legendre, 2015] Legendre, Romain et Schwarzentruber, F. (2015). Compilation : Analyse lexicale et
syntaxique.
[Lesesvre et al., 2020] Lesesvre, D., Barbenchon, P., Montagnon, P., and Pierron, T. (2020). 131
développements pour l’oral : agrégation externe mathématiques-informatique. Je prépare. Dunod.
285
Leçons d’Informatique Préparation à l’Agrégation d’Informatique 2022
[Mitzenmacher and Upfal, 2005] Mitzenmacher, M. and Upfal, E. (2005). Probability and Computing :
Randomized Algorithms and Probabilistic Analysis. Cambridge University Press, USA.
[Motwani and Raghavan, 1995] Motwani, R. and Raghavan, P. (1995). Randomized Algorithms. Cam-
bridge University Press.
[Myers et al., 2012] Myers, G. J., Sandler, C., and Badgett, T. (2012). The art of software testing. John
Wiley & Sons, Hoboken and N.J, 3rd ed edition.
[Narbel, 2005] Narbel, P. (2005). Programmation fonctionnelle, générique et objet : une introduction avec
le langage OCaml. En pratique. Série Langages-programmation. Vuibert.
[Nielson and Nielson, 2007] Nielson, H. R. and Nielson, F. (2007). Semantics with Applications : An
Appetizer. Undergraduate Topics in Computer Science. Springer.
[Papadimitriou, 1994] Papadimitriou, C. H. (1994). Computational complexity. Addison-Wesley.
[Patterson and Hennessy, 2017] Patterson, D. and Hennessy, J. (2017). Computer Organization and De-
sign RISC-V Edition : The Hardware Software Interface. The Morgan Kaufmann Series in Computer
Architecture and Design. Elsevier Science.
[Perifel, 2014] Perifel, S. (2014). Complexité algorithmique. Références sciences. Ellipses.
[Pinchinat et al., 2022] Pinchinat, S., Schwarzentruber, F., and Barbenchon, P. (2022). Logique : fonde-
ments et applications. Dunod.
[Shalev-Shwartz and Ben-David, 2014] Shalev-Shwartz, S. and Ben-David, S. (2014). Understanding Ma-
chine Learning - From Theory to Algorithms. Cambridge University Press.
[Silberschatz et al., 2020] Silberschatz, A., Korth, H. F., and Sudarshan, S. (2020). Database System
Concepts, Seventh Edition. McGraw-Hill Book Company.
[Sipser, 2013] Sipser, M. (2013). Introduction to the Theory of Computation. Course Technology, Boston,
MA, third edition.
[Stallings, 2003] Stallings, W. (2003). Computer Organization and Architecture : Designing for Perfor-
mance. Pearson Education.
[Tanenbaum, 1976] Tanenbaum, A. (1976). Structured Computer Organization. Prentice-Hall series in
automatic computation. Prentice-Hall.
[Tanenbaum and Bos, 2014] Tanenbaum, A. S. and Bos, H. (2014). Modern Operating Systems. Pearson,
Boston, MA, 4 edition.
[Tanenbaum and Wetherall, 2011] Tanenbaum, A. S. and Wetherall, D. (2011). Computer Networks. Pren-
tice Hall, Boston, 5 edition.