0% ont trouvé ce document utile (0 vote)
40 vues166 pages

Notes

Ce document contient des notes de cours pour le cours IFT436 – Algorithmes et structures de données à l'Université de Sherbrooke, fournissant des informations sur divers concepts mathématiques et algorithmiques. Il aborde des sujets tels que l'analyse des algorithmes, les techniques de tri, les graphes, et les paradigmes algorithmiques. Les notes incluent également des exercices et des études de cas pour approfondir la compréhension des thèmes présentés.

Transféré par

issakaibrahim368
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd
0% ont trouvé ce document utile (0 vote)
40 vues166 pages

Notes

Ce document contient des notes de cours pour le cours IFT436 – Algorithmes et structures de données à l'Université de Sherbrooke, fournissant des informations sur divers concepts mathématiques et algorithmiques. Il aborde des sujets tels que l'analyse des algorithmes, les techniques de tri, les graphes, et les paradigmes algorithmiques. Les notes incluent également des exercices et des études de cas pour approfondir la compréhension des thèmes présentés.

Transféré par

issakaibrahim368
Copyright
© © All Rights Reserved
Nous prenons très au sérieux les droits relatifs au contenu. Si vous pensez qu’il s’agit de votre contenu, signalez une atteinte au droit d’auteur ici.
Formats disponibles
Téléchargez aux formats PDF, TXT ou lisez en ligne sur Scribd

IFT436 – Algorithmes et structures de

données

notes de cours

Michael Blondin

2 décembre 2019
Ce document

Ce document sert de notes complémentaires au cours IFT436 – Algorithmes et


structures de données de l’Université de Sherbrooke.

Si vous trouvez des coquilles ou des erreurs dans le document, veuillez s.v.p.
me les indiquer par courriel à [Link]@[Link].
Légende

Observation.

Les passages compris dans une région comme celle-ci correspondent à


des observations jugées intéressantes mais qui dérogent légèrement du
contenu principal.

Remarque.

Les passages compris dans une région comme celle-ci correspondent à des
remarques jugées intéressantes mais qui dérogent légèrement du contenu
principal.

Les passages compris dans une région rectangulaire colorée sans bordure
comme celle-ci correspondent à du contenu qui ne sera pas présenté en classe,
mais qui peut aider à une compréhension plus approfondie. La plupart de ces
passages contiennent des preuves ou des propositions techniques.

Les exercices marqués par « ⋆ » sont considérés plus avancés que les autres.
Table des matières

I Fondements 1

0 Rappel de notions mathématiques 2


0.1 Notation et objets élémentaires . . . . . . . . . . . . . . . . . . 2
0.1.1 Ensembles . . . . . . . . . . . . . . . . . . . . . . . . . . 2
0.1.2 Nombres . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
0.1.3 Séquences . . . . . . . . . . . . . . . . . . . . . . . . . . 4
0.1.4 Relations . . . . . . . . . . . . . . . . . . . . . . . . . . 4
0.1.5 Combinatoire . . . . . . . . . . . . . . . . . . . . . . . . 4
0.2 Techniques de preuve . . . . . . . . . . . . . . . . . . . . . . . . 5
0.2.1 Preuves directes . . . . . . . . . . . . . . . . . . . . . . 5
0.2.2 Preuves par contradiction . . . . . . . . . . . . . . . . . 5
0.2.3 Preuves par induction . . . . . . . . . . . . . . . . . . . 6
0.3 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12

1 Analyse des algorithmes 13


1.1 Temps d’exécution . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.1.1 Temps en fonction d’un paramètre . . . . . . . . . . . . 13
1.1.2 Temps en fonction de plusieurs paramètres . . . . . . . 14
1.2 Notation asymptotique . . . . . . . . . . . . . . . . . . . . . . . 15
1.2.1 Notation O . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.2.2 Notation Ω . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.2.3 Notation Θ . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.3 Étude de cas: vote à majorité absolue . . . . . . . . . . . . . . 21
1.4 Simplification du décompte des opérations . . . . . . . . . . . . 24
1.5 Règle de la limite . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.6 Notation asymptotique à plusieurs paramètres . . . . . . . . . . 26
1.7 Correction et terminaison . . . . . . . . . . . . . . . . . . . . . 27
1.7.1 Correction . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.7.2 Terminaison . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.8 Étude de cas: vote à majorité absolue (suite) . . . . . . . . . . 28

i
TABLE DES MATIÈRES ii

1.9 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

2 Tri 34
2.1 Approche générique . . . . . . . . . . . . . . . . . . . . . . . . 34
2.2 Tri par insertion . . . . . . . . . . . . . . . . . . . . . . . . . . 38
2.3 Tri par monceau . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.4 Tri par fusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.5 Tri rapide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.6 Propriétés intéressantes . . . . . . . . . . . . . . . . . . . . . . 43
2.7 Tri sans comparaison . . . . . . . . . . . . . . . . . . . . . . . . 43
2.8 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45

3 Graphes 46
3.1 Graphes non dirigés . . . . . . . . . . . . . . . . . . . . . . . . 46
3.2 Graphes dirigés . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
3.3 Chemins et cycles . . . . . . . . . . . . . . . . . . . . . . . . . . 48
3.4 Sous-graphes et connexité . . . . . . . . . . . . . . . . . . . . . 48
3.5 Représentation . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
3.5.1 Matrice d’adjacence . . . . . . . . . . . . . . . . . . . . 49
3.5.2 Liste d’adjacence . . . . . . . . . . . . . . . . . . . . . . 49
3.5.3 Complexité des représentations . . . . . . . . . . . . . . 50
3.6 Accessibilité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
3.6.1 Parcours en profondeur . . . . . . . . . . . . . . . . . . 51
3.6.2 Parcours en largeur . . . . . . . . . . . . . . . . . . . . . 51
3.7 Calcul de plus court chemin . . . . . . . . . . . . . . . . . . . . 52
3.8 Ordre topologique et détection de cycle . . . . . . . . . . . . . 54
3.9 Arbres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
3.10 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57

II Paradigmes 58

4 Algorithmes gloutons 59
4.1 Arbres couvrants minimaux . . . . . . . . . . . . . . . . . . . . 59
4.1.1 Algorithme de Prim–Jarník . . . . . . . . . . . . . . . . 59
4.1.2 Algorithme de Kruskal . . . . . . . . . . . . . . . . . . . 61
4.2 Approche générique . . . . . . . . . . . . . . . . . . . . . . . . 65
4.3 Problème du sac à dos . . . . . . . . . . . . . . . . . . . . . . . 66
4.3.1 Approche gloutonne . . . . . . . . . . . . . . . . . . . . 67
4.3.2 Variante fractionnelle . . . . . . . . . . . . . . . . . . . 68
4.3.3 Approximation . . . . . . . . . . . . . . . . . . . . . . . 69
4.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72

5 Algorithmes récursifs et
approche diviser-pour-régner 73
5.1 Tours de Hanoï . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
TABLE DES MATIÈRES iii

5.2 Récurrences linéaires . . . . . . . . . . . . . . . . . . . . . . . . 76


5.2.1 Cas homogène . . . . . . . . . . . . . . . . . . . . . . . 76
5.2.2 Cas non homogène . . . . . . . . . . . . . . . . . . . . . 79
5.3 Exponentiation rapide . . . . . . . . . . . . . . . . . . . . . . . 79
5.4 Multplication rapide . . . . . . . . . . . . . . . . . . . . . . . . 81
5.5 Théorème maître . . . . . . . . . . . . . . . . . . . . . . . . . . 83
5.6 Problème de la ligne d’horizon . . . . . . . . . . . . . . . . . . 85
5.7 Racines multiples et changement de domaine . . . . . . . . . . 87
5.8 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90

6 Force brute 94
6.1 Problème des n dames . . . . . . . . . . . . . . . . . . . . . . . 94
6.2 Problème du sac à dos . . . . . . . . . . . . . . . . . . . . . . . 97
6.3 Problème du retour de monnaie . . . . . . . . . . . . . . . . . . 98
6.4 Satisfaction de formules de logique propositionnelle . . . . . . . 100
6.5 Programmation linéaire entière . . . . . . . . . . . . . . . . . . 101
6.6 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102

7 Programmation dynamique 103


7.1 Approche descendante . . . . . . . . . . . . . . . . . . . . . . . 103
7.2 Approche ascendante . . . . . . . . . . . . . . . . . . . . . . . . 104
7.2.1 Problème du retour de monnaie . . . . . . . . . . . . . . 104
7.2.2 Problème du sac à dos . . . . . . . . . . . . . . . . . . . 106
7.3 Plus courts chemins . . . . . . . . . . . . . . . . . . . . . . . . 107
7.3.1 Algorithme de Dijkstra . . . . . . . . . . . . . . . . . . 108
7.3.2 Algorithme de Floyd-Warshall . . . . . . . . . . . . . . 110
7.3.3 Algorithme de Bellman-Ford . . . . . . . . . . . . . . . 114
7.3.4 Sommaire . . . . . . . . . . . . . . . . . . . . . . . . . . 116
7.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117

8 Algorithmes et analyse probabilistes 119


8.1 Nombres aléatoires . . . . . . . . . . . . . . . . . . . . . . . . . 119
8.2 Paradigmes probabilistes . . . . . . . . . . . . . . . . . . . . . . 121
8.2.1 Algorithmes de Las Vegas et temps espéré . . . . . . . . 122
8.2.2 Algorithmes de Monte Carlo et probabilité d’erreur . . . 123
8.3 Coupe minimum: algorithme de Karger . . . . . . . . . . . . . 123
8.4 Amplification de probabilité . . . . . . . . . . . . . . . . . . . . 126
8.5 Temps moyen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
8.6 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129

A Solutions des exercices 131

B Fiches récapitulatives 153

Bibliographie 157

Index 158
Fondements

1
0
Rappel de notions mathématiques

Dans ce chapitre, nous rappelons certaines notions élémentaires de logique et


de mathématiques discrètes utiles à la conception et à l’analyse d’algorithmes.

0.1 Notation et objets élémentaires

0.1.1 Ensembles
Rappelons qu’un ensemble est une collection (finie ou infinie) d’éléments non
ordonnés et sans répétitions. Par exemple, {2, 3, 7, 11, 13} est l’ensemble des
cinq premiers nombres premiers, ∅ est l’ensemble vide, {aa, ab, ba, bb} est l’en-
semble des mots de taille deux formés des lettres a et b, {0, 2, 4, 6, 8, . . .} est
l’ensemble des nombres pairs non négatifs.
Nous utiliserons souvent des définitions en compréhension. Par exemple,
l’ensemble des nombres pairs non négatifs peut être écrit de la façon suivante:

{2n : n ∈ N}.

La taille d’un ensemble fini X est dénoté par |X|, par ex. |{a, b, c}| = 3 et
|∅| = 0. Nous écrivons x ∈ X et x ̸∈ X afin de dénoter que x appartient et
n’appartient pas à X, respectivement. Nous écrivons X ⊆ Y afin de dénoter
que tous les éléments de X appartiennent à Y , et nous écrivons X ⊂ Y lorsque
X ⊆ Y et X ̸= Y . Lorsque X ⊆ E où E est considéré comme un univers, nous
écrivons X afin de dénoter le complément
déf
X = {e ∈ E : e ̸∈ X}.

Rappelons que ∪, ∩, \ et × dénotent l’union, intersection, la différence et

2
CHAPITRE 0. RAPPEL DE NOTIONS MATHÉMATIQUES 3

le produit cartésien:
déf
X ∪ Y = {x : x ∈ X ∨ x ∈ Y },
déf
X ∩ Y = {x : x ∈ X ∧ x ∈ Y },
déf
X \ Y = {x : x ∈ X ∧ y ̸∈ Y },
déf
X × Y = {(x, y) : x ∈ X, y ∈ Y }.

L’ensemble des sous-ensembles de X est l’ensemble:


déf
P(X) = {Y : Y ⊆ X}.

Par exemple, P({a, b}) = {∅, {a}, {b}, {a, b}} et P(∅) = {∅}.

0.1.2 Nombres
Ensembles de nombres. Nous utiliserons certains ensembles standards de
nombres dont les nombres naturels, entiers, rationnels et réels:

N = {0, 1, 2, . . .},
Z = N ∪ {−n : n ∈ N},
Q = {a/b : a, b ∈ Z, b ̸= 0},

R = Q ∪ {π, 2, . . .}.

Nous restreindrons parfois ces ensembles, par ex. R>0 dénote l’ensemble des
déf
nombres réels positifs. L’intervalle des entiers de a à b est dénoté [a, b] = {a,
déf
a + 1, . . . , b}. Le cas particulier où a = 1 s’écrit [b] = [1, b].

Logarithmes et exponentielles. Le logarithme en base b ∈ N≥2 est la


fonction logb : R>0 → R telle que blogb (x) = x; autrement dit, l’inverse de l’ex-
ponentielle. Lorsque b = 2, nous écrivons simplement log sans indice. Rappelons
quelques propriétés des logarithmes et des exponentielles. Pour tous x, y ∈ R>0
et a, b ∈ N≥2 , nous avons:

logb (xy) = logb (x) + logb (y), logb (x/y) = logb (x) − logb (y),
logb (x)
logb (xy ) = y · logb (x), loga (x) = ,
logb (a)
logb (1) = 0, x < y =⇒ logb (x) < logb (y).

Pour tout b ∈ N et tous x, y ∈ R≥0 , nous avons:

bx
bx · by = bx+y , = bx−y , (bx )y = bx·y .
by
CHAPITRE 0. RAPPEL DE NOTIONS MATHÉMATIQUES 4

Théorie des nombres. Rappelons que pour tout n ∈ N et d ∈ N>0 , la


division entière de n par d est dénotée par n ÷ d. Le reste de cette division est
déf
défini par n mod d = n−(n÷d)·d. Par exemple, 39 mod 2 = 1 et 39 mod 5 = 4.

0.1.3 Séquences
Une séquence est une suite d’éléments. Contrairement aux ensembles, les élé-
ments d’une séquence sont ordonnés (par un indice) et peuvent se répéter.
Nous décrivons les séquences à l’aide de crochets (plutôt que d’accolades pour
les ensembles). Par exemple, s = [k, a, y, a, k] représente la chaîne de caractères
« kayak » et f = [1, 1, 2, 3, 5, 8, . . .] est la séquence de Fibonacci. Nous dénotons
le ième élément d’une séquence t par t[i] (en débutant par 1), par ex. s[1] = k
et f [3] = 2. La taille d’une séquence finie t est dénotée par |t|, par ex. |s| = 5.
Nous écrivons s[i : j] afin de dénoter la sous-séquence:
déf
s[i : j] = [s[i], s[i + 1], . . . , s[j]].
déf
Par convention, s[i : j] = [ ] lorsque i > j. Nous écrivons s + t afin de dénoter
la séquence obtenue en ajoutant t à la suite de s.

0.1.4 Relations
Une relation (binaire) de deux ensembles X et Y est un ensemble R ⊆ X × Y .
Chaque paire (x, y) ∈ R indique que x est en relation avec y. Afin d’alléger
la notation, nous écrivons R(x, y) afin de dénoter que (x, y) ∈ R. Nous disons
qu’une relation R ⊆ X × Y est:
— surjective si ∀y ∈ Y ∃x ∈ X R(x, y);
— injective si ∀x, x′ ∈ X ∀y ∈ Y [R(x, y) ∧ R(x′ , y)] → (x = x′ );
— bijective si elle est surjective et injective.
Nous disons qu’une relation R ⊆ X × X est:
— réflexive si ∀x ∈ X R(x, x);
— symmétrique si ∀x, y ∈ X R(x, y) → R(y, x);
— transitive si ∀x, y, z ∈ X [R(x, y) ∧ R(y, z)] → R(x, z);
— une relation d’équivalence si elle est réflexive, symmétrique et transitive.
Une fonction est une relation f ⊆ X × Y qui met chaque élément de X en
relation avec exactement un élément de Y ; autrement dit, qui satisfait ∀x ∈
X ∃y ∈ Y f (x, y) et ∀x ∈ X ∀y, y ′ ∈ Y [f (x, y) ∧ f (x, y ′ )] → (y = y ′ ). Afin
d’alléger la notation, nous écrivons f (x) = y afin de dénoter que f (x, y).

0.1.5 Combinatoire
déf déf
La factorielle d’un entier n ∈ N est définie par n! = 1 · 2 · · · n avec 0! = 1
par convention. Par exemple, 5! = 120 et 6! = 720. Il y a n! séquences façons
CHAPITRE 0. RAPPEL DE NOTIONS MATHÉMATIQUES 5

d’ordonner n objets distincts. Par exemple, il y a 3! = 6 façons de permuter la


séquence [a, b, c]:

[a, b, c], [a, c, b], [b, a, c], [b, c, a], [c, a, b], [c, b, a].

Le coefficient binomial, lu « k parmi n », est défini par:


( )
n déf n!
= pour tous n, k ∈ N tels que n ≥ k.
k k!(n − k)!
( ) déf
Par convention, nk = 0 lorsque k > n ou k < 0.
Le coefficient binomial donne le nombre de façons de choisir k éléments
()
parmi n éléments, sans tenir compte de leur ordre. Par exemple, il y a 42 = 6
façons de choisir deux éléments parmi l’ensemble { , , , }:

{ , }, { , }, { , }, { , }, { , }, { , }.

0.2 Techniques de preuve

0.2.1 Preuves directes


La technique de preuve probablement la plus simple consiste à prouver un
énoncé directement à partir de définitions et de propositions déjà connues. Par
exemple, démontrons la proposition suivante à l’aide d’une preuve directe:

Proposition 1. Pour tous m, n ∈ N, si mn est impair, alors m et n sont


impairs.

Démonstration. Soient m, n ∈ N tels que mn est impair. Il existe a, b ∈ N et


r, s ∈ {0, 1} tels que m = 2a + r et n = 2b + s. Nous avons:

(2a + r)(2b + s) = 4ac + 2as + 2br + rs


= 2(2ac + as + br) + rs

Puisque mn est impair, nous avons rs = 1, et donc r = s = 1 car r, s ∈ {0, 1}.


Par conséquent, m = 2a + 1 et n = 2b + 1, et ainsi m et n sont impairs.

0.2.2 Preuves par contradiction


L’une des techniques de preuve les plus utilisées consiste à supposer que l’énoncé
dont nous cherchons à prouver la véracité est faux, puis en dériver une contra-
diction afin de conclure que l’énoncé était finalement vrai. Voyons un exemple
d’une telle preuve:

Proposition 2. Soit s une séquence de n éléments provenant de R. Il existe


∑n ou égal à la moyenne de s; autrement dit, il existe i ∈ [n]
un élément inférieur
tel que s[i] ≤ n1 j=1 s[j].
CHAPITRE 0. RAPPEL DE NOTIONS MATHÉMATIQUES 6

Démonstration. Dans le but d’obtenir une


∑n contradiction, supposons qu’il n’existe
aucun tel i. Nous avons donc s[i] > n1 j=1 s[j] pour tout i ∈ [n]. Ainsi:
 
∑n ∑n
1 ∑n
s[i] >  s[j] (par hypothèse)
i=1 i=1
n j=1

1 ∑∑
n n
= s[j]
n i=1 j=1

1 ∑n
= ·n· s[j] (car aucun terme ne dépend de i)
n j=1

n
= s[j].
j=1

Nous en concluons que la somme des éléments de s est strictement supérieure


à elle-même, ce qui est une contradiction. Cela conclut la preuve.

0.2.3 Preuves par induction


Rappelons la technique de preuve par induction. Soit φ : N → {faux, vrai}
un prédicat. Afin de démontrer que φ(n) est vrai pour tout n ≥ b, il suffit de
démontrer que:
— φ(b) est vrai;
— φ(n) =⇒ φ(n + 1) est vrai pour tout n ≥ b.
Ces deux étapes se nomment respectivement cas de base et étape d’induction.
Voyons quelques exemples de preuves par induction.

Carré d’un nombre. En inspectant quelques valeurs, la somme des n pre-


miers nombres impairs semble égaler n2 :

1 = 1 = 12 ,
1+3 = 4 = 22 ,
1+3+5 = 9 = 32 ,
1 + 3 + 5 + 7 = 16 = 42 .

Cherchons à démontrer que cela est vrai pour tout n ∈ N≥1 . Considérons le cas
où n = 4. Nous pouvons visualiser graphiquement que 1 + 3 + 5 + 7 = 42 :

En ajoutant 9 cases aux carré précédent, nous obtenons un carré de 52 cases:


CHAPITRE 0. RAPPEL DE NOTIONS MATHÉMATIQUES 7

Cela nous apprend que 1 + 3 + 5 + 7 + 9 = 52 et ainsi que la conjecture est vraie


pour n = 5. Cependant, nous n’apprenons rien sur n = 6. En ajoutant cette fois
11 cases, nous obtiendrions un carré de 62 cases. En continuant de procéder de
cette manière, nous pourrions couvrir toutes les valeurs de n. Malheureusement,
il y en a une infinité et nous ne terminerions jamais. Cependant, ce processus
se généralise de façon plus abstraite: en ajoutant 2n + 1 cases à un carré de n2
cases, nous obtenons un carré de (n + 1)2 cases:

n+1
n

n n+1

Ainsi, il nous suffit de démontrer que:


— Notre conjecture est vraie pour le premier cas, c-à-d. n = 1 (cas de base);
— Si notre conjecture est vraie pour les n premiers nombres impairs, alors
est vraie pour les n + 1 premiers nombres impairs (étape d’induction).
Autrement dit, nous pouvons démontrer notre conjecture par induction:

Proposition 3.∑La somme des n premiers nombres impairs est égale à n2 .


n
Autrement dit, i=1 (2i − 1) = n2 pour tout n ∈ N≥1 .
déf ∑n
Démonstration. Posons sn = i=1 (2i − 1) pour tout n ∈ N≥1 . Démontrons
que sn = n2 par induction sur n.
Case de base (n = 1). Nous avons sn = s1 = 2 · 1 − 1 = 1 = 12 = n2 .
CHAPITRE 0. RAPPEL DE NOTIONS MATHÉMATIQUES 8

Étape d’induction. Soit n ≥ 1. Supposons que sn = n2 . Nous avons:


n+1
sn+1 = (2i − 1) (par définition de sn+1 )
i=1
∑n
= (2i − 1) + (2(n + 1) − 1)
i=1
∑n
= (2i − 1) + (2n + 1)
i=1
= sn + (2n + 1) (par définition de sn )
2
= n + (2n + 1) (par hypothèse d’induction)
= (n + 1)(n + 1)
= (n + 1)2 .

Observation.
La proposition précédente donne lieu à un algorithme qui calcule le carré
d’un entier naturel.

Algorithme 1 : Algorithme qui calcule n2 en sommant les n


premiers nombres impairs.
Entrées : n ∈ N≥1
Résultat : n2
1 c ← 0; m ← 1
2 pour i ← 1 . . . n
3 c ← c + m; m ← m + 2
4 retourner c

Pavage. Voyons un autre exemple de preuve par induction, appliqué cette


fois à une autre structure discrète que N. Considérons le scénario suivant. Nous
avons une grille de 2n × 2n cases que nous désirons paver à l’aide de tuiles de
trois cases en « forme de L »:

Il est impossible d’accomplir cette tâche puisqu’un pavage contient forcément


un nombre de cases divisible par 3, alors que la grille contient 22n cases qui
n’est pas un multiple de 3. Par exemple, voici quelques tentatives de pavage
d’une grille de taille 8 × 8:
CHAPITRE 0. RAPPEL DE NOTIONS MATHÉMATIQUES 9

Dans chaque cas, toutes les cases sont pavées à l’exception d’une seule case (co-
lorée en noire). Cela porte à croire qu’il est toujours possible de paver la totalité
d’une grille 2n × 2n à l’exception d’une seule case de notre choix. Démontrons
cette conjecture:
Proposition 4. Pour tout n ∈ N et pour tous i, j ∈ [2n ], il est possible de
paver la totalité d’une grille 2n × 2n à l’exception de la case (i, j).
Démonstration. Nous prouvons la proposition par induction sur n.
Cas de base (n = 0). La grille possède une seule case qui est forcément la case
(i, j). Le pavage ne nécessite donc aucune tuile.
Étape d’induction. Soit n ≥ 0. Considérons une grille de taille 2n+1 × 2n+1 et
supposons que toute grille de taille 2n × 2n soit pavable à l’exception d’une
case arbitraire. Découpons notre grille en quatre sous-grilles. Remarquons que
chacune d’elles est de taille 2n × 2n . La case (i, j) se trouve dans l’une des
quatre sous-grilles. Par exemple, la case (i, j) se trouve ici (colorée en noire)
dans la sous-grille supérieure gauche:

Nous retranchons une case à chacune des trois autres sous-grilles de façon à
former un « L ». Par exemple, dans le cas précédent, nous retranchons ces trois
cases hachurées en rouge:
CHAPITRE 0. RAPPEL DE NOTIONS MATHÉMATIQUES 10

Nous pavons ces trois cases retranchées par une tuile. Remarquons qu’exacte-
ment une case a été retranchée à chaque sous-grille. De plus, chaque sous-grille
est de taille 2n × 2n . Ainsi, par hypothèse d’induction, chaque sous-grille peut
être pavée, ce qui donne un pavage de la grille entière.

Observation.
La preuve précédente est constructive: elle ne montre pas seulement
l’existence d’un pavage, elle explique également comment en obtenir
un. En effet, l’étape d’induction effectue essentiellement quatre appels
récursifs d’un algorithme, dont les paramètres sont (n, i, j), jusqu’à son
cas de base qui est n = 0. Par exemple, une implémentation simple en
Python pave la grille suivante de taille 64 × 64, avec (i, j) = (2, 2), en
quelques millisecondes:

Observation.

La preuve démontre également indirectement que 22n mod 3 = 1 pour


tout n ∈ N. Comme exercice supplémentaire, tentez de le démontrer par
induction sur n (sans considérer le concept de pavage).

Jeu de Nim. Considérons un jeu de Nim à deux participant·e·s où:


— il y a deux piles contenant chacune n ∈ N≥1 allumettes;
— les participant·e·s jouent en alternance;
— à chaque tour, un·e participant·e choisit une pile et retire autant d’allu-
mettes que désiré de cette pile (mais au moins une);
CHAPITRE 0. RAPPEL DE NOTIONS MATHÉMATIQUES 11

— la personne qui vide la dernière pile gagne.


Nous appelons la première personne Alice et la deuxième personne Bob.
Montrons que Bob possède toujours une stratégie gagnante. Pour ce faire, nous
utiliserons l’induction généralisée qui se prête mieux à ce type de problème. Afin
de démontrer qu’un prédicat φ est vrai pour tout n ≥ b, on démontre que:
— φ(b) est vrai;
— (∀m ∈ [b, n] φ(m)) =⇒ φ(n + 1) est vrai pour tout n ≥ b.
Autrement dit, dans l’étape d’induction, on suppose que φ(b), φ(b+1), . . . , φ(n)
sont tous vrais et on montre que φ(n + 1) est vrai.

Proposition 5. Bob possède une stratégie gagnante au jeu de Nim pour tout
n ∈ N≥1 , où n désigne le nombre d’allumettes initialement dans chaque pile.

Démonstration. Nous montrons que Bob gagne s’il retire toujours la même
quantité d’allumettes qu’Alice.
Cas de base (n = 1). Le seul coup possible pour Alice consiste à retirer une
allumette d’une pile. Bob retire donc la dernière allumette et gagne.
Étape d’induction. Soit n ≥ 1. Supposons que la stratégie fonctionne pour toute
valeur initiale m ∈ [1, n]. Considérons l’instance du jeu où il y a initialement
n + 1 allumettes dans chaque pile. Si Alice retire n + 1 allumettes d’une pile,
alors Bob retire les n + 1 allumettes de l’autre pile et gagne. Sinon, si elle retire
k ∈ [1, n] allumettes d’une pile, alors Bob retire k allumettes de l’autre pile. Il
déf
reste donc m = n+1−k allumettes dans chaque pile. Observons que m ∈ [1, n].
Ainsi, par hypothèse d’induction, Bob possède peut gagner le jeu.
CHAPITRE 0. RAPPEL DE NOTIONS MATHÉMATIQUES 12

0.3 Exercices
0.1) Quelle est la taille de l’ensemble {{a, b}, {∅, {1, 2}}, {∅, {∅, ∅}}}?
déf déf
0.2) Soient les ensembles X = {1, 5, 6, a, 9, 23, c} et Y = {−23, 3, 5, 9, a}.
Donnez le contenu des ensembles X ∪ Y , X ∩ Y , X \ Y et Y \ X.

0.3) Donnez tous les éléments de P({a, b, c}).

0.4) Montrez que la somme d’un nombre pair et d’un nombre impair est for-
cément impaire.

0.5) Montrez que si une séquence de nombre réels contient au moins deux
éléments distincts, alors elle contient au moins un élément strictement
inférieur à sa moyenne.

0.6) ⋆ Montrez que 2 ̸∈ Q par contradiction.

0.7) Montrez que |P(X)| = 2|X| pour tout ensemble fini X.

0.8) Montrez que n! > 2n pour tout n ∈ N≥4 .


( ) ( n) ( n )
k+1 = k + k+1 pour tous k, n ∈ N
0.9) Démontrez la formule de Pascal: n+1
tels que n ≥ k.
( )
0.10) La formule de Pascal permet de conclure que nk ∈ N pour tous k, n ∈ N.
Pourquoi?
( ) ( ) ( )
0.11) Montrez que n0 + n1 + . . . + nn = 2n pour tout n ∈ N.

0.12) Montrez que la somme des n premiers nombres pairs non négatifs donne
n2 −n. Tentez de le prouver d’au moins deux manières différentes: (1) par
induction sur n; (2) directement en utilisant la proposition 3.
∑n n(n+1)
0.13) Montrez que i=1 i= 2 pour tout n ∈ N>0 .

0.14) Montrez que 22n mod 3 = 1 pour tout n ∈ N.

0.15) Montrez qu’il y a |X|n séquences de taille n composées d’éléments d’un


ensemble fini X.

0.16) Montrez que 1$ et 3$ sont les seuls montants entiers qui ne peuvent pas
être payés à l’aide de pièces de 2$ et de billets de 5$.
1
Analyse des algorithmes

1.1 Temps d’exécution

1.1.1 Temps en fonction d’un paramètre


Soient A un algorithme et f la fonction telle que f (x) dénote le nombre d’opé-
rations élémentaires exécutées par A sur entrée x. Le temps d’exécution de A
dans le pire cas est la fonction tmax : N → N telle que:
déf
tmax (n) = max{f (x) : entrée x de taille n}.

Autrement dit, t(n) indique le plus grand nombre d’opérations exécutées parmi
toutes les entrées de taille n. Nous considérerons parfois également le temps
d’exécution dans le meilleur cas, défini par:
déf
tmin (n) = min{f (x) : entrée x de taille n}.

Par défaut, le « temps d’exécution » fera référence au pire cas lorsque nous ne
spécifions pas de quel cas il s’agit.

Algorithme 2 : Algorithme de calcul du maximum d’une séquence.


Entrées : séquence s de n ∈ N>0 entiers
Sorties : valeur maximale apparaissant dans s
i ← 2, max ← s[1]
tant que i ≤ n
si s[i] > max alors max ← s[i]
i←i+1
retourner max

Par exemple, analysons le temps d’exécution de l’algorithme 2 qui retourne


la valeur maximale d’une séquence non vide. Nous considérons les opérations

13
CHAPITRE 1. ANALYSE DES ALGORITHMES 14

suivantes comme élémentaires: l’affectation, la comparaison, l’addition et l’ac-


cès à un élément d’une séquence. La première ligne exécute toujours 3 opéra-
tions élémentaires (deux affectations et un accès). La boucle s’exécute toujours
précisément n − 1 fois, et en particulier exécute n − 1 comparaisons. Si la condi-
tion du si n’est jamais satisfaite, alors le corps de la boucle exécute 4 opérations
élémentaires (un accès, une comparaison, une addition et une affectation). Si
la condition du si est toujours satisfaite, alors le corps de la boucle exécute
6 opérations élémentaires (deux accès, une comparaison, deux affectations et
une addition). Ainsi, le temps d’exécution est compris entre 3 + 5(n − 1) et
3 + 7(n − 1). Autrement dit:

4n − 2 ≤ tmin (n) ≤ tmax (n) ≤ 7n − 4 pour tout n ≥ 1.

Intuitivement, l’algorithme 2 fonctionne donc en temps linéaire par rapport


à n, c’est-à-dire que peu importe l’entrée, le temps d’exécution est d’environ n à
certaines constantes près. À la section suivante, nous formaliserons cette notion
et développerons des outils afin de faciliter l’analyse du temps d’exécution.

1.1.2 Temps en fonction de plusieurs paramètres


Pour certains algorithmes, la « taille » d’une entrée dépend de plusieurs para-
mètres, par ex. le nombre de lignes m et de colonnes n d’une matrice; le nombre
d’éléments m d’une séquence et le nombre de bits n de ses éléments; le nombre
de sommets m et d’arêtes n d’un graphe, etc. Pour ces algorithmes, la notion
de temps est étendue naturellement:
déf
tmax (n1 , . . . , nk ) = max{f (x) : entrée x de taille (n1 , . . . , nk )},
déf
tmin (n1 , . . . , nk ) = min{f (x) : entrée x de taille (n1 , . . . , nk )}.

Algorithme 3 : Algorithme de calcul du maximum d’une matrice.


Entrées : matrice A d’entiers de taille m × n, où m, n ∈ N>0
Sorties : valeur maximale parmi toutes les entrées de A
i ← 1, j ← 2, max ← A[1, 1]
tant que i ≤ m
tant que j ≤ n
si A[i, j] > max alors max ← A[i, j]
j ←j+1
i←i+1
j←1
retourner max

Par exemple, considérons l’algorithme 3 qui calcule la valeur maximale ap-


paraissant dans une matrice de taille m × n. Analysons le cas où la condition
du si est toujours satisfaite. La première boucle de l’algorithme est toujours
CHAPITRE 1. ANALYSE DES ALGORITHMES 15

exécutée m fois. À sa première itération, la seconde boucle est exécutée n − 1


fois, et pour ses itérations subséquentes, la seconde boucle est exécutée n fois.
Le corps de la seconde boucle exécute 6 opérations élémentaires. La première
boucle exécute ensuite 3 opérations supplémentaires. Observons finalement que
chaque tour d’une boucle exécute une comparaison. Au total, nous obtenons:

1ère itér. boucle princip. autres itér. boucle princip.


z }| { z }| {
tmax (m, n) ≤ 4 + (1 + 7(n − 1) + 3) + (m − 1)(1 + 7n + 3)
= 7mn + 4m − 3.

Dans le cas où la condition du si n’est jamais satisfaite, l’analyse demeure


la même à l’exception des 6 opérations du corps de la seconde boucle qui
deviennent 4 opérations. Ainsi:

1ère itér. boucle princip. autres itér. boucle princip.


z }| { z }| {
tmin (m, n) ≥ 4 + (1 + 5(n − 1) + 3) + (m − 1)(1 + 5n + 3)
= 5mn + 4m − 1.

Nous en concluons donc que pour tous m, n ≥ 1:

5mn + 4m − 1 ≤ tmin (m, n) ≤ tmax (m, n) ≤ 7mn + 4m − 3.

Intuitivement, l’algorithme 3 fonctionne donc en temps ≈ m · n, c’est-à-dire


que peu importe l’entrée, le temps d’exécution est d’environ m · n à quelques
termes négligeables près. En contraste avec l’exemple précédent, les « termes
négligeables » ici ne sont pas tous des constantes. La section suivante nous
permettra de définir ce que nous entendons par « terme négligeable ».

1.2 Notation asymptotique

1.2.1 Notation O
Nous formalisons les notions de la section précédente en introduisant une no-
tation qui permet de comparer des fonctions asymptotiquement, c’est-à-dire
lorsque leur entrée tend vers l’infini. Nous nous concentrons sur les fonctions à
un seul paramètre. Plus précisément, nous considérons les fonctions de N vers
R qui sont éventuellement positives, c’est-à-dire:
déf
F = {f : N → R : ∃m ∈ N ∀n ≥ m f (n) > 0} .

Définition 1. Soit g ∈ F . L’ensemble O(g) est défini par:


déf
O(g) = {f ∈ F : ∃c ∈ R>0 ∃n0 ∈ N ∀n ≥ n0 f (n) ≤ c · g(n)} .

Nous appelons les valeurs c et n0 une constante multiplicative et un seuil.


CHAPITRE 1. ANALYSE DES ALGORITHMES 16

Intuitivement, f ∈ O(g) indique que f croît aussi ou moins rapidement


que la fonction g. Reconsidérons l’algorithme 2. Nous avons déjà établi que son
temps d’exécution est d’au plus 7n − 4 pour tout n ≥ 1. Nous avons:
tmax (n) ≤ 7n − 4 pour tout n ≥ 1
≤ 7n pour tout n ≥ 0.
Ainsi, en prenant 7 comme constante multiplicative et 1 comme seuil, nous
concluons que l’algorithme 2 fonctionne en temps O(n).

Exemples.
déf
Considérons d’autres exemples de fonctions. Posons f (n) = 5n2 +400n+
9. La fonction f est supérieure à n2 et peut sembler croître beaucoup
plus rapidement que n2 :

5n2 + 400n + 9

4 000 000

2 000 000
n2

n
500 1 000

Cependant, nous pouvons montrer que f ∈ O(n2 ). Nous avons:

f (n) = 5n2 + 400n + 9


≤ 5n2 + 400n + n · n pour tout n ≥ 3
≤ 5n + n · n + n · n
2
pour tout n ≥ 400
2
= 7n .

Ainsi, en prenant 7 comme constante multiplicative et 400 comme seuil,


nous concluons que f ∈ O(n2 ).
Comme autre exemple, montrons que n2 ∈ O(n!). Pour tout n ≥ 2:

n2 = (n − 1)n + n
≤ n! + n (car n ≥ 2 et n! = 1 · · · (n − 1)n)
≤ n! + n!
= 2n!.

Ainsi, en prenant 2 à la fois comme constante multiplicative et comme


seuil, nous concluons que n2 ∈ O(n!).
CHAPITRE 1. ANALYSE DES ALGORITHMES 17

Nous avons vu que f croît au plus aussi rapidement que n2 asymptotique-


ment, et pareillement pour n2 par rapport à n!. Nous nous attendons donc
intuitivement à ce que f ∈ O(n!). Cela est bien le cas, puisque la relation
induite par O est transitive:
Proposition 6. Pour toutes fonctions f, g, h ∈ F , si f ∈ O(g) et g ∈ O(h),
alors f ∈ O(h).
Démonstration. Soient c′ , n′0 et c′′ , n′′0 les constantes multiplicative et les seuils
qui montrent que f ∈ O(g) et g ∈ O(h) respectivement. Nous montrons que
f ∈ O(h) en prenant c = c′ · c′′ comme constante multiplicative et n0 =
déf déf

′ ′′
max(n0 , n0 ) et comme seuil. Pour tout n ≥ n0 , nous avons:
f (n) ≤ c′ · g(n) (car n ≥ n0 ≥ n′0 )
′ ′′
≤ c · c · h(n) (car n ≥ n0 ≥ n′′0 )
= c · h(n) (par définition de c).
Rappelons que nous avons vu que 5n2 +400n+9 ∈ O(n2 ). Cela est plutôt in-
tuitif puisque 5n2 est le terme dominant et puisque les constantes « n’importent
pas » asymptotiquement. Nous formalisons ce raisonnement. Pour toutes fonc-
tions f, g ∈ F et tout coefficient c ∈ R>0 , les fonctions f + g, c · f et max(f, g)
sont définies par:
déf
(f + g)(n) = f (n) + g(n),
déf
(c · f )(n) = c · f (n),
déf
(max(f, g))(n) = max(f (n), g(n)).
Les deux propositions suivantes montrent que les constantes apparaissant
dans une somme de fonctions n’importent pas asymptotiquement, et qu’une
somme de fonctions se comporte asymptotiquement comme leur maximum.
Proposition 7. f1 + . . . + fk ∈ O(c1 · f1 + . . . + ck · fk ) pour toutes fonctions
f1 , . . . , fk ∈ F et tous coefficients c1 , . . . , ck ∈ R>0 .

Démonstration. Soit mi le seuil à partir duquel fi est positive. Nous dé-


déf
montrons la proposition en prenant c = max(1/c1 , 1/c2 , . . . , 1/ck ) comme
déf
constante multiplicative et n0 = max(m1 , m2 , . . . , mk ) comme seuil. Pour
tout n ≥ n0 :

n ∑n
1
fi (n) = · (ci · fi (n)) (puisque ci ̸= 0)
i=1
c
i=1 i

n
≤ c · (ci · fi (n)) (par déf. de c et car ci · fi (n) > 0)
i=1

n
=c· ci · fi (n).
i=1
CHAPITRE 1. ANALYSE DES ALGORITHMES 18

Proposition 8. f1 + . . . + fk ∈ O(max(f1 , . . . , fk )) pour toutes f1 , . . . , fk ∈ F .


déf
Démonstration. Nous démontrons la proposition en prenant c = k comme
déf
constante multiplicative et n0 = 0 comme seuil. Pour tout n ≥ n0 , nous
avons:

k
fi (n) ≤ k · max(f1 (n), f2 (n), . . . , fk (n))
i=1
= c · max(f1 (n), f2 (n), . . . , fk (n)).

Exemples.

Reconsidérons l’exemple f (n) = 5n2 + 400n + 9. Nous montrons à nou-


veau que f ∈ O(n2 ), cette fois plus brièvement à l’aide des observations
précédentes. Par la proposition 7, nous avons f ∈ O(n2 + n + 1). Ainsi,
puisque max(n2 , n, 1) = n2 , la proposition 8 et la proposition 6 im-
pliquent f ∈ O(n2 ).
déf
Voyons un autre exemple. Posons g(n) = 9000n2 +3n +8. Par la pro-
position 7, nous avons g ∈ O(n +3 +1). Observons que max(n2 , 3n , 1) =
2 n

3n . Ainsi, par la proposition 8 et la proposition 6, nous avons g ∈ O(3n ).

Lors de l’analyse d’algorithmes, nous obtiendrons souvent des fonctions qui


sont des polynômes. Les observations précédentes suggèrent que tout polynôme
de degré d appartient à O(nd ) car cela correspond à son terme dominant. Cela
s’avère vrai. Cependant, nous ne pouvons pas simplement combiner les proposi-
tions précédentes pour s’en convaincre. En effet, un polynôme peut contenir des
termes négatifs, alors que nous n’avons raisonné jusqu’ici que sur des sommes
de fonctions éventuellement positives. Démontrons donc cette observation:
Proposition 9. f ∈ O(nd ) pour tout polynôme f ∈ F de degré d.

Démonstration. Nous procédons par induction sur d. Si d = 0, alors f (n) = c


pour un certain c ∈ R>0 . Ainsi, f ∈ O(1), ce qui démontre le cas de base.
Supposons que d > 0 et que la proposition soit satisfaite pour tout degré
inférieur à d. Remarquons que tout polynôme est ou bien éventuellement
positif, ou bien éventuellement négatif 1 . Il existe donc un coefficient c et un
polynôme g ∈ F de degré d′ < d tel que f (n) = c · nd + g(n) ou f (n) =
c · nd − g(n). Considérons ces deux cas.

Cas f (n) = c · nd + g(n). Par hypothèse d’induction, nous avons g ∈ O(nd ).
′ ′
Par la proposition 7, nous avons donc f ∈ O(nd +nd ). Puisque max(nd , nd ) =
nd , nous avons f ∈ O(nd ) par la proposition 8 et la proposition 6.
Cas f (n) = cnd − g(n). Soit n0 un seuil à partir duquel g est positive. Nous
avons f (n) ≤ c · nd pour tout n ≥ n0 . Ainsi, f ∈ O(nd ) en prenant c comme
constante multiplicative et n0 comme seuil.
CHAPITRE 1. ANALYSE DES ALGORITHMES 19

Exemples.

Par la proposition précédente, nous pouvons directement déterminer,


par exemple, que 2n3 −5n2 +3n−100 ∈ O(n3 ) et n2 /8−9000n ∈ O(n2 ).

1.2.2 Notation Ω
La notation O nous permet de borner des fonctions supérieurement. Afin d’ana-
lyser des algorithmes, il sera également utile de borner des fonctions inférieu-
rement. Dans ce but, nous introduisons la notation Ω.
Définition 2. Soit g ∈ F . L’ensemble Ω(g) est défini par:
déf
Ω(g) = {f ∈ F : ∃c ∈ R>0 ∃n0 ∈ N ∀n ≥ n0 f (n) ≥ c · g(n)} .

Nous appelons les valeurs c et n0 une constante multiplicative et un seuil.

Intuitivement, f ∈ Ω(g) indique que f croît au moins aussi rapidement que


la fonction g. Remarquons que la définition de Ω(g) ne diffère de celle de O(g)
que par la comparaison « ≤ » qui est remplacée par « ≥ ». Ainsi, nous pouvons
en déduire que
f ∈ Ω(g) ⇐⇒ g ∈ O(f ).

Exemple.
déf
Voyons un exemple. Soit f (n) = n2 − 400n + 5 la fonction suivante:

1 500 000 n2

n2 − 400n + 5
1 000 000

500 000

n
500 1 000

1. Cela découle du fait que tout polynôme possède un nombre fini de zéros.
CHAPITRE 1. ANALYSE DES ALGORITHMES 20

Bien que f soit inférieure à n2 , nous avons f ∈ Ω(n2 ). En effet:

f (n) = n2 − 400n + 5
≥ n2 − 400n
n
≥ n2 − · n pour tout n ≥ 800
2
1 2
= ·n .
2
Ainsi, en prenant 1/2 comme constante multiplicative et 800 comme
seuil, nous concluons que f ∈ Ω(n2 ).

De façon similaire à la notation O, il est possible de démontrer que:


Proposition 10. f ∈ Ω(nd ) pour tout polynôme f ∈ F de degré d.

1.2.3 Notation Θ
Nous introduisons une troisième et dernière notation asymptotique qui permet
de borner une fonction à la fois inférieurement et supérieurement. Pour toute
fonction g ∈ F , nous définissons:
déf
Θ(g) = O(g) ∩ Ω(g).
Autrement dit, f ∈ Θ(g) si f ∈ O(g) et f ∈ Ω(g). Intuitivement, Θ(g) décrit
donc l’ensemble des fonctions qui croissent aussi rapidement que g.

Exemples.
déf
Par exemple, considérons la fonction f (n) = 42n + 5 suivante:
42n + 5
200 000

100 000

n
n
2 000 4 000

Puisque f est un polynôme de degré 1, nous avons f ∈ O(n). De plus,


f (n) = 42n + 5 ≥ 42n pour tout n ∈ N. Ainsi, en prenant 42 comme
constante multiplicative et 0 comme seuil, nous concluons que f ∈ Ω(n)
et donc que f ∈ Θ(n).
CHAPITRE 1. ANALYSE DES ALGORITHMES 21

déf
Voyons un autre exemple. Soit la fonction g(n) = 9000n. Nous avons
g ∈ O(n2 ) puisque g est un polynôme de degré 1 alors que n2 est un
polynôme de degré 2. Cependant, nous avons g ̸∈ Ω(n2 ) puisque g croît
moins rapidement que n2 . Cet argument demeure intuitif et ne démontre
pas cette affirmation. Prouvons-la par contradiction. Supposons que g ∈
Ω(n2 ). Il existe donc c ∈ R>0 et n0 ∈ N tels que 9000n ≥ c · n2 pour
tout n ≥ n0 . En supposant que n ≥ 1, nous pouvons diviser des deux
côtés par c et n afin d’obtenir:
9000
≥n pour tout n ≥ max(n0 , 1).
c
Nous avons donc une constante du côté gauche qui borne supérieurement
une valeur arbitrairement grande du côté droit. Il y a donc contradiction,
ce qui démontre g ̸∈ Ω(n2 ). Nous en concluons donc que g ̸∈ Θ(n2 ).

Par la proposition 9 et la proposition 10, nous avons:

Proposition 11. f ∈ Θ(nd ) pour tout polynôme f ∈ F de degré d.

1.3 Étude de cas: vote à majorité absolue


Considérons le problème suivant: étant donnée une séquence T de n éléments,
nous cherchons à déterminer si T contient une valeur majoritaire, c’est-à-dire
une valeur qui apparaît plus de n/2 fois dans T . Par exemple, la séquence
[a, b, b, a, a, a, c, a] contient la valeur majoritaire a qui apparaît 5 fois parmi
8 éléments; alors que la séquence [a, b, a, b, c] ne possède pas de valeur ma-
joritaire. Ce problème peut être vu comme un scénario où des participant·e·s
votent pour une option à une élection, et où nous cherchons à déterminer si
une option a remportée la majorité absolue des voix. Un algorithme simple
qui détermine si c’est le cas (et qui retourne l’option gagnante le cas échéant)
consiste à compter le nombre d’occurrences de chaque valeur T [i] et à vérifier
chaque fois si ce décompte excède n/2. Cette approche est décrite sous forme
de pseudocode à l’algorithme 4.
Analysons le temps de calcul t de l’algorithme 4. Nous considérons toutes
les opérations comme élémentaires, c’est-à-dire l’affectation, la comparaison,
l’addition, la division et l’accès à un élément de T . Remarquons que les deux
boucles sont exécutées exactement n fois, et que c est incrémenté entre 0 à n
fois à la ligne 4. Ainsi, nous obtenons:

n · (1 + 3n + 2) ≤ t(n) ≤ n · (1 + 5n + 3).
CHAPITRE 1. ANALYSE DES ALGORITHMES 22

Algorithme 4 : Recherche d’une valeur majoritaire.


Entrées : séquence T de n ∈ N éléments comparables
Résultat : une valeur x t.q. T contient plus de n/2 occurrences de x
s’il en existe une, et « aucune » sinon
1 pour i ← 1, . . . , n
2 c←0
3 pour j ← 1, . . . , n // Compter # d'occur. de T [i]
4 si T [j] = T [i] alors c ← c + 1
5 si c > n ÷ 2 alors retourner T [i]
6 retourner aucune

Nous avons donc t ∈ O(n2 ) puisque:

t(n) ≤ 5n2 + 4n pour tout n ≥ 0


≤ 5n + 4n
2 2
pour tout n ≥ 0
2
= 9n .

Cherchons maintenant à borner t inférieurement. Si T ne contient pas de


valeur majoritaire, alors la condition « c > n ÷ 2 » n’est jamais satisfaite. Nous
avons donc t(n) ≥ 3n2 +3n ≥ 6n2 et ainsi t ∈ Ω(n2 ). Par conséquent, t ∈ Θ(n2 )
et ainsi l’algorithme 4 fonctionne en temps quadratique. Notons que l’analyse
aurait pu pu être simplifiée en gardant le terme dominant de chaque expression,
puisqu’il s’agit de polynômes.

Algorithme 5 : Recherche d’une valeur majoritaire, sans considérer


une valeur plus d’une fois.
Entrées : séquence T de n ∈ N éléments comparables
Résultat : une valeur x t.q. T contient plus de n/2 occurrences de x
s’il en existe une, et « aucune » sinon
1 pour i ← 1, . . . , n
2 si T [i] ̸= ⊥ alors
3 c ← 0; x ← T [i]
4 pour j ← i, . . . , n // Compter # d'occur. de T [i]
5 si T [j] = x alors
6 c←c+1
7 T [j] ← ⊥ // Retirer en remplaçant par ⊥
8 si c > n ÷ 2 alors retourner x
9 retourner aucune

L’algorithme 4 peut être amélioré. En effet, si la séquence contient plu-


sieurs copies d’une même valeur non majoritaire, alors celle-ci risque d’être
CHAPITRE 1. ANALYSE DES ALGORITHMES 23

reconsidérée plusieurs fois, ce qui augmente inutilement le temps d’exécution.


L’algorithme 5 décrit une version modifiée où nous considérons chaque valeur
au plus une fois.
Analysons le temps d’exécution t de l’algorithme 5. En supposant que les
conditions des trois si sont toujours satisfaites, nous obtenons:
 
∑n ∑ n
t(n) ≤ 5 + 5 + 2
i=1 j=i


n
= (5 + 5(n − i + 1) + 2)
i=1
∑n
= (5n − 5i + 12)
i=1

n
= 5n + 12n − 5
2
i
i=1
= 5n2 + 12n − 5n(n + 1)/2
5 19
= · n2 + · n.
2 2
Ainsi, t est borné supérieurement par un polyôme de degré 2, ce qui implique
que t ∈ O(n2 ).
Cherchons maintenant à borner t inférieurement. Considérons le cas où T
contient des éléments qui sont tous distincts. Une telle séquence ne contient pas
d’élément majoritaire, et ainsi la condition « c > n ÷ 2 » n’est jamais satisfaite.
De plus, la condition « T [i] ̸= ⊥ » est toujours satisfaite car tous les éléments
sont distincts. Les deux boucles sont donc exécutées un nombre maximal de
fois. Ainsi:
n ∑
∑ n
t(n) ≥ 1
i=1 j=i
∑n
= (n − i + 1)
i=1

n
= n2 + n − i
i=1
= n2 + n − n(n + 1)/2
n2 n
= + .
2 2
Nous obtenons donc t ∈ Ω(n2 ) et par conséquent t ∈ Θ(n2 ). Les deux algo-
rithmes ont donc un temps d’exécution quadratique (dans le pire cas). Ainsi,
bien que notre deuxième algorithme puisse être légèrement plus efficace, l’amé-
lioration du temps d’exécution est négligeable.
CHAPITRE 1. ANALYSE DES ALGORITHMES 24

Nous verrons qu’il existe des algorithmes plus efficaces pour ce problème:
Θ(n log n) (laissé en exercice) et Θ(n) (présenté à la section 1.8).

1.4 Simplification du décompte des opérations


Une ligne de code d’un algorithme est dite élémentaire si son exécution en-
gendre un nombre d’opérations élémentaires indépendant de la taille de l’en-
trée. Par exemple, ces lignes de l’algorithme 5 sont toutes élémentaires:

1 si T [j] = x alors
2 c←c+1
3 T [j] ← ⊥

En général, toutes les lignes d’un algorithme sont élémentaires à l’exception


des appels de sous-fonctions complexes.
Le bloc de code ci-dessus exécute 2 ou 5 opérations élémentaires. Lors de
l’analyse asymptotique de l’algorithme, cette constante précise n’importe pas.
Nous pouvons donc simplement considérer ce bloc comme une seule « grande »
opération élémentaire. En général, si le nombre de fois qu’un algorithme atteint
certaines lignes élémentaires j2 , . . . , jℓ est borné par le nombre de fois qu’il at-
teint une ligne élémentaire j1 , alors il suffit de compter le nombre d’occurrences
de j1 et d’ignorer les lignes j2 , . . . , jℓ . Par exemple, dans le code ci-dessus, le
déf déf déf
nombre d’exécutions de la ligne j1 = 1 borne celle des lignes j2 = 2 et j3 = 3.

Proposition 12. Soit J = {j1 , j2 , . . . , jℓ } un ensemble de lignes élémentaires


d’un algorithme A. Soit f ′ la fonction où f ′ (x) dénote la somme du nombre
d’exécutions de la ligne j1 et du nombre d’opérations élémentaire exécutées
par les lignes n’appartenant pas à J, sur entrée x. Si sur toute entrée, chaque
ligne de J est atteinte au plus le nombre de fois que j1 est atteinte, alors
Θ(t′max ) = Θ(tmax ), où t′max (n) = max{f ′ (x) : entrée x de taille n}.
déf

Démonstration. Observons d’abord que f ′ (x) ≤ f (x) pour toute entrée x,


puisque f ′ compte moins d’opérations que f . Nous avons donc immédiatement
t′max ∈ O(tmax ).
Ainsi, il suffit de montrer que tmax ∈ O(t′max ). Soit x une entrée de A. Soit
m le nombre d’opérations élémentaires exécutées, sur entrée x, par les lignes
de A qui n’appartiennent pas à J; soit cj le nombre d’opérations élémentaires
contenues sur la ligne j ∈ J; et soit kj le nombre de fois où la ligne j ∈ J est
CHAPITRE 1. ANALYSE DES ALGORITHMES 25

atteinte sur entrée x. Nous avons:



f (x) ≤ m + cj · kj
j∈J

≤ m + k1 · cj (car k1 = max{k1 , . . . , kℓ })
j∈J

≤ m + k1 · ℓ · max(c1 , . . . , cℓ )
≤ ℓ · max(c1 , . . . , cℓ ) · (m + k1 )
≤ ℓ · max(c1 , . . . , cℓ ) · (m + c1 · k1 )
= ℓ · max(c1 , . . . , cℓ ) · f ′ (x).

Nous obtenons donc f (x) ≤ c · f ′ (x), où c = ℓ · max(c1 , . . . , cℓ ). Observons


déf

que c est indépendant de la taille de l’entrée puisque ℓ est une constante et


puisque chaque cj est indépendant par hypothèse. Ainsi tmax (n) ≤ c · t′max (n)
pour tout n ∈ N, et par conséquent tmax ∈ O(t′max ).

Algorithme 6 : Squelette de l’algorithme 5.


pour i ← 1, . . . , n
si /* élémentaire */ alors
pour j ← i, . . . , n
/* élémentaire */
retourner

Appliquons cette observation à l’algorithme 5. Ses lignes 3 et 8 sont exécu-


tées au plus autant de fois que la ligne 2, et ses lignes 6 et 7 sont exécutées au
plus autant de fois que sa ligne 5. Ainsi, nous en dérivons un « squelette » tel
que décrit à l’algorithme 6. Cela facilite l’analyse de l’algorithme puisque nous
pouvons en dériver une expression plus simple:

n ∑
n
t′max (n) ≤ 1+ i.
i=1 i=1

Nous appliquerons de plus en plus ce type de simplification afin de faciliter


l’analyse d’algorithmes plus complexes.

1.5 Règle de la limite


Afin d’analyser des fonctions plus complexes, il s’avère parfois pratique d’uti-
liser la règle de la limite:
déf f (n)
Proposition 13. Soient f, g ∈ F et ℓ = lim . Nous avons:
n→+∞ g(n)
— si ℓ ∈ R>0 , alors f ∈ O(g) et g ∈ O(f ),
CHAPITRE 1. ANALYSE DES ALGORITHMES 26

— si ℓ = 0, alors f ∈ O(g) et g ̸∈ O(f ),


— si ℓ = +∞, alors f ̸∈ O(g) et g ∈ O(f ).

Afin d’illustrer cette règle, considérons un exemple tiré de [BB96, p. 84]. Posons
déf déf √
f (n) = log n et g(n) = n. Nous avons:

f (n) f ′ (n)
lim = lim ′ (par la règle de L’Hôpital)
n→+∞ g(n) n→+∞ g (n)

1/(loge 2 · n)
= lim √
n→+∞ 1/(2 · n)

2· n
= lim
n→+∞ loge 2 · n

2 n
= · lim
loge 2 n→+∞ n
2 1
= · lim √
loge 2 n→+∞ n
= 0.

Par la proposition
√ 13,√ nous obtenons donc f ∈ O(g) et g ̸∈ O(f ), ou autrement
dit log n ∈ O( n) et n ̸∈ O(log n).

1.6 Notation asymptotique à plusieurs paramètres


Dans le cas d’un algorithme dont la taille des entrées dépend de plusieurs
paramètres, nous pouvons utiliser les notations asymptotiques étendues natu-
rellement à des fonctions multivariées. Nous présentons brièvement le cas à
deux paramètres qui sera le plus fréquent. Soit F2 l’ensemble des fonctions
éventuellement non négatives à deux entrées:
déf { }
F2 = f : N2 → R : ∃k, k ′ ∈ N ∀m ≥ k ∀n ≥ k ′ f (m, n) > 0 .

Définition 3. Soit g ∈ F2 . L’ensemble O(g) est défini par:

déf
O(g) = {f ∈ F2 : ∃c ∈ R>0 ∃m0 , n0 ∈ N ∀m ≥ m0 ∀n ≥ n0
f (m, n) ≤ c · g(m, n)} .

Les ensembles Ω(g) et Θ(g) sont définis par:


déf
Ω(g) = {f ∈ F2 : g ∈ O(f )},
déf
Θ(g) = O(f ) ∩ Ω(g).
CHAPITRE 1. ANALYSE DES ALGORITHMES 27

Reconsidérons l’algorithme 3 qui calcule la valeur maximale apparaissant


dans une matrice de taille m × n. Nous avons déjà montré que son temps
d’exécution t satisfait:
5mn + 4m − 1 ≤ t(m, n) ≤ 7mn + 4m − 3 pour tous m, n ≥ 1.
Montrons que t ∈ Θ(m · n). Nous avons:
t(m, n) ≤ 7mn + 4m − 3 pour tous m, n ≥ 1
≤ 7mn + 4m
≤ 7mn + 4mn pour tout n ≥ 1
= 11mn.
Ainsi en prenant m0 = n0 = 1 comme seuils, et c = 11 comme constante
multiplicative, nous obtenons t ∈ O(m · n). Nous pouvons dériver t ∈ Ω(m · n)
similairement et en conclure que t ∈ Θ(m · n).

1.7 Correction et terminaison

1.7.1 Correction
Nous nous sommes intéressés au temps d’exécution d’algorithmes, en prenant
pour acquis que ceux-ci fonctionnent. Cependant, il n’est pas toujours simple de
se convaincre qu’un algorithme fonctionne correctement. Formalisons le concept
de correction.
Un algorithme possède un domaine d’entrée D, par ex. l’ensemble des en-
tiers, des séquences, des matrices, des arbres binaire, des paires d’entiers, etc.
Sur toute entrée x ∈ D, on s’attend à ce que l’algorithme retourne une sortie
y telle qu’une propriété φ(x, y) soit satisfaite. Par exemple, reconsidérons l’al-
gorithme 2 qui cherche à calculer la valeur maximale apparaissant dans une
séquence non vide. Dans le cas de cet algorithme, nous avons:
D = {x : x est une séquence de n ∈ N>0 éléments},
φ(x, y) = (y = max{x[i] : 1 ≤ i ≤ n}).
Définition 4. Nous disons qu’un algorithme est correct si pour toute entrée
x ∈ D, la sortie y de l’algorithme sur entrée x est telle que φ(x, y) soit vraie.
L’appartenance d’une entrée à D s’appelle la pré-condition, et la propriété
φ s’appelle la post-condition. En mots, un algorithme est donc correct si sur
chaque entrée qui satisfait la pré-condition, la sortie satisfait la post-condition.
Afin de démontrer qu’un algorithme est correct, nous avons souvent re-
cours à un invariant, c’est-à-dire une propriété qui demeure vraie à chaque fois
qu’une ou certaines lignes de code sont atteintes. Par exemple, dans le cas de
l’algorithme 2, il est possible d’établir l’invariant suivant par induction sur i:
Proposition 14. À chaque fois que l’algorithme 2 atteint le tant que, l’égalité
suivante est satisfaite: max = max{s[j] : 1 ≤ j ≤ i − 1}.
CHAPITRE 1. ANALYSE DES ALGORITHMES 28

Puisque l’algorithme termine avec i = n + 1, l’invariant nous indique que


l’algorithme retourne max = max{s[j] : 1 ≤ j ≤ n}, ce qui satisfait la post-
condition. Ainsi, l’algorithme 2 est correct.

1.7.2 Terminaison
Pour la plupart des algorithmes que nous considérerons, il sera évident que
ceux-ci terminent, c’est-à-dire qu’une instruction retourner est exécutée sur
toute entrée. Toutefois, cela n’est pas toujours le cas. Par exemple, à ce jour
personne ne sait si l’algorithme 7 termine sur toute entrée.

Algorithme 7 : Calcul de la séquence de Collatz.


Entrées : n ∈ N
Sorties : 1
tant que n ̸= 1
si n est pair alors
n←n÷2
sinon
n ← 3n + 1
retourner n

Remarques.

— Techniquement parlant, si un algorithme n’est pas correct ou ne ter-


mine pas, nous devons plutôt parler de « procédure ». Ainsi, un al-
gorithme est une procédure qui est correcte et qui termine sur toute
entrée. Pour être précis, il faudrait donc dire « la procédure est cor-
recte » et « la procédure termine » plutôt que « l’algorithme est cor-
rect » et « l’algorithme termine ». Nous nous permettons toutefois cet
abus de langage.
— Certains ouvrages nomment correction partielle ce que nous appelons
ici la correction. Dans ces ouvrages, « correction » réfère à « correction
partielle + terminaison ».

1.8 Étude de cas: vote à majorité absolue (suite)


Revisitons le problème de la section 1.3, c’est-à-dire la recherche d’une valeur
majoritaire dans une séquence de n éléments. Les deux algorithmes que nous
avons présentés fonctionnaient en temps Θ(n2 ). L’algorithme 8 décrit une pro-
cédure élégante, conçue par Robert S. Boyer et J. Strother Moore [BM91],
CHAPITRE 1. ANALYSE DES ALGORITHMES 29

qui recherche une valeur majoritaire en temps Θ(n). La correction de cet al-
gorithme est loin d’être évidente. Ainsi, nous expliquons le fonctionnement de
l’algorithme et démontrons qu’il est correct.

Algorithme 8 : Recherche de Boyer–Moore d’une valeur majoritaire.


Entrées : séquence T de n ∈ N éléments comparables
Résultat : une valeur x t.q. T contient plus de n/2 occurrences de x
s’il en existe une, et « aucune » sinon
1 x ← aucune; c ← 0 // Chercher une valeur majoritaire x
2 pour i ← 1, . . . , n
3 si c = 0 alors x ← T [i]; c ← 1
4 sinon si T [i] = x alors c ← c + 1
5 sinon c ← c − 1
6 c←0 // Vérifier que x est bien majoritaire
7 pour i ← 1, . . . , n
8 si T [i] = x alors c ← c + 1
9 si c > n ÷ 2 alors
10 retourner x
11 sinon
12 retourner aucune

L’algorithme 8 fonctionne en deux phases:


(a) on trouve une valeur x qui est majoritaire si T en contient une; et
(b) on vérifie que x est bien majoritaire.
La première phase est plutôt subtile. Celle-ci garde un compteur c à jour et
parcourt tous les éléments de T . Initialement, on suppose que x = T [1] est
majoritaire et on pose c = 1. Par la suite, on incrémente c pour chaque occur-
rence de x, et on décrémente c pour chaque non occurrence de x. Si c atteint
0 à l’élément T [i], alors x n’était pas majoritaire puisqu’il y avait « plus de
votes en défaveur de x qu’en faveur de x ». On continue donc le processus,
mais cette fois en prenant x = T [i] comme nouvelle candidate. Intuitivement,
si T possède une valeur majoritaire, celle-ci « survivra aux incrémentations
et décrémentations ». La seconde phase confirme simplement que x est bien
majoritaire.
Par exemple, sur la séquence T = [a, b, b, a, a, a, c, a], la première phase de
l’algorithme termine avec x = a et c = 2 comme en témoigne sa trace:
i 1 2 3 4 5 6 7 8
T [i] a b b a a a c a
x − a a b b a a a a
c 0 1 0 1 0 1 2 1 2
La deuxième phase confirme que a est bel et bien majoritaire. Comme second
exemple, considérons la séquence T ′ = [a, b, a, b, c]. La première phase termine
CHAPITRE 1. ANALYSE DES ALGORITHMES 30

avec x = c et c = 1:
i 1 2 3 4 5

T [i] a b a b c
x − a a a a c
c 0 1 0 1 0 1
La deuxième phase vérifie si x apparaît au moins 3 fois, ce qui n’est pas le cas,
et conclut donc que T ′ ne possède pas de valeur majoritaire.
Afin de démontrer que l’algorithme 8 est correct, nous établissons un inva-
riant satisfait par sa première boucle. Intuitivement, celui-ci affirme qu’après
la ième itération de la première boucle, x est l’unique valeur possiblement ma-
joritaire dans la sous-séquence T [1 : i] = [T [1], T [2], . . . , T [i]].
Proposition 15. Après l’exécution du corps de la première boucle de l’algo-
rithme 8, l’invariant suivant est satisfait:
(a) c ≥ 0;
(b) x apparaît au plus i+c
2 fois dans T [1 : i];
(c) y apparaît au plus i−c
2 fois dans T [1 : i], pour tout y ̸= x.
Démonstration. Nous procédons par induction sur i ≥ 1.
Cas de base (i = 1). Puisque x = T [1] et c = 1, x apparaît au plus i+c2 = 1 fois
dans T [1 : 1], et tout y ̸= x apparaît au plus 2 = 0 fois dans T [1 : 1].
i−c

Étape d’induction. Soit i > 1. Supposons que l’invariant soit satisfait après
l’exécution pour i − 1. Soient c′ et x′ la valeur des variables c et x au début
du corps de la boucle (et ainsi à la fin du corps de l’itération précédente). Par
hypothèse d’induction, nous avons:
(a’) c ≥ 0;
i−1+c′
(b’) x′ apparaît au plus 2 fois dans T [1 : i − 1];
i−1−c′
(c’) y apparaît au plus 2 tout y ̸= x′ .
fois dans T [1 : i − 1], pour
Observons qu’exactement une des lignes 3, 4 et 5 est exécutée lors de l’exécution
du corps de la boucle. Nous considérons ces trois cas séparément.
Ligne 3. Nous avons c′ = 0, c = 1 et x = T [i]. Clairement, c ≥ 0. Puisque
c′ = 0, (b’) et (c’) affirment que chaque valeur apparaît au plus i−1 2 fois dans
T [1 : i−1]. Puisque T [i] = x, la valeur x apparaît donc au plus i−1
2 +1 = i+1
2 =
i+c
2 fois dans T [1 : i]. Soit y ̸
= x. Puisque T [i] ̸
= y, le nombre d’occurrences
de y n’a pas changé. Ainsi, y apparaît au plus i−1 i−c
2 = 2 fois dans T [1 : i].
Ligne 4. Nous avons c = c′ + 1 et T [i] = x′ = x. Par (a’), nous avons
′ ′
c > c′ ≥ 0. Par (b’), x apparaît au plus i−1+c2 + 1 = i+c2 +1 = i+c
2 fois dans
T [1 : i]. Soit y ̸= x. Le nombre d’occurrences de y n’a pas changé. Par (c’), y

apparaît donc au plus i−1−c 2 = i−c
2 fois dans T [1 : i].
Ligne 5. Nous avons c = c′ − 1 et T [i] ̸= x′ = x. Par (a’), nous avons c′ ≥ 0.
Puisque la ligne 5 est exécutée, nous avons c′ ̸= 0 et donc c′ > 0. Ainsi,
CHAPITRE 1. ANALYSE DES ALGORITHMES 31


c ≥ 0. Par (b’) et puisque T [i] ̸= x, x apparaît au plus i−1+c 2 = i+c
2 fois
dans T [1 : i]. Soit y ̸= x. Le nombre d’occurrences de y a augmenté d’au plus
′ ′
1. Ainsi, par (c’), y apparaît au plus i−1−c2 + 1 = i−c2 +1 = i−c
2 fois dans
T [1 : i].

Théorème 1. L’algorithme 8 est correct.

Démonstration. Si l’algorithme retourne une valeur différente de « aucune »,


alors clairement celle-ci est majoritaire, car la deuxième phase s’assure qu’on
ne peut retourner qu’une valeur majoritaire. Il suffit donc de prouver que si T
possède une valeur majoritaire, alors la première phase se termine avec cette
valeur.
Nous prouvons cette affirmation par contradiction. Supposons que T pos-
sède une valeur majoritaire y et que la première phase termine avec x ̸= y. Par
la proposition 15 (c), y apparaît au plus n−c 2 fois dans T [1 : n] = T , où c ≥ 0
par la proposition 15 (a). La valeur y apparaît donc au plus n−c 2 ≤ 2 dans T ,
n

ce qui contredit le fait qu’elle soit majoritaire.

Observation.
La première phase de l’algorithme de Boyer et Moore considère chaque
élément de T une et une seule fois, et les consomme du début vers la fin
de la séquence. De plus, la seconde phase n’est pas nécessaire si nous
savons à coup sûr que l’entrée possède une valeur majoritaire. L’algo-
rithme peut donc s’avérer particulièrement efficace avec les entrées sous
forme de flux, c.-à-d. où l’algorithme reçoit son entrée progressivement
sans que sa taille ne soit connue à priori.
CHAPITRE 1. ANALYSE DES ALGORITHMES 32

1.9 Exercices
1.1) Montrez que si f ∈ O(g), alors O(f ) ⊆ O(g). Déduisez-en l’équivalence
suivante: O(f ) = O(g) ⇐⇒ f ∈ O(g) et g ∈ O(f ). Est-ce aussi le cas si
l’on remplace O par Ω? Et par Θ?

1.2) Montrez que pour toutes fonctions f, g, h ∈ F , si f ∈ Ω(g) et g ∈ Ω(h),


alors f ∈ Ω(h). Est-ce aussi le cas si l’on remplace Ω par Θ?

1.3) Montrez que f ∈ Ω(g) ⇐⇒ g ∈ O(f ).

1.4) Ordonnez les fonctions suivantes selon la notation O:

5n2 − n, 3n2 , 4n , 8(n + 2) − 1 + 9n, n!, n3 − n2 + 7, n log n, 1000000, 2n .

1.5) Dites si 3n ∈ Θ(2n ).


déf
1.6) Montrez que la relation R ⊆ F × F définie par R(f, g) ⇐⇒ f ∈ Θ(g)
est une relation d’équivalence. Est-ce aussi le cas si l’on remplace Θ par
O ou Ω?
déf déf
1.7) Comparez f (n) = n2 et g(n) = 2n à l’aide de la règle de la limite.

1.8) Montrez que loga n ∈ Θ(logb n) pour tous a, b ∈ N≥2 .


( )
1.9) Montrez que nd ∈ Θ(nd ) pour tout d ∈ N.
∑n
1.10) Montrez que i=1 id ∈ Θ(nd+1 ) pour tout d ∈ N.

1.11) Montrez que nd ∈ O(n!) pour tout d ∈ N≥2 .



1.12) Montrez que log n ∈ O( d n) et (log n)d ∈ O(n) pour tout d ∈ N>0 .
√ √
1.13) ⋆ Montrez que log n ∈ O( d n) et d n ̸∈ O(log n) pour tout d ∈ N>0 .

1.14) ⋆ Soient f (n) = n et g(n) = 2⌊log n⌋ . Montrez que f ∈ Θ(g), mais que la
déf déf

limite limn→∞ f (n)/g(n) n’existe pas. Autrement dit, la règle de la limite


ne peut pas toujours être appliquée. (tiré de [BB96, p. 85])

1.15) Soit f (m, n) = mn


2 + 3m log(n · 2n ) + 7n. Montrez que f ∈ O(mn).

1.16) Quel est le temps d’exécution de l’algorithme 4 dans le meilleur cas?

1.17) Nous avons vu qu’il est possible de déterminer si une séquence possède
une valeur majoritaire en temps Θ(n2 ) et Θ(n). Donnez un algorithme
intermédiaire qui résout ce problème en temps O(n log n), en supposant
que vous ayez accès à une primitive qui permet de trier une séquence en
temps O(n log n).
CHAPITRE 1. ANALYSE DES ALGORITHMES 33

1.18) Démontrez la proposition 14 par induction sur i. Comme cas de base,


considérez la première fois que le tant que est atteint. Pour l’étape d’in-
duction, supposez que l’invariant soit vrai et montrez qu’il est préservé
après l’exécution du corps de la boucle.

1.19) Argumentez que l’algorithme 8 fonctionne en temps Θ(n).

1.20) ⋆ Montrez que l’algorithme ci-dessous termine (sur toute entrée).


(basé sur une question d’Etienne D. Massé, A2019)

Entrées : n ∈ N>0
Sorties : vrai
tant que n est pair
n ← n + (n ÷ 2)
retourner vrai
2
Tri

Dans ce chapitre, nous traitons d’un problème fondamental en algorithmique:


le tri. Celui-ci consiste à ordonner les éléments d’une séquence donnée selon un
certain ordre. Plus formellement, nous dirons que des éléments sont comparables
s’ils proviennent d’un ensemble muni d’un ordre total, par ex. N sous l’ordre
usuel ≤, ou l’ensemble des mots (d’un langage formel) ordonnés sous l’ordre
lexicographique. Nous disons qu’une séquence s de n éléments comparables est
triée si s[1] ≤ s[2] ≤ · · · ≤ s[n]. Un algorithme de tri est un algorithme dont la
pré-condition et la post-condition correspondent à:
D = {s : s est une séquence d’éléments comparables},
φ(s, t) = t est triée et t est une permutation de s.
Autrement dit, un algorithme de tri prend une séquence s d’éléments compa-
rables en entrée, et produit une séquence triée t dont les éléments sont ceux de
s, mais dans un ordre (possiblement) différent. Dans le reste du chapitre, nous
présentons quelques algorithmes de tri répandus.

2.1 Approche générique


Nous débutons par une approche générique qui permet de trier une séquence à
l’aide d’une seule opération. Celle-ci est utilisée implicitement ou explicitement
par essentiellement tous les algorithmes de tri 1 . Pour toute séquence s de n
éléments comparables, posons:
déf
inv(s) = {(i, j) ∈ [n]2 : i < j et s[i] > s[j]}.
Autrement dit, inv(s) est l’ensemble des inversions de s, c’est-à-dire les paires
d’indices dont le contenu est mal ordonné. Nous présentons un algorithme de
tri simple: tant que s possède une inversion (i, j), le contenu de s[i] et s[j] sont
intervertis. Cette procédure est décrite à l’algorithme 9.

1. Sauf pour quelques exceptions, tels les algorithmes loufoques comme « bogosort ».

34
CHAPITRE 2. TRI 35

Algorithme 9 : Algorithme générique de tri.


Entrées : séquence s d’éléments comparables
Résultat : s est triée
1 tant que ∃(i, j) ∈ inv(s)
2 s[i] ↔ s[j] // inverser s[i] et s[j]

Exemple.

Considérons la séquence s = [30, 50, 10, 40, 20]. Ses inversions sont

inv(s) = {(1, 3), (1, 5), (2, 3), (2, 4), (2, 5), (4, 5)}.

En corrigeant l’inversion (1, 3), nous obtenons la séquence s′ = [10, 50, 30,
40, 20] dont les inversions sont:

inv(s′ ) = {(2, 3), (2, 4), (2, 5), (3, 5), (4, 5)}.

Remarquons que bien que des inversions aient disparues, une nouvelle
inversion est aussi apparue. Toutefois, le nombre d’inversions a diminué.
En corrigeant l’inversion (2, 5) de s′ , nous obtenons la séquence s′′ =
[10, 20, 30, 40, 50] qui ne possède aucune inversion, c.-à-d. inv(s′′ ) = ∅.
Observons que s′′ est à la fois triée et une permutation de s. Nous avons
donc bel et bien trié s.

Intuitivement, l’algorithme 9 fonctionne car il y a une forme de progrès à


chaque itération: la séquence est « de plus en plus triée ». En effet, bien que
des inversions puissent être créées, le nombre d’inversions diminue à chaque
itération, et ce peu importe l’ordre dans lequel les inversions sont corrigées.
Plus formellement:
Proposition 16. Soit s′ la séquence obtenue à partir d’une séquence s après
l’exécution de la ligne 2 de l’algorithme 9. Nous avons |inv(s)| > |inv(s′ )|.

Démonstration. Soit n la taille de s et soit f la fonction telle que:


{
déf 1 si (x, y) ∈ inv(s),
f (x, y) =
0 sinon.

Nous définissons f ′ de la même façon pour s′ . Autrement dit, f (x, y) indique


si (x, y) est une inversion dans s, et f ′ (x, y) indique la même chose pour s′ .
Nous devons vérifier que le nombre d’inversions a diminué en passant de s
vers s′ . Puisque l’inversion (i, j) a été corrigée, nous avons:

f (i, j) = 1 > 0 = f ′ (i, j). (2.1)


CHAPITRE 2. TRI 36

déf
Posons X = [n]\{i, j}. Puisque le contenu de s aux positions X n’a pas changé,
nous avons:

f (x, y) = f ′ (x, y) pour tous x, y ∈ X. (2.2)

Il suffit donc de vérifier que le nombre d’inversions entre X et {i, j} n’a


pas augmenté. Pour chaque position de X, il y a trois cas possibles: elle est
plus petite que i et j, comprise entre i et j, ou plus grande que i et j. Plus
informellement: elle se situe à « gauche », au « centre » ou à « droite », que
nous dénotons:
déf déf déf
G = {x ∈ [n] : x < i}, C = {x ∈ [n] : i < x < j}, D = {x ∈ [n] : j < x}.

Gauche. Soit x ∈ G. Informellement, puisque les éléments à droite de la po-


sition x sont demeurés les mêmes (mais dans un ordre différent), le nombre
d’inversions avec i et j n’a pas changé. Plus formellement, nous avons (x, i) ∈
inv(s) ⇐⇒ (x, j) ∈ inv(s′ ) et (x, j) ∈ inv(s) ⇐⇒ (x, i) ∈ inv(s′ ). Ainsi:

f (x, i) + f (x, j) = f ′ (x, i) + f ′ (x, j) pour tout x ∈ G. (2.3)

Droite. Par un raisonnement symmétrique au cas précédent, nous obtenons:

f (i, x) + f (j, x) = f ′ (i, x) + f ′ (j, x) pour tout x ∈ D. (2.4)

Centre. Soit x ∈ C. Observons d’abord qu’il est impossible que s[i] ≤ s[x] ≤
s[j] puisque s[i] > s[j]. Il y a donc trois ordonancemments possibles:

Avant Après
Ord. dans s f (i, x) + f (x, j) Ord. dans s′ f ′ (i, x) + f ′ (x, j)

s[i] > s[x] ≤ s[j] 1 s [i] ≥ s[x] < s[j] 1 ou 0
s[i] ≤ s[x] > s[j] 1 s[i] < s[x] ≥ s[j] 1 ou 0
s[i] > s[x] > s[j] 2 s[i] < s[x] < s[j] 0

Cela démontre que:

f (i, x) + f (x, j) ≥ f ′ (i, x) + f ′ (x, j) pour tout x ∈ C. (2.5)

déf
Fin de la preuve. Posons P = {(x, y) ∈ [n]2 : |X ∩ {x, y}| = 1}. Autrement
dit, P contient les paires de positions où précisément une position appartient
CHAPITRE 2. TRI 37

à X et l’autre à {i, j}. En combinant nos observations, nous concluons que:



|inv(s)| = f (x, y)
(x,y)∈[n]2
x<y
∑ ∑
= f (i, j) + f (x, y) + f (x, y)
(x,y)∈X 2 (x,y)∈P
x<y x<y
∑ ∑
> f ′ (i, j) + f (x, y) + f (x, y) (par (2.1))
2 (x,y)∈P
(x,y)∈X
x<y x<y
∑ ∑
= f ′ (i, j) + f ′ (x, y) + f (x, y) (par (2.2))
2 (x,y)∈P
(x,y)∈X
x<y x<y
∑ ∑
≥ f ′ (i, j) + f ′ (x, y) + f ′ (x, y) (par (2.3)–(2.5))
2 (x,y)∈P
(x,y)∈X
x<y x<y

= f ′ (x, y)
(x,y)∈[n]2
x<y

= |inv(s′ )|.

Théorème 2. L’algorithme 9 est correct et termine sur toute entrée. De plus,


le nombre d’itérations effectuées par l’algorithme appartient à O(n2 ).

Démonstration. Soit s une séquence de n éléments comparables. Prouvons


d’abord que l’algorithme termine sur entrée s. Soit ti la séquence obtenue après
i exécutions de la boucle en débutant à partir de s. Supposons que l’algorithme
ne termine pas. Par la proposition 16, nous avons |inv(t0 )| > |inv(t1 )| > · · · ce
qui est impossible puisque |inv(ti )| ne peut pas décroître sous 0. Ainsi, il y a
contradiction et l’algorithme termine après k itérations pour un certain k ∈ N.
Remarquons que les éléments de tk et s sont les mêmes (dans un ordre
possiblement différent) puisque tk a été obtenue en permutant les éléments de
s. Ainsi, afin de montrer que l’algorithme est correct, il suffit de montrer que
tk est triée. Supposons que ce ne soit pas le cas. Il existe donc des indices
i, j ∈ [n] tels que i < j et tk [i] > tk [j]. La condition de la boucle est donc
satisfaite et ainsi l’algorithme ne termine pas à l’itération k. Nous obtenons
donc une contradiction, ce qui implique que tk est bien triée.
Puisque le nombre d’inversions diminue à chaque itération et que s possède
au plus (n − 1) + . . . + 1 + 0 = n(n − 1)/2 inversions, l’algorithme effectue O(n2 )
itérations.
CHAPITRE 2. TRI 38

2.2 Tri par insertion


Le tri par insertion trie une séquence s en construisant un préfixe trié de s de
plus en plus grand:
— on débute avec i = 1;
— on considère s[1 : i − 1] comme étant triée (trivialement vrai au départ);
— on insère s[i] à l’endroit qui rend s[1 : i] triée;
— on répète le processus en incrémentant i jusqu’à n.
Cette procédure est décrite sous forme de pseudocode à l’algorithme 10

Algorithme 10 : Algorithme de tri par insertion.


Entrées : séquence s de n éléments comparables
Sorties : séquence s triée
i←1
tant que i ≤ n
j←i // insérer s[i] dans s[1 : i] en corrigeant
tant que j > 1 ∧ s[j − 1] > s[j] // les inversions
s[j − 1] ↔ s[j]
j ←j−1
i←i+1
retourner s

On peut démontrer que le tri par insertion est correct en observant que
l’invariant « s[1 : i − 1] est triée » est satisfait chaque fois que l’on atteint la
boucle principale, ce qui résulte en « s[1 : (n + 1) − 1] = s[1 : n] = s est triée »
à la sortie de la boucle principale.
Analysons le temps de calcul de l’algorithme. Si la condition de la boucle
interne n’est jamais satisfaite, alors le temps d’exécution appartient à Ω(n).
Cela se produit lorsque s est déjà triée. Ainsi, le temps d’exécution dans le
meilleur cas appartient à Θ(n). Si la condition de la boucle interne est satisfaite
un nombre maximal de fois, c.-à-d. i − 1 fois, alors l’algorithme fonctionne en
temps:
( n ) (n−1 )
∑ ∑
O (i − 1) = O i = O(n(n − 1)/2) = O(n2 /2 − n/2) = O(n2 ).
i=1 i=1

Cela se produit lorsque les éléments de s sont en ordre décroissant. En effet,


l’insertion de s[i] dans s[1 : i] requiert i − 1 tours de la boucle interne, puisque
tous les éléments à sa gauche lui sont supérieurs. Ainsi, le temps d’exécution
dans le pire cas appartient à Θ(n2 ). Cette analyse se raffine de la façon suivante:
Proposition 17. Le tri par insertion fonctionne en temps Θ(n + k) où k est
le nombre d’inversions de la séquence en entrée.
CHAPITRE 2. TRI 39

Puisqu’il n’y a aucune inversion dans une séquence triée, et n(n − 1)/2 in-
versions dans une séquence dont les éléments apparaissent en ordre décroissant,
la proposition 17 donne bien Θ(n) et Θ(n2 ) pour ces deux cas.
La proposition 17 montre notamment que le tri par insertion est un bon
choix pour les séquences quasi-triées. De plus, le tri par insertion performe
généralement bien en pratique sur les séquences de petite taille.

2.3 Tri par monceau


Le tri par monceau s’appuie sur la structure de données connue sous le nom de
monceau (ou de tas), qui offre les opérations suivantes:
— transformer une séquence en un monceau;
— retirer un élément de valeur minimale;
— insérer un élément.
Une implémentation décente de ces opérations offre une complexité de Θ(n),
Θ(log n) et Θ(log n) dans le pire cas, respectivement, où n dénote le nombre
d’éléments.
Grâce à ces opérations, il est possible de trier une séquence s en la trans-
formant en monceau, puis en retirant ses éléments itérativement. Puisque le
retrait donne toujours un élément minimal, les éléments de s sont retirés en
ordre croissant, ce qui permet de construire une séquence triée t. Cette procé-
dure est décrite sous forme de pseudocode à l’algorithme 11.

Algorithme 11 : Algorithme de tri par monceau.


Entrées : séquence s d’éléments comparables
Sorties : séquence s triée
t ← []
transformer s en monceau
tant que |s| > 0
retirer x ∈ s
ajouter x à t
retourner t

La correction du tri par monceau est immédiate (en supposant que le mon-
ceau est lui-même bien implémenté). Observons que la boucle est itérée préci-
sément n fois. Ainsi, le temps d’exécution dans le pire cas appartient à

Θ(1 + n + n(log n + 1)) = Θ(1 + n + n log n + n) = Θ(n log n).

Cet algorithme s’avère donc plus rapide que le tri par insertion sur les séquences
de grande taille.
CHAPITRE 2. TRI 40

Remarque.

Selon l’implémentation du monceau, le temps dans le meilleur cas peut


appartenir à Θ(n). En effet, si tous les éléments de s sont égaux, alors le
retrait d’un élément se fait en temps Θ(1), ce qui donne Θ(n) au total.

2.4 Tri par fusion


Le tri par fusion (ou tri fusion) trie une séquence s grâce aux règles suivantes:
— on découpe s en sous-séquences x et y, c.-à-d. s = x + y;
— on trie x et y séparément;
— on fusionne les deux sous-séquences triées afin qu’elles soient collective-
ment triées.
On découpe généralement s en deux: x = s[1 : m] et y = s[m + 1 : n], où n est la
taille de s et m = n ÷ 2. Cette approche peut sembler circulaire puisqu’il faut
savoir trier afin de trier. Cependant, puisque t et u sont de taille inférieure à n,
il suffit de les trier récursivement jusqu’à l’obtention de séquences de taille ≤ 1,
qui sont trivialement triées. La fusion s’effectue en sélectionnant itérativement
le plus petit élément de x et y, en gardant des pointeurs i et j vers leur plus
petit élément respectif. Cette procédure est décrite sous forme de pseudocode
à l’algorithme 12.

Algorithme 12 : Algorithme de tri par fusion.


Entrées : séquence s d’éléments comparables
Sorties : séquence s triée
trier(s):
fusion(x, y ): // fusionne deux séq. triées
i ← 1; j ← 1; z ← [ ]
tant que i ≤ |x| ∧ j ≤ |y|
si x[i] ≤ y[j] alors
ajouter x[i] à z
i←i+1
sinon
ajouter y[j] à z
j ←j+1
retourner z + x[i : |x|] + y[j : |y|]
si |s| ≤ 1 alors retourner s
sinon
m ← |s| ÷ 2
retourner fusion(trier(s[1 : m]), trier(s[m + 1 : |s|]))
CHAPITRE 2. TRI 41

La sous-routine fusion fonctionne en temps Θ(|x| + |y|) puisqu’elle itère


exactement une fois sur chacun des éléments de x et y. Le temps d’exécution
de trier s’avère difficile à analyser puisque s n’est généralement pas décou-
pée parfaitement en deux. Afin de simplifier l’analyse, supposons que fusion
effectue au plus c · (|x| + |y|) opérations pour une certaine constante c ∈ R>0 ,
et que |s| soit une puissance de 2. Nous avons:
{
( k) 1 si k = 0,
t 2 ≤ ( k−1 )
2·t 2 + c · 2 si k > 0.
k

Proposition 18. t(2k ) ≤ 2k (ck + 1) pour tout k ∈ N>0 .

Démonstration. Procédons par induction sur k. Si k = 1, alors t(21 ) ≤ 2+2c =


2(c + 1) par définition de t. Soit k ∈ N>0 . Supposons que l’énoncé soit vrai
pour k. Nous devons montrer que t(2k+1 ) ≤ 2k+1 (c(k + 1) + 1). Nous avons:

t(2k+1 ) ≤ 2 · t(2k ) + c · 2k+1 (par définition de t)


≤ 2 · (2k (ck + 1)) + c · 2k+1 (par hypothèse d’induction)
=2 k+1
(ck + 1) + c · 2 k+1

k+1
=2 (ck + 1 + c)
k+1
=2 (c(k + 1) + 1).
Ainsi, lorsque n est de la forme n = 2k , nous avons t(n) ≤ 2k (ck + 1) =
n(c log n + 1) = c · n log n + n. Informellement, nous concluons donc que:
t ∈ O(n log n : n est une puissance de 2).
Nous verrons dans un chapitre subséquent que t ∈ O(n log n) sans se restreindre
aux puissances de 2.

2.5 Tri rapide


Le tri rapide (ou « quicksort » en anglais) trie une séquence s à l’aide des règles
suivantes:
— on choisit un élément pivot s[i];
— on divise s en une séquence t qui contient tous les éléments inférieurs
au pivot, et une séquence u qui contient tous les éléments supérieurs ou
égaux au pivot;
— on trie t et u récursivement.
Il existe plusieurs façons de choisir le pivot, et d’autres variantes du découpage
de s. Par exemple, une implémentation simple choisit le pivot aléatoirement,
et découpe s en trois parties tel que décrit à l’algorithme 13.
La description du tri rapide donnée à l’algorithme 13 nécessite une quantité
linéaire de mémoire auxiliaire. Il est possible d’utiliser une quantité constante
de mémoire en modifiant directement s comme à l’algorithme 14.
CHAPITRE 2. TRI 42

Algorithme 13 : Algorithme de tri rapide (implémentation simple).


Entrées : séquence s d’éléments comparables
Sorties : séquence s triée
trier(s):
si |s| = 0 alors retourner s
sinon
choisir pivot ∈ s
gauche ← [x ∈ s : x < pivot]
milieu ← [x ∈ s : x = pivot]
droite ← [x ∈ s : x > pivot]
retourner trier(gauche) + milieu + trier(droite)

Algorithme 14 : Algorithme de tri rapide (implémentation sur place


avec partition de Lomuto).
Entrées : séquence s d’éléments comparables
Sorties : séquence s triée
trier(s):
partition(lo, hi): // partionne s autour de x = s[hi] et
x ← s[hi]; i ← lo // retourne le nouvel index i de x
pour j ← lo, . . . , hi
si s[j] < x alors
s[i] ↔ s[j]
i←i+1
s[i] ↔ s[hi]
retourner i
trier'(lo, hi):
si lo < hi alors
pivot ← partition(lo, hi)
trier'(lo, pivot − 1) // Trier le côté gauche
trier'(pivot + 1, hi) // Trier le côté droit

trier'(1, |s|)
retourner s

Dans le pire cas, le temps d’exécution du tri rapide appartient à Θ(n2 ),


et ce indépendamment de l’implémentation. Toutefois, en choisissant un pivot
aléatoirement ou avec une bonne heuristique, le temps d’exécution est réduit
à Θ(n log n) en moyenne (avec une faible constante multiplicative), ce qui le
rend attrayant en pratique.
CHAPITRE 2. TRI 43

2.6 Propriétés intéressantes


Nous introduisons deux notions intéressantes concernant les algorithmes de
tri. Nous disons qu’un algorithe de tri fonctionne sur place s’il n’utilise pas de
séquence auxiliaire, et nous disons qu’il est stable s’il préserve l’ordre relatif des
éléments égaux. Plus formellement, considérons un algorithme de tri A. Nous
écrivons σs afin de dénoter la permutation telle que σs (i) est la position de s[i]
dans la sortie de A sur entrée s. Nous disons que A est stable s’il satisfait:

∀s ∀i < j (s[i] = s[j]) → (σs (i) < σs (j)).

Par exemple, soit la séquence s = [ 5 , 2 , 1 , 2 ] dont nous cherchons à ordon-


ner les éléments selon leur valeur numérique. Un algorithme qui produirait la
sortie [ 1 , 2 , 2 , 5 ] ne serait pas stable puisque l’ordre relatif des éléments
2 et 2 a été altéré.
Les propriétés satisfaites par un algorithme de tri dépendent de son implé-
mentation. Voici un sommaire des propriétés typiquement satisfaites par les
algorithmes présentés jusqu’ici. Notons qu’elles ne sont pas nécessairement sa-
tisfaites simultanément par une seule implémentation; et qu’il faut donc consi-
dérer ce sommaire avec précaution:

Complexité (par cas)


Algorithme Sur place Stable
meilleur moyen pire
insertion Θ(n) Θ(n2 ) Θ(n2 ) 3 3
monceau Θ(n) Θ(n log n) Θ(n log n) 3 7
fusion Θ(n log n) Θ(n log n) Θ(n log n) 7 3
rapide Θ(n) Θ(n log n) Θ(n2 ) 3 7

Remarque.

La plupart des langages de programmation utilisent le tri par insertion


pour les séquences de petite taille; le tri rapide ou le tri par monceau
pour les séquences de grande taille; et le tri par fusion pour les séquences
de grande taille lorsque la stabilité est requise. Souvent, une combinaison
de ceux-ci est utilisée, par ex. « Timsort » qui combine tris par fusion et
par insertion, et « introsort » qui combine tris rapide et par monceau.

2.7 Tri sans comparaison


Tous les algorithmes de tri présentés jusqu’ici trient en comparant les éléments
de la séquence en entrée. Il est possible de démontrer que tout algorithme de
ce type fonctionne en temps Ω(n log n) dans le pire cas. Il existe cependant
des algorithmes de tri qui fonctionnent plus rapidement pour certains types de
CHAPITRE 2. TRI 44

données. Par exemple, considérons la séquence [7, 3, 5, 1, 2]. En représentation


binaire, celle-ci correspond à:

s = [111, 011, 101, 001, 010].

Réorganisons s sous la forme s = lo + hi, où lo contient les séquences qui


terminent par 0, et hi celles qui terminent par 1 (en préservant l’ordre relatif
des éléments à l’intérieur de lo et hi). Nous obtenons:

s′ = [|{z}
010 , 111, 011, 101, 001].
| {z }
lo hi

En répétant ce processus, mais maintenant en considérant l’avant-dernier bit,


nous obtenons:
s′′ = [101, 001, 010, 111, 011].
| {z } | {z }
lo hi

En répétant une dernière fois, en considérant le premier bit, nous obtenons:

s′′′ = [001, 010, 011, 101, 111].


| {z } | {z }
lo hi

Remarquons que s′′′ correspond numériquement à la séquence triée [1, 2, 3, 5, 7].

Algorithme 15 : Algorithme de tri sans comparaison


Entrées : séquence s de n séquences de m bits
Résultat : s est triée
1 pour j ← m, . . . , 1
2 lo ← [ ]; hi ← [ ]
3 pour i ← 1, . . . , n
4 si s[i][j] = 0 alors
5 ajouter s[i] à lo
6 sinon
7 ajouter s[i] à hi
8 s ← lo + hi
9 retourner s

Cette procédure, décrite sous forme de pseudocode à l’algorithme 15, est


une variante du tri radix. Son temps d’exécution appartient à Θ(mn) dans le
meilleur et pire cas. Lorsque n est grand et que m est une constante, cela donne
un tri qui fonctionne en temps Θ(n). Cela surpasse la barrière du Ω(n log n),
ce qui n’est pas contradictoire, car cet algorithme n’est pas basé sur la compa-
raison d’éléments et se limite à un type spécifique de données.
CHAPITRE 2. TRI 45

2.8 Exercices
2.1) Donnez un algorithme qui détermine si une séquence s possède au moins
une inversion, et en retourne une le cas échéant. Votre algorithme doit
fonctionner en temps O(|s|).

2.2) Une séquence binaire est une séquence dont chaque élément vaut 0 ou 1,
par ex. s = [0, 1, 0, 0, 1, 0, 1]. Donnez un algorithme qui trie des séquences
binaires en temps linéaire avec une quantité constante de mémoire auxi-
liaire. Autrement dit, donnez un algorithme sur place. Votre algorithme
est-il stable?

2.3) ⋆ Donnez un algorithme itératif pour le problème précédent qui n’utilise


aucun branchement (explicite ou implicite), à l’exception de la boucle
principale (donc pas de si, min, max, etc. et au plus une boucle pour,
tant que, faire ... tant que, etc.) Votre algorithme doit fonctionner en
temps linéaire et sur place.

2.4) Donnez un algorithme qui regroupe les éléments égaux d’une séquence s
de façon contigüe. Par exemple, [a, a, c, b, b] et [c, a, a, b, b] sont des re-
groupements valides de s = [b, a, a, c, b]. Votre algorithme doit fonction-
ner en temps O(|s|2 ). L’algorithme peut utiliser les comparaisons {=, ̸=},
mais pas {<, ≤, ≥, >}.

2.5) Donnez un algorithme qui détermine si une séquence s est un regroupe-


ment (au sens de la question précédente). Est-ce que la possibilité d’uti-
liser {<, ≤, ≥, >} fait une différence?

2.6) Considérez une pile de n crêpes de diamètres différents, où les crêpes sont
numérotées de 1 à n du haut vers le bas. Vous pouvez réorganiser la pile
à l’aide d’une spatule en l’insérant sous une crêpe k, puis en renversant
l’ordre des crêpes 1 à k. Donnez un algorithme qui trie les crêpes avec
O(n) renversements. Vous avez accès au diamètre de chaque crêpe.

2.7) Décrivez une façon de rendre tout algorithme de tri stable.

2.8) Décrivez une version améliorée du tri par insertion qui utilise la recherche
dichotomique afin d’accélérer l’insertion. Le temps d’exécution dans le
pire cas appartient-il toujours à Θ(n2 )?

2.9) Déterminez pourquoi le temps d’exécution du tri rapide appartient à


Θ(n2 ) dans le pire cas

2.10) ⋆ Implémentez le tri par fusion de façon purement itérative; donc sans
récursion et sans émulation de la récursion à l’aide d’une pile.

2.11) ⋆ Démontrez la proposition 17.


3
Graphes

3.1 Graphes non dirigés


Un graphe non dirigé est une paire G = (V, E) d’ensembles (finis ou non) tels
que E ⊆ {{u, v} : u, v ∈ V, u ̸= v}. Nous appelons V et E respectivement
l’ensemble des sommets et des arêtes de G. Si V est fini, nous disons que G
est fini, et sinon qu’il est infini. Par défaut, nous supposerons qu’un graphe
est fini. Nous écrivons u − → v afin d’indiquer que {u, v} ∈ E. Observons que
u −→ v ⇐⇒ v − → u. Deux sommets u et v sont adjacents si {u, v} ∈ E.
Nous disons que v est un voisin de u, si u et v sont adjacents. Le degré d’un
sommet u, dénoté deg(u), correspond à son nombre de voisins. Remarquons
( )
qu’un graphe non dirigé possède entre 0 et |V2 | ∈ Θ(|V |2 ) arêtes.

b e

a c d

Figure 3.1 – Exemple de graphe (fini) non dirigé.

Exemple.

Considérons le graphe non dirigé


G = ({a, b, c, d, e}, {{a, b}, {a, c}, {b, e}, {c, d}, {c, e}, {d, e}})
représenté graphiquement à la figure 3.1. Ce graphe possède 5 sommets
et 6 arêtes. Les voisins du sommet c sont {a, e, d}. Nous avons deg(a) =
deg(b) = deg(d) = 2 et deg(c) = deg(e) = 3.

46
CHAPITRE 3. GRAPHES 47

3.2 Graphes dirigés


Un graphe dirigé est une paire G = (V, E) d’ensembles (finis ou non) tels
que E ⊆ {(u, v) ∈ V × V : u ̸= v}. Comme pour les graphes non dirigés,
nous appelons V et E respectivement l’ensemble des sommets et des arêtes
de G, et nous considérerons les graphes finis par défaut. Nous écrivons u −
→v
afin d’indiquer que (u, v) ∈ E. Lorsque u − → v, nous disons que u est un
prédecesseur de v, et que v est un successeur de u. Le degré entrant d’un
sommet u, dénoté deg− (u), correspond à son nombre de prédecesseurs, et le
degré sortant d’un sommet u, dénoté deg+ (u), correspond à son nombre de
successeurs. Remarquons qu’un graphe dirigé possède entre 0 et |V |·(|V |−1) ∈
Θ(|V |2 ) arêtes. De plus, nous avons:
∑ ∑
|E| = deg− (v) = deg+ (v).
v∈V v∈V

Exemple.

Considérons le graphe dirigé

G = ({a, b, c, d, e}, {(a, b), (b, c), (c, a), (c, d), (d, b), (d, c), (d, e)})

représenté graphiquement à la figure 3.2. Ce graphe possède 5 sommets


et 7 arêtes. Nous avons a −
→ b, mais pas b −
→ a. De plus, nous avons:

deg− (a) = deg− (d) = deg− (e) = 1, deg+ (a) = deg+ (b) = 1,
deg− (b) = deg− (c) = 2, deg+ (c) = 2,
deg+ (d) = 3,
deg+ (e) = 0.

a b

c d e

Figure 3.2 – Exemple de graphes fini dirigé.


CHAPITRE 3. GRAPHES 48

Remarque.

Certains ouvrages font la distinction entre sommets et arêtes pour les


graphes non dirigés, et noeuds et arcs pour les graphes dirigés. Nous ne
ferons pas cette distinction.

3.3 Chemins et cycles


Soit G = (V, E) un graphe. Un chemin C, allant d’un sommet u vers un sommet
v, est une séquence finie de sommets

C = [v0 , v1 , . . . , vk ]

telle que v0 = u, vk = v, et vi−1 − → vi pour tout i ∈ [k]. Nous disons qu’un


chemin est simple s’il ne répète aucun sommet. La longueur d’un tel chemin
déf ∗
correspond à |C| = k. Nous écrivons u − → v afin d’indiquer qu’il existe un

chemin de u vers v. Observons que u − → u pour tout sommet u. Un cycle est
un chemin d’un sommet vers lui-même. Nous disons qu’un cycle est simple s’il
ne répète aucun sommet, à l’exception du sommet de départ qui apparaît au
début et à la fin, et n’utilise pas deux fois la même arête 1 . Autrement dit, un
cycle est simple s’il ne contient pas d’autres cycles. Nous disons qu’un graphe
est acyclique s’il ne possède aucun cycle.

Exemple.

Le chemin a − →b− →e− →d− →c− → a du graphe de la figure 3.1 est un


cycle simple de a vers a. Le chemin a −→ b − → c −
→ d −→ e du graphe
de la figure 3.2 est un chemin simple de longueur 4 qu’on ne peut pas
étendre.

3.4 Sous-graphes et connexité


Soit G = (V, E) un graphe. Un sous-graphe de G est un graphe G ′ = (V ′ , E ′ ) tel
que V ′ ⊆ V , E ′ ⊆ E et n’utilise que des sommets de V ′ . Le sous-graphe induit
par un ensemble de sommets V ′ ⊆ V est le sous-graphe G[V ′ ] = (V ′ , E ′ ) où
déf

E contient toutes les arêtes de E dont les sommets appartiennent à V ′ .




Si u −→ v pour tous u, v ∈ V , nous disons que G est connexe s’il est non
dirigé, et fortement connexe s’il est dirigé. Une composante connexe d’un graphe
non dirigé est un sous-graphe connexe maximal. Une composante fortement
connexe d’un graphe dirigé est un sous-graphe fortement connexe maximal.

1. Cette deuxième contrainte est redondante pour les graphes dirigés, mais elle empêche
de considérer u −
→v−→ u comme étant un cycle simple pour les graphes non dirigés.
CHAPITRE 3. GRAPHES 49

Exemple.

Le graphe G de la figure 3.1 est connexe et ne possède donc qu’une seule


composante connexe, c.-à-d. G lui-même. Le graphe G ′ de la figure 3.2
n’est pas fortement connexe puisqu’il n’existe aucun chemin du sommet
e vers les autres sommets. Les composantes fortement connexes de G ′
sont G ′ [{a, b, c, d}] et G ′ [{e}].

3.5 Représentation

3.5.1 Matrice d’adjacence


Les graphes sont généralement représentés sous forme de matrice ou de liste
d’adjacence. La matrice d’adjacence d’un graphe G est la matrice A définie par:
{
déf 1 si u −
→ v,
A[u, v] =
0 sinon.

Ainsi, A[u, v] indique si G possède une arête du sommet u vers le sommet v.


Observons que dans le cas des graphes non dirigés, nous avons A[u, v] = A[v, u]
pour tous sommets u et v.

Exemple.

Les graphes de la figure 3.1 et de la figure 3.2 sont représentés respecti-


vement par les matrices d’adjacence:

a b c d e a b c d e
   
a 0 1 1 0 0 a 0 1 0 0 0
b
1 0 0 0 1 b
0 0 1 0 0
c
1 0 0 1 1 et c
1 0 0 1 0.
d0 0 1 0 1 d0 1 1 0 1
e 0 1 1 1 0 e 0 0 0 0 0

3.5.2 Liste d’adjacence


Une liste d’adjacence d’un graphe G = (V, E) est une séquence adj telle que
déf
adj[u] = [v ∈ V : u −→ v] pour tout sommet u. Autrement dit, adj[u] dénote
l’ensemble des voisins de u si G est non dirigé, ou l’ensemble des successeurs de
u si G est dirigé. Une liste d’adjacence s’implémente généralement sous forme
de tableau lorsque V = {1, 2, . . . , n}, et de tableau associatif sinon. À l’interne,
chaque entrée adj[u] se représente sous forme de liste.
CHAPITRE 3. GRAPHES 50

Exemple.

Les graphes de la figure 3.1 et de la figure 3.2 sont représentés respecti-


vement par les listes d’adjacence:

adj[a] = [b, c] adj ′ [a] = [b]


adj[b] = [a, e] adj ′ [b] = [c]
adj[c] = [a, d, e] adj ′ [c] = [a, d]
adj[d] = [c, e] adj ′ [d] = [b, c, e]
adj[e] = [b, c, d], et adj ′ [e] = [ ].

3.5.3 Complexité des représentations


Le tableau suivant dresse un sommaire de la complexité de l’identification des
voisins, successeurs et prédecesseurs d’un sommet, l’ajout et le retrait d’arêtes,
ainsi que de la mémoire utilisée par les deux représentations 2 . Puisque nous
utiliserons principalement les graphes comme structures de données statiques,
nous n’entrons pas ici dans le détail des opérations dynamiques comme l’ajout
et le retrait de sommets. Notons également que certaines des complexités ci-
dessous peuvent être réduites à l’aide d’implémentations plus sophistiquées.

Matrice Liste d’adjacence


d’adj. graphe non dirigé graphe dirigé
u−→ v? Θ(1) O(1 + min(deg(u), deg(v))) O(1 + deg+ (u))
{v : u −
→ v} Θ(|V |) O(1 + deg(u)) O(1 + deg+ (u))
{u : u −
→ v} Θ(|V |) O(1 + deg(v)) O(|V | + |E|)
Ajouter u −
→v Θ(1) O(1 + deg(u) + deg(v)) O(1 + deg+ (u))
Retirer u −
→v Θ(1) O(1 + deg(u) + deg(v)) O(1 + deg+ (u))
Mémoire Θ(|V |2 ) Θ(|V | + |E|) Θ(|V | + |E|)

3.6 Accessibilité
Nous considérons deux façons de parcourir un graphe. Plus précisément, étant
donné un sommet de départ u, nous présentons deux approches afin de calculer

l’ensemble des sommets accessibles par u, c.-à-d. l’ensemble {v : u −
→ v}. Cela
permet notamment de déterminer si un sommet cible est accessible à partir
d’un sommet de départ.
2. Nous supposons que l’accès à adj[u] se fait en temps constant dans le pire cas, mais en
pratique ce n’est pas nécessairement le cas lorsque V ̸= {1, 2, . . . , n}. En effet, si les sommets
proviennent d’un domaine plus complexe, alors adj risque d’être implémenté par une table
de hachage qui offre généralement un temps constant amorti.
CHAPITRE 3. GRAPHES 51

3.6.1 Parcours en profondeur


La première approche consiste à débuter au sommet u, de considérer l’un de
ses successeurs, puis de visiter le graphe récursivement le plus profondément
possible, et finalement de rebrousser chemin et répéter avec un autre successeur
de u. Afin d’éviter l’exploration d’un sommet plus d’une fois, nous marquons
progressivement chaque sommet visité. Cette approche est décrite sous forme
de pseudocode à l’algorithme 16.

Algorithme 16 : Parcours en profondeur (version récursive).


Entrées : graphe G = (V, E) et sommet u ∈ V

Résultat : une séquence s = [v ∈ V : u −
→ v]
1 s ← []
2 parcours(x):
3 si x n’est pas marqué alors
4 marquer x
5 pour y ∈ V : x −
→y // explorer voisins/succ.
6 parcours(y )
7 ajouter x à s // ajouter après l'exporation
8 parcours(u)
9 retourner s

Observons que l’algorithme 16 n’explore un sommet qu’au plus une fois:


une fois s’il est accessible à partir du sommet de départ, et aucune autrement.
De plus, chaque sommet visité lance une exploration pour chacun de ses suc-
cesseurs. Ainsi, l’algorithme fonctionne en temps O(|V | + |E|) dans le pire cas.
Comme l’algorithme cherche à explorer le graphe le plus profondément pos-
sible, son implémentation récursive peut consommer une quantité non négli-
geable de mémoire en pratique, selon l’architecture. L’algorithme 17 donne une
description itérative de la même approche, cette fois en utilisant explicitement
une pile. Remarquons que la taille de la pile ne peut pas excéder O(|E|) puisque
chaque arête est explorée au plus une fois. Une légère modification permet de
borner sa taille par O(min(|V |, |E|)).

3.6.2 Parcours en largeur


Une alternative au parcours en profondeur consiste à explorer le graphe en
largeur. Autrement dit, nous débutons au sommet u, puis nous explorons cha-
cun de ses successeurs, puis chacun de leurs successeurs, et ainsi de suite.
Cette approche s’apparente à la version itérative du parcours en profondeur:
on remplace simplement la pile par une file. Nous décrivons cette procédure à
l’algorithme 18.
CHAPITRE 3. GRAPHES 52

Algorithme 17 : Parcours en profondeur (version itérative).


Entrées : graphe G = (V, E) et sommet u ∈ V

Résultat : une séquence s = [v ∈ V : u −
→ v]
1 initialiser une pile P ← [u]
2 s ← []
3 tant que P n’est pas vide
4 dépiler x de P
5 si x n’est pas marqué alors
6 marquer x
7 pour y ∈ V : x −
→y // explorer voisins/succ.
8 empiler y dans P
9 ajouter x à s // ajouter après l'exporation
10 retourner s

Algorithme 18 : Parcours en largeur.


Entrées : graphe G = (V, E) et sommet u ∈ V

Résultat : une séquence s = [v ∈ V : u −
→ v]
1 initialiser une file F ← [u]
2 s ← []
3 tant que F n’est pas vide
4 retirer x de F
5 si x n’est pas marqué alors
6 marquer x
7 pour y ∈ V : x −
→y // explorer voisins/succ.
8 ajouter y à F
9 ajouter x à s // ajouter avant l'exporation
10 retourner s

3.7 Calcul de plus court chemin


Les algorithmes d’exploration de la section précédente permettent de détermi-

ner si u −→ v étant donné deux sommets u et v. Il serait possible d’adapter
légèrement ces algorithmes afin qu’ils construisent explicitement un chemin
simple de u vers v. Le parcours en profondeur aurait tendance à construire des
chemins longs, parfois même de longueur maximale. Cependant, le parcours
en largeur construit toujours un chemin de longueur minimale puiqu’il explore
progressivement les sommets à distance 1, 2, . . . , |V | − 1 du sommet de départ.
L’algorithme 19 présente une adaptation du parcours en largeur qui construit
un chemin de longueur minimale (s’il en existe un). Celle-ci débute au sommet
CHAPITRE 3. GRAPHES 53

de départ u et parcourt le graphe en largeur. Lors de l’exploration d’un nou-


veau sommet y, on stocke le prédecesseur pred[y] qui a mené à son exploration.
Si le sommet cible v est atteint, alors on arrête l’exploration. On reconstruit le
chemin qui mène de u vers v en calculant la séquence [v, pred[v], pred[pred[v]],
. . . , u] qui décrit le chemin inverse. Il suffit donc de renverser cette séquence
afin d’obtenir le chemin minimal. Notons qu’on peut éviter ce renversement en
ajoutant v, pred[v], pred[pred[v]], . . . directement dans une file.

Algorithme 19 : Calcul de plus court chemin.


Entrées : graphe G = (V, E) et sommet u ∈ V

Résultat : un chemin de longueur minimale tel que u −
→ v s’il en
existe un, ou « aucun » sinon
1 si u = v alors retourner [ ] // chemin trivial?
// Parcourir le graphe en largeur
2 initialiser une file F ← [u]
3 trouvé ← faux
4 pred ← [ ]
5 tant que ¬trouvé et F n’est pas vide
6 retirer x de F
7 marquer x
8 pour y ∈ V : x −
→y // explorer voisins/succ.
9 si y n’est pas marqué alors
10 pred[y] ← x // se souvenir d'où on arrive
11 si y = v alors // cible v atteinte?
12 trouvé ← vrai
13 sinon
14 ajouter y à F
// Construire le chemin de u vers v
15 si ¬trouvé alors
16 retourner aucun
17 sinon
18 s ← []
19 x←v
20 tant que x ̸= u // rebrousser chemin de v vers u
21 ajouter x à s
22 x ← pred[x]
23 renverser s
24 retourner s
CHAPITRE 3. GRAPHES 54

3.8 Ordre topologique et détection de cycle


Un ordre topologique d’un graphe dirigé G = (V, E) est une séquence de som-
mets v1 , v2 , . . . , vk qui satisfait:
— V = {v1 , v2 , . . . , vk },
— ∀i, j ∈ [k] : (vi , vj ) ∈ E =⇒ j < i.
Par exemple, considérons le graphe G = (V, E) où V dénote l’ensemble des
cours obligatoires qu’un·e étudiant·e doit suivre dans son parcours universitaire,
et où (u, v) ∈ E indique que le cours u est préalable au cours v. Un ordre
topologique de G décrit un ordre dans lequel cette personne peut s’inscrire
à ses cours. S’il n’existe aucun ordre topologique, alors cela signifie que cette
personne ne pourra jamais graduer! En effet, ce scénario indiquerait qu’il existe
un cycle dans G:
Proposition 19. Un graphe possède un ordre topologique ssi il est acyclique.
La recherche d’un ordre topologique dans un graphe acyclique peut être
vue comme un problème de tri selon l’ordre défini sur les sommets par u ⪯
déf ∗
v ⇐⇒ u − → v. Cependant, comme cet ordre est un ordre partiel, et non un
ordre total, nous ne pouvons pas utiliser les algorithmes de tri du chapitre 2 3 .
De plus, même si cela était possible, évaluer u ⪯ v nécessiterait un temps
linéaire. Nous présentons donc, à l’algorithme 20, une procédure qui calcule un
ordre topologique d’un graphe en temps linéaire, s’il en existe, et qui détecte
la présence d’un cycle autrement.
Cette procédure utilise l’approche suivante:
— on identifie les sommets qui ne dépendent d’aucun sommet, c.-à-d. les
sommets de degré entrant 0;
— on ajoute ces sommets (arbitrairement) à l’ordre topologique;
— on retire implicitement ces sommets du graphe en mettant à jour le degré
entrant de leurs successeurs;
— on recommence le processus tant que possible.
S’il existe un sommet dont le degré entrant ne décroît pas à 0, alors cela signifie
qu’il apparaît sur un cycle.
Analysons le temps d’exécution de l’algorithme 20. Le calcul des degrés en-
trants s’effectue en temps Θ(|V | + |E|), et l’identification des sommets de degré
entrant 0 s’effectue en temps Θ(|V |). L’exploration d’un sommet u requiert un
temps appartenant à O(1 + deg+ (u)). Puisque tous les sommets peuvent être
explorés, le temps total de l’exploration du graphe appartient donc à
( ) ( )
∑ ∑ ∑
O (1 + deg (u)) = O
+
1+ deg (u) = O(|V | + |E|).
+

v∈V v∈V v∈V

Ainsi, le temps total de l’algorithme appartient à O(|V | + |E|).


3. En fait, le calcul d’un ordre toplogique correspond précisément à la construction d’un
ordre total compatible avec ⪯, c.-à-d. une extension linéaire de ⪯.
CHAPITRE 3. GRAPHES 55

Algorithme 20 : Algorithme de Kahn: tri topologique.


Entrées : graphe dirigé G = (V, E)
Résultat : un ordre topologique de G s’il en existe un
// Calcul des degrés entrants
1 d ← [v 7→ 0 : v ∈ V ]
2 pour (u, v) ∈ E
3 d[v] ← d[v] + 1
// Calcul des sommets de degré entrant 0
4 initialiser une file F ← [ ]
5 pour v ∈ V
6 si d[v] = 0 alors ajouter v à F
// Tri topologique
7 ordre ← [ ]
8 tant que F n’est pas vide
9 retirer u de F
10 ajouter u à ordre
11 pour v ∈ V : u −
→v // explorer successeurs
12 d[v] ← d[v] − 1
13 si d[v] = 0 alors ajouter v à F
14 si |ordre| = |V | alors retourner ordre
15 sinon retourner cycle détecté

3.9 Arbres
Nous disons qu’un graphe non dirigé est acyclique s’il ne possède pas de cycle
simple. Une forêt est un graphe non dirigé acyclique et un arbre est une forêt
connexe. Les arbres peuvent être caractérisés de plusieurs façons équivalentes:
Proposition 20. Soit G = (V, E) un graphe non dirigé. Les propriétés sui-
vantes sont toutes équivalentes:
— G est connexe et acyclique;
— G est connexe et |E| = |V | − 1;
— G est acyclique et |E| = |V | − 1.
Nous disons qu’un sommet v d’une forêt est une feuille si deg(v) = 1, et un
sommet interne sinon. Nous disons qu’un arbre est une arborescence s’il possède
un sommet spécial r appelé sa racine. Dans ce cas, nous ne considérons pas r
comme étant une feuille, même lorsque deg(r) = 1.
Un arbre couvrant d’un graphe non dirigé G est un sous-graphe de G qui
contient tous ses sommets et qui est un arbre. Un tel arbre correspond à
une façon de relier tous les sommets de G avec le moins d’arêtes possibles.
Par exemple, la figure 3.3 identifie un arbre couvrant. Tout graphe non dirigé
CHAPITRE 3. GRAPHES 56

connexe possède au moins un arbre couvrant, et il est possible d’en construire


un en adaptant, par exemple, le parcours en profondeur.
Proposition 21. Un graphe non dirigé possède au moins un arbre couvrant
ssi il est connexe.

b d

c e

Figure 3.3 – Exemple d’arbre couvrant identifié par un trait gras de couleur.
CHAPITRE 3. GRAPHES 57

3.10 Exercices
3.1) Nous avons vu comment détecter la présence de cycle dans un graphe
dirigé à l’aide du tri topologique. Donnez un autre algorithme de détection
de cycle basé sur le parcours en profondeur.

3.2) Considérez le scénario suivant: n personnes participent à une fête où les


rafraîchissements sont gratuits. Une personne qui a vu une annonce de
l’événement sur Internet s’est invitée à la fête. Cette personne connaît
tout le monde (via la liste d’invité·e·s affichée en ligne), mais personne ne
la connaît. Aidez les personnes participant à la fête à sauver les stocks
de rafraîchissements en donnant un algorithme qui identifie l’intrus en
temps O(n), étant donné une matrice d’adjacence A de taille n × n, où
A[i, j] indique si la personne i connaît la personne j.

3.3) Identifiez d’autres arbres couvrants du graphe de la figure 3.3.

3.4) Un graphe non dirigé G = (V, E) est dit biparti s’il existe X, Y ⊆ V tels
que X ∩ Y = ∅, X ∪ Y = V , et {u, v} ∈ E =⇒ (u ∈ X ⇐⇒ v ∈ Y ).
Autrement dit, G est biparti si on peut partitionner ses sommets en deux
ensembles X et Y de telle sorte que toute arête possède un sommet dans
X et un sommet dans Y . Donnez un algorithme qui détermine effica-
cement si un graphe est biparti. Analysez sa complexité. Adaptez votre
algorithme afin qu’il calcule une partition X, Y si le graphe est biparti.
Paradigmes

58
4
Algorithmes gloutons

4.1 Arbres couvrants minimaux


Afin d’introduire le concept d’algorithme glouton, nous considérons le problème
de calcul d’arbre couvrant minimal et présentons deux algorithmes (gloutons)
pour ce problème.
Un graphe pondéré est un graphe G = (V, E) dont chaque arête e ∈ E est
étiquetée par un poids p[e], souvent un nombre entier, naturel ou rationnel. Le
déf ∑
poids d’un graphe correspond à p(G) = e∈E p[e].
Un arbre couvrant (de poids) minimal d’un graphe pondéré G est un arbre
couvrant de G dont le poids est minimal parmi tous les arbres couvrants de
G. Par exemple, la figure 4.1 identifie un arbre couvrant de poids 12, ce qui
correspond au poids minimal parmi tous les arbres couvrants du graphe.

2 5
b d f
2

1
a 4 3 2

3
c e g
1 4

Figure 4.1 – Arbre couvrant minimal identifié par un trait gras de couleur.

4.1.1 Algorithme de Prim–Jarník


L’algorithme de Prim–Jarník suit l’approche suivante:
— on débute par E ′ = ∅ et V ′ = {v} où v est un sommet arbitraire;

59
CHAPITRE 4. ALGORITHMES GLOUTONS 60

— on choisit une arête e de plus petit poids qui possède exactement un


sommet appartenant à V ′ ;
— on ajoute e à E ′ et son nouveau sommet à V ′ ;
— on répète jusqu’à ce que V ′ contienne tous les sommets.
Autrement dit, l’algorithme débute par un arbre constitué d’un seul sommet et
le fait croître d’un sommet à la fois en choisissant toujours la plus petite arête
qui ne crée pas de cycle. La figure 4.2 illustre l’exécution de l’algorithme.
2 5 2 5 2 5
b d f b d f b d f
2 2 2

1 1 1
a 4 3 2 a 4 3 2 a 4 3 2

3 3 3
c e g c e g c e g
1 4 1 4 1 4

2 5 2 5 2 5
b d f b d f b d f
2 2 2

1 1 1
a 4 3 2 a 4 3 2 a 4 3 2

3 3 3
c e g c e g c e g
1 4 1 4 1 4

2 5
b d f
2

1
a 4 3 2

3
c e g
1 4

Figure 4.2 – Arbre couvrant minimal obtenu par l’algorithme de Prim–Jarník.

Remarquons que l’algorithme doit déterminer à répétition la plus petite


arête disponible. Afin d’implémenter efficacement cette opération, nous ajou-
tons les arêtes découvertes dans un monceau ordonné par leur poids. Cette
approche est décrite sous forme de pseudocode à l’algorithme 21.
Analysons la complexité de l’algorithme de Prim–Jarník, en supposant l’uti-
lisation d’un monceau binaire. L’initialisation du monceau se fait en temps
O(|E|). Puisque chaque arête ne peut être considérée que deux fois (via chacun
de ses sommets), le temps total des retraits de candidats s’effectue en temps
O(|E| log |E|). Remarquons que le corps du bloc si est exécuté au plus une fois
CHAPITRE 4. ALGORITHMES GLOUTONS 61

Algorithme 21 : Algorithme de Prim–Jarník.


Entrées : graphe non dirigé connexe G = (V, E) pondéré par p
Résultat : un arbre couvrant minimal de G
E′ ← ∅
marquer un sommet v ∈ V
transformer candidats ← [e ∈ E : v ∈ E] en monceau ordonné par p
tant que candidats est non vide
retirer e des candidats
si exactement un sommet de e est marqué alors
ajouter e à E ′
x ← sommet non marqué de e
marquer x
pour tout voisin y de x
ajouter {x, y} aux candidats
retourner (V, E ′ )

par sommet. Le temps total de l’exécution du bloc si est donc de:


 ajout des voisins

z
ajouts
}| {
marquage
z}|{ z∑ }| {
 
f (|V |, |E|) ∈ O|V | · log |E| + |V | + deg(x) · log |E|
x∈V
( )

= O |V | · log |E| + log |E| · deg(x)
x∈V

= O(|V | · log |E| + log |E| · 2|E|)


⊆ O(|E| log |E|),

où nous avons utilisé |V | ∈ O(|E|) car |E| ≥ |V | − 1 par connexité de G.


En faisant la somme des trois analyses, nous obtenons un temps total de
l’algorithme appartenant à:

O(|E| + |E| log |E| + f ) ⊆ O(|E| log |E|).

Puisque log |E| ≤ log(|V |2 ) = 2 log |V | ∈ O(log |V |), nous concluons donc que
l’algorithme fonctionne en temps O(|E| log |V |).

4.1.2 Algorithme de Kruskal


L’algorithme de Kruskal suit l’approche suivante:
— on débute avec E ′ = ∅ et on considère chaque sommet comme un arbre;
— on choisit une arête e de plus petit poids qui connecte deux arbres;
CHAPITRE 4. ALGORITHMES GLOUTONS 62

— on ajoute e à E ′ ;
— on répète jusqu’à ce qu’il ne reste qu’un seul arbre.
Autrement dit, l’algorithme débute par une forêt de |V | arbres et réduit pro-
gressivement le nombre d’arbres en choisissant toujours la plus petite arête
disponible.
2 5 2 5 2 5
b d f b d f b d f
2 2 2

1 1 1
a 4 3 2 a 4 3 2 a 4 3 2

3 3 3
c e g c e g c e g
1 4 1 4 1 4

2 5 2 5 2 5
b d f b d f b d f
2 2 2

1 1 1
a 4 3 2 a 4 3 2 a 4 3 2

3 3 3
c e g c e g c e g
1 4 1 4 1 4

2 5
b d f
2

1
a 4 3 2

3
c e g
1 4

Figure 4.3 – Arbre couvrant minimal obtenu par l’algorithme de Kruskal.

La figure 4.3 illustre l’exécution de l’algorithme de Kruskal, et l’algorithme 22


décrit l’algorithme sous forme de pseudocode.

Algorithme 22 : Algorithme de Kruskal.


Entrées : graphe non dirigé connexe G = (V, E) pondéré par p
Résultat : un arbre couvrant minimal de G
E′ ← ∅
considérer chaque sommet v ∈ V comme un arbre
pour {u, v} ∈ E en ordre croissant selon p
si u et v n’appartiennent pas au même arbre alors
fusionner les arbres contenant u et v
ajouter {u, v} à E ′
retourner (V, E ′ )

Ensembles disjoints. Une implémentation efficace de l’algorithme de Krus-


kal requiert un mécanisme afin d’identifier si une arête connecte deux arbres
distincts ou non. Autrement dit, nous devons pouvoir répondre rapidement à
CHAPITRE 4. ALGORITHMES GLOUTONS 63

des questions de la forme « est-ce que u et v appartiennent au même arbre? »


Pour ce faire, nous représentons chaque arbre par l’ensemble des sommets qui
le constituent. Par exemple, l’exécution illustrée à la figure 4.3 fait évoluer ces
ensembles ainsi:

{a} {b} {c} {d} {e} {f } {g}


{a} {b, e} {c} {d} {f } {g}
{a} {b, c, e} {d} {f } {g}
{a} {b, c, e} {d} {f, g}
{a} {b, c, e, d} {f, g}
{a, b, c, e, d} {f, g}
{a, b, c, e, d, f, g}

À tout moment, ces ensembles sont disjoints puisqu’un sommet ne peut pas
appartenir simultanément à deux arbres. Nous décrivons donc une structure
de données qui permet de manipuler une collection d’ensembles disjoints. Nous
représentons chaque ensemble par une arborescence. Par exemple, la figure 4.4
illustre une représentation possible de la partition {a}, {b, c, e, d}, {f, g}.

a b f

c d e g

Figure 4.4 – Représentation de {a}, {b, c, e, d}, {f, g} sous arborescences.

Notre structure de données implémentera ces opérations:


— init(X): crée la collection {{x} : x ∈ X};
— trouver(x): retourne un représentant de l’ensemble auquel x appartient;
— union(x, y): fusionne les ensembles auxquels x et y appartiennent.
Initialement, chaque élément x ∈ X appartient à une arborescence dont x
est l’unique sommet. Pour chaque élément x ∈ X, nous stockons parent[x], qui
correspond au parent de x dans son arborescence, et hauteur[x], qui correspond
à la hauteur de cette arborescence. Si x forme la racine de son arborescence,
alors nous stockons parent[x] = x afin de le représenter.
La recherche d’un élément x remonte son arborescence, puis retourne l’élé-
ment à la racine comme représentant de l’ensemble. L’union de deux sommets
x et y considère l’arborescence qui contient x et celle qui contient y, puis ad-
joint l’arbre de plus petite hauteur à l’autre arbre. Une telle implémentation
est décrite sous forme de pseudocode à l’algorithme 23.
Puisque l’union cherche à minimiser la hauteur des arborescence, celles-ci
demeurent toujours de hauteur au plus logarithmique par rapport au nombre de
CHAPITRE 4. ALGORITHMES GLOUTONS 64

Algorithme 23 : Implémentation d’ensembles disjoints.


init(X ):
// Représenter chaque élément par une arborescence
parent ← [x 7→ x : x ∈ X]
hauteur ← [x 7→ 0 : x ∈ X]
trouver(x):
// Remonter l'arborescence jusqu'à sa racine
tant que x ̸= parent[x]
x ← parent[x]
retourner x
union(x, y ):
x ← trouver(x)
y ← trouver(y )
// Adjoindre l'arborescence de hauteur min. à l'autre
si hauteur[x] > hauteur[y] alors
parent[y] ← x
sinon si hauteur[y] > hauteur[x] alors
parent[x] ← y
sinon
parent[y] ← x
hauteur[x] ← hauteur[x] + 1

sommets qu’elles contiennent 1 . Ainsi, nous obtenons les complexités suivantes


dans le pire cas:
Opération Complexité
init(X) Θ(|X|)
trouver(x) O(log |X|)
union(x, y) O(log |X|)

Analyse de l’algorithme de Kruskal. Analysons l’algorithme de Kruskal


implémenté à l’aide d’ensembles disjoints, tel que décrit à l’algorithme 24.
L’initialisation de E et D se fait respectivement en temps Θ(1) et Θ(|V |).
Le tri implicite dans la boucle principale requiert un temps de O(|E| log |E|).
La boucle principale est exécutée précisément |E| fois. Les opérations union et
trouver se font en temps O(log |V |), et l’ajout à E ′ en temps constant. Ainsi,
nous obtenons une complexité totale de:

O(1 + |V | + |E| log |E| + |E| log |V |) = O(|E| log |E|) = O(|E| log |V |).
1. On peut démontrer, par induction généralisée sur k, qu’une arborescence de k ∈ N≥1
sommets possède une hauteur d’au plus ⌊log k⌋.
CHAPITRE 4. ALGORITHMES GLOUTONS 65

Algorithme 24 : Algorithme de Kruskal avec ensembles disjoints.


Entrées : graphe non dirigé connexe G = (V, E) pondéré par p
Résultat : un arbre couvrant minimal de G
E′ ← ∅
D ← init(V )
pour {u, v} ∈ E en ordre croissant selon p
si [Link](u) ̸= [Link](v ) alors
[Link](u, v )
ajouter {u, v} à E ′
retourner (V, E ′ )

Amélioration. Il est possible de rendre l’algorithme de Kruskal plus efficace


en améliorant l’implémentation d’ensembles disjoints. L’idée consiste à modifier
la fonction de recherche afin de « compresser » le chemin exploré. Après avoir
remonté d’un élément x vers sa racine r, on réexplore le chemin à nouveau
en rattachant tous les sommets du chemin directement à r. Cela produit des
arborescences de hauteurs quasi constantes. Cette approche est implémentée à
l’algorithme 25.
Cette légère modification réduit la complexité de l’algorithme de Kruskal à
O(α(|V |) · |E|), où α est l’inverse de la fonction d’Ackermann. Bien que α(|V |)
ne soit pas une constante, la fonction α croît si lentement que sa valeur demeure
inférieure à 5 pour toute entrée envisageable en pratique.

Algorithme 25 : Recherche avec compression de chemin.


trouver(x):
r←x
// Remonter l'arborescence jusqu'à sa racine
tant que r ̸= parent[r]
r ← parent[r]
// Compression du chemin de x vers r
tant que x ̸= r
x, parent[x] ← parent[x], r
retourner r

4.2 Approche générique


Les algorithmes de Prim–Jarník et de Kruskal partagent un certain nombre
de caractéristiques communes. Ils considèrent chaque arête une seule fois, ils
vérifient si l’ajout d’une arête est admissible, et ils l’ajoutent le cas échéant. Ce
type d’algorithme est dit glouton (ou vorace) puisqu’on construit une solution
CHAPITRE 4. ALGORITHMES GLOUTONS 66

partielle en ajoutant le plus de candidats possibles jusqu’à l’obtention d’une


solution complète, et ce sans jamais reconsidérer nos choix. Un patron générique
d’algorithme glouton est décrit à l’algorithme 26.
Les algorithmes de Prim–Jarník et de Kruskal implémentent ce patron ainsi:
Prim–Jarník Kruskal
candidats ensemble des arêtes
sélectionner c plus petite arête e avec plus petite arête e non
un sommet non exploré considérée
admissible(S , c) l’ajout de e à E ′ ne crée l’ajout de e à E ′ connecte
pas de cycle? deux arbres?
S est une solution? l’ensemble E ′ touche à l’ensemble E ′ ne forme
tous les sommets? qu’un seul arbre?

Algorithme 26 : Patron générique d’algorithme glouton.


Entrées : Représentation d’un ensemble de candidats
Résultat : Une solution formée de candidats
S←∅
tant que l’ens. des candidats est non vide et S n’est pas une solution
sélectionner et retirer c des candidats
si admissible(S, c) alors
ajouter c à S
si S est une solution alors retourner S
sinon retourner impossible

4.3 Problème du sac à dos


Soit c ∈ N>0 , soit v une séquence de n éléments appartenant à N>0 , et soit
p une séquence de n éléments appartenant à {1, 2, . . . , c}. Nous appelons c la
capacité, v la séquence de valeurs et p la séquence de poids. Informellement, le
problème du sac à dos consiste à mettre des objets de poids p, dans un sac à
dos qui peut contenir un poids maximal de c, de telle sorte que la valeur des
objets choisis est maximisée. Plus formellement, le problème du sac dos consiste
à maximiser
déf

n
val(x) = x[i] · v[i]
i=1
∑n
sous la contrainte c ≥ i=1 x[i] · p[i], où x ∈ {0, 1}n indique les objets choisis.
CHAPITRE 4. ALGORITHMES GLOUTONS 67

Exemple.

Considérons les objets suivants et un sac de capacité c = 900:


1 2 3 4 5 6
Objets
     
Valeurs v 50 5 65 10 12 20
Poids p 700 320 845 70 420 180
En choisissant les objets 4, 5 et 6, nous obtenons une valeur de 42 et un
poids de 670. En choisissant les objets 1 et 4, nous obtenons une valeur de
60 et un poids de 770. En choisissant les objets 1 et 6, nous obtenons une
valeur de 70 et un poids de 880. Cette dernière solution est optimale. En
effet, en explorant toutes les combinaisons, nous remarquerions qu’il est
impossible d’obtenir une valeur supérieure à 70 sans excéder la capacité.

4.3.1 Approche gloutonne


Une approche simple et naturelle afin de résoudre le problème du sac à dos
consiste à considérer le ratio entre la valeur et le poids de chaque objet. Dans
l’exemple précédent, le quatrième objet possède un ratio de 1/7, ce qui cor-
respond à la valeur qu’offre l’objet pour chaque unité de poids. Une procédure
gloutonne trie simplement les objets en ordre décroissant de ce ratio et ajoute
les objets au sac tant que sa capacité n’est pas excédée.

Exemple.

Reconsidérons les objets précédents en les ordonnant par leur ratio:


4 6 3 1 5 2
Objets
     
Valeurs v 10 20 65 50 12 5
Poids p 70 180 845 700 420 320
Ratio 1/7 1/9 1/13 1/14 1/35 1/64
En prenant les objets 4 et 6, nous atteignons une valeur de 30 et un poids
de 250. Il est impossible d’ajouter l’objet suivant, c’est-à-dire l’objet 3,
puisque le poids du sac excéderait sa capacité de c = 900.

L’exemple ci-dessus montre que l’approche gloutonne ne fonctionne pas tou-


jours puisque la solution obtenue n’est pas maximale. En fait:
Proposition 22. La solution retournée par la procédure gloutonne pour le
problème du sac à dos peut être arbitrairement mauvaise.
Démonstration. Soit c ≥ 3. Considérons l’entrée v = [2, c] et p = [1, c]. La
procédure gloutonne calcule les ratios [2/1, c/c] = [2, 1]. Ainsi, elle choisit le
CHAPITRE 4. ALGORITHMES GLOUTONS 68

premier objet, ce qui donne une valeur de 2 et un poids de 1. Il est impossible


d’ajouter le deuxième objet puisque cela mènerait à un poids de c + 1 > c.
Cette solution n’est pas maximale puisque choisir le deuxième objet donne une
valeur et un poids de c. Remarquons que plus c est grand, plus l’écart se creuse
entre la solution retournée et la solution maximale.

4.3.2 Variante fractionnelle


L’approche gloutonne permet néanmoins de résoudre une variante du problème
du sac à dos où on peut choisir une fraction d’un objet. Plus formellement, dans
la variante fractionnelle, on cherche à maximiser

n
val(x) = x[i] · v[i]
i=1
∑n
sous la contrainte c ≥ i=1 x[i] · p[i], où x ∈ [0, 1] indique quelle fraction
n

de chaque objet est choisie. Remarquons que la seule différence entre les deux
problèmes est le passage de l’intervalle discret {0, 1} à l’intervalle continu [0, 1].
Dans le contexte des objets d’un sac, cette variante fait du sens si on peut les
découper, par ex. comme la pomme des exemples précédents.

Exemple.

Reconsidérons à nouveau le même exemple:


4 6 3 1 5 2
Objets
     
Valeurs v 10 20 65 50 12 5
Poids p 70 180 845 700 420 320
Ratio 1/7 1/9 1/13 1/14 1/35 1/64
En prenant entièrement les objets 4 et 6, nous atteignons une valeur de
30 et un poids de 250. En prenant 10/13 de l’objet 3, nous obtenons une
valeur de 30+(10/13)·65 = 80 avec un poids de 250+(10/13)·845 = 900.

L’exemple précédent suggère donc une légère modification de la procédure


gloutonne: on prend entièrement les objets en ordre décroissant de ratio, puis
on ajoute la plus grande fraction du prochain objet qui n’entre pas dans le
sac. Cette approche est décrite sous forme de pseudocode à l’algorithme 27.
Contrairement à sa variante discrète, cet algorithme retourne toujours une
solution maximale pour le problème du sac à dos fractionnel:

Proposition 23. L’algorithme 27 résout correctement la variante fractionnelle


du problème du sac à dos.
CHAPITRE 4. ALGORITHMES GLOUTONS 69

Algorithme 27 : Algorithme pour le problème du sac à dos fraction-


nel.
Entrées : capacité c ∈ N>0 , valeurs v ∈ Nn>0 et poids p = {1, 2, . . . , c}n
Résultat : solution (maximale) au problème du sac à dos fractionnel
indices ← [1, 2, . . . , n]
trier indices en ordre décroissant selon v[i] / p[i]
valeur ← 0
poids ← 0
x ← [0, 0, . . . , 0]
pour i ∈ indices
si poids + p[i] ≤ c alors
valeur ← valeur + v[i]
poids ← poids + p[i]
x[i] ←1
sinon
λ ← (c − poids) / p[i]
valeur ← valeur + λ · v[i]
poids ← poids + λ · p[i]
x[i] ←λ
retourner x

4.3.3 Approximation
Bien que l’approche gloutonne ne fonctionne pas pour le problème du sac à dos
(discret), il est possible d’approximer une solution optimale en s’inspirant de
la variante fractionnelle du problème:
— on calcule une solution x selon l’approche gloutonne;
— on choisit la meilleure solution entre x et la solution y constituée unique-
ment du prochain objet qui n’entre pas dans le sac (s’il en reste un).

Exemple.

Reconsidérons les objets précédents en les ordonnant par leur ratio:


4 6 3 1 5 2
Objets
     
Valeurs v 10 20 65 50 12 5
Poids p 70 180 845 700 420 320
Ratio 1/7 1/9 1/13 1/14 1/35 1/64
L’approche gloutonne choisit les objets 4 et 6, et obtient une valeur de
30 et un poids de 250. L’objet suivant, c’est-à-dire l’objet 3, possède une
valeur de 65 > 30. On choisit donc simplement l’objet 3.
CHAPITRE 4. ALGORITHMES GLOUTONS 70

Cette procédure, décrite à l’algorithme 28, ne retourne pas nécessairement


une solution maximale, mais l’approxime toujours:
Proposition 24. La solution retournée par l’algorithme 28 possède une valeur
d’au moins 12 de la valeur optimale.

Démonstration. Si la solution x obtenue par l’algorithme contient tous les ob-


jets, alors x est forcément optimale. Nous considérons donc une entrée où x
ne contient pas tous les objets. Soient v ∗ et vfrac

respectivement les valeurs
maximales de la variante discrète et fractionnelle du problème du sac à dos sur
l’entrée de l’algorithme. À la sortie de la boucle principale, nous avons:
∗ ∗
valeur + v[indices[i]] ≥ vfrac (car vfrac = valeur + λv[indices[i]] où λ ∈ [0, 1])
≥ v∗ (on ne peut pas faire pire dans la variante frac.)

Ainsi, max(valeur, v[indices[i]]) ≥ v ∗ /2, ce qui implique que la solution retour-


née par l’algorithme possède une valeur d’au moins 12 · v ∗ .

Algorithme 28 : Approximation pour le problème du sac à dos.


Entrées : valeurs v ∈ Nn>0 , poids Nn>0 et capacité c ∈ N>0
Résultat : solution (maximale) au problème du sac à dos fractionnel
indices ← [1, 2, . . . , n]
trier indices en ordre décroissant selon v[i] / p[i]
valeur ← 0
poids ← 0
x ← [0, 0, . . . , 0]
i ←1
tant que (i ≤ n) ∧ (poids + p[indices[i]] ≤ c)
j ← indices[i]
valeur ← valeur + v[j]
poids ← poids + p[j]
x[j] ← 1
i ←i+1
// Retourner meilleure solution entre x et prochain objet
si (i > n) ∨ (valeur > v[indices[i]]) alors // (s'il en reste un)
retourner x
sinon
y ← [0, 0, . . . , 0]
y[indices[i]] ← 1
retourner y
CHAPITRE 4. ALGORITHMES GLOUTONS 71

Remarque.

Pour tout ε ∈ (0, 1], il existe un algorithme qui approxime une solution
maximale du problème du sac à dos à un facteur d’au moins 1 − ε.
Autrement dit, il est possible d’approximer le problème du sac à dos de
façon arbitrairement précise. De plus, cette approximation se calcule en
temps polynomial.
CHAPITRE 4. ALGORITHMES GLOUTONS 72

4.4 Exercices
4.1) Expliquez comment calculer un arbre couvrant de poids maximal.

4.2) Supposons le poids d’un graphe soit défini par le produit de ses poids plu-
tôt que la somme. Expliquez comment calculer un arbre de poids minimal
sous cette nouvelle définition.

4.3) Lorsqu’un graphe non dirigé G n’est pas connexe, il est impossible d’obte-
nir un arbre couvrant de G. Toutefois, nous pouvons relaxer cette notion
et considérer une forêt couvrante, c’est-à-dire un sous-graphe de G qui
est une forêt telle que chacun de ses arbres est un arbre couvrant d’une
composante connexe de G. Une forêt couvrante correspond à un arbre
couvrant standard pour les graphes connexes. Les algorithmes de Prim–
Jarník et de Kruskal permettent-ils de calculer une forêt couvrante de
poids minimal? Si ce n’est pas le cas, est-il possible de les adapter?

4.4) Donnez un algorithme simple qui calcule les composantes connexes d’un
graphe non dirigé grâce à une structure d’ensembles disjoints.

4.5) ⋆ L’approche gloutonne pour le problème du sac à dos fonctionne en


temps O(n log n) dû au tri. Donnez une implémentation de cette approche
qui fonctionne en temps O(n). Vous pouvez supposer l’existence d’une
procédure qui retourne la médiane d’une séquence en temps∑linéaire.

(Indice: pensez récursivement et exploitez i=0 21i = 2)
5
Algorithmes récursifs et
approche diviser-pour-régner

Ce chapitre traite des algorithmes récursifs et plus particulièrement de l’ap-


proche diviser-pour-régner. Cette approche consiste essentiellement à résoudre
un problème en le découpant en sous-problèmes plus simples qu’on résout ré-
cursivement afin d’obtenir une solution globale. Par exemple, le tri par fusion
suit cette approche: afin de trier une séquence de 2k éléments, on trie deux
sous-séquences de k éléments puis on les fusionne.
Dû à leur nature récursive, la correction de ces algorithmes se prouve gé-
néralement par induction, et leur complexité s’analyse à l’aide de récurrences.
Nous nous pencherons principalement sur l’analyse de leur complexité et sur
certains problèmes où cette approche mène à des algorithmes efficaces.

5.1 Tours de Hanoï


Considérons le problème classique des tours de Hanoï :
— n disques de diamètre n, . . . , 2, 1 sont empilés sur une première pile;
— deux autres piles sont vides;
— on peut déplacer le disque x du dessus d’une pile i vers le dessus d’une
pile j si le disque y au-dessus de la pile j possède un diamètre supérieur
au disque x;
— on doit déplacer l’entièreté de première pile sur la deuxième pile.
Par exemple, la figure 5.1 illustre une solution, pour le cas n = 4, qui effectue
15 déplacements de disques.
Ce problème peut être résolu récursivement en suivant cette approche:
— on cherche à déplacer le contenu d’une pile source vers une pile destination
à l’aide d’une pile temporaire;
— si la source possède k disques, on déplace (récursivement) ses k−1 disques
du dessus vers la pile temporaire;

73
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 74

0 1 2 3

4 5 6 7

8 9 10 11

12 13 14 15

Figure 5.1 – Solution au problème des tours de Hanoï pour n = 4 disques.

— on déplace le disque restant de la source vers la destination;


— on déplace (récursivement) les k − 1 disques de la pile temporaire vers la
destination.
Cette procédure est décrite sous forme de pseudocode à l’algorithme 29.

Algorithme 29 : Algorithme pour le problème des tours de Hanoï.


Entrées : n ∈ N
Résultat : séquence de déplacements résolvant le problème des tours
de Hanoï pour n disques
hanoi(n):
deplacements ← [ ]
hanoi'(k, src, dst, tmp):
si k > 0 alors
// Déplacer k − 1 disques de pile src vers pile tmp
hanoi'(k − 1, src, tmp, dst)
// Déplacer un disque de pile src vers pile dst
ajouter (src, dst) à deplacements
// Déplacer k − 1 disques de pile tmp vers pile dst
hanoi'(k − 1, tmp, dst, src)
hanoi'(n, 1, 2, 3) // Déplacer première pile vers deuxième
retourner deplacements
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 75

Cherchons à déterminer la taille de la solution calculée par l’algorithme 29


en examinant la sous-procédure hanoi'. Lorsque k = 0, la solution est vide
(cela correspond au fait qu’il n’y a aucun disque à déplacer). Lorsque k > 0, la
solution possède un déplacement, plus les déplacements obtenus par les deux
appels récursifs. Ainsi, en définissant t(k) comme étant la taille de la solution,
nous obtenons: {
0 si k = 0,
t(k) =
2 · t(k − 1) + 1 si k > 0.
Nous appelons ce type de relation une relation de récurrence puisque t dépend
d’elle-même. À priori, identifier la complexité asymptotique d’une telle relation
s’avère ardu. Il existe plusieurs façons d’y arriver. L’une des plus élémentaires
consiste à identifier une forme close en substitutant la récurrence à répétition:

t(k) = 2 · t(k − 1) + 1
= 2 · (2 · t(k − 2) + 1) + 1 (par la relation de récurrence)
= 4 · t(k − 2) + 3
= 4 · (2 · t(k − 3) + 1) + 3 (par la relation de récurrence)
= 8 · t(k − 3) + 7
..
.
= 2i · t(k − i) + (2i − 1) (en répétant i fois)
..
.
= 2k · t(0) + (2k − 1) (en répétant k fois)
=2 −1
k
(car t(0) = 0).

Cette approche suggère donc que t(k) = 2k − 1. Afin de s’en convaincre formel-
lement, on pourrait prouver par induction que c’est bien le cas (ce l’est!)
Comme la taille de la solution est une borne inférieure sur le temps d’exé-
cution de l’algorithme et que celui-ci débute par un appel à hanoi' avec k = n,
nous en concluons qu’il fonctionne en temps Ω(2n ). Une analyse plus fine mon-
trerait que son temps d’exécution appartient à Θ(2n ). En fait, il est impossible
de résoudre le problème en moins de 2n − 1 déplacements, donc aucun algo-
rithme ne peut faire mieux.

Remarque.

Une autre façon de se convaincre que t(k) = 2k − 1 consiste à imaginer


k sous représentation binaire et t comme une opération de décalage à
gauche suivi d’une incrémentation. Puisqu’on applique cette opération
k fois à partir de 0, on obtient 11 · · · 12 = 2k − 1.
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 76

5.2 Récurrences linéaires


Afin d’illustrer une récurrence plus complexe, considérons le problème qui
consiste à identifier tous les pavages d’une grille de 3 × n cases à l’aide de
tuiles de cette forme:

L’algorithme 30 décrit une procédure récursive qui calcule tous les pavages en
observant que tout pavage débute forcément par l’un de ces trois motifs:

Algorithme 30 : Algorithme récursif de pavage d’une grille 3 × n.


Entrées : n ∈ N≥1
Résultat : séquence de tous les pavages d’une grille 3 × n
pavages(n):
si n = 1 alors [ ]
retourner
si n = 2 alors [ ]
retourner , ,
sinon
P ← pavages(n − 1)
Q ← pavages(n − 2)
[ ] [ ] [ ]
retourner +p : p ∈ P + +q : q ∈ Q + +q : q ∈ Q

5.2.1 Cas homogène


Afin d’estimer le temps d’exécution de l’algorithme 30, évaluons le nombre de
pavages t(n) d’une grille 3 × n. En examinant l’algorithme, nous remarquons
que dans le cas général la séquence de retour possède une taille de |P | + 2 · |Q|.
Ainsi, nous obtenons cette récurrence:


1 si n = 1,
t(n) = 3 si n = 2,


t(n − 1) + 2 · t(n − 2) sinon.

La méthode de substitution mène plus difficilement à une forme close. Nous


empruntons donc une autre approche. En déplaçant tous les termes à gauche de
l’égalité, la relation se réécrit sous la forme t(n)−t(n−1)−2·t(n−2) = 0. Nous
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 77

disons que cette récurrence est linéaire, car son côté gauche est une combinaison
linéaire, et homogène, car son côté droit vaut 0. Il existe une méthode afin de
résoudre les récurrences linéaires homogènes. Nous la décrivons en trois étapes
en l’illustrant sur notre problème de pavage.

A) Polynôme caractéristique. Étant donné une récurrence linéaire homo-


gène a0 · t(n) + a1 · t(n − 1) + . . . + ad · t(n − d) = 0, nous considérons son
polynôme caractéristique:

a0 · xd + a1 · xd−1 + . . . + ad · x0 .

Dans notre problème de pavage, nous obtenons donc le polynôme:

p(x) = x2 − x − 2 = (x − 2)(x + 1).

B) Forme close. Nous identifions ensuite les racines λ1 , λ2 , . . . , λd du poly-


nôme caractéristique. Si toutes les racines sont distinctes 1 , la récurrence pos-
sède cette forme close:

c1 · λn1 + c2 · λn2 + . . . + cd · λnd ,

où c1 , c2 , . . . , cd sont des constantes à identifier. Dans notre cas, nous avons:

t(n) = c1 · 2n + c2 · (−1)n .

C) Identification des constantes. Afin d’identifier la valeur des constantes


c1 , c2 , . . . , cd , nous construisons un système d’équations linéaires en évaluant d
valeurs de t. Par exemple, dans notre cas nous avons:

t(1) = c1 · 21 + c2 · (−1)1 ,
t(2) = c1 · 22 + c2 · (−1)2 .

Puisque t(1) = 1 et t(2) = 3, nous obtenons le système:

1 = 2c1 − c2 ,
3 = 4c1 + c2 .

En résolvant le système, nous obtenons c1 = 2/3 et c2 = 1/3. Ainsi, nous avons:


2 n 1
t(n) = · 2 + · (−1)n .
3 3
Puisque (1/3) · (−1)n vaut toujours −1/3 ou 1/3, nous concluons que t ∈ Θ(2n )
et par conséquent qu’il y a un nombre exponentiel de pavages.

1. Le cas où certaines racines apparaissent plusieurs fois est légèrement plus compliqué.
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 78

Exemple.

Examinons la célèbre suite de Fibonacci F. Ses deux premiers termes


sont 0 et 1, et chacun de ses termes subséquents correspond à la somme
des deux termes précédents:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, . . .

Nous avons: 

0 si n = 0,
Fn = 1 si n = 1,


Fn−1 + Fn−2 sinon.
En déplaçant tous les termes à gauche de l’égalité, la récurrence se
réécrit Fn − Fn−1 − Fn−2 = 0. Son polynôme caractéristique est donc:

p(x) = x2 − x − 1 = (x − λ1 )(x − λ2 ),
déf
√ déf

où λ1 = (1 + 5)/2 ≈ 1,618 et λ2 = (1 − 5)/2 = 1 − λ1 ≈ −0,618.
Ainsi, Fn = c1 · λn1 + c2 · λn2 pour certaines constantes c1 et c2 .
Afin d’identifier la valeur des constantes, on utilise t(0) = 0 et t(1) =
1 afin de construire ce système d’équations:

0= c1 + c2 ,
1 = λ1 · c1 + λ2 · c2 .
√ √
En résolvant le système, nous obtenons c1 = 1/ 5 et c2 = −1/ 5.
Ainsi:
1 1
Fn = √ · λn1 − √ · λn2
5 5
1
= √ · (λn1 − λn2 )
5
≈ 0,447 · (1,618n − (−0,618)n ).

Nous en concluons que Fn ∈ Θ(λn1 ).

Remarque.

Le nombre λ1 = 1+2 5 ≈ 1,618, souvent dénoté φ, est le célèbre
nombre d’or qui se manifeste par ex. dans la nature et en art.
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 79

5.2.2 Cas non homogène


Reconsidérons l’algorithme 29 pour le problème des tours de Hanoï. Nous avons
déjà analysé le nombre de déplacements calculés par l’algorithme. Analysons
maintenant le nombre d’appels récursifs r(k) effectués par hanoi' sur entrée
k. Lorsque k = 0, aucun appel n’est effectué. Autrement, deux appels récursifs
sont effectués avec k − 1 comme entrée. Nous avons donc:
{
0 si k = 0,
r(k) =
2 · r(k − 1) + 2 sinon.

En réécrivant le cas général, nous obtenons r(k) − 2 · r(k − 1) = 2. Nous avons


donc une récurrence linéaire, mais non homogène puisque le terme de droite
ne vaut pas 0. Bien que notre approche ne s’applique pas ici, une méthode
similaire permet de résoudre une telle récurrence.
Étant donné une récurrence a0 · t(n) + a1 · t(n − 1) + . . . + ad · t(n − d) = c · bn ,
nous construisons le polynôme caractéristique usuel et le multiplions par (x−b).
Dans notre cas, nous avons c = 2 et b = 1. Ainsi, nous multiplions le polynôme
caractéristique par x − 1 afin d’obtenir:

q(x) = (x − 2)(x − 1).

À partiri d’ici, la méthode demeure la même. Nous savons que r(k) peut s’écrire
sous la forme
r(k) = c1 · 2k + c2 · 1k .
Afin d’identifier les valeurs de c1 et c2 , nous évaluons r(0) = 0 et r(1) = 2, et
construisons le système d’équations:

0= c1 + c2 ,
2 = 2 · c1 + c2 .

En résolvant le système, nous obtenons c1 = 2 et c2 = −2. Ainsi:

r(k) = 2 · 2k − 2 = 2(2k − 1).

Le nombre d’appels récursifs effectués par hanoi' appartient donc à Θ(2k ).

5.3 Exponentiation rapide


Considérons le problème où nous cherchons à calculer bn étant donnés b, n ∈ N.
Une approche simple, décrite à l’algorithme 31, consiste à utiliser la définition
de l’exponentiation et ainsi de multiplier n fois b avec lui-même.
Il est possible de calculer bn avec bien moins de multiplications en utilisant
une approche diviser-pour-régner:
— si n est pair, on calcule bn÷2 et on le multiplie avec lui-même;
— sinon, on calcule bn÷2 et on le multiplie avec lui-même ainsi que b.
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 80

Algorithme 31 : Exponentiation naïve.


Entrées : b, n ∈ N
Résultat : bn
exp(b, n):
si n = 0 alors
retourner 1
sinon
retourner b · exp(b, n − 1)

Algorithme 32 : Exponentiation rapide.


Entrées : b, n ∈ N
Résultat : bn
exp-rapide(b, n):
si n = 0 alors
retourner 1
sinon
m ← exp-rapide(b, n ÷ 2)
k ←1
si n est impair alors k ← b
retourner m · m · k

Cette procédure est décrite à l’algorithme 32. Sa correction découle du fait que
l’ordre des multiplications n’a aucune importance; ou en termes plus techniques,
par la commutativité et l’associativité de la multiplication.
Analysons le nombre de multiplications t(n) de exp-rapide par rapport à
n. Comme la procédure effectue deux multiplications et un appel récursif avec
l’exposant n ÷ 2, nous obtenons la récurrence:
{
0 si n = 0,
t(n) =
t(n ÷ 2) + 2 sinon.

Comme cette récurrence n’est pas linéaire, nous ne pouvons pas la résoudre
avec la méthode décrite précédemment. Cependant, comme on divise n par deux
à répétition, on peut imaginer que le nombre d’appels récursifs est d’au plus
log(n) + 1. De plus, à chaque appel, on effectue précisément 2 multiplications.
Nous pouvons donc naturellement conjecturer que t(n) ≤ 2 · log n + 2 pour tout
n ≥ 1. Cela se vérifie par induction généralisée sur n. Pour n = 1, nous avons
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 81

t(1) = 2 = 2 · log(1) + 2. De plus:

t(n) = t(n ÷ 2) + 2 (par définition de t)


≤ 2 log(n ÷ 2) + 4 (par hypothèse d’induction)
≤ 2 log(n/2) + 4 (car n ÷ 2 ≤ n/2)
= 2 log n − 2 log 2 + 4
= 2 log n + 2.

L’algorithme 32 effectue donc t ∈ Θ(log n) multiplications, ce qui offre un gain


exponentiel par rapport à l’algorithme 31.

5.4 Multplication rapide


Dans la plupart des algorithmes présentés jusqu’ici, nous avons supposé que les
opérations arithmétiques (addition, soustraction, multiplication, etc.) s’effec-
tuent en temps constant. Cela est relativement réaliste si on les imagine comme
faisant partie du jeu d’instruction d’une architecture, où les nombres sont gé-
néralement représentés sur un nombre fixe de bits, par ex. 64 bits. Cependant,
cela dissimule la réelle complexité des opérations arithmétiques qui n’est pas
constante lorsqu’on permet un nombre arbitraire de bits (ce que nous avons
permis à plusieurs reprises!) On ignore souvent ce détail car il ne s’agit pas du
« coeur du problème » ou puisque le coût s’avère « marginal » pour plusieurs
applications. Toutefois, on peut difficilement l’ignorer pour des applications
où l’on doit manipuler de très grands nombres à répétition, par ex. en calcul
numérique ou en cryptographie.
En fait, en utilisant des algorithmes élémentaires, l’addition et la multi-
plication de deux nombres de n chiffres prend un temps de O(n) et O(n2 )
respectivement. Nous présentons l’algorithme de Karatsuba qui permet de mul-
tiplier deux entiers naturels de n chiffres plus rapidement qu’en temps quadra-
tique. Plutôt que de considérer la base 2, considérons la base 10. Remarquons
d’abord qu’un nombre de n chiffres peut être décomposé en deux nombres
de k = ⌈n/2⌉ chiffres, en ajoutant des zéros non significatifs au besoin. Par
exemple, 6789 = 102 · 67 + 89 et 345 = 102 · 03 + 45. En général, étant donnés
deux nombres x et y de n chiffres, ceux-ci s’écrivent sous la forme x = 10k ·a+b
et y = 10k · c + d. Nous avons donc:

x · y = (10k · a + b)(10k · c + d)
= 102k · ac + 10k · (ad + bc) + bd.

Ainsi, la multiplication de deux nombres de ≈ 2k chiffres est équivalente à


quatre multiplications de nombres de k chiffres (si on ignore décalages et addi-
tions). Cela ne semble pas nous aider. Toutefois, nous pouvons nous ramener
à trois multiplications. En effet, observons que

ad + bc = ac + bd − (a − b)(c − d).
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 82

Ainsi, si nous connaissons la valeur de ac, bd et (a − b)(c − d), nous pouvons


reconstruire x · y, tel que décrit à l’algorithme 33.

Algorithme 33 : Algorithme de multiplication rapide de Karatsuba.


Entrées : x, y ∈ N représentés sous n ∈ N≥1 chiffres en base 10
Résultat : x · y
mult(n, x, y ):
si n = 1 alors
retourner x · y
sinon
k ← ⌈n/2⌉
a, b ← x ÷ 10k , x mod 10k
c, d ← y ÷ 10k , y mod 10k
e ← mult(k, a, c)
f ← mult(k, b, d)
g ← mult(k, a − b, c − d)
retourner 102k · e + 10k · (e + f − g) + f

Analysons l’algorithme 33 par rapport à n. Lorsque n = 1, le temps d’exé-


cution est constant. Lorsque n > 1, nous multiplions trois nombres d’au plus
n chiffres. Les additions et soustractions prennent un temps de O(n) si l’on
utilise une implémentation standard. Les divisions entières et modulos par 10k
correspondent respectivement à des décalages et des troncations. Ces opéra-
tions prennent donc aussi un temps de O(n). Soit t(n) le temps d’exécution de
mult pour n chiffres. Nous avons:
{
c si n = 1,
t(n) ≤
3 · t(⌈n/2⌉) + c · n sinon,

où c est une constante.


Estimons la complexité asymptotique de t à l’aide d’un arbre de récursion.
déf
Posons k = log n. En supposant que n se divise toujours par deux, nous ob-
tenons l’arbre de récursion suivant où chaque sommet correspond à un appel
récursif de mult et où le coût indiqué à droite correspond au coût total de tous
les appels d’un même niveau:
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 83

Coût
n c·n

n/2 n/2 n/2 c· 3n


2

n/4 n/4 n/4 n/4 n/4 n/4 n/4 n/4 n/4 c· 9n


4
k
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 c· 32kn

Ainsi, nous avons:


k ( )i
∑ 3
t(n) ≈ cn ·
i=0
2
(3/2)k+1 − 1
= cn · (série géométrique de raison 3/2)
3/2 − 1
(3/2)k+1
≤ cn ·
3/2 − 1
= 3cn · (3/2)k
= 3cn · (3/2)log n (par définition de k)
log(3/2)
= 3cn · n
= 3c · n1+log(3/2)
= 3c · nlog 3 (car 1 + log(3/2) = log(2) + log(3/2) = log(2 · 3/2)).

Nous nous sommes donc convaincus semi-formellement que le temps d’exécu-


tion de l’algorithme 33 appartient à O(nlog 3 ). Puisque log 3 ≤ 1,585, cela offre
un gain considérable sur un algorithme quadratique. Par exemple, 10002 =
1 000 000 alors que 1000log 3 ≤ 56 871.

Remarque.

Il existe des algorithmes plus efficaces que l’algorithme de Karatsuba,


par ex. les algorithmes de Toom–Cook, Schönhage–Strassen (O(n·log n·

log log n)) et de Fürer (O(n log n · 2c·log n )). En 2019, Harvey et van der
Hoeven ont annoncé l’existence d’un algorithme fonctionnant en temps
O(n log n), ce qui est conjecturé comme optimal (asymptotiquement).

5.5 Théorème maître


La plupart des algorithmes qui empruntent l’approche diviser-pour-régner donnent
lieu à des récurrences de la forme

t(n) = a · t(⌈n/b⌉) + a′ · t(⌊n/b⌋) + f (n).


CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 84

En général, nous pouvons ignorer les plafonds et planchers 2 et plutôt considérer

t(n) = c · t(n ÷ b) + f (n) où c = a + a′ .


déf

Comme il peut s’avérer fastidieux de résoudre une telle récurrence t, il existe


une caractérisation générique de la complexité asymptotique de t par rapport à
c, b et f (n). Celle-ci est connue sous le nom de théorème maître. Nous présentons
ici une version allégée de ce théorème suffisant pour l’analyse de la plupart des
algorithmes:

Théorème 3. Soient t, f ∈ F , b ∈ N≥2 et c, d ∈ R>0 telles que f ∈ O(nd ) et

t(n) = c · t(n ÷ b) + f (n) pour tout n suffisamment grand.

La fonction t appartient à:
— O(nd ) si c < bd ,
— O(nd · log n) si c = bd ,
— O(nlogb c ) si c > bd .

Appliquons ce théorème à certains algorithmes.

Tri par fusion. L’algorithme de tri par fusion découpe une séquence en deux,
fait un appel récursif sur chacune des deux sous-séquences, puis effectue une
fusion en temps linéaire. Nous obtenons donc une récurrence:

t(n) = 2 · t(n ÷ 2) + f (n) où f ∈ O(n).

Nous avons b = c = 2, d = 1 et ainsi c = bd . Par conséquent, le deuxième cas


du théorème maître s’applique, ce qui implique que t ∈ O(n log n).

Exponentiation rapide. Nous avons déjà établi que l’algorithme d’expo-


nentiation rapide donne lieu à la récurrence t(n) = t(n ÷ 2) + 2. Nous avons
b = 2, c = 1 et d = 0. Le deuxième cas du théorème maître s’applique car
c = bd . Nous obtenons donc t ∈ O(log n).

Multiplication rapide. Nous avons déjà établi que l’algorithme de Karat-


suba donne lieu à la récurrence t(n) = 3 · t(n ÷ 2) + f (n) où f ∈ O(n). Nous
avons b = 2, c = 3 et d = 1. Le troisième cas du théorème maître s’applique
car c = 3 > 2 = bd . Nous obtenons donc t ∈ O(nlog 3 ).
2. Si les détails techniques vous intéressent, voir [Eri19, chapitre 1.7 (Ignoring Floors and
Ceilings Is Okay, Honest)].
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 85

5.6 Problème de la ligne d’horizon


Un bloc est un triplet (g, h, d) ∈ N3≥1 qui décrit ce rectangle dans le plan:

(g, h) (d, h)

(g, 0) (d, 0)

Un paysage est une séquence de blocs. Par exemple, le paysage

[(1, 2, 3), (2, 1, 5), (3, 4, 8), (9, 3, 10), (11, 2, 12), (11, 1, 15), (13, 2, 17)]

correspond graphiquement à:

4
3
2
1
0
1 2 3 5 8 9 10 11 12 13 15 17

Puisque plusieurs blocs peuvent se chevaucher, le calcul de la surface d’un


paysage n’est pas immédiat. Par exemple, celui du paysage ci-dessus est de
2 · (3 − 1) + 4 · (8 − 3) + 3 · (10 − 9) + 2 · (12 − 11) + 1 · (13 − 12) + 2 · (17 − 13) = 38.
Une approche possible afin de calculer la surface d’un paysage consiste à le
découper en blocs disjoints possédant la même surface, par exemple:

4
3
2
1
0
1 3 8 9 10 11 12 13 17

Nous présentons un algorithme qui découpe un paysage en blocs disjoints


à la manière du tri par fusion:
— on divise le paysage en deux sous-séquences de blocs;
— on découpe chaque sous-séquence (récursivement);
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 86

— on fusionne les deux sous-paysages découpés.


Les deux premières étapes s’implémentent exactement comme celles du tri par
fusion. Voyons donc comment implémenter la troisième, c.-à-d. la fusion.
Nous devons fusionner deux paysages découpés x et y dans une nouvelle
séquence z. Nous allons consommer les blocs de x et y de la gauche vers la droite,
et les ajouter à la fin de z. De plus, nous allons parfois remettre temporairement
des blocs dans x et y à partir de leur gauche. Ainsi, nous voyons x et y comme
des piles dont les éléments sont empilés/dépilés à partir de la gauche, et z
comme une séquence dont les éléments sont ajoutés à droite.
Soient a et b les premiers blocs de x et y respectivement. Supposons sans
perte de généralité que gauche(a) ≤ gauche(b) (autrement on peut intervertir
x et y). Si droite(a) ≤ gauche(b), on peut simplement retirer a de x et l’ajouter
à z puisque les deux blocs sont disjoints:

a b

Si droite(a) > gauche(b), cela signifie que les deux blocs se chevauchent. On
découpe donc a et b de cette façon:
Cas Avant Après

droite(a) ≤ droite(b)
hauteur(a) ≤ hauteur(b)

droite(a) ≤ droite(b)
hauteur(a) > hauteur(b)

droite(a) > droite(b)


hauteur(a) ≤ hauteur(b)

droite(a) > droite(b)


hauteur(a) > hauteur(b)

Pour effectuer le découpage ci-dessus:


— on retire a et b de x et y, respectivement;
— on identifie le cas qui s’applique (parmi les quatre possibles);
— on découpe a et b en un, deux, ou trois blocs selon le cas;
— on remet les blocs découpés dans x et y (selon leur origine).
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 87

Lorsque la fusion de x et y est complétée, l’un des deux peut encore contenir
des blocs (comme dans le tri par fusion); on les ajoute simplement à z. L’al-
gorithme 34 décrit cette approche sous forme de pseudocode. Notons qu’afin
d’éviter des cas limites embêtants, il faudrait remplacer chaque instruction de
la forme « empiler c sur w » par

si gauche(c) ̸= droite(c) alors empiler c sur w.

Autrement dit, on se débarasse des blocs dégénérés avec une surface de 0.


Analysons le temps d’exécution t de l’algorithme 34. On découpe les n blocs
de P en deux sous-séquences de taille ⌊n/2⌋ et ⌈n/2⌉ respectivement. Remar-
quons que les appels récursifs et la fusion peuvent créer de nouveaux blocs.
Toutefois, on ne peut pas obtenir plus de blocs qu’il y a de lignes verticales,
donc au plus 2n. Ainsi, nous avons:
{
α si n ≤ 1,
t(n) =
t(⌊n/2⌋) + t(⌈n/2⌉) + α · 2n sinon,

où α est une certaine constante. Dans le jargon du théorème maître, on obtient


b = c = 2 et d = 1. Puisque c = bd , nous obtenons donc t ∈ O(n log n).

5.7 Racines multiples et changement de domaine


Lors de l’identification de la forme close d’une récurrence linéaire, nous avons
supposé que les racines de son polynôme caractéristique étaient distinctes.
Cela n’est pas toujours le cas. Si une racine λ apparaît k fois, alors le terme
correspondant est de la forme:

d0 · n0 · λn + d1 · n1 · λn + . . . + dk−1 · nk−1 · λn .

Par exemple, considérons une récurrence t dont le polynôme est t est (x −


3)(x − 2)(x − 2). Nous obtenons:

t(n) = c1 · 3n + c2 · 2n + c3 · n · 2n .

Maintenant que nous savons traiter les racines multiples, nous pouvons
donner un aperçu du raisonnement derrière le théorème maître. Considérons
la récurrence suivante qui capture essentiellement le temps d’exécution du tri
par fusion et de l’algorithme du problème de la ligne d’horizon:
{
1 si n = 0,
t(n) =
2 · t(n ÷ 2) + n sinon.

Cette récurrence n’est pas linéaire, on ne peut donc pas appliquer directement
notre méthode. Cependant, nous pouvons analyser t pour les valeurs de n qui
sont des puissances de deux en effectuant un changement de domaine. Posons
s(k) = t(2k ). Remarquons que s(0) = t(1) = 3 et que s(k) = 2 · t(2k−1 ) + 2k
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 88

Algorithme 34 : Découpage récursif d’un paysage en blocs disjoints.


Entrées : un paysage P de n blocs
Résultat : découpage de P en blocs disjoints
découper(n):
si n ≤ 1 alors
retourner P
sinon
x ← découper(P [1 : n ÷ 2])
y ← découper(P [n ÷ 2 + 1 : n])
z ← []
// Fusionner blocs de x et y dans z
tant que |x| > 0 ∧ |y| > 0
dépiler a de x
dépiler b de y
si gauche(b) < gauche(a) alors // intervertir?
a, b ↔ b, a
x, y ↔ y, x
si droite(a) ≤ gauche(b) alors // sans chevauchement?
empiler b sur y
ajouter a à z
sinon // découper a et b
si droite(a) ≤ droite(b) alors
si hauteur(a) ≤ hauteur(b) alors
empiler (gauche(a), hauteur(a), gauche(b)) sur x
empiler b sur y
sinon
empiler a sur x
empiler (droite(a), hauteur(b), droite(b)) sur y
sinon
si hauteur(a) ≤ hauteur(b) alors
empiler (droite(b), hauteur(a), droite(a)) sur x
empiler (gauche(a), hauteur(a), gauche(b)) sur x
empiler b sur y
sinon
empiler a sur x
// Retourner z avec les blocs restants de x ou y
retourner z + x + y

pour k > 0. Ainsi:


{
3 si k = 0,
s(k) =
2 · s(k − 1) + 2k sinon.
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 89

Nous obtenons donc la récurrence linéaire non homogène s(k)−2·s(k−1) = 2k ,


ce qui mène au polynôme:

obtenu de 2k
z }| {
(x − 2) · (x − 2)
| {z }
poly. car.

et ainsi la forme close:

s(k) = c1 · 2k + c2 · k · 2k .

En résolvant le système:

s(0) = 3 = c1
s(1) = 8 = 2c1 + 2c2

nous obtenons c1 = 3 et c2 = 1. Ainsi:

s(k) = 3 · 2k + k · 2k .

Pour une valeur n = 2k , on obtient donc t(n) = 3n + n log n. Informellement,


on conclut donc que t ∈ O(n log n : n est une puissance de deux). Il existe des
notions relativement simples a (que nous ne couvrirons pas) qui permettent
de lever la condition et d’en conclure que t ∈ O(n log n).
a. Par exemple, voir la notion de « b-smoothness » dans [BB96, chap. 3.4].
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 90

5.8 Exercices
5.1) Identifiez une forme close pour la récurrence suivante:


1 si n = 0,
t(n) = 2 si n = 1,


t(n − 1) + 6 · t(n − 2) sinon.

Identifiez sa complexité asymptotique (aussi précisément que possible).

5.2) Identifiez une forme close pour la récurrence suivante:




0 si n = 0,
t(n) = 1 si n = 1,


t(n − 1) + 2 · t(n − 2) + 3 sinon.

Identifiez sa complexité asymptotique (aussi précisément que possible).

5.3) Identifiez une forme close pour la récurrence suivante:




1 si n = 0,
t(n) = 0 si n = 1,


t(n − 2) sinon.

Identifiez sa complexité asymptotique (aussi précisément que possible).

5.4) Identifiez une forme close pour la récurrence suivante:


{
0 si n ≤ 2,
t(n) =
t(n − 1) + 10 · t(n − 2) + 8 · t(n − 3) + 1 sinon.

Identifiez sa complexité asymptotique (aussi précisément que possible).

5.5) Soient c, d ∈ R≥1 . Identifiez une forme close pour la récurrence suivante:
{
0 si n = 0,
t(n) =
d · t(n − 1) + c sinon.

Identifiez sa complexité asymptotique (aussi précisément que possible).

5.6) Nous avons vu que la récurrence définie par t(0) = 0 et t(k) = 2·t(k−1)+1
possède la forme close t(k) = 2k − 1. Montrez que c’est bien le cas par
induction sur k.

5.7) Donnez un algorithme qui calcule le nombre d’inversions d’une séquence


de n éléments comparables en temps O(n log n).
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 91

5.8) Donnez un algorithme qui calcule tous les pavages d’une grile 2 × n à
l’aide de tuiles de cette forme:

Donnez une récurrence indiquant le nombre de pavages obtenus par l’al-


gorithme par rapport à n.

5.9) Combien y a-t-il de séquences binaires de n bits qui ne contiennent pas


deux occurrences consécutives de 1?

5.10) Adaptez l’algorithme d’exponentiation rapide afin qu’il calcule bn mod m


où m ∈ N≥2 .

5.11) Afin d’élaborer l’algorithme de Karatsuba, nous avons d’abord considéré


l’identité (10k · a + b)(10k · c + d) = 102k · ac + 10k · (ad + bc) + bd. Nous
aurions donc pu implémenter la multiplication de cette façon:
Entrées : x, y ∈ N représentés sous n ∈ N≥1 chiffres en base 10
Résultat : x · y
mult(n, x, y ):
si n = 1 alors
retourner x · y
sinon
k ← ⌈n/2⌉
a, b ← x ÷ 10k , x mod 10k
c, d ← y ÷ 10k , y mod 10k
e ← mult(k, a, c)
f ← mult(k, a, d)
g ← mult(k, b, c)
h ← mult(k, b, d)
retourner 102k · e + 10k · (f + g) + h
Quelle est la complexité de cet algorithme? Est-il aussi efficace que l’al-
gorithme de Karatsuba?

5.12) Donnez un algorithme qui reçoit une séquence binaire non décroissante
et retourne le nombre de bits égaux à 1 en temps O(log n). Par exemple,
votre algorithme doit retourner 5 sur entrée [0, 0, 0, 1, 1, 1, 1, 1].

5.13) Un terrain est une matrice A ∈ Nm×n où m, n ≥ 3. Nous disons qu’une


paire (i, j) ∈ [n] × [n] est un sommet de A si 1 < i < m, 1 < j < n et ces
CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 92

inégalités sont satisfaites:

A[i − 1, j]

>
A[i, j − 1] < A[i, j] > A[i, j + 1]

<
A[i + 1, j]

Donnez un algorithme qui détermine si un terrain possède un sommet en


temps O(m · log n).

5.14) Nous disons qu’une séquence s de n ∈ N≥1 éléments comparables est triée
circulairement s’il existe i ∈ [n] tel que

s[i] ≤ s[i + 1] ≤ . . . ≤ s[n] ≤ . . . ≤ s[i − 1].

En particulier, si i = 1, alors s est triée au sens usuel. Donnez un algo-


rithme qui reçoit en entrée une séquence s triée circulairement et dont
tous les éléments sont distincts, et qui retourne le plus grand élément de
s en temps O(log n). Par exemple, votre algorithme devrait retourner 19
sur entrée s = [10, 12, 19, 1, 3, 4, 7].

5.15) Donnez un algorithme qui reçoit une séquence s de n entiers et qui re-
tourne la plus grande somme contigüe en temps O(n log n), c’est-à-dire:

max{s[i] + . . . + s[j] : 1 ≤ i ≤ j ≤ n}.

Par exemple, votre algorithme doit retourner 9 sur entrée

s = [3, 1, −5, 4, −2, 1, 6, −3].

5.16) Imaginons une implémentation du tri rapide (quicksort) où le choix du


pivot partitionne la séquence environ au tiers de sa taille. Autrement dit,
la séquence est découpée en deux sous-séquences: l’une de taille n ÷ 3
et l’autre de taille n − (n ÷ 3). Analysez semi-formellement le temps
d’exécution asymptotique de l’algorithme à l’aide d’un arbre de récursion.
(basé sur un passage de [Eri19, chap. 1.7])

5.17) Rappelons le problème de vote à majorité absolue de la section 1.3: étant


donné une séquence s de n éléments, on cherche à identifier une valeur qui
apparaît plus de n/2 fois dans s (s’il en existe une). Donnez un algorithme
diviser-pour-régner qui résout ce problème en temps O(n log n). Tentez de
concevoir un algorithme qui n’utilise pas les comparaisons {<, ≤, ≥, >}.

5.18) ⋆⋆ (requiert des connaissances non couvertes dans ce cours)


CHAPITRE 5. ALGO. RÉCURSIFS ET DIVISER-POUR-RÉGNER 93

Identifiez une forme close pour la récurrence suivante:




1 si n = 0,
t(n) = 0 si n = 1,


−t(n − 2) sinon.

Identifiez sa complexité asymptotique (aussi précisément que possible).

5.19) ⋆⋆ (dépasse le cadre du cours)


Soit S un ensemble muni d’un élément neutre e et d’une opération binaire
⊕ : S × S → S à la fois associative et commutative. Autrement dit, S
satisfait les contraintes suivantes pour tous a, b, c ∈ S:
— a ⊕ e = a = e ⊕ a,
— a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c,
— a ⊕ b = b ⊕ a.
déf déf
Définissons b0 = e et bn = ((b ⊕ b) ⊕ · · · ) ⊕ b pour tous b ∈ S et n ∈ N≥1 .
| {z }
n fois

(a) Adaptez l’algorithme d’exponentiation rapide afin de calculer bn


avec O(log n) applications de ⊕.
(b) Montrez que l’algorithme est correct par induction généralisée.
(c) Montrez que chacun de ces triplets (S, e, ⊕) satisfait la précondition:
déf déf déf
(i) S = N, e = 1 et a ⊕ b = a · b;
déf déf déf
(ii) S = N, e = 0 et a ⊕ b = a + b;
déf déf déf
(iii) S = {0, 1, . . . , m − 1}, e = 0 et a ⊕ b = (a + b) mod m;
déf déf déf
(iv) S = {0, 1, . . . , m − 1}, e = 1 et a ⊕ b = (a · b) mod m;
déf déf déf
(v) S = {A ∈ Nk×k : A est diagonale}, e = I et A ⊕ B = AB.

Remarque.

Cet exercice montre que l’algorithme d’exponentiation rapide s’ap-


plique plus généralement à tout monoïde commutatif. En particu-
lier, le monoïde décrit en (iv) s’avère utile en cryptographie.
6
Force brute

Ce chapitre traite de la force brute, une approche simple qui consiste à explo-
rer exhaustivement un ensemble de solutions candidates jusqu’à l’identification
d’une véritable solution. Par exemple, afin de trier une séquence s de n élé-
ments comparables, on pourrait naïvement énumérer toutes les permutations
de s jusqu’à l’identification de sa forme triée. Cela nécessiterait n! itérations
dans le pire cas, ce qui est impraticable. En général, on nomme « explosion com-
binatoire » le phénomène où l’espace de recherche croît rapidement par rapport
à la taille des entrées. Malgré cette limitation générale, la force brute possède
certains avantages. Premièrement, elle permet d’établir rapidement un algo-
rithme de référence contre lequel on peut tester nos algorithmes plus efficaces.
Deuxièmement, pour plusieurs problèmes, on ne connaît simplement aucune
autre approche algorithmique, par ex. plusieurs problèmes dits NP-complets.
Nous présentons quelques-uns de ces problèmes.

6.1 Problème des n dames


Le célèbre problème des huit dames consiste à placer huit dames sur un échiquier
sans qu’elles puissent s’attaquer. De façon plus générale, ce problème consiste à
place n pièces sur une grille n × n sans qu’il y ait plus d’une pièce par ligne, par
colonne et par diagonale. Cherchons à résoudre ce problème algorithmiquement.
Nous pouvons représenter une solution par une matrice A ∈ {0, 1}n×n où A[i, j]
2
indique si une dame apparaît ou non à la position (i, j). Il y a 2n telles matrices.
Pour n = 8, on obtient

18 446 744 073 709 551 616 possibilités.

On peut réduire significativement le nombre de possibilités en observant que A


( 2)
doit contenir exactement n occurrences de 1. Il y a nn telles matrices. Pour
n = 8, on obtient
4 426 165 368 possibilités.

94
CHAPITRE 6. FORCE BRUTE 95

Ces matrices sont éparses, c-à-d. qu’il y a peu d’occurrences de 1 par rap-
port aux occurrences de 0. Nous pouvons donc simplifier leur représentation.
Remarquons qu’une solution assigne nécessairement une dame à chaque ligne
et à chaque colonne. Ainsi, nous pouvons décrire une solution par une séquence
sol de taille n où sol[i] indique la colonne de la dame sur la ligne i. Par exemple,
pour n = 8, la solution [1, 5, 8, 6, 3, 7, 2, 4] correspond graphiquement à:

1 2 3 4 5 6 7 8
1 X
2 X
3 X
4 X
5 X
6 X
7 X
8 X

( 2)
Peu des nn possibilités forment une solution. Par exemple, il existe 92
solutions pour le cas n = 8. Nous avons donc intérêt à ne pas explorer toutes
les possibilités.

Algorithme 35 : Force brute pour le problème des huit dames.


Entrées : n ∈ N
Résultat : solution au problème des n dames (s’il en existe une)
dames(n):
dames'(sol):
si |sol| = n alors
retourner sol
sinon
pour j ∈ {1, . . . , n} \ sol // essayer colonnes dispo.
sol′ ← sol + [j]
si sol′ respecte les contraintes de diagonales alors
r ← dames'(sol′ )
si r ̸= aucune alors
retourner r
retourner aucune
retourner dames'([ ])

Nous allons débuter avec la solution partielle triviale [ ] et progressivement:


— assigner une colonne non utilisée à la dame de la prochaine ligne;
CHAPITRE 6. FORCE BRUTE 96

— poursuivre l’assignation récursivement s’il y a au plus une dame par dia-


gonale;
— si l’assignation ne mène pas à une solution, on répète avec les autres
colonnes non utilisées.
Cette approche s’implémente de façon récursive tel que décrit à l’algorithme 35.
Cette stratégie générale se nomme retour arrière puisqu’on revient sur nos
décisions lorsqu’on n’indentifie aucune solution.
L’algorithme 36 implémente le test des contraintes de diagonales en se ba-
sant sur les observations suivantes. Soit (i, j) une position de la grille. Remar-
quons que la diagonale « sud-ouest → nord-est » est caractérisée par i + j,
alors que la diagonale « nord-ouest → sud-est » est caractérisée par i − j. Par
exemple, pour n = 8, nous avons:

1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
1 2 3 4 5 6 7 8 9 1 0 -1 -2 -3 -4 -5 -6 -7
2 3 4 5 6 7 8 9 10 2 1 0 -1 -2 -3 -4 -5 -6
3 4 5 6 7 8 9 10 11 3 2 1 0 -1 -2 -3 -4 -5
4 5 6 7 8 9 10 11 12 4 3 2 1 0 -1 -2 -3 -4
5 6 7 8 9 10 11 12 13 5 4 3 2 1 0 -1 -2 -3
6 7 8 9 10 11 12 13 14 6 5 4 3 2 1 0 -1 -2
7 8 9 10 11 12 13 14 15 7 6 5 4 3 2 1 0 -1
8 9 10 11 12 13 14 15 16 8 7 6 5 4 3 2 1 0

Algorithme 36 : Test des contraintes de diagonales.


Entrées : assignation partielle des dames sol
Résultat : sol respecte les contraintes de diagonales?
diag(sol):
nord ← ∅; sud ← ∅
pour i ← 1, . . . , |sol|
j ← sol[i]
si i + j ∈ nord alors retourner faux
sinon ajouter i + j à nord
si i − j ∈ sud alors retourner faux
sinon ajouter i − j à sud
retourner vrai
CHAPITRE 6. FORCE BRUTE 97

6.2 Problème du sac à dos


Reconsidérons le problème du sac à dos introduit à la section 4.3. Nous avons
présenté un algorithme d’approximation pour ce problème, mais aucun algo-
rithme qui permette de le résoudre exactement. Un algorithme de force brute
pour ce problème consiste à explorer les 2n possibilités. L’algorithme 37 im-
plémente cette approche de façon récursive: si nous sommes rendus à l’objet
i, on explore d’une part l’assignation où on ne choisit pas i, puis l’assignation
où on choisit i, ce qui augmente la valeur et le poids du sac de v[i] et p[i]
respectivement.

Algorithme 37 : Force brute pour le problème du sac à dos.


Entrées : valeurs v ∈ Nn , poids p ∈ Nn et capacité c ∈ N
Résultat : valeur maximale du sac à dos
sac-à-dos-naïf(v, p, c):
remplir(i, valeur, poids):
si i > n alors
si poids ≤ c alors retourner valeur
sinon retourner 0
sinon
valeur′ ← valeur + v[i]
poids′ ← poids + p[i]
retourner max(remplir(i + 1, valeur, poids),
remplir(i + 1, valeur ′ , poids′ ))
retourner remplir(1, 0, 0)

L’algorithme 37 fonctionne en temps Ω(2n ), même dans le meilleur cas,


puisqu’il explore systématiquement les 2n assignations possibles sans s’arrêter
prématurément. Nous pouvons améliorer l’algorithme en n’explorant pas une
assignation si elle excède la capacité du sac. Si nous imaginons la recherche
d’une solution comme l’exploration de l’arbre des assignations, nous élaguons 1
donc certaines de ses branches. L’algorithme 38 décrit cette modification.
Par exemple, considérons cette instance introduite à la section 4.3:

v = [50, 5, 65, 10, 12, 20],


p = [700, 320, 845, 70, 420, 180],
c = 900.

L’algorithme 37 teste 26 = 64 combinaisons, alors que l’algorithme 38 n’en


teste que 18.
Nous pouvons améliorer l’algorithme 38 à l’aide d’un élagage plus agressif:
— on stocke la meilleure solution identifiée jusqu’ici;
1. On parle de « pruning » en anglais.
CHAPITRE 6. FORCE BRUTE 98

Algorithme 38 : Force brute avec élagage basé sur la capacité.


Entrées : valeurs v ∈ Nn , poids p ∈ Nn et capacité c ∈ N
Résultat : valeur maximale du sac à dos
sac-à-dos-élagage(v, p, c):
remplir(i, valeur, poids):
si i > n alors
retourner valeur
sinon
valeur′ ← valeur + v[i]
poids′ ← poids + p[i]
sol ← remplir(i + 1, valeur, poids) // sans objet i

si poids ≤ c alors // avec objet i
sol ← max(sol, remplir(i + 1, valeur′ , poids′ ))
retourner sol
retourner remplir(1, 0, 0)

— si une branche ne permet pas d’excéder la meilleure solution, on l’ignore.


Afin d’implémenter la deuxième étape, nous utilisons l’observation suivante: si
l’ajout de tous les objets restants (en ignorant la capacité du sac) n’excéderait
pas la meilleure solution découverte, alors il est inutile de poursuivre.
Afin de débuter cette approche, nous pourrions considérer ∞ comme la
meilleure solution connue. Cependant, nous pouvons faire mieux en obtenant
la solution d’un algorithme d’approximation qui s’approche de la solution op-
timale. L’algorithme 39 implémente cette procédure.
Par exemple, considérons cette instance:
v = [1, 2, . . . , 200],
p = [200, . . . , 2, 1],
c = 150.
L’algorithme 38 effectue 327 670 103 appels récursifs et explore 144 034 914
assignations complètes, alors que l’algorithme 39 effectue 4 325 153 appels ré-
cursifs et n’explore qu’une seule assignation complète. De plus, une implémen-
tation directe en Python3 donne un temps d’exécution d’approximativement
1 min. 52 sec. et 2,45 sec., respectivement, sur cette instance.
Ce type d’approche s’inscrit plus généralement dans le cadre du « branch
and bound », où l’on guide l’exploration d’un arbre de possibilités grâce à des
bornes pouvant être identifiées efficacement.

6.3 Problème du retour de monnaie


Le problème du retour de monnaie consiste à identifier la plus petite quantité
de pièces permettant de rendre la monnaie sur un certain montant:
CHAPITRE 6. FORCE BRUTE 99

Entrée: un montant m ∈ N et une séquence s de n ∈ N


nombres naturels représentant un système monétaire
Sortie: plus petit nombre de pièces du système s permettant
de former m

Pour le dollar canadien, l’euro, le dinar et le dirham, par exemple, on peut


simplement rendre les pièces en ordre décroissant de leur valeur. Cependant,
cette approche gloutonne ne fonctionne pas en général. Par exemple, si m = 10
et s = [1, 5, 7], alors cette procédure retourne 4 pièces (10 = 7 + 1 + 1 + 1), bien
que la solution optimale soit constituée de 2 pièces (10 = 5 + 5).
Nous pouvons résoudre ce problème par force brute:
— on considère les pièces itérativement;
— si la pièce actuelle s[i] est supérieure au montant à rendre, alors on passe
à la prochaine pièce;
— sinon on choisit la meilleure solution entre celle qui prend s[i] et celle qui
ne la prend pas.
L’algorithme 40 présente cette procédure sous forme de pseudocode.

Algorithme 39 : Force brute sans exploration des branches qui ne


permettent pas d’excéder la meilleure solution découverte.
Entrées : valeurs v ∈ Nn , poids p ∈ Nn et capacité c ∈ N
Résultat : valeur maximale du sac à dos
sac-à-dos-turbo(v, p, c):
meilleure ← approx(v, p, c) // valeur d'un algo. d'approx.
potentiel ← [v[1] + . . . + v[i] : i ∈ [n]]
remplir(i, valeur, poids):
meilleure ← max(meilleure, valeur)
si (i < n) ∧ (valeur + potentiel[i] > meilleure) alors
valeur′ ← valeur + v[i]
poids′ ← poids + p[i]
remplir(i + 1, valeur, poids) // sans objet i

si poids ≤ c alors // avec objet i
remplir(i + 1, valeur ′ , poids′ )
remplir(1, 0, 0)
retourner meilleure
CHAPITRE 6. FORCE BRUTE 100

Algorithme 40 : Force brute pour retour de monnaie.


Entrées : montant m ∈ N, séquence s de n ∈ N pièces
Résultat : nombre minimal de pièces afin de rendre m
monnaie-brute(m, s):
// k : montant qu'on doit encore rendre
// num: nombre de pièces utilisées jusqu'ici
// i: indice de la pièce considérée
aux(k, num, i):
si i = n + 1 alors
si k = 0 alors retourner num
sinon retourner ∞
sinon
sans ← aux(k, num, i + 1) // sol. sans s[i]
avec ← ∞
si k ≥ s[i] alors
avec ← aux(k − s[i], num + 1, i) // sol. avec s[i]
retourner min(sans, avec)
retourner aux(m, 0, 1)

6.4 Satisfaction de formules de logique propositionnelle


Il existe un certain nombre de problèmes qu’on ne sait pas résoudre autrement
que par force brute 2 . L’un des plus importants, nommé SAT, consiste à déter-
miner si une formule de logique propositionnelle est satisfaisable. Par exemple,
la formule
(x ∨ y ∨ ¬z) ∧ (¬x ∨ y ∨ z) ∧ (¬x ∨ ¬y ∨ ¬z)
est satisfaite par l’assignation x = vrai, y = vrai et z = faux. Comme ce
problème trouve des applications dans une foule de domaines, dont l’intelli-
gence artificielle, la vérification formelle et l’élaboration de circuits, il existe
un large éventail d’algorithmes de force brute sophistiqués qui n’explorent pas
nécessairement les 2n assignations possibles des n variables booléennes.
Ainsi, afin de résoudre un problème difficile, on peut le traduire vers une
formule de logique propositionnelle qu’on envoie à un solveur SAT. Par exemple,
considérons le problème des n dames. Pour chaque paire (i, j), on introduit une
variable booléenne xi,j qui indique si une dame apparaît ou non à la case (i, j)
de l’échiquier. On formule ensuite le problème avec les contraintes suivantes:
— il y a au moins une dame par ligne:
∧ ∨
xi,j
1≤i≤n 1≤j≤n

2. Ou par programmation dynamique, que nous verrons au chapitre suivant.


CHAPITRE 6. FORCE BRUTE 101

— il ne peut y avoir deux dames (ou plus) sur une même ligne:
∧ ∧ ∧
(¬xi,j ∨ ¬xi,k )
1≤i≤n 1≤j≤n j<k≤n

— il ne peut y avoir deux dames (ou plus) sur une même colonne:
∧ ∧ ∧
(¬xj,i ∨ ¬xk,i )
1≤i≤n 1≤j≤n j<k≤n

— il ne peut y avoir deux dames (ou plus) sur une même diagonale:
∧ ∧
(¬xi,j ∨ ¬xi′ ,j ′ ) ∧ (¬xi,j ∨ ¬xi′ ,j ′ )
i,i′ ,j,j ′ ∈[n] i,i′ ,j,j ′ ∈[n]
(i,j)̸=(i′ ,j ′ ) (i,j)̸=(i′ ,j ′ )
i+j=i′ +j ′ i−j=i′ −j ′

Un solveur SAT de pointe risque d’identifier une solution d’une telle formule
bien plus rapidement qu’un algorithme par force brute « maison » (cela dépend
des problèmes, des formules et de leur taille). Par exemple, voici la formulation
du problème pour n = 4 avec le solveur z3 de Microsoft Research.

6.5 Programmation linéaire entière


Pour les problèmes où l’on doit raisonner sur des variables entières non bor-
nées, on peut difficilement exploiter un solveur SAT. Alternativement, nous
pouvons tenter de traduire notre problème en programmation linéaire entière,
un paradigme important notamment en recherche opérationnelle. Dans sa forme
canonique, on cherche à identifier un vecteur x ∈ Nn qui maximise ou minimise
une fonction objectif linéaire sujet à des contraintes linéaires de cette forme:
optimiser cT · x,
sujet à A · x ≤ b et x ∈ Nn .

Par exemple, l’instance m = 10 et s = [1, 5, 7] du problème du retour de mo-


nnaie s’exprime par:

minimiser x + y + z sujet à x + 5y + 7z = 10.

L’instance v = [50, 5, 65, 10, 12, 20], p = [700, 320, 845, 70, 420, 180], c = 900 du
problème du sac à dos s’exprime quant à elle par:

maximiser 50 x1 + 5 x2 + 65 x3 + 10 x4 + 12 x5 + 20 x6
sujet à 700 x1 + 320 x2 + 845 x3 + 70 x4 + 420 x5 + 180 x6 ≤ 900.

Ces programmes peuvent être résolus efficacement en pratique par des solveurs
exploitant la force brute combinée à des heuristiques.
CHAPITRE 6. FORCE BRUTE 102

6.6 Exercices
6.1) La distance de Levenshtein entre deux chaînes de caractères u et v, dé-
notée dist(u, v), est définie comme étant la plus petite quantité d’ajouts,
de retraits et de modifications de lettres qui transforment u en v. Nous
écrivons ε afin de dénoter la chaîne vide. Par exemple: dist(ab, ac) = 1,
dist(abc, ba) = 2 et dist(ε, ab) = 2. Donnez un algorithme de force brute
qui calcule la distance entre deux chaînes données. Pensez d’abord à une
borne supérieure sur dist(u, v).

6.2) Donnez un algorithme qui identifie la plus plus longue sous-chaîne conti-
guë commune entre deux chaînes de caractères u et v. Par exemple, si
u = abcaba et v = abaccab, alors votre algorithme devrait retourner cab.
Analysez sa complexité.

6.3) Revisitons le problème de l’exercice 6.2). Donnez cette fois un algorithme


pour la variante du problème où la sous-chaîne commune n’a pas à être
contiguë. Par exemple, si u = abcaba et v = abaccab, votre algorithme
devrait retourner abcab. Fonctionne-t-il en temps polynomial?

6.4) Un chemin hamiltonien est un chemin qui passe par chaque sommet d’un
graphe exactement une fois. Donnez un algorithme qui détermine s’il
existe un chemin hamiltonien entre deux sommets s et t d’un graphe G.

6.5) Donnez un algorithme qui complète une grille partielle de sudoku.


7
Programmation dynamique

La programmation dynamique constitue une approche algorithmique qui s’ap-


parente à l’approche diviser-pour-régner et qui surmonte certaines limitations
des stratégies gloutonnes et par force brute. Elle repose sur le principe d’opti-
malité de Bellman qui affirme essentiellement qu’une solution de certains pro-
blèmes s’exprime en fonction de celles de sous-problèmes. Nous présentons la
programmation dynamique en revisitant certains problèmes et en considérant
des problèmes de plus courts chemins dans les graphes.

7.1 Approche descendante


Plusieurs algorithmes récursifs, tels que ceux issus de la force brute, recalculent
des solutions intermédiaires à répétition. L’algorithme 41 de calcul de la suite
de Fibonacci forme un exemple classique de ce phénomène.

Algorithme 41 : Calcul d’un élément de la suite de Fibonacci.


Entrées : n ∈ N
Résultat : nème terme de la suite de Fibonacci
fib(n):
si n ≤ 1 alors
retourner n
sinon
retourner fib(n − 1) + fib(n − 2)

Par exemple, l’arbre des appels récursifs de fib(4) montre que les valeurs de
fib(2), fib(1) et fib(0) sont recalculées plusieurs fois:

103
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 104

fib(4)

fib(3) fib(2)

fib(2) fib(1) fib(1) fib(0)

fib(1) fib(0)

Nous pouvons éviter ces calculs redondants à l’aide de la mémoïsation: on


stocke chaque valeur nouvellement calculée, par ex. à l’aide d’un tableau asso-
ciatif, et on retourne cette valeur lorsqu’elle est requise. Par exemple, l’algo-
rithme 42 utilise la mémoïsation pour le calcul de la suite de Fibonacci. Notons
que le gain en temps offert par la mémoïsation augmente l’usage de mémoire.
Cependant, le gain en temps est souvent exponentiel alors que l’augmentation
en mémoire peut être polynomiale voire linéaire.

Algorithme 42 : Calcul de la suite de Fibonacci avec mémoïsation.


Entrées : n ∈ N
Résultat : nème terme de la suite de Fibonacci
mem ← [ ]
fib(n):
si mem ne contient pas n alors
si n ≤ 1 alors
mem[n] ← n
sinon
mem[n] ← fib(n − 1) + fib(n − 2)
retourner mem[n]

7.2 Approche ascendante


La programmation dynamique est probablement mieux connue sous sa forme
ascendante: on résout les sous-problèmes itérativement des plus petites ins-
tances aux plus grandes, généralement en stockant les résultats intermédiaires
dans un tableau.

7.2.1 Problème du retour de monnaie


Afin d’illustrer l’approche ascendante, reconsidérons le problème du retour de
monnaie. Supposons que nous cherchions à rendre le montant m = 10 dans le
système monétaire s = [1, 5, 7]. Nous considérons les sous-problèmes suivants:
« quelle est la plus petite quantité de pièces afin de rendre le montant j avec
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 105

les i premières pièces du système? » Nous inscrivons la réponse à ces questions


dans un tableau T :
0 1 2 3 4 5 6 7 8 9 10
0 (sans pièce) 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞
1 (avec pièce 1) 0 1 2 3 4 5 6 7 8 9 10
2 (avec pièce 5) 0 1 2 3 4 1 2 3 4 5 2
3 (avec pièce 7) 0 1 2 3 4 1 2 1 2 3 2

Par exemple, nous avons T [ 2, 7 ] = 3 car on peut retourner le montant 7 avec


trois pièces: 1 + 1 + 5. L’entrée T [ 3, 7 ] = 1 raffine cette solution car on peut
rendre la pièce 7 maintenant qu’elle est permise. La solution au problème de
départ apparaît dans le coin inférieur droit: T [ 3, 10 ] = 2.

Algorithme 43 : Programmation dynamique pour retour de monnaie.


Entrées : montant m ∈ N, séquence s de n ∈ N pièces
Résultat : nombre minimal de pièces afin de rendre m
monnaie-dyn(m, s):
// T [i, j] = # pièces pour rendre j avec s[1 : i]
initialiser tableau T [0 . . . n, 0 . . . m] avec ∞
// Aucun pièce pour rendre 0
T [0, 0] ← 0
// Calculer # pièces progressivement
pour i ← 1, . . . , n
pour j ← 0, . . . , m
sans ← T [i − 1, j] // sol. sans s[i]
avec ← ∞
si j ≥ s[i] alors avec ← T [i, j − s[i]] + 1 // sol. avec s[i]
T [i, j] ← min(sans, avec)
retourner T [n, m]

Afin de remplir le tableau algorithmiquement, nous exploitons l’identité:

T [i, 0] = 0, si 0 ≤ i ≤ n,
T [0, j] = ∞ si 1 ≤ j ≤ n,
T [i, j] = min(T [i − 1, j], T [i, j − s[i]] + 1) sinon,

où nous considérons T [i, j − s[i]] = ∞ lorsque j < s[i]. En mots:


— on peut toujours rendre le montant 0 avec aucune pièce,
— on ne peut pas rendre d’autre montant sans pièce,
— pour rendre le montant j avec les i premières pièces: ou bien on n’utilise
pas la ième pièce; ou bien on l’utilise au moins une fois, ce qui réduit le
montant à rendre de s[i] et augmente le nombre de pièces utilisées de 1.
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 106

L’algorithme 43 implémente cette procédure en remplissant le tableau de la


gauche vers la droite et du haut vers le bas.

7.2.2 Problème du sac à dos


Utilisons maintenant la programmation dynamique afin de résoudre le problème
du sac à dos. Posons cette instance:

v = [5, 3, 4] (valeurs),
p = [4, 2, 3] (poids),
c =8 (capacité).

Nous considérons les sous-problèmes suivants: « quelle est la valeur maximale


d’un sac à dos de capacité j qu’on peut remplir avec les i premiers objets? »
Nous inscrivons la réponse à ces questions dans un tableau T :

0 1 2 3 4 5 6 7 8
0 (sans objet) 0 0 0 0 0 0 0 0 0
1 (avec objet 1) 0 0 0 0 5 5 5 5 5
2 (avec objet 2) 0 0 3 3 5 5 8 8 8
3 (avec objet 3) 0 0 3 4 5 7 8 9 9

Par exemple, nous avons T [ 2, 5 ] = 5 puisqu’on ne peut pas prendre les deux
premiers objets simultanément et puisque le premier objet possède la valeur
maximale entre ces deux objets. L’entrée T [ 3, 5 ] = 7 raffine cette solution car
on peut prendre les objets 2 et 3 pour une valeur combinée de 7. La solution
au problème de départ apparaît dans le coin inférieur droit: T [ 3, 8 ] = 9.

Algorithme 44 : Prog. dynamique pour le problème du sac à dos.


Entrées : valeurs v ∈ Nn , poids p ∈ Nn et capacité c ∈ N
Résultat : valeur maximale du sac à dos
sac-à-dos-dyn(v, p, c):
// T [i, j] = valeur max. avec objets s[1 : i] et capacité j
initialiser tableau T [0 . . . n, 0 . . . c] avec 0
// Calculer valeur du sac à dos progressivement
pour i ← 1, . . . , n
pour j ← 1, . . . , c
sans ← T [i − 1, j] // sans obj. i
avec ← 0
si j ≥ p[i] alors avec ← T [i − 1, j − p[i]] + v[i] // + obj. i
T [i, j] ← max(sans, avec)
retourner T [n, c]
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 107

Afin de remplir le tableau algorithmiquement, nous exploitons l’identité:

T [i, 0] = 0, si 0 ≤ i ≤ n,
T [0, j] = 0 si 0 ≤ j ≤ n,
T [i, j] = max(T [i − 1, j], T [i − 1, j − p[i]] + v[i]) sinon,

où nous considérons T [i, j − p[i]] = 0 lorsque j < p[i]. En mots:


— on ne peut rien mettre dans un sac de capacité 0,
— on ne peut rien mettre dans un sac s’il n’y a aucun objet,
— pour remplir un sac de capacité j avec les i premiers objets: ou bien on
n’utilise pas le ième objet; ou bien on l’utilise une unique fois, ce qui réduit
le poids du sac de p[i] et augmente sa valeur de v[i].
L’algorithme 44 implémente cette procédure en remplissant une ligne à la fois.

7.3 Plus courts chemins


Soit G un graphe pondéré par une séquence de poids entiers p. Le poids d’un
e1 e2 en
chemin v0 −→ v1 −→ v2 · · · −→ vn correspond à p[e1 ] + p[e2 ] + . . . + p[en ].
Autrement dit, le poids d’un chemin allant de v0 vers vn , en empruntant les
arêtes e1 , e2 , . . . , en , correspond à la somme des poids des arêtes traversées.
Dans ce contexte, nous parlons de distance ou de longueur plutôt que de poids.
Nous nous intéressons au calcul de plus courts chemins, c’est-à-dire de chemins
qui minimisent la distance entre deux sommets. Par exemple, il y a deux plus
courts chemins de a vers e à la figure 7.1: a − →b− →d− → e et a − →b− → e qui sont
de longueur 6. Remarquons que la notion de plus court chemin n’est bien définie
que s’il n’existe aucun cycle de longueur négative. Ainsi, nous considérons les
plus courts comme étant simples.

2
b d
3 3

3
a 5 1 f

1 7
c e
7

Figure 7.1 – Exemple de graphe (non dirigé) pondéré.


CHAPITRE 7. PROGRAMMATION DYNAMIQUE 108

7.3.1 Algorithme de Dijkstra


Nous présentons l’algorithme de Dijkstra qui calcule la distance du plus court
chemin d’un sommet de départ s vers tous les autres sommets du graphe. Cet
algorithme fonctionne en attribuant une distance partielle à chaque sommet
qu’on raffine itérativement jusqu’à l’obtention de la distance minimale. Plus
précisément:
— on attribue une distance partielle d[v] à chaque sommet v: 0 à s et ∞ aux
autres sommets. La distance partielle d’un sommet v indique la distance
minimale de s vers v identifiée jusqu’ici;
— on choisit un sommet u non marqué dont la distance partielle est minimale
parmi tous les sommets, puis on marque ce sommet;
— pour chaque voisin/successeur v de u, on raffine d[v] par d[u] + p[u, v]
si cette valeur est inférieure à d[v]. Autrement dit, on considère le plus
court chemin de s vers u suivi de l’arête de u vers v. Si ce chemin est plus
court que le meilleur chemin connu de s vers v, alors on a découvert un
chemin plus court;
— On répète tant qu’il existe au moins un sommet non marqué.
L’algorithme 45 décrit cette procédure sous forme de pseudocode de haut ni-
veau.

Algorithme 45 : Algorithme de Dijkstra.


Entrées : graphe G = (V, E) pondéré par une séquence p de poids non
négatifs, et un sommet de départ s ∈ V
Résultat : séquence d t.q. d[v] indique la longueur d’un plus court
chemin de s vers v
d ← [v 7→ ∞ : v ∈ V ]
d[s] ← 0
tant que ∃ un sommet u non marqué t.q. d[u] ̸= ∞
choisir u ∈ V t.q. d[u] est min. parmi les sommets non marqués
marquer u
pour v : u −
→v
d[v] ← min(d[v], d[u] + p[u, v])
retourner d

Remarquons que si certains sommets sont inaccessibles à partir de s, alors


leur distance demeure à ∞ jusqu’à la terminaison. Ainsi, l’algorithme de Dijks-
tra permet aussi d’identifier l’ensemble d’accessibilité de s.

Identification des chemins. La procédure telle que décrite à l’algorithme 45


ne construit pas les plus courts chemins. Afin de les construire, on peut sto-
cker le prédecesseur de chaque sommet qui a mené à sa distance (partielle).
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 109

Autrement dit, à chaque évaluation de

min(d[v], d[u] + p[u, v]),

si la valeur est réduite, alors le prédecesseur de v devient u. La figure 7.2


donne une trace de l’exécution de l’algorithme de Dijkstra sur le graphe de
la figure 7.1, incluant la construction des prédecesseurs.

∞ ∞ 3 ∞
2 2
b d b d
3 3 3 3
0 ∞ 0 ∞
a 3 a 3
5 1 f 5 1 f
1 7 1 7
c e c e
7 7
∞ ∞ 1 ∞
3 ∞ 3 5
2 2
b d b d
3 3 3 3
0 ∞ 0 ∞
a 3 a 3
5 1 f 5 1 f
1 7 1 7
c e c e
7 7
1 8 1 6
3 5 3 5
2 2
b d b d
3 3 3 3
0 8 0 8
a 3 a 3
5 1 f 5 1 f
1 7 1 7
c e c e
7 7
1 6 3 5 1 6
2
b d
3 3
0 8
a 3
5 1 f
1 7
c e
7
1 6
Figure 7.2 – Exemple d’exécution de l’algorithme de Dijkstra.

Correction. On peut montrer que l’algorithme de Dijkstra est correct en


démontrant la proposition suivante par induction:
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 110

Proposition 25. La boucle principale de l’algorithme de Dijkstra satisfait cet


invariant: pour tout sommet v ∈ V ,
— si v est marqué, alors d[v] est la longueur minimale de s vers v;
— si v n’est pas marqué, alors d[v] est la longueur minimale de s vers v
parmi les chemins qui n’utilisent que les sommets marqués et v.

Complexité. La boucle principale de l’algorithme 45 est exécutée au plus


|V | fois puisque les sommets marqués ne sont plus considérés. L’identification
d’un sommet non marqué u qui minimise d[u] peut se faire en temps O(|V |)
avec une implémentation naïve où on itère sur tous les sommets. Le voisinage
de chaque sommet u est exploré au plus une fois. Au total, nous obtenons donc
un temps d’exécution de:
( )
∑ ( )
O |V | · |V | + deg (u) = O |V |2 + |E| .
+

u∈V

Le goulot d’étranglement de l’algorithme se situe à l’identification du som-


met non marqué. On peut améliorer le temps d’exécution à l’aide d’une struc-
ture de données plus sophistiquée: un monceau de Fibonacci. Ce type de mon-
ceau offre notamment ces opérations:
— ajout d’un élément (en temps constant),
— retrait du plus petit élément (en temps logarithmique amorti),
— diminution d’une clé (en temps constant amorti).
Nous pouvons donc initialiser, en temps O(|V |), un monceau tel que la clé
de chaque sommet v est d[v]. L’instruction choisir peut ainsi être implémentée
en retirant le plus petit élément du monceau. Lorsqu’on met d[v] à jour, on met
également la clé à jour dans le monceau (qui ne peut que décroître). Comme le
retrait et la mise à jour prennent un temps constant et logarithmique amorti,
cela raffine le temps d’exécution total à:
( )

O |V | · log |V | + deg (u) = O(|V | log |V | + |E|).
+

u∈V

Poids négatifs. L’algorithme de Dijkstra suppose que le poids des arêtes sont
non négatifs, autrement l’algorithme peut échouer. Par exemple, considérons
le graphe de la figure 7.3 à partir du sommet a.
L’algorithme marque les sommets dans l’ordre [a, e, b, d, c] et retourne no-
tamment la distance d[e] = 2, alors que le plus court chemin de a vers e est de
longueur 3 + 2 − 2 − 2 = 1.

7.3.2 Algorithme de Floyd-Warshall


Nous présentons maintenant l’algorithme de Floyd-Warshall qui permet d’iden-
tifier les plus courts chemins entre toutes les paires de sommets d’un graphe,
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 111

3
a b
2

2 1 c

−2
e d
−2

Figure 7.3 – Graphe avec poids négatifs sur lequel l’algo. de Dijkstra échoue.

et ce sans restriction de non négativité sur les poids. Cet algorithme exploite
l’observation suivante: si on découpe un plus court chemin de vi vers vj , alors
on obtient un plus court chemin de vi vers un sommet intermédiaire vk , ainsi
qu’un plus court chemin de vk vers vj .

Algorithme 46 : Algorithme de Floyd-Warshall.


Entrées : graphe G = (V, E) par une séquence p de poids entiers (sans
cycle négatif), où V = {v1 , v2 , . . . , vn }
Résultat : matrice d t.q. d[u, v] indique la longueur d’un plus court
chemin de u vers v
d ← [(u, v) 7→ ∞ : u, v ∈ V ]
pour v ∈ V // chemins vides
d[v, v] ← 0
pour (u, v) ∈ E // chemins directs
d[u, v] ← p[u, v]
pour k ← 1, . . . , n // autres chemins
pour i ← 1, . . . , n
pour j ← 1, . . . , n
d[vi , vj ] ← min(d[vi , vj ], d[vi , vk ] + d[vk , vj ])
retourner d

Considérons le graphe illustré à la figure 7.4. En exécutant l’algorithme de


Floyd-Warshall, nous obtenons la trace suivante:
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 112

v1 2 v2

4 5 3
1

v4 2 v3

Figure 7.4 – Exemple de graphe (dirigé) pondéré.

— v1 v2 v3 v4
v1 0 2 ∞ 5
v2 ∞ 0 ∞ 1
v3 ∞ 3 0 ∞
v4 4 ∞ 2 0

k=1 v1 v2 v3 v4 k=2 v1 v2 v3 v4
v1 0 2 ∞ 5 v1 0 2 ∞ 3
v2 ∞ 0 ∞ 1 v2 ∞ 0 ∞ 1
v3 ∞ 3 0 ∞ v3 ∞ 3 0 4
v4 4 6 2 0 v4 4 6 2 0

k=3 v1 v2 v3 v4 k=4 v1 v2 v3 v4
v1 0 2 ∞ 3 v1 0 2 5 3
v2 ∞ 0 ∞ 1 v2 5 0 3 1
v3 ∞ 3 0 4 v3 8 3 0 4
v4 4 5 2 0 v4 4 5 2 0

Ainsi, par exemple, la distance minimale de v4 vers v2 est de 5, et la distance


minimale de v3 vers v1 est de 8.

Identification des chemins. La procédure telle que décrite à l’algorithme 46


ne construit pas les plus courts chemins. Afin de les construire, on utilise la
même approche que pour l’algorithme de Dijkstra: on construit simultanément
une matrice pred où pred[u, v] indique le sommet qui a mené à la valeur d[u, v].

Complexité. La construction de la matrice d requiert un temps de:

Θ(|V |2 + |V | + |E| + |V |3 ).

Puisque |E| ∈ O(|V |2 ), l’algorithme fonctionne donc en temps Θ(|V |3 ).


CHAPITRE 7. PROGRAMMATION DYNAMIQUE 113

Correction. Pour tous sommets u, v ∈ V , définissons δk (u, v) comme étant


la longueur d’un plus court chemin de u vers v dont les sommets intermédiaires
appartiennent à {v1 , v2 , . . . vk }. Nous avons:
Proposition 26. La boucle principale de l’algorithme de Floyd-Warshall sa-
tisfait cet invariant: d[u, v] = δk−1 (u, v) pour tous u, v ∈ V .

Démonstration. Remarquons que δ0 (v, v) = 0 et δ0 (u, v) = p[u, v] pour tous


sommets u ̸= v, car aucun sommet intermédiaire n’est permis. Le cas de base
k = 1 est donc satisfait.
Soit k ≥ 1. Écrivons d′ afin de dénoter la matrice obtenue à partir de d
après l’exécution du corps de la boucle principale. Remarquons que

d′ [vi , vk ] = d[vi , vk ] ∀i ∈ [n], (7.1)



d [vk , vj ] = d[vk , vj ] ∀j ∈ [n]. (7.2)

En effet, imaginons par ex. qu’une entrée d[vi , vk ] soit modifiée, alors cela si-
gnifierait que d[vi , vk ] > d[vi , vk ] + d[vk , vk ] et ainsi que d[vk , vk ] < 0. Cela est
impossible car G ne possède aucun cycle négatif.
Soient i, j ∈ [n] \ {k} et soit C un plus court chemin de vi vers vj dont
les sommets intermédiaires appartiennent à {v1 , v2 , . . . , vk }. Si C n’utilise pas
le sommet vk , alors δk (vi , vj ) = δk−1 (vi , vj ). Sinon, nous avons δk (vi , vj ) =
δk−1 (vi , vk ) + δk−1 (vk , vj ). Par conséquent:

δk (vi , vj ) = min(δk−1 (vi , vj ), δk−1 (vi , vk ) + δk−1 (vk , vj )). (7.3)


| {z }
longueur du chemin qui passe par vk

Nous avons donc:

d′ [vi , vj ] = min(d[vi , vj ], d[vi , vk ] + d[vk , vj ]) (par déf., (7.1) et (7.2))


= min(δk−1 (vi , vj ), δk−1 (vi , vk ) + δk−1 (vk , vj )) (par hyp. d’ind.)
= δk (vi , vj ) (par (7.3)).

Corollaire 1. L’algorithme de Floyd-Warshall est correct.

Démonstration. Soient u, v ∈ V . À la sortie de la boucle principale, k vaut n+1.


Par la proposition 26, nous avons donc d[u, v] = δn+1−1 (u, v) = δn (u, v). Ainsi,
par définition de δn (u, v), d[u, v] dénote la longueur d’un plus court chemin de
u vers v dont les sommets intermédiaires appartiennent à V .

Détection de cycle négatif. On peut adapter l’algorithme de Floyd-


Warshall afin de détecter la présence d’un cycle négatif:
— on exécute l’algorithme (tel quel, sans modification),
— on vérifie s’il existe un sommet v ∈ V tel que d[v, v] < 0,
— si c’est le cas, il existe un cycle négatif qui passe par v, autrement, il
n’existe aucun cycle négatif.
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 114

Accessibilité. On peut adapter l’algorithme de Floyd-Warshall afin de cal-



culer la relation d’accessibilité 1 −
→ d’un graphe (non pondéré), en remplaçant:
— le poids p[u, v] par vrai si u = v ou u − → v, et faux sinon,
— l’addition par l’opération ∧,
— le minimum par l’opération ∨.
À la sortie, on obtient une matrice booléenne d telle que d[u, v] = vrai ssi

u−→ v. Cette modification est décrite à l’algorithme 47.

Algorithme 47 : Algorithme de calcul de relation d’accessibilité.


Entrées : graphe G = (V, E) où V = {v1 , v2 , . . . , vn }

Résultat : matrice d t.q. d[u, v] indique si u −
→v
d ← [(u, v) 7→ faux : u, v ∈ V ]
pour v ∈ V // chemins vides
d[v, v] ← vrai
pour (u, v) ∈ E // chemins directs
d[u, v] ← vrai
pour k ← 1, . . . , n // autres chemins
pour i ← 1, . . . , n
pour j ← 1, . . . , n
d[vi , vj ] ← d[vi , vj ] ∨ (d[vi , vk ] ∧ d[vk , vj ])
retourner d

Remarque.

L’algorithme 46 (avec poids) est attribué à Robert W. Floyd, alors que


l’algorithme 47 (sans poids) est attribué à Stephen Warshall.

7.3.3 Algorithme de Bellman-Ford


Nous présentons une alternative à l’algorithme de Dijkstra qui permet la pré-
sence de poids négatifs: l’algorithme de Bellman-Ford.
Cet algorithme associe une distance partielle à chaque sommet: 0 pour le som-
met de départ et ∞ pour les autres sommets. Il raffine ensuite itérativement
ces distances en explorant les chemins d’au moins une arête, deux arêtes, trois
arêtes, etc. Puisque tout chemin simple est de longueur au plus |V |−1, on cesse
de raffiner après |V | − 1 itérations. L’algorithme 48 décrit cette procédure.
L’identification des chemins requiert simplement l’ajout d’une séquence de
prédecesseurs comme pour les autres algorithmes. La figure 7.5 donne une trace
de l’exécution de l’algorithme de Bellman-Ford sur le graphe de la figure 7.3,
incluant la construction des prédecesseurs.
1. Aussi connue sous le nom de clôture réflexive transitive de la relation −
→.
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 115

Algorithme 48 : Algorithme de Bellman-Ford.


Entrées : graphe G = (V, E) pondéré par une séquence p de poids
entiers (sans cycle négatif), et un sommet de départ s ∈ V
Résultat : séquence d t.q. d[v] indique la longueur d’un plus court
chemin de s vers v
d ← [v 7→ ∞ : v ∈ V ]
d[s] ← 0
faire |V | − 1 fois fois
pour chaque arête u −→v
d[v] ← min(d[v], d[u] + p[u, v])
retourner d

0 ∞ 0 3
a 3 a 3
b 2 ∞ b 2 5
2 1 c 2 1 c [inchangé ensuite]
e d −2 e d −2
−2 −2
∞ ∞ 1 3

Figure 7.5 – Exemple d’exécution de l’algorithme de Bellman-Ford.

Complexité. Le temps d’exécution de l’algorithme appartient à

Θ(|V | + 1 + (|V | − 1) · |E|) = Θ(|V | · |E|).

Correction. Soit δi (v) la longueur d’un plus court chemin de s vers v utili-
sant au plus i arêtes. On peut montrer que l’algorithme de Bellman-Ford est
correct en démontrant la proposition suivante par induction:
Proposition 27. Après i exécutions de la boucle principale de l’algorithme
de Bellman-Ford, pour tout sommet v ∈ V ,
— si d[v] ̸= ∞, alors d[v] correspond à la longueur d’un chemin de s vers
v,
— d[v] ≤ δi (v).

Détection de cycle négatif. On peut adapter l’algorithme afin d’identifier


la présence d’un cycle négatif en lui ajoutant le pseudocode suivant:

pour chaque arête u − →v


si d[u] + p[u, v] < d[v] alors
retourner cycle négatif détecté
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 116

En effet, si d[u] + p[u, v] < d[v], alors une itération supplémentaire aurait
diminué d[v]. Or, cela est impossible en l’absence d’un cycle négatif.

7.3.4 Sommaire
Voici un sommaire des trois algorithmes de plus courts chemins introduits:

Dijkstra Bellman-Ford Floyd-Warshall


Types de chemins d’un sommet vers les autres paires de sommets
Poids négatifs? 7 3 3
Temps d’exécution O(|V | log |V | + |E|) Θ(|V | · |E|) Θ(|V |3 )
Temps (si |E| ∈ Θ(1)) O(|V | log |V |) Θ(|V |) Θ(|V |3 )
Temps (si |E| ∈ Θ(|V |)) O(|V | log |V |) Θ(|V |2 ) Θ(|V |3 )
Temps (si |E| ∈ Θ(|V |2 )) O(|V |2 ) Θ(|V |3 ) Θ(|V |3 )
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 117

7.4 Exercices
7.1) La quantité de mémoire utilisée par l’algorithme 43 appartient à O(m·n).
Adaptez l’algorithme afin de la réduire à O(m).

7.2) L’algorithme 43 et l’algorithme 44 retournent la valeur de la solution


optimale mais pas les pièces/objets choisis. Adaptez ces algorithmes afin
qu’ils retournent l’ensemble des pièces/objets.

7.3) Donnez un algorithme de programmation dynamique qui calcule la dis-


tance entre deux chaînes données, telle que définie à l’exercice 6.1).

7.4) Donnez un algorithme de programmation dynamique qui identifie une


sous-chaîne contiguë de longueur maximale entre deux chaînes données;
voir l’exercice 6.2) pour un exemple. Analysez sa complexité.

7.5) Donnez un algorithme de programmation dynamique qui identifie une


sous-chaîne (pas nécessairement contiguë) de longueur maximale entre
deux chaînes données; voir l’exercice 6.3) pour un exemple. Analysez sa
complexité.

7.6) Identifiez une famille de graphes pour laquelle il existe un nombre expo-
nentiel de plus courts chemins entre deux des sommets.

7.7) Montrez que si un graphe ne possède pas de cycle négatif et qu’il existe
un plus court chemin entre deux sommets, alors il en existe un simple.

7.8) Adaptez le pseudocode des algorithmes de Dijkstra, Floyd-Warshall et


Bellman-Ford afin de construire des plus courts chemins (et non seulement
les distances).

7.9) Comment pourrait-on adapter l’algorithme de Floyd-Warshall afin de cal-


+
culer la relation −
→ d’un graphe? Cette relation est définie par:
+ déf
u−
→ v ⇐⇒ il existe un chemin non vide de u vers v.

7.10) Modifiez l’algorithme de Bellman-Ford afin qu’il puisse parfois terminer


plus rapidement qu’en |V | − 1 itérations.

7.11) Si nous voulons seulement identifier un plus court chemin d’un sommet
s vers un sommet t, à quel moment pouvons-nous arrêter l’exécution de
l’algorithme de Dijkstra?

7.12) Pouvons-nous adapter l’algorithme de Dijkstra afin de déterminer la dis-


tance minimale entre chaque paire de sommets? Si c’est le cas, y a-t-il
un avantage en comparaison à l’algorithme de Floyd-Warshall? Sinon,
pourquoi?
CHAPITRE 7. PROGRAMMATION DYNAMIQUE 118

7.13) L’algorithme de Dijkstra ne fonctionne pas sur les graphes avec des poids
négatifs. Peut-on le faire fonctionner avec le prétraitement suivant?
Prétraitement: on remplace le poids p[e] de chaque arête e par le nouveau
poids p[e] + |d|, où d est le plus petit poids négatif du graphe?

7.14) Deux personnes situées dans des villes distinctes veulent se rencontrer.
Elles désirent le faire le plus rapidement possible et décident donc de se
rejoindre dans une ville intermédiaire. Expliquez comment identifier cette
ville algorithmiquement. Considérez un graphe pondéré G = (V, E) où V
est l’ensemble des villes, et où chaque arête u −
→ v de poids d représente
une route directe de u vers v dont le temps (idéalisé) pour la franchir est
de d minutes.
(tiré de [Eri19, chap. 8, ex. 14])

7.15) Nous pouvons raffiner la notion de plus court chemin en minimisant


d’abord le poids d’un chemin, puis son nombre d’arêtes. Par exemple,
il n’y a qu’un seul plus court chemin de a vers e à la figure 7.1, puisque le
chemin a − →b− → e possède moins d’arêtes que le chemin a − →b− →d− → e,
bien qu’ils soient tous deux de poids minimal. Donnez un algorithme qui
identifie des plus courts chemins (sous la nouvelle définition) d’un som-
met de départ s vers tous les autres sommets.
(tiré de [Eri19, chap. 8, ex. 12])
8
Algorithmes et analyse probabilistes

Dans l’ensemble des chapitres précédents, nous avons étudié les algorithmes dits
déterministes: les algorithmes dont la valeur de retour et le temps d’exécution
ne diffèrent jamais sur une même entrée. Dans ce chapitre, nous considérons
les algorithmes probabilistes ayant accès à une source d’aléa (idéalisée).

8.1 Nombres aléatoires


Considérons le scénario suivant: vous désirez jouer à un jeu de société qui
requiert un dé à six faces, mais vous n’avez qu’une pièce de monnaie (non
biaisée). Comment pouvez-vous simuler un dé à l’aide de votre pièce?
Une solution algorithmique simple consiste à:
— choisir trois bits aléatoires y2 y1 y0 avec trois tirs à pile ou face;
— retourner x si le nombre binaire y2 y1 y0 vaut x ∈ [1, 6], et recommencer
sinon, c.-à-d. lorsque y2 = y1 = y0 = 0 ou y2 = y1 = y0 = 1.
Cette procédure, décrite à l’algorithme 49 génère un nombre aléatoire x ∈
[1, 6] de façon uniforme. Cependant, elle peut en théorie effectuer un nombre
arbitraire d’itérations. Cherchons à identifier le « nombre moyen » de tirs à pile
ou face effectués par l’algorithme.
Rappelons que l’espérance d’une variable aléatoire X, qui prend ses valeurs
dans N, est définie par


E[X] = Pr(X = i) · i.
i=1

Celle-ci correspond intuitivement à la moyenne pondérée d’un grand nombre de


résultats d’une expérience aléatoire. Dans notre cas, nous devons donc identifier
la valeur E[X] où X est la variable aléatoire qui dénote le nombre d’itérations
effectuées par la boucle principale.
Remarquons que chaque itération de la procédure est indépendante de la
précédente. De plus, la probabilité de quitter la boucle à une itération donnée

119
CHAPITRE 8. ALGORITHMES ET ANALYSE PROBABILISTES 120

Algorithme 49 : Lancer de dé à l’aide d’une pièce.


Entrées : —
Résultat : nombre x ∈ [1, 6] choisi de façon aléatoire et uniforme
lancer-dé():
faire
choisir un bit y0 à pile ou face
choisir un bit y1 à pile ou face
choisir un bit y2 à pile ou face
tant que y2 = y1 = y0
retourner 4 · y2 + 2 · y1 + y0

déf
est de p = 6/8 = 3/4. Ainsi, avec probabilité p, on effectue un seul tour de
boucle, et avec probabilité (1 − p), on effectue un tour de boucle, plus E[X]
tours supplémentaires. Nous avons donc:

E[X] = p · 1 + (1 − p) · (1 + E[X])
= p + 1 + E[X] − p − p · E[X]
= (1 − p) · E[X] + 1.

Ainsi, p · E[X] = 1 et par conséquent E[X] = 1/p. Nous concluons donc que
E[X] = 4/3 et ainsi que le nombre espéré de tirs à pile ou face est de 3·(4/3) = 4.

Observation.
Nous aurions pu obtenir l’espérance en observant que X suit une loi
géométrique de paramètre p = 3/4 et ainsi que E[X] = 1/p = 4/3.

Nous pouvons généraliser cette approche afin de générer un nombre x ∈ [a, b]


de façon uniforme. Remarquons d’abord que ce problème se réduit à générer
un nombre appartenant à [0, b − a], qu’on peut ensuite additionner à a. Nous
supposons donc sans perte de généralité que l’intervalle débute à 0. Pour générer
un nombre x ∈ [0, c − 1]:
— on choisit k bits, où k est assez grand pour représenter 0 à c − 1;
— on retourne la valeur x du nombre binaire si x < c, et recommence sinon.
Cette procédure est décrite sous forme de pseudocode à l’algorithme 50.
Soit X la variable aléatoire qui dénote le nombre d’itérations effectuées par
la boucle principale pour une entrée c fixée. Comme pour l’algorithme précé-
dent, chaque itération est indépendante des précédentes. De plus, la probabilité
CHAPITRE 8. ALGORITHMES ET ANALYSE PROBABILISTES 121

Algorithme 50 : Génération de nombre aléatoire à l’aide d’une pièce.


Entrées : c ∈ N≥1
Résultat : nombre x ∈ [0, c − 1] choisi de façon aléatoire et uniforme
uniforme(c):
k ← ⌈log c⌉
faire
x←0
pour i ← 0, . . . , k − 1
choisir un bit y à pile ou face
x ← x + 2i · y
tant que x ≥ c
retourner x

déf
de quitter la boucle pour une itération donnée est de p = c/2k . Ainsi:

E[X] = 1/p (car X suit une loi géométrique de paramètre p)


k
= 2 /c (par définition de p)
= 2⌈log c⌉ /2log c (par définition de k et par c = 2log c )
= 2⌈log c⌉−log c
<2 (car ⌈log c⌉ < 1 + log c).

Puisque chaque itération effectue k tirs à pile ou face, le nombre espéré


de tirs est de E[X] · k < 2k = 2⌈log c⌉. Intuitivement, cela signifie qu’en
« moyenne » l’algorithme lance O(log c) fois une pièce pour générer un nombre.

Remarque.

En pratique, les ordinateurs n’ont généralement pas accès à une source


d’aléa parfaite et utilisent donc des générateurs de nombres pseudo-
aléatoires comme « Mersenne Twister ».

8.2 Paradigmes probabilistes


Considérons le problème suivant:

Entrée: une séquence s de taille paire dont la moitié des éléments


sont égaux à a ∈ N et l’autre moitié à b ∈ N, où a ̸= b
Sortie: max(s)

Intuitivement, tout algorithme doit itérer sur au moins la moitié des éléments de
s afin de retourner le maximum. Autrement dit, un algorithme résout forcément
CHAPITRE 8. ALGORITHMES ET ANALYSE PROBABILISTES 122

ce problème en temps Ω(n) dans le pire cas. Nous présentons deux algorithmes
probabilistes qui surmontent cette barrière.

8.2.1 Algorithmes de Las Vegas et temps espéré


Un algorithme de Las Vegas est un algorithme probabiliste qui retourne toujours
le bon résultat, mais dont le temps d’exécution dépend des choix probabilistes.
Par exemple, considérons l’algorithme 51. Celui-ci:
— choisit un élément aléatoire s[i];
— retourne max(s[i], s[1]) si s[1] ̸= s[i], et recommence sinon.

Algorithme 51 : Maximum probabiliste: Las Vegas.


Entrées : séquence s de taille n paire dont la moitié des éléments sont
égaux à a ∈ N et l’autre moitié à b ∈ N où a ̸= b
Résultat : max(s)
max-las-vegas(s):
boucler
choisir i ∈ [1, . . . , |s|] de façon uniforme
si s[i] > s[1] alors
retourner s[i]
sinon si s[i] < s[1] alors
retourner s[1]

Ainsi, lorsque l’algorithme termine, la valeur retournée est forcément max(s).


Toutefois, le temps d’exécution varie selon l’ordonnancement de s et les choix
probabilistes de i. Nous pouvons néanmoins borner son « temps espéré ».
Soient A un algorithme probabiliste et Yx la variable aléatoire qui dénote
le nombre d’opérations élémentaires exécutées par A sur entrée x. Le temps
espéré de A (dans le pire cas) est la fonction tesp : N → N telle que:
déf
tesp (n) = max {E[Yx ] : entrée x de taille n} .
Soit Xs la variable aléatoire qui dénote le nombre d’itérations effectuées par
la boucle principale de l’algorithme 51. À une itération donnée, la probabilité
déf
de choisir s[i] ̸= s[1] est de p = 1/2, puisque la moitié des éléments sont égaux
à s[1]. Comme chaque itération est indépendante de la précédente, nous avons
à nouveau une loi géométrique de paramètre p, ce qui mène à E[Xs ] = 1/p = 2.
Ainsi, le temps espéré de l’algorithme appartient à O(2) = O(1), en supposant
toutes les opérations élémentaires.
Remarquons que bien que le temps espéré soit constant, l’exécution de l’al-
gorithme 51 peut être d’une durée arbitrairement grande avec faible probabilité,
et infinie avec probabilité 0. De façon générale, on peut remédier à ce problème
en arrêtant l’exécution d’un algorithme de Las Vegas après un certain nombre
d’itérations. En contrepartie, l’algorithme indique alors qu’aucune valeur de
retour n’a été identifiée.
CHAPITRE 8. ALGORITHMES ET ANALYSE PROBABILISTES 123

8.2.2 Algorithmes de Monte Carlo et probabilité d’erreur


Un algorithme de Monte Carlo est un algorithme probabiliste dont le temps
d’exécution peut être borné indépendamment des choix aléatoires, mais qui
peut retourner des valeurs erronnées. Par exemple, considérons l’algorithme 52.
Celui-ci:
— choisit un élément aléatoire s[i];
— retourne s[i] si s[i] > s[1], et recommence au plus 275 fois sinon.
L’algorithme ne retourne pas nécessairement la bonne valeur, par ex. avec
une certaine malchance on pourrait obtenir s[i] = s[1] = min(s) à chaque itéra-
tion, auquel cas la valeur de sortie serait le minimum plutôt que le maximum.
Cependant, le nombre d’itérations ne peut jamais excéder 275. Ainsi, le temps
d’exécution appartient à O(1), en supposant toutes les opérations comme élé-
mentaires.

Algorithme 52 : Maximum probabiliste: Monte Carlo.


Entrées : séquence s de taille paire dont la moitié des éléments sont
égaux à a ∈ N et l’autre moitié à b ∈ N où a ̸= b
Résultat : max(s)
max-monte-carlo(s):
faire 275 fois
choisir i ∈ [1, . . . , |s|] de façon uniforme
si s[i] > s[1] alors
retourner s[i]
retourner s[1]

Soit A un algorithme probabiliste et soit Yx la variable aléatoire qui dénote


la valeur de sortie de A sur entrée x. La probabilité d’erreur de A est la fonction
err : N → R≥0 telle que:
déf
err(n) = max{Pr(Yx ̸= bonne sortie sur x) : entrées x de taille n}.

Analysons la probabilité d’erreur de l’algorithme 52. Nous voulons borner


Pr(Ys ̸= max(s)). Si s[1] = max(s), alors l’algorithme retourne forcément la
bonne valeur. Sinon, l’algorithme retourne la mauvaise valeur si et seulement si
s[i] = s[1] à chacune des 275 itérations. Ainsi, Pr(Ys ̸= max(s)) ≤ (1/2)275 =
1
2275 . Comme cette probabilité est indépendante de la taille de s, nous concluons
que err(n) ≤ 1/2275 .

8.3 Coupe minimum: algorithme de Karger


Nous introduisons un algorithme de Monte Carlo élégant pour un problème
plus complexe: la coupe minimum. Nous disons qu’une coupe d’un graphe non
dirigé G = (V, E) est une partition non triviale (X, Y ) de V , c.-à-d. X, Y ̸= ∅,
CHAPITRE 8. ALGORITHMES ET ANALYSE PROBABILISTES 124

X ∪ Y = V et X ∩ Y = ∅. La taille d’une coupe correspond au nombre d’arêtes


de G qui relient X et Y . Plus formellement:
déf
taille(X, Y ) = |{{x, y} ∈ E : x ∈ X, y ∈ Y }|.

Le problème de la coupe minimum consiste à identifier une coupe de G qui


minimise sa taille. Par exemple, considérons le graphe G illustré à la figure 8.1.
Les coupes ({a, b, c}, {d, e}), ({a, b, d, e}, {c}) et ({a, b, c, d}, {e}) possèdent une
taille de 5, 4 et 2 respectivement. En inspectant toutes les coupes, nous conclu-
rions que la coupe minimum possède une taille de 2. Comme il existe 2|V |−1 − 1
coupes en général, la force brute se bute à une complexité exponentielle.

b d

a e

Figure 8.1 – Exemple de coupe minimum identifiée par un trait tireté.

L’algorithme de Karger est un algorithme de Monte Carlo qui identifie une


coupe minimum en:
— choisissant une arête e = {u, v} aléatoirement de façon uniforme;
— contractant e par la fusion de u et v;
— répétant tant qu’il existe plus de deux sommets.
L’opération de contraction peut créer des arêtes parallèles et des boucles. À
chaque contraction, nous conservons les arêtes parallèle et nous nous débarras-
sons des boucles. La taille de la coupe obtenue correspond au nombre d’arêtes
parallèles entre les deux derniers sommets tel que décrit à l’algorithme 53.
Par exemple, voici une exécution de l’algorithme de Karger:
CHAPITRE 8. ALGORITHMES ET ANALYSE PROBABILISTES 125

c c

b d d

a e ab e

cd

ab e abcd e

Puisque l’algorithme retire un sommet par itération, il effectue toujours


|V | − 2 itérations au total. De plus, l’opération de contraction peut être implé-
mentée en temps O(|V |), ce qui mène à un temps total de O(|V |2 ). Cependant,
la valeur de retour n’est pas nécessairement la bonne. Cherchons donc à borner
la probabilité d’erreur.

Algorithme 53 : Algorithme de Karger.


Entrées : graphe non dirigé G = (V, E)
Résultat : taille d’une coupe minimum de G
karger(V, E ):
tant que |V | > 2
choisir {u, v} ∈ E aléatoirement de façon uniforme
retirer u et v de V
ajouter uv à V
pour {x, y} ∈ E
retirer {x, y} de E
si x ∈ {u, v} alors x ← uv
si y ∈ {u, v} alors y ← uv
si x ̸= y alors ajouter {x, y} à E
retourner |E|

Proposition 28. Une coupe minimum possède une taille d’au plus 2|E|/|V |.

Démonstration. Soit k la taille d’une coupe minimum. La coupe ({v}, V \ {v})


CHAPITRE 8. ALGORITHMES ET ANALYSE PROBABILISTES 126

possède une taille égale à deg(v). Donc, k ≤ deg(v) pour tout v ∈ V , et ainsi:
|V | fois
z }| {
|V | · k = k + k + . . . + k

≤ deg(v)
v∈V
= 2|E|.
En divisant les deux côtés par |V |, nous obtenons k ≤ 2|E|/|V |.
Théorème 4. La probabilité d’erreur de l’algorithme de Karger est inférieure
ou égale à 1 − 1/|V |2 .
Démonstration. Fixons une coupe minimum C = (X, Y ). Observons d’abord
que la probabilité de choisir une arête qui traverse C est d’au plus 2/|V |. En
effet, il y a |E| choix d’arêtes et au plus 2|E|/|V | arêtes qui traversent C par
la proposition 28. Si l’algorithme contracte une arête qui ne traverse pas C à
chaque itération, alors la valeur de retour est correcte. Soit p la probabilité de
contracter une arête qui ne traverse pas C à chaque itération. Nous avons:
( )( )( ) ( ) ( )
2 2 2 2 2
p≥ 1− 1− 1− ··· 1 − · 1−
|V | |V | − 1 |V | − 2 4 3
|V | − 2 |V | − 3 |V | − 4 2 1
= · · ··· ·
|V | |V | − 1 |V | − 2 4 3
2
=
|V | · (|V | − 1)
1
≥ .
|V |2
Remarquons que C n’est pas nécessairement l’unique coupe minimum. La pro-
babilité de succès est donc d’au moins p. Ainsi, la probabilité d’erreur est d’au
plus 1 − p ≤ 1 − 1/|V |2 .

8.4 Amplification de probabilité


Le théorème 4 affirme que la probabilité d’erreur de l’algorithme de Karger est
déf
d’au plus q = 1 − 1/|V |2 . Par exemple, q = 24/25 = 0,96 sur le graphe de la
figure 8.1. Nous pouvons réduire cette probabilité en répétant l’algorithme k
fois et en conservant la plus petite taille identifiée. Dans ce cas, la probabilité
d’erreur est d’au plus q · q · · · q = q k car il y a un échec si et seulement si les k
itérations échouent. Nous avons:
q k ≤ (1 − 1/|V |2 )k
( )
2 k
≤ 2−1/|V | (car 1 − x ≤ 2−x pour tout x ∈ R≥0 )

= 2−k/|V | .
2
CHAPITRE 8. ALGORITHMES ET ANALYSE PROBABILISTES 127

déf
Ainsi, en prenant k = ε · |V |2 , la probabilité d’erreur est réduite à au plus 1/2ε .
En général, nous pouvons réduire la probabilité d’erreur d’un algorithme de
Monte Carlo (et ainsi amplifier sa probabilité de succès) en le répétant k fois,
puis en retournant la meilleure valeur ou la valeur majoritaire selon le type de
problème.

8.5 Temps moyen


Il ne faut pas confondre le temps espéré avec le temps moyen. Ce-dernier corres-
pond à la moyenne du temps d’exécution parmi toutes les entrées d’une même
taille. Plus formellement, soient A un algorithme (déterministe) et f la fonction
telle que f (x) dénote le nombre d’opérations élémentaires exécutées par A sur
entrée x. Le temps d’exécution moyen de A est la fonction tmoy : N → N telle
que:
déf

tmoy (n) = f (x) / (nombre d’entrées de taille n).
entrée x
de taille n

Ainsi, l’analyse en temps moyen correspond à faire l’hypothèse que les en-
trées d’un algorithme sont distribuées uniformément, et à faire la moyenne sur
toutes les entrées d’une même taille. La validité de cette hypothèse dépend
donc grandement de l’application.
À titre d’exemple, nous analysons le temps moyen du tri par insertion:
Proposition 29. Le temps moyen du tri par insertion appartient à Θ(n2 ).

Démonstration. Nous nous limitons au cas où l’entrée s est une permutation


de n éléments distincts. Rappelons que par la proposition 17, le tri par inser-
tion fonctionne en temps Θ(n + k) où k est le nombre d’inversions de s. Soit
f (n) le nombre total d’inversions parmi toutes les séquences constituées de
n éléments distincts. Si le ième plus grand élément apparaît au début d’une
séquence, alors (i − 1) inversions sont engendrées par cet élément. De plus, il
existe (n − 1)! séquences qui débutent par cet élément. Ainsi:


n
f (n) = [(n − 1)! · (i − 1) + f (n − 1)]
i=1

n
= n · f (n − 1) + (n − 1)! · (i − 1)
i=1
= n · f (n − 1) + (n − 1)! · n(n − 1)/2
= n · f (n − 1) + n! · (n − 1)/2.
CHAPITRE 8. ALGORITHMES ET ANALYSE PROBABILISTES 128

En substitutant f à répétition, nous obtenons:

f (n) = n · f (n − 1) + n! · (n − 1)/2
= n · [(n − 1) · f (n − 2) + (n − 1)! · (n − 2)/2] + n! · (n − 1)/2
= n · (n − 1) · f (n − 2) + n!/2 · [(n − 2) + (n − 1)]
..
.

n−1
= n! · f (0) + n!/2 · i
i=0

n−1
= n!/2 · i
i=1
= n!/2 · n(n − 1)/2
= n! · n(n − 1)/4.

Puisqu’il y a n! permutations, le nombre moyen d’inversions est égal à f (n)/n! =


n(n − 1)/4. Ainsi, le temps moyen du tri par insertion appartient à

Θ(n + n(n − 1)/4) = Θ(n2 ).


CHAPITRE 8. ALGORITHMES ET ANALYSE PROBABILISTES 129

8.6 Exercices
8.1) Montrez que la distribution des nombres générés par l’algorithme 49 est
bien uniforme.

8.2) Donnez une procédure afin de générer un nombre aléatoire uniformément


parmi [0, 2k − 1] à l’aide d’une pièce. Votre algorithme doit toujours fonc-
tionner en temps O(k).

8.3) Dites si l’algorithme diviser-pour-régner suivant génère un nombre aléa-


toire x ∈ [a, b] de façon uniforme:
Entrées : a, b ∈ N≥1
Résultat : nombre x ∈ [a, b] choisi de façon aléatoire et uniforme
uniforme'(a, b):
si a = b alors
retourner a
sinon
choisir un bit y à pile ou face
m ← (a + b)/2
si y = 0 alors
retourner uniforme'(a, ⌊m⌋)
sinon
retourner uniforme'(⌈m⌉, b)

8.4) Supposez que vous ayez accès à une pièce de monnaie biaisée: elle retourne
déf
pile avec probabilité 0 < p < 1 et face avec probabilité q = 1 − p. Vous
ne connaissez pas la valeur de p. Donnez un algorithme qui simule une
pièce non biaisée.

8.5) L’algorithme de Freivalds permet de tester si A · B = C où A, B et C


sont des matrices carrées. Dites s’il est de Las Vegas ou de Monte Carlo.
S’il s’agit du premier cas, analysez le temps espéré, sinon, analysez la
probabilité d’erreur.

Entrées : A, B, C ∈ Qn×n
Résultat : A · B = C?
générer un vecteur aléatoire v ∈ {0, 1}n de façon uniforme
retourner A · (B · v) = C · v

déf
Indice: pensez à la probabilité que D · v = 0, où D = A · B − C.

8.6) En supposant qu’on puisse supprimer un élément d’une séquence en temps


constant, l’algorithme suivant améliore l’algorithme 51 puisque le nombre
CHAPITRE 8. ALGORITHMES ET ANALYSE PROBABILISTES 130

d’itérations ne peut plus être arbitrairement grand:


Entrées : séquence s de taille n paire dont la moitié des éléments sont
égaux à a ∈ N et l’autre moitié à b ∈ N où a ̸= b
Résultat : max(s)
max-las-vegas'(s):
boucler
choisir i ∈ [2, . . . , |s|] de façon uniforme
si s[i] > s[1] alors
retourner s[i]
sinon si s[i] < s[1] alors
retourner s[1]
sinon
retirer le ième élément de s

— Dites combien d’itérations peuvent être effectuées au maximum.


— Quelle est la probabilité que l’algorithme termine à la ième itération?
— ⋆ Bornez le temps espéré de l’algorithme.
(basé sur un algorithme proposé par Etienne D. Massé, A2019)
A
Solutions des exercices

Cette section présente des solutions à certains des exercices du document. Dans
certains cas, il ne s’agit que d’ébauches de solutions.

131
ANNEXE A. SOLUTIONS DES EXERCICES 132

Chapitre 0
0.4) Soient m, n ∈ N tels que m est pair et n est impair. Démontrons que m+n
est impair. Par définition, il existe a, b ∈ N tels que m = 2a et n = 2b + 1
Nous avons donc m + n = 2a + 2b + 1 = 2(a + b) + 1. Ainsi m + n = 2k + 1
déf
où k = a + b. Nous concluons donc que m + n est impair.

0.8) Montrons que n! > 2n pour tout n ∈ N≥4 par induction sur n.
Cas de base (n = 4). Nous avons 4! = 1 · 2 · 3 · 4 = 24 > 16 = 24 .

Étape d’induction. Soit n ≥ 4. Supposons que n! > 2n . Montrons que


(n + 1)! > 2n+1 . Nous avons:

(n + 1)! = 1 · 2 · · · n · (n + 1)
> 2n · (n + 1) (par hypothèse d’induction)
>2 ·2n
(car n + 1 ≥ 4 + 1 > 2)
n+1
=2 .

0.9) Soient n, k ∈ N. Si n < k, alors les deux côtés de l’équation sont triviale-
ment égaux à 0. Si n ≥ k, nous avons:
( ) ( )
n n n! n!
+ = +
k k+1 k! · (n − k)! (k + 1)! · (n − k − 1)!
( )
1 1
= n! · +
k! · (n − k)! (k + 1)! · (n − k − 1)!
( )
(k + 1) (n − k)
= n! · +
(k + 1)! · (n − k)! (k + 1)! · (n − k)!
(k + 1) + (n − k)
= n! ·
(k + 1)! · (n − k)!
n+1
= n! ·
(k + 1)! · (n − k)!
(n + 1)!
=
(k + 1)! · (n − k)!
( )
n+1
= .
k+1
(0) (0)
0.10) Observons que (n) 0 = 1 et k = 0 pour k > 0. Ainsi, la formule de Pascal
montre que k se calcule récursivement comme une grande somme de 1
et 0, ce qui en fait forcément un entier naturel. Nous pourrions démontrer
ce fait de façon un peu plus formelle en procédant par induction sur n.
ANNEXE A. SOLUTIONS DES EXERCICES 133

0.16) Observons d’abord qu’il est impossible de payer 1$ puisque toute combi-
naison de 2$ et 5$ excède forcément ce montant. De plus, il est impossible
de payer 3$ puisqu’il faut nécessairement prendre une pièce de 2$ et qu’il
est impossible de payer le 1$ restant (par l’argument précédent). Claire-
ment, il est possible de payer 2$. Montrons maintenant qu’il est possible
de payer tout montant n ≥ 4 par induction généralisée.
Cas de base (n ∈ {4, 5}). Pour n = 4, il suffit de prendre deux pièces de
2$, et pour n = 5 il suffit de prendre un billet de 5$.
Étape d’induction. Soit n ≥ 5. Supposons qu’il soit possible de payer
tout montant compris dans [4, n] et cherchons à montrer qu’il est possible
de payer (n + 1)$. Nous prenons une pièce de 2$, puis il reste à payer
(n − 1)$. Observons que n − 1 ∈ [4, n]. Ainsi, par hypothèse d’induction,
il est possible de payer le reste du montant.
ANNEXE A. SOLUTIONS DES EXERCICES 134

Chapitre 1
1.1) Soient f, g ∈ F tels que f ∈ O(g). Soit h ∈ O(f ). Puisque h ∈ O(f ) et
f ∈ O(g), nous avons h ∈ O(g) par transitivité. Ainsi, O(f ) ⊆ O(g).

1.4)

O(1000000) ⊂ O(8(n + 2) − 1 + 9n) ⊂ O(n log n) ⊂ O(5n2 − n)


= O(3n2 ) ⊂ O(n3 − n2 + 7) ⊂ O(2n ) ⊂ O(4n ) ⊂ O(n!).

1.7) Nous avons:

f (n) f ′ (n)
lim = lim ′ (par la règle de L’Hôpital)
n→∞ g(n) n→∞ g (n)
2n
= lim
n→∞ loge (2) · 2n
2 n
= · lim
loge (2) n→∞ 2n
2 n′
= · lim (par la règle de L’Hôpital)
loge (2) n→∞ (2n )′
2 1
= · lim
loge (2) n→∞ loge (2) · 2n
2 1
= · lim
loge (2)2 n→∞ 2n
= 0.

Ainsi, par la règle de la limite, n2 ∈ O(2n ) et 2n ̸∈ O(n2 ).


déf ∑n
1.10) Soit d ∈ N. Posons f (n) = i=1 id . Montrons d’abord que f ∈ O(nd+1 ).
Nous avons:

f (n) = 1d + 2d + . . . + nd
≤ nd + nd + . . . + nd (pour tout n ≥ 1)
=n·n d

= nd+1 .

Ainsi, en prenant 1 à la fois comme constante multiplicative et comme


seuil, nous obtenons f ∈ O(nd+1 ).
ANNEXE A. SOLUTIONS DES EXERCICES 135

Montrons maintenant que f ∈ Ω(nd+1 ). Nous avons:


n
f (n) = id
i=1

n
≥ id (on garde les termes ≥ à la médiane)
i=⌈(n+1)/2⌉

n
≥ ⌈(n + 1)/2⌉d (car chaque i ≥ ⌈(n + 1)/2⌉)
i=⌈(n+1)/2⌉

= ⌈n/2⌉ · ⌈(n + 1)/2⌉d


≥ (n/2) · (n/2)d
= (n/2)d+1
1
= · nd+1 .
2d+1
Ainsi, en prenant 1/2d+1 comme constante multiplicative et 0 comme
seuil, nous obtenons f ∈ Ω(nd+1 ), et par conséquent f ∈ Θ(nd+1 ).
k2
1.11) Soit k ∈ N≥2 . Observons d’abord que k(n − k) ≥ n pour tout n ≥ k−1 .
En effet:

k(n − k) ≥ n ⇐⇒ kn − k 2 ≥ n
⇐⇒ kn − n ≥ k 2
⇐⇒ n(k − 1) ≥ k 2
k2
⇐⇒ n ≥ (car k − 1 ≥ 2 − 1 > 0).
k−1

En utilisant cette observation, nous obtenons donc:

nd = n · nd−1

d
d2
≤n· k(n − k) pour tout n ≥
d−1
k=2
= n · 2 · (n − 2) · · · d · (n − d)
= 2 · 3 · · · d · (n − d) · · · (n − 3) · (n − 2) · n
≤ n! pour tout n ≥ 2d + 1.

Ainsi, en prenant max(d2 /(d − 1), 2d + 1) comme seuil et 1 comme con-


stante multiplicative, nous concluons que nd ∈ O(n!).
ANNEXE A. SOLUTIONS DES EXERCICES 136


1.12) Soit d ∈ N>0 . Montrons d’abord que log n ∈ O ( d n). Pour tout n ∈ N≥1 :
( √ )
log n = log ( d n)d

= d · log d n

≤d· dn (car log x ≤ x pour tout x ∈ R>0 ).

Ainsi en prenant d comme constante


√ multiplicative et 1 comme seuil,
nous concluons que log n ∈ O ( d n).
Montrons maintenant que (log n)d ∈ O(n). Pour tout n ∈ N≥1 :
( √ )d
(log n)d ≤ d · d n (par l’observation ci-dessus)
= d · n.
d

Ainsi en prenant dd comme constante multiplicative et 1 comme seuil,


nous concluons que (log n)d ∈ O(n).

1.15) Nous avons:


mn
f (m, n) = + 3m log(n · 2n ) + 7n
2
≤ mn + 3m log(n · 2n ) + 7n
= mn + 3m log(n) + 3m log(2n ) + 7n
= mn + 3m log(n) + 3mn + 7n

≤ mn + 3mn + 3mn + 7n pour tout n ≥ 1


≤ mn + 3mn + 3mn + mn pour tout m ≥ 7
= 8mn.
déf
Ainsi, nous concluons que f ∈ O(mn) en prenant c = 8 comme constante
déf déf
multiplicative, et m0 = 7 et n0 = 1 comme seuils.
ANNEXE A. SOLUTIONS DES EXERCICES 137

Chapitre 2
2.2) Algorithme sur place non stable:

Entrées : séquence binaire s


Sorties : séquence s triée
i ← 1; j ← |s|
tant que i < j // Invar.: s[i : i − 1] ne contient que des 0
si s[i] = 0 alors // et s[j + 1 : n] ne contient que des 1
i←i+1
sinon
s[i] ↔ s[j]
j ←j−1
retourner s

2.6) On insère la spatule sous la plus grande crêpe, puis on renverse. Ensuite,
on insère la spatule sous la pile et on l’inverse au complet. La plus grande
crêpe est maintenant à la dernière position. En répétant ce processus n
fois, on obtient une pile de crêpes triée. Puisque chaque itération nécessite
deux renversements, on obtient O(n) renversements au total. Une analyse
légèrement plus détaillée montre que cette procédure effectue 2n − 3 ren-
versements. Notons qu’il est possible d’obtenir une meilleure constante
multiplicative.
ANNEXE A. SOLUTIONS DES EXERCICES 138

Chapitre 3
3.2)
Entrées : matrice A ∈ {0, 1}n×n
Résultat : indice de l’intrus
i←1
j←1
tant que j ≤ n
si i = j alors
j ←j+1
sinon si A[i, j] = 0 alors // i n'est pas l'intrus
i←i+1
sinon // j n'est pas l'intrus
j ←j+1
retourner i

3.4) On marque les sommets en alternance entre rouge et noir:


Entrées : graphe non dirigé G = (V, E)
Résultat : G est biparti?
biparti ← vrai
couleur ← [v 7→ aucune : v ∈ V ]
colorier(u, v ):
si couleur[u] = aucune alors
couleur[u] ← c
pour v : u −
→v
colorier(v, couleur inverse de c)
sinon si couleur[u] ̸= c alors
biparti ← faux
pour v ∈ V
si couleur[v] = aucune alors
colorier(v, rouge)
retourner biparti
ANNEXE A. SOLUTIONS DES EXERCICES 139

Chapitre 4
4.1) Les deux algorithmes présentés fonctionnent également sur les poids né-
gatifs. Il suffit donc de multiplier chaque poids par −1 et de rechercher
un arbre couvrant minimal.
Si l’on ne croît pas que ces algorithmes fonctionnent avec des poids néga-
tifs (à défaut d’avoir vu une preuve!), on peut argumenter qu’ils peuvent
être adaptés. Soit G = (V, E) un graphe non dirigé pondéré par p et soit
c son plus petit poids, c.-à-d. c = min{p[e] : e ∈ E}. On remplace p par
p′ tel que p′ [e] = p[e] + |c| + 1. Un arbre couvrant de G est minimal sous
déf

p si et seulement si il est minimal sous p′ . De plus, p′ ne possède que des


poids positifs.

4.2) Minimiser p[1] · p[2] · · · p[n] est équivalent à minimiser son logarithme:
log(p[1] · p[2] · · · p[n]) = log p[1] + log p[2] + . . . + log p[n]. Il suffit donc
de remplacer chaque poids par son logarithme et de rechercher un arbre
couvrant minimal sous la définition standard.

4.4) En O(max(|V |, |E|) · log |V |):


Entrées : graphe non dirigé G = (V, E)
Résultat : composantes connexes de G
init(V )
pour u ∈ V
pour v : u −
→v
union(u, v )
c ← [v 7→ [ ] : v ∈ V ]
pour v ∈ V
u ← trouver(v )
ajouter v à c[u]
composantes ← [ ]
pour v ∈ V
si c[v] ̸= [ ] alors ajouter c[v] à composantes
retourner composantes
ANNEXE A. SOLUTIONS DES EXERCICES 140

Chapitre 5
5.1) A) Polynôme caractéristique: x2 − x − 6 = (x − 3)(x + 2).
B) Forme close: t(n) = c1 · 3n + c2 · (−2)n .
C) Identification des constantes:

1 = c1 + c2
2 = 3c1 − 2c2

Donc, c1 = 4/5 et c2 = 1/5, et ainsi

t(n) = (4/5) · 3n + (1/5) · (−2)n ∈ Θ(3n ).

5.2) A) Polynôme caractéristique: x2 −x−2 = (x−2)(x+1), puis on multiplie


par (x − 1) car non homogène.
B) Forme close: t(n) = c1 · 2n + c2 · (−1)n + c3 · 1n .
C) Identification des constantes:

0 = c1 + c2 + c3
1 = 2c1 − c2 + c3
4 = 4c1 + c2 + c3

Donc, c1 = 4/3, c2 = 1/6 et c3 = −3/2, et ainsi

t(n) = (4/3) · 2n + (1/6) · (−1)n − (3/2) ∈ Θ(2n ).

5.3) A) Polynôme caractéristique: x2 − 1 = (x − 1)(x + 1).


B) Forme close: t(n) = c1 · 1n + c2 · (−1)n .
C) Identification des constantes:

1 = c1 + c2
0 = c1 − c2

Donc, c1 = c2 = 1/2, et ainsi

1 + (−1)n
t(n) = (1/2) · 1n + (1/2) · (−1)n = ∈ Θ(1).
2
ANNEXE A. SOLUTIONS DES EXERCICES 141

5.7) On adapte le tri par fusion afin d’identifier les inversions en triant:
Entrées : séquence s d’éléments comparables
Sorties : séquence s triée et nombre d’inversions
trier(s):
fusion(x, y ):
i ← 1; j ← 1; z ← [ ]
c←0 // nombre d'inversions entre x et y
tant que i ≤ |x| ∧ j ≤ |y|
si x[i] ≤ y[j] alors
ajouter x[i] à z
i←i+1
sinon
ajouter y[j] à z
j ←j+1
c ← c + (|x| − i + 1)
retourner (z + x[i : |x|] + y[j : |y|], c)
si |s| ≤ 1 alors retourner (s, 0)
sinon
m ← |s| ÷ 2
x, a ← trier(s[1 : m])
y, b ← trier(s[m + 1 : |s|])
z, c ← fusion(x, y)
retourner (z, a + b + c)

5.12) On adapte la recherche dichotomique afin d’identifier la première occur-


ANNEXE A. SOLUTIONS DES EXERCICES 142

rence de 1:
Entrées : séquence de bits s ordonnée de façon croissante
Sorties : nombre d’occurrences de 1 dans s
compter(s):
premier-un(lo, hi):
si lo = hi alors
si s[lo] = 1 alors retourner lo
sinon retourner aucune
sinon
mid ← (lo + hi) ÷ 2
si s[mid] = 0 alors
retourner premier-un(mid + 1, hi)
sinon
retourner premier-un(lo, mid)
pos ← premier-un(1, |s|)
si pos ̸= aucune alors retourner |s| − pos + 1
sinon retourner 0

5.14) On adapte la recherche dichotomique afin d’identifier le côté de la sé-


quence qui contient l’index de départ i:
Entrées : séquence s de n ∈ N≥1 éléments comparables distincts triés
circulairement
Sorties : max(s)
max-circ(s):
max-circ'(lo, hi):
si hi − lo ≤ 1 alors
retourner s[lo]
sinon
mid ← (lo + hi) ÷ 2
si s[lo] > s[mid] alors
retourner max-circ'(lo, hi)
sinon si s[mid] > s[hi] alors
retourner max-circ'(mid, hi)
sinon
// Impossible car les éléments sont distincts

si s[1] > s[n] alors


retourner max-circ'(1, n)
sinon
retourner s[n]
ANNEXE A. SOLUTIONS DES EXERCICES 143

5.15) Approche diviser-pour-régner:


Entrées : séquence s de n ∈ N≥1 entiers
Sorties : somme maximale parmi toutes les sous-séquences contigües
non vides
somme-max(s):
si n = 0 alors
retourner −∞
sinon si n = 1 alors
retourner s[1]
sinon
m←n÷2
gauche ← s[1 : m]
droite ← s[m + 1 : n]
// Cumulatif maximal vers la droite
cumul, c ← 0, −∞
pour x ∈ droite
cumul ← cumul + x
c ← max(c, cumul)
// Cumulatif maximal vers la gauche
cumul, d ← 0, −∞
pour x ∈ gauche en ordre inverse
cumul ← cumul + x
d ← max(d, cumul)
// Somme maximale des deux côtés
a ← somme-max(gauche)
b ← somme-max(droite)
// Somme maximale
retourner max(a, b, c + d)
ANNEXE A. SOLUTIONS DES EXERCICES 144

Chapitre 6
6.2) Cet algorithme essaie toutes les sous-chaînes contiguës en temps O(mn ·
min(m, n)) où m = |u| et n = |v|:
Entrées : chaînes u et v
Résultat : plus longue sous-chaîne contiguë commune à u et v
sous-chaine-contiguë(u, v ):
préfixe(i, j ):
p ← []
tant que i ≤ |u| ∧ j ≤ |v| ∧ u[i] = v[j]
ajouter u[i] à p
i←i+1
j← j + 1
retourner p
s ← []
pour i ← 1, . . . , |u|
pour j ← 1, . . . , |v|
s′ ← préfixe(i, j )
si |s′ | > |s| alors s ← s′
retourner s

6.3) Cet algorithme essaie toutes les sous-chaînes en temps Ω(2|u|+|v| ):


Entrées : chaînes u et v
Résultat : plus longue sous-chaîne contiguë commune à u et v
sous-chaine(u, v ):
aux(x, y, i, j ):
si i ≤ |u| alors
a ← aux(x + u[i], y, i + 1, j )
b ← aux(x, y, i + 1, j )
si |a| ≥ |b| alors retourner a
sinon retourner b
sinon si j ≤ |v| alors
a ← aux(x, y + v[j], i, j + 1)
b ← aux(x, y, i, j + 1)
si |a| ≥ |b| alors retourner a
sinon retourner b
sinon
si x = y alors retourner x
sinon retourner [ ]
retourner aux([ ], [ ], 1, 1)
ANNEXE A. SOLUTIONS DES EXERCICES 145

Chapitre 7
7.1) On mémorise seulement la dernière ligne de T :

Algorithme 54 : Variante de l’algorithme 43 économe en mémoire.


Entrées : montant m ∈ N, séquence s de n ∈ N pièces
Résultat : nombre minimal de pièces afin de rendre m
monnaie-dyn(m, s):
initialiser séquence T [0 . . . m] avec ∞
T [0] ← 0
pour i ← 1, . . . , n
initialiser séquence U [0 . . . m] avec ∞
pour j ← 0, . . . , m
sans ← T [j] // sol. sans s[i]
avec ← ∞
si j ≥ s[i] alors avec ← U [j − s[i]] + 1 // sol. avec s[i]
U [j] ← min(sans, avec)
T ←U
retourner T [m]

7.5) On calcule la taille d’un plus long suffixe commun de u[1 : i] et v[1 : j]
en temps O(|u| · |v|), puis on retourne la plus grande taille identifiée:
Entrées : chaînes u et v
Résultat : plus longue sous-chaîne commune à u et v
sous-chaine-contiguë-dyn(u, v ):
initialiser T [0 . . . |u|, 0 . . . |v|] avec 0
i′ , j ′ ← 0, 0
pour i ← 1, . . . , |u|
pour j ← 1, . . . , |v|
si u[i] = v[j] alors
T [i, j] ← T [i − 1, j − 1] + 1
sinon
T [i, j] ← 0
// Nouvelle solution meilleure que l'ancienne?
si T [i, j] > T [i′ , j ′ ] alors
i′ , j ′ ← i, j
retourner T [i′ , j ′ ]

7.5) On calcule la taille d’une plus longue sous-chaîne commune de u[1 : i] et


ANNEXE A. SOLUTIONS DES EXERCICES 146

v[1 : j] en temps O(|u| · |v|):


Entrées : chaînes u et v
Résultat : plus longue sous-chaîne commune à u et v
sous-chaine-dyn(u, v ):
initialiser T [0 . . . |u|, 0 . . . |v|] avec 0
pour i ← 1, . . . , |u|
pour j ← 1, . . . , |v|
si u[i] = v[j] alors
T [i, j] ← T [i − 1, j − 1] + 1
sinon
T [i, j] ← max(T [i − 1, j], T [i, j − 1])
retourner T [m, n]

7.6)

s t

7.9) On remplace l’initialisation d[v, v] ← vrai par d[v, v] ← faux pour ne


pas tenir compte des chemins vides.

7.13) Non, contre-exemple:

6 −2

s t
1 1 1

7.14) Soient s et t les sommets qui correspondent aux deux villes. On cherche
à identifier
min{max(d[v], d′ [v]) : v ∈ V },
où d[v] et d′ [v] dénotent respectivement la distance minimale de s vers
v, et t vers v. Ainsi, on calcule d et d′ avec l’algorithme de Dijkstra
à partir de s et t respectivement. Ensuite, on identifie itérativement le
sommet v qui minimise max(d[v], d′ [v]). L’algorithme fonctionne en temps
O(|V | log |V | + |E| + |V |) = O(|V | log |V | + |E|).
ANNEXE A. SOLUTIONS DES EXERCICES 147

Chapitre 8
8.1) Considérons la dernière itération de l’algorithme. Soit A l’événement
« y2 = y1 = y0 ». Nous avons:

Pr[A] = Pr[y2 = y1 = y0 = 0 ∨ y2 = y1 = y0 = 1]
= Pr[y2 = y1 = y0 = 0] + Pr[y2 = y1 = y0 = 1] (évén. disjoints)

2 ∏
2
= Pr[yi = 0] + Pr[yi = 1] (par indép.)
i=0 i=0


2 ∏
2
= (1/2) + (1/2)
i=0 i=0

= 1/8 + 1/8
= 1/4.

Ainsi,
Pr[A] = 1 − Pr[A] = 1 − (1/4) = 3/4.
Soit X la variable aléatoire qui dénote la valeur retournée et soit k ∈ [1, 6].
Nous avons:

Pr[X = k] = Pr[y2 y1 y0 = bin(k) | A]


= Pr[y2 y1 y0 = bin(k) ∧ A]/ Pr[A] (prob. cond.)
= Pr[y2 y1 y0 = bin(k)] / Pr[A]
= (1/8) / (3/4)
= 4/(3 · 8)
= 1/6.

Alternativement et plus succinctement, la probabilité de choisir une va-


leur k ∈ [1, 6] à l’itération i est de (1/4)i · (1/8). La probabilité de choisir
k à une itération quelconque est donc de:
∑ ∞ ( )i ∞ ( )i
1 1 1 ∑ 1
· = ·
i=0
4 8 8 i=0 4
1 1
= · (série géométrique)
8 1 − 1/4
1 4
= ·
8 3
1
= .
6
ANNEXE A. SOLUTIONS DES EXERCICES 148

8.2) On choisit le nombre en adaptant la recherche dichotomique:


Entrées : k ∈ N≥1
Résultat : nombre x ∈ [0, 2k − 1] choisi de façon aléatoire et uniforme
uniforme-puissance(i, j ):
si i = j alors
retourner i
sinon
choisir un bit b à pile ou face
m ← (i + j) ÷ 2
si b = 0 alors
retourner uniforme-puissance(i, m)
sinon
retourner uniforme-puissance(m + 1, j )
retourner uniforme-puissance(0, 2k − 1)

déf déf
8.3) Avec a = 1 et b = 6, la probabilité de générer 2, par exemple, est de 1/4
plutôt que 1/6. On peut s’en convaincre en dessinant l’arbre de récursion.

8.4) On lance deux pièces jusqu’à ce qu’elles donnent un résultat différent:


Entrées : —
Résultat : pile ou face
répéter
choisir un bit x à pile ou face avec la pièce biaisée
choisir un bit y à pile ou face avec la pièce biaisée
jusqu’à x = y
si x = 0 alors retourner pile
sinon retourner face

Soient X et Y les variables aléatoires qui dénotent respectivement les


valeurs de x et y à la dernière itération. Nous simulons bien une pièce
non biaisée puisque:
ANNEXE A. SOLUTIONS DES EXERCICES 149

Pr[retourner pile]
= Pr[X = 0 | X ̸= Y ]
= Pr[X = 0 ∧ X ̸= Y ] / Pr[X ̸= Y ]
= Pr[X = 0 ∧ Y = 1] / Pr[X ̸= Y ]
= (Pr[X = 0] · Pr[Y = 1]) / Pr[X ̸= Y ] (par indép.)
= pq/ Pr[X ̸= Y ]
= pq/ Pr[(X = 0 ∧ Y = 1) ∨ (X = 1 ∧ Y = 0)]
= pq/(Pr[X = 0 ∧ Y = 1] + Pr[X = 1 ∧ Y = 0]) (évén. disjoints)
= pq/(Pr[X = 0] · Pr[Y = 1] + Pr[X = 1] · Pr[Y = 0]) (par indép.)
= pq/[pq + qp]
= pq/2pq
= 1/2.

Le nombre d’itérations espéré est de 2 puisqu’il s’agit d’une loi géomé-


trique de paramètre 1/2.

Alternativement et plus succinctement, la probabilité de terminer à l’ité-


ration i avec x = 0 est de (p2 + q 2 )i · pq. La probabiltié d’obtenir x = 0
à une itération quelconque est donc de:

∑ ∞

( )i ( )i
p2 + q 2 · pq = pq · p2 + q 2
i=0 i=0
1
= pq · (série géométrique)
1 − (p2 + q 2 )
1
= p(1 − p) · (car q = 1 − p)
1 − p2 − (1 − p)2
1
= p(1 − p) ·
1 − p2 − 1 + 2p − p2
1
= p(1 − p) ·
2p(1 − p)
1
= .
2

8.5) Il s’agit d’un algorithme de Monte Carlo puisque l’algorithme fonctionne


toujours en temps O(n2 ), mais n’est pas toujours correct. En effet, lorsque
ANNEXE A. SOLUTIONS DES EXERCICES 150

A · B ̸= C, il est possible que A · (B · v) = C · v, par ex. avec v = 0.


Analysons donc la probabilité p que A · (B · v) = C · v lorsque A · B ̸= C.
déf déf
Posons D = A · B − C et x = D · v. Remarquons que p correspond à la
probabilité que x = 0. Puisque A · B ̸= C, nous avons D ̸= 0. Ainsi, il
existe i, j ∈ [n] tels que D[i, j] ̸= 0. Nous avons

n
x(i) = D[i, k] · v(k)
k=1

n
= D[i, j] · v(j) + D[i, k] · v(k).
k=1
k̸=j

déf déf ∑n
Posons a = D[i, j] et b = k=1 D[i, k] · v(k). Nous avons
k̸=j

Pr[x = 0] ≤ Pr[x(i) = 0]

= Pr[x(i) = 0 | b = 0] · Pr[b = 0] +
Pr[x(i) = 0 | b ̸= 0] · Pr[b ̸= 0]

= Pr[v(j) = 0] · Pr[b = 0] + (A.1)


Pr[v(j) = 1 ∧ b = −a] · Pr[b ̸= 0]

≤ Pr[v(j) = 0] · Pr[b = 0] +
Pr[v(j) = 1] · Pr[b ̸= 0]

= (1/2) · Pr[b = 0] +
(1/2) · Pr[b ̸= 0]

= (1/2) · (Pr[b = 0] + Pr[b ̸= 0])

= 1/2,
où (A.1) découle du fait que
x(i) = 0 ⇐⇒ [v(j) = b = 0 ∨ (v(j) = 1 ∧ b = −a)].

8.6) Soit X la variable aléatoire qui dénote le nombre d’itérations effectuées


déf déf
par l’algorithme. Posons pi = (n/2)/(n − i) et qi = 1 − pi . Nous avons:
Pr[X = i] = q1 · q2 · · · qi−1 · pi .
| {z } |{z}
échecs succès

De plus, l’algorithme effectue entre 1 et n/2 itérations. Ainsi,


 

n/2

i−1
E[X] = i · pi · qj  .
i=1 j=1
ANNEXE A. SOLUTIONS DES EXERCICES 151

⋆ Afin de borner cette expression, remarquons d’abord que


i · (n/2)
i · pi ≤ i ⇐⇒ ≤ i ⇐⇒ (n/2) ≤ n − i ⇐⇒ i ≤ n/2. (A.2)
n−i
De plus, pour tout 0 ≤ i < n, nous avons:
(n/2) (n/2)
qi = 1 − ≤1− = 1/2. (A.3)
n−i n
Ainsi, nous obtenons:
 

n/2

i−1
E[X] = i · pi · qj 
i=1 j=1


n/2
( )
≤ i · pi · (1/2)i−1 (par (A.3))
i=1


n/2
( )
≤ i · (1/2)i−1 (par (A.2))
i=1

∑ ( )
≤ i · (1/2)i−1 (car chaque terme est non négatif)
i=1
1
= (série géométrique dérivée)
(1 − 1/2)2
= 4.
⋆⋆ Voici une preuve de Gabriel McCarthy (A2019) qui montre que
E[X] < 2, et par conséquent que le nombre espéré d’itérations n’excède
pas celui de l’algorithme 51. Observons d’abord que:

i−1
(n/2 − 1)(n/2 − 2) · · · (n/2 − i + 1)
qj =
j=1
(n − 1)(n − 2) · · · (n − i + 1)

(n/2 − 1)! (n − i)!


= ·
(n/2 − i)! (n − 1)!
(n − i)! (n − 1)!
= /
(n/2)! · (n/2 − i)! (n/2)! · (n/2 − 1)!
( ) ( )
n−i n−1
= / . (A.4)
n/2 n/2
De plus, remarquons les propriétés suivantes du coefficient binomial:
( ) ( ) ( ) ∑ m ( )
m m m−1 m+1 j
= · = . (A.5)
k k k−1 k+1 k
j=k
ANNEXE A. SOLUTIONS DES EXERCICES 152

Ainsi, nous avons:


 

n/2

i−1
E[X] = i · pi · qj 
i=1 j=1

n/2 ( ( ) ( ))
∑ n−i n−1
= i · pi · / (par (A.4))
i=1
n/2 n/2

n/2 ( ( ) ( ))
∑ n/2 n−i n−1
= i· · /
i=1
n−i n/2 n/2


n−1 ( ( ) ( ))
n−j j n−1
= n/2 · · /
j n/2 n/2
j=n/2

n/2 ∑ ( n − j ( j ))
n−1
= (n−1) · ·
n/2
j n/2
j=n/2
 
( ) ∑ ( j )
n/2  ∑ n
n−1 n−1
j 
= (n−1) · · −
n/2
j n/2 n/2
j=n/2 j=n/2
 
n/2  n ∑ ( j−1 )
n−1 ∑ ( j )
n−1
= (n−1) · · −  (par (A.5))
n/2
n/2 n/2 − 1 n/2
j=n/2 j=n/2
[ ) ( ( )]
n/2 n n−1 n
= (n−1) · · − (par (A.5))
n/2
n/2 n/2 n/2 + 1
( n )
n/2+1
= n − (n/2) · (n−1)
n/2
(n−1)
n n/2
= n − (n/2) · ·( ) (par (A.5))
n/2 + 1 n−1n/2
n
= n − (n/2) ·
n/2 + 1
n2
=n−
n+2
n2 + 2n n2
= −
n+2 n+2
2n
=
n+2
2(n + 2) − 4
=
n+2
4
=2−
n+2
< 2.
B
Fiches récapitulatives

Les fiches des pages suivantes résument le contenu de chacun des chapitres.
Elles peuvent être imprimées recto-verso, ou bien au recto seulement afin d’être
découpées et pliées en deux. À l’ordinateur, il est possible de cliquer sur la
plupart des puces « ▶ » pour accéder à la section du contenu correspondant.

153
1. Analyse des algorithmes

Temps d’exécution Notation asymptotique (suite)


▶ Opérations élémentaires: dépend du contexte, souvent com- ▶ Simplification: lignes élem. comptées comme une seule opér.
paraisons, affectations, arithmétique, accès, etc. ▶ Règle de la limite: 
0 f ∈ O(g) et g ̸∈ O(f )
▶ Pire cas tmax (n): nombre maximum d’opérations élémen- f (n) 
taires exécutées parmi les entrées de taille n lim = +∞ f ̸∈ O(g) et g ∈ O(f )
n→+∞ g(n) 

▶ Meilleur cas tmin (n): même chose avec « minimum » const. Θ(f ) = Θ(g)
▶ tmax (m, n), tmin (m, n): même chose par rapport à m et n ▶ Multi-params.: O, Ω, Θ étendues avec plusieurs seuils
Correction et terminaison
Notation asymptotique
▶ Correct: sur toute entrée x qui satisfait la pré-condition, x
▶ Déf.: f ∈ O(g) si n ≥ n0 → f (n) ≤ cg(n) pour certains c, n0
et sa sortie y satisfont la post-condition
▶ Signifie: f croît moins ou aussi rapid. que g pour n → ∞
▶ Termine: atteint instruction retourner sur toute entrée
▶ Transitivité: f ∈ O(g) et g ∈ O(h) → f ∈ O(h)
▶ Invariant: propriété qui demeure vraie à chaque fois qu’une
▶ Règle des coeff.: f1 + . . . + fk ∈ O(c1 · f1 + . . . + ck · fk ) ou certaines lignes de code sont atteintes
▶ Règle du max.: f1 + . . . + fk ∈ O(max(f1 , . . . , fk )) Exemples de complexité
▶ Déf.: f ∈ Ω(g) ↔ g ∈ O(f ); f ∈ Θ(g) ↔ f ∈ O(g) ∩ Ω(g) O(1) ⊂ O(log n) ⊂ O(n) ⊂ O(n log n) ⊂ O(n2 ) ⊂ O(n2 log n)
▶ Règle des poly.: f polynôme de degré d → f ∈ Θ(nd ) ⊂ O(n3 ) ⊂ O(nd ) ⊂ O(2n ) ⊂ O(3n ) ⊂ O(bn ) ⊂ O(n!)

2. Tri
Sommaire
Approche générique Complexité (par cas)
Algorithme Sur place Stable
▶ Inversion: indices (i, j) t.q. i < j et s[i] > s[j] meilleur moyen pire
insertion Θ(n) Θ(n2 ) Θ(n2 ) 3 3
▶ Progrès: corriger une inversion en diminue la quantité monceau Θ(n) Θ(n log n) Θ(n log n) 3 7
▶ Procédure: sélectionner et corriger une inversion, jusqu’à ce fusion Θ(n log n) Θ(n log n) Θ(n log n) 7 3
qu’il n’en reste plus rapide Θ(n) Θ(n log n) Θ(n2 ) 3 7

Usage
Algorithmes (par comparaison) ▶ Petite taille: tri par insertion
▶ Insertion: considérer s[1 : i−1] triée et insérer s[i] dans s[1 : i] ▶ Grande taille: tri par monceau ou tri rapide
▶ Monceau: transformer s en monceau et retirer ses éléments ▶ Grande taille + stabilité: tri par fusion
▶ Fusion: découper s en deux, trier chaque côté et fusionner
Tri sans compraison
▶ Rapide: réordonner autour d’un pivot et trier chaque côté
▶ Par comparaison: barrière théorique de Ω(n log n)
Propriétés ▶ Sans comparaison: possible de faire mieux pour certains cas
▶ Sur place: n’utilise pas de séquence auxiliaire ▶ Représentation binaire: trier en ordonnant du bit de poids
faible vers le bit de poids fort
▶ Stable: l’ordre relatif des éléments égaux est préservé
▶ Complexité: Θ(mn) où m = nombre de bits et n = |s|

3. Graphes
Graphes Représentation Mat. Liste (non dirigé) Liste (dir.)
u− → v? Θ(1) O(min(deg(u), deg(v))) O(deg+ (u))
▶ Graphe: G = (V , E) où V = sommets et E = arêtes 
a b c
 [a 7→ [b,c], {v : u −
→ v} Θ(|V |) O(deg(u)) O(deg+ (u))
a 0 1 1
0 b → 7 {u : u −
→ v} Θ(|V |) O(deg(v)) O(|V | + |E|)
▶ Dirigé vs. non dirigé: {u, v} ∈ E vs. (u, v) ∈ E b 1 0 [a],
Modif. u −
→v O(deg(u) + deg(v)) O(deg+ (u))
c 0 1 0 c → 7 [b]] Θ(1)
▶ Degré (cas non dirigé): deg(u) = # de voisins Mémoire Θ(|V |2 ) Θ(|V | + |E|)

▶ Degré (cas dirigé): deg− (u) = # préd., deg+ (u) = # succ. Propriétés et algorithmes
▶ Taille: |E| ∈ Θ(somme des degrés) et |E| ∈ O(|V |2 ) ▶ Plus court chemin: parcours en largeur + stocker préd.
▶ Ordre topologique: u1 ⪯ · · · ⪯ un où i < j =⇒ (uj , ui ) ̸∈ E
▶ Chemin: séq. u0 −
→ ··· −
→ uk (taille = k, simple si sans rép.)
▶ Tri topologique: mettre sommets de degré 0 en file, retirer en
▶ Cycle: chemin de u vers u (simple si sans rép. sauf début/fin) mettant les degrés à jour, répéter tant que possible
▶ Sous-graphe: obtenu en retirant sommets et/ou arêtes ▶ Détec. de cycle: tri topo. + vérifier si contient tous sommets
▶ Composante: sous-graphe max. où sommets access. entre eux ▶ Temps d’exécution: tous linéaires
Parcours Arbres
▶ Profondeur: explorer le plus loin possible, puis retour (pile) ▶ Arbre: graphe connexe et acyclique (ou prop. équivalentes)
▶ Largeur: explorer successeurs, puis leurs succ., etc. (file) ▶ Forêt: graphe constitué de plusieurs arbres
▶ Temps d’exécution: O(|V | + |E|) ▶ Arbre couv.: arbre qui contient tous les sommets d’un graphe
4. Algorithmes gloutons
Arbres couvrants minimaux Ensembles disjoints
▶ Graphe pondéré: G = (V, E) où p[e] est le poids de l’arête e ▶ But: manipuler une partition d’un ensemble V

▶ Poids d’un graphe: p(G) = e∈E p[e] ▶ Représentation: chaque ensemble sous une arborescence
▶ Arbre couv. min.: arbre couvrant de G de poids minimal {a} {b, c, d, e} {f, g} init(V ) Θ(|V |)
Algorithmes a b f trouver (v) O(log |V |)
union(u, v) O(log |V |)
▶ Prim–Jarník: faire grandir un arbre en prenant l’arête min. c d e g
▶ Complexité: O(|E| log |V |) avec monceau
Algorithme glouton
▶ Kruskal: connecter forêt avec l’arête min. jusqu’à un arbre
1) Choisir un candidat c itérativement (sans reconsidérer)
▶ Complexité: O(|E| log |V |) avec ensembles disjoints
2) Ajouter c à solution partielle S si admissible
Prim–Jarník Kruskal 3) Retourner S si solution (complète), « impossible » sinon
2 5 2 5 2 5 2 5 2 5 2 5
b d f b d f b d f b d f b d f b d f
2 2 2 2 2 2

Problème du sac à dos


1 1 1 1 1 1
a 4 3 2 a 4 3 2 a 4 3 2 a 4 3 2 a 4 3 2 a 4 3 2

3 3 3 3 3 3
c e g c e g c e g c e g c e g c e g

▶ But: choisir objets pour maxim. valeur sans excéder capacité


1 4 1 4 1 4 1 4 1 4 1 4

2 5 2 5 2 5 2 5 2 5 2 5
b d f b d f b d f b d f b d f b d f
2 2 2 2 2 2

▶ Algo. glouton: trier en ordre décroissant par val[i]/poids[i]


1 1 1 1 1 1
a 4 3 2 a 4 3 2 a 4 3 2 a 4 3 2 a 4 3 2 a 4 3 2

3 3 3 3 3 3
c e g c e g c e g c e g c e g c e g
1 4 1 4 1 4 1 4 1 4 1 4

2
b
2
d
5
f
2
b
2
d
5
f
▶ Fonctionne seulement si on peut découper objets
1 1
a 4 3 2 a 4 3 2

3
c
1
e
4
g
3
c
1
e
4
g ▶ Approxime solution discrète à facteur 1/2

5. Algorithmes récursifs et approche diviser-pour-régner


Autres méthodes
Diviser-pour-régner
▶ Substitution: remplacer t(n), t(n − 1), t(n − 2), . . . par sa déf.
▶ A) découper en sous-problèmes disjoints
jusqu’à deviner la forme close
▶ B) obtenir solutions récursivement
▶ Arbres: construire un arbre représentant la récursion et iden-
▶ C) s’arrêter aux cas de base (souvent triviaux) tifier le coût de chaque niveau
▶ D) combiner solutions pour obtenir solution globale
Quelques algorithmes
▶ Exemple: tri par fusion O(n log n)
▶ Hanoï : src[1 : n−1] → tmp, src[n] → dst, tmp[1 : n−1] → dst O(2n )
Récurrences linéaires ▶ Exp. rapide: exploiter bn = (bn÷2 )2 · bn mod 2 O(log n)
∑d
▶ Cas homogène: i=0 ai · t(n − i) = 0 ▶ Mult. rapide: calculer (a + b)(c + d) en 3 mult. O(nlog 3 )
∑d
▶ Polynôme caractéristique: i=0 ai · xd−i ▶ Horizon: découper blocs comme tri par fusion O(n log n)
∑d
▶ Forme close: t(n) = i=1 ci · λni où les λi sont les racines Théorème maître (allégé)
▶ Constantes ci : obtenues en résolvant un sys. d’éq. lin. ▶ t(n) = c · t(n ÷ b) + f (n) où f ∈ O(nd ):
▶ Cas non homo.: si = c · bn , on multiplie poly. par (x − b) – O(nd ) si c < bd
▶ Exemple: • Récurrence: t(n) = 3 · t(n − 1) + 4 · t(n − 2)
– O(nd · log n) si c = bd
• Poly. carac.: x2 − 3x − 4 = (x − 4)(x + 1)
• Forme close: t(n) = c1 · 4n + c2 · (−1)n
– O(nlogb c ) si c > bd

6. Force brute

Approche
▶ Exhaustif : essayer toutes les sol. ou candidats récursivement Problème des n dames
▶ But: placer n dames sur échiquier sans attaques
▶ Explosion combinatoire: souvent # solutions ≥ bn , n!, nn
▶ Algo.: placer une dame par ligne en essayant colonnes dispo.
▶ Avantage: simple, algo. de test, parfois seule option
▶ Désavantage: généralement très lent et/ou avare en mémoire Sac à dos
▶ But: maximiser valeur sans excéder capacité
Techniques pour surmonter explosion ▶ Algo.: essayer sans et avec chaque objet
▶ Élagage: ne pas développer branches inutiles ▶ Mieux: élaguer dès qu’il y a excès de capacité
▶ Contraintes: élaguer si contraintes enfreintes ▶ Mieux++: élaguer si aucune amélioration avec somme valeurs
▶ Bornes: élaguer si impossible de faire mieux
Retour de monnaie
▶ Approximations: débuter avec approx. comme meilleure sol.
▶ But: rendre montant avec le moins de pièces
▶ Si tout échoue: solveurs SAT ou d’optimisation
▶ Algo.: pour chaque pièce, essayer d’en prendre 0 à # max.
7. Programmation dynamique
Approche
▶ Principe d’optimalité: solution optimale obtenue en combi-
Plus courts chemins
nant solutions de sous-problèmes qui se chevauchent
▶ Plus court chemin: chemin simple de poids minimal
▶ Descendante: algo. récursif + mémoïsation (ex. Fibonacci)
▶ Bien défini: si aucun cycle négatif
▶ Ascendante: remplir tableau itér. avec solutions sous-prob.
▶ Approche générale: raffiner distances partielles itérativement
Retour de monnaie
▶ Dijkstra: raffiner en marquant sommet avec dist. min.
▶ Sous-question: # pièces pour rendre j avec pièces 1 à i?
▶ Floyd-Warshall: raffiner via sommet intermédiaire vk
▶ Identité: T [i, j] = min(T [i − 1, j], T [i, j − s[i]] + 1)
▶ Bellman-Ford: raffiner avec ≥ 1, 2, . . . , |V | − 1 arêtes
▶ Exemple: montant m = 10 et pièces s = [1, 5, 7]
▶ Sommaire:
0 1 2 3 4 5 6 7 8 9 10
0 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ Dijkstra Bellman-Ford Floyd-Warshall
1 0 1 2 3 4 5 6 7 8 9 10 Types de chemins d’un sommet vers les autres paires de sommets
2 0 1 2 3 4 1 2 3 4 5 2 Poids négatifs? 7 3 3
3 0 1 2 3 4 1 2 1 2 3 2 Temps d’exécution O(|V | log |V | + |E|) Θ(|V | · |E|) Θ(|V |3 )
Sac à dos Temps (|E| ∈ Θ(1)) O(|V | log |V |) Θ(|V |) Θ(|V |3 )
Temps (|E| ∈ Θ(|V |)) O(|V | log |V |) Θ(|V |2 ) Θ(|V |3 )
▶ Sous-question: val. max. avec capacité j et les objets 1 à i? Temps (|E| ∈ Θ(|V |2 )) O(|V |2 ) Θ(|V |3 ) Θ(|V |3 )
▶ Identité: T [i, j] = max(T [i − 1, j], T [i − 1, j − p[i]] + v[i])

8. Algorithmes et analyse probabilistes


Modèle probabiliste Coupe minimum: algorithme de Karger
▶ Modèle: on peut tirer à pile ou face (non déterministe) ▶ Coupe: partition (X, Y ) des sommets d’un graphe non dirigé
▶ Aléa: on peut obtenir une loi uniforme avec une pièce ▶ Taille: # d’arêtes qui traversent X et Y
▶ Idéalisé: on suppose avoir accès à une source d’aléa parfaite ▶ Coupe min.: identifier la taille minimale d’une coupe
(en pratique: source plutôt pseudo-aléatoire) ▶ Algorithme: contracter itérativement une arête aléatoire en
Algorithmes de Las Vegas gardant les multi-arêtes, mais pas les boucles
▶ Temps: varie selon les choix probabilistes a b c 7→ ac b

▶ Valeur de retour: toujours correcte


▶ Prob. d’erreur: ≤ 1 − 1/|V |2 (Monte Carlo)
▶ Exemple: tri rapide avec pivot aléatoire
▶ Amplification: on peut réduire (augmenter) la prob. d’erreur
▶ Temps espéré: dépend de E[Yx ] où Yx = # opér. sur entrée x (de succès) arbitrairement (en général: avec min., maj., etc.)
Algorithmes de Monte Carlo Temps moyen

▶ Temps: ne varie pas selon les choix probabilistes ▶ Temps moyen: temps instances de taille n / # instances
▶ Valeur de retour: pas toujours correcte ▶ Attention: pas la même chose que le temps espéré
▶ Exemple: algorithme de Karger ▶ Hypothèse: entrées distribuées uniformément (± réaliste)
▶ Prob. d’erreur: dépend de Pr(Yx ̸= bonne sortie sur x) ▶ Exemple: Θ(n2 ) pour le tri par insertion
Bibliographie

[BB96] Gilles Brassard and Paul Bratley. Fundamentals of Algorithmics.


Prentice-Hall, Inc., 1996.
[BM91] Robert S. Boyer and J. Strother Moore. MJRTY: A fast majority
vote algorithm. In Automated Reasoning: Essays in Honor of Woody
Bledsoe, pages 105–118, 1991.
[Eri19] Jeff Erickson. Algorithms. 2019.

157
Index

N, 3 analyse, 13
O, 15 approche
Ω, 19 ascendante, 104
P, 2 descendant, 103
Q, 3 approximation, 69
R, 3 arborescence, 55
Θ, 20 arbre, 55
Z, 3 couvrant, 55
couvrant minimal, 59
accessibilité, 50, 114 de récursion, 82
Ackermann, 65 arithmétique, 79, 81
acyclique, 48, 54 arête, 46
adjacence, 46
algorithme Bellman-Ford, 114
d’approximation, 69 bijection, 4
de Bellman-Ford, 114 binaire, 43
de Dijkstra, 108 branch-and-bound, 97
de Floyd-Warshall, 110
de Freivalds, 129 carré, 6
de Karatsuba, 81 chemin, 48
de Karger, 123 plus court, 107
de Kruskal, 61 simple, 48, 107
de Las Legas, 122 coefficient binomial, 4
de Monte Carlo, 123 combinatoire, 4
de Prim–Jarník, 59 complément, 2
déterministe, 119 composante
glouton, 59 connexe, 48
probabiliste, 119 fortement connexe, 48
récursif, 73 connexité, 48
vorace, 59 constante multiplicative, 15, 19, 20
amplification, 126 correction, 27

158
INDEX 159

coupe minimum, 123 Karatsuba, 81


cycle, 48, 54 knapsack problem, 66
négatif, 107, 113, 115
simple, 48 Las Vegas, 122
ligne d’horizon, 85
degré, 46 limite, 25
entrant, 46 liste d’adjacence, 49
sortant, 46 logarithme, 3
différence, 2 logique propositionnelle, 100
Dijkstra, 108 loi géométrique, 119
diviser-pour-régner, 73 longueur, 48
division entière, 4
détection de cycle, 54 mathématiques discrètes, 2
matrice d’adjacence, 49
ensemble, 2 meilleur cas, 13
ensemble vide, 2 modulo, 4
ensembles disjoints, 62 monceau, 39, 59
entiers, 3 de Fibonacci, 110
espérance, 119 Monte Carlo, 123
explosion combinatoire, 94 multiplication, 81
exponentiation, 79 mémoïsation, 103
exponentielle, 3
naturels, 3
factorielle, 4 nombre
feuille, 55 aléatoire, 119
Floyd-Warshall, 110 pseudo-aléatoire, 119
fonction, 4 nombres, 3
fonction d’Ackermann, 65 notation asymptotique, 15
force brute, 94 NP-complétude, 94
forêt, 55
opération élémentaire, 13
graphe, 46 ordonnancements, 4
acyclique, 48 ordre
pondéré, 59 partiel, 54
représentation, 49 topologique, 54
total, 34
Hanoï, 73
homogène, 76, 79 paramètres, 14, 26
parcours, 50
induction généralisée, 10 en largeur, 51
injectivité, 4 en profondeur, 51
intersection, 2 pavage, 8
invariant, 27 paysage, 85
inversion, 34 permutations, 4
pire cas, 13
jeu de Nim, 10 pivot, 41
plus court chemin, 52
INDEX 160

plus courts chemins, 107 sommet, 46


poids interne, 55
négatif, 110 sous-graphe, 48
polynômes, 15, 19, 20 induit par, 48
post-condition, 27 sous-séquence, 4
preuve, 5 stabilité, 43
directe, 5 stable, 43
par contradiction, 5 successeur, 47
par induction, 6 suite de Fibonacci, 4, 77
par l’absurde, 5 sur place, 43
par récurrence, 6 surjectivité, 4
principe d’optimalité, 103 symmétrie, 4
probabilité, 119 séquence, 4
d’erreur, 123 de Collatz, 28
de succès, 123 de Fibonacci, 4, 77
problème
des n dames, 94 tableaux, 104
du retour de monnaie, 98, 104 taille, 14
du sac à dos, 66, 97, 106 tas, 39, 59
produit cartésien, 2 temps d’exécution, 13
programmation temps espéré, 122
dynamique, 103 temps moyen, 127
linéaire entière, 101 terminaison, 28
pruning, 97 théorème maître, 83
pré-condition, 27 tours de Hanoï, 73
prédecesseur, 47 transitivité, 4
tri, 34
racine, 55 fusion, 40
radix, 43 heapsort, 39
rationnels, 3 merge sort, 40
relation, 4 par fusion, 40
relation d’équivalence, 4 par insertion, 38, 127
retour arrière, 94 par monceau, 39
règle de la limite, 25 par tas, 39
récurrence quicksort, 41
homogène, 76 radix, 43
linéaire, 76 rapide, 41
non homogène, 79 sans comparaison, 43
récursivité, 73 topologique, 54
réels, 3
réflexivité, 4 union, 2
union-find, 62
sac à dos, 66
variable aléatoire, 119
SAT, 100
satisfaction, 100 voisin, 46
seuil, 15, 19, 20 élagage, 97

Vous aimerez peut-être aussi