Académique Documents
Professionnel Documents
Culture Documents
Aphes
Aphes
Chapitre 2
Graphes
1. Introduction
De manière générale, un graphe permet de représenter les connexions d’un ensemble en exprimant les relations
entre ses éléments : réseau de communication, réseau routier, circuit électronique, . . . , mais aussi relations
sociales ou interactions entre espèces animales.
Le vocabulaire de la théorie des graphes est utilisé dans de nombreux domaines : chimie, biologie, sciences
sociales, etc., mais c’est avant tout une branche à part entière et déjà ancienne des mathématiques (le fameux
problème des ponts de Königsberg d’Euler date de ). Néanmoins, l’importance accrue que revêt l’aspect
algorithmique dans ses applications pratiques en fait aussi un domaine incontournable de l’informatique. Pour
schématiser, les mathématiciens s’intéressent avant tout aux propriétés globales des graphes (graphes eulériens,
graphes hamiltoniens, arbres, coloration, dénombrement, . . .) là où les informaticiens vont plutôt chercher à
concevoir des algorithmes efficaces pour résoudre un problème faisant intervenir un graphe (recherche du plus
court chemin, problème du voyageur de commerce, recherche d’un arbre couvrant, . . .). Tout ceci forme un
ensemble très vaste, dont nous n’aborderons que quelques aspects, essentiellement de nature algorithmique.
1 1 5
4
2 3 5
4 2 3
G est un graphe d’ordre 5 ; il possède un sommet de degré 1 (le sommet 2), trois sommets de degré 3 (les
sommets 1, 4 et 5) et un sommet de degré 4 (le sommet 3).
Remarque. On peut observer que ce graphe a la particularité de posséder une représentation (celle de droite)
pour laquelle les arêtes ne se coupent pas. Un tel graphe est dit planaire. Cette notion ne sera pas abordée dans
la suite de ce cours.
Un graphe est dit simple lorsqu’aucun sommet n’est adjacent à lui-même. Par la suite, nous nous restreindrons à
l’étude des graphes simples 1 .
1. Un graphe présentant des arêtes reliant un sommet à lui-même ou plusieurs arêtes reliant les deux mêmes sommets est appelé un
multigraphe.
http://info-llg.fr
2.2 option informatique
Théorème. — Si G est un graphe non orienté simple, La somme des degrés de ses sommets est égal à deux fois le
nombre de ses arêtes : X
deg(v) = 2|E|.
v∈V
Preuve. Lorsqu’on fait la somme des degrés des sommets, chaque arête est comptée deux fois.
• Chemins
Un chemin de longueur k reliant les sommets a et b est une suite finie x0 = a, x1 , . . . , xk = b de sommets tel que
pour tout i ∈ ~0, k − 1, la paire (xi , xi+1 ) soit une arête de G. Ce chemin est dit cyclique lorsque a = b.
La distance entre deux sommets a et b est la plus petite des longueurs des chemins reliant a et b, si tant est qu’il
en existe. Lorsque tous les sommets sont à distance finie les uns des autres, on dit que le graphe est connexe.
Un graphe non connexe peut être décomposé en plusieurs composantes connexes, qui sont des sous-graphes
connexes maximaux.
Par exemple, le graphe suivant possède trois composantes connexes :
8
7
3
6
2 1 5
4 9
Démontrons maintenant quelques résultats généraux qui nous seront utiles dans la suite de ce cours.
Preuve. Ce résultat, comme la plus-part des résultats de ce chapitre, se prouve par récurrence sur l’ordre n du
graphe.
– Ce résultat est évident si n = 1.
– Si n > 1, supposons ce résultat acquis au rang n − 1, et considérons un graphe connexe G d’ordre n.
Nous allons distinguer deux cas. Si G possède un sommet x de degré 1, supprimons-le ainsi que l’unique
arête qui le relie au reste du graphe. Le graphe ainsi obtenu est toujours connexe donc comporte au moins
n − 2 arêtes, ce qui prouve que G possède au moins 1 + (n − 2) = n − 1 arêtes.
Dans le cas contraire, tous les sommets de G sont au moins de degré 2. Or la somme des degrés d’un
graphe est égal à deux fois son nombre d’arêtes, donc G possède au moins n arêtes.
Lemme. — Si dans un graphe G tout sommet est de degré supérieur ou égal à 2, alors G possède au moins un cycle.
Preuve. Partons d’un sommet arbitraire v1 , et construisons une suite finie de sommets v1 , v2 , . . . , vk de la façon
suivante :
– vi < {v1 , v2 , . . . , vi−1 } ;
– vi est voisin de vi−1 .
Puisque les sommets de G sont en nombre fini, cette construction se termine. Or vk est au moins de degré 2
donc il possède, outre vk−1 , un autre voisin vj dans la séquence. Alors (vj , vj+1 , . . . , vk , vj ) est un cycle de G.
• Arbres
Un arbre est un graphe connexe acyclique 2 . D’après les deux théorèmes précédents, un arbre d’ordre n possède
exactement n − 1 arêtes. Plus précisément, on démontre le résultat suivant :
D’une certaine façon, un arbre est un graphe connexe minimal et un graphe acyclique maximal.
Preuve. Compte tenu de la définition d’un arbre, il suffit de montrer l’équivalence des propriétés (ii) et (iii).
– Supposons qu’un graphe connexe G à n − 1 arêtes possède un cycle. La suppression d’une arête de ce cycle
créerai un graphe à n − 2 arêtes toujours connexe, ce qui est absurde. G est donc aussi acyclique.
– Supposons qu’un graphe acyclique G à n − 1 arêtes ne soit pas connexe. Il existerait alors deux sommets
x et y qui ne peuvent être reliés par un chemin. Rajoutons alors une arête entre ces deux sommets. Le
graphe obtenu est toujours acyclique mais possède maintenant n arêtes, ce qui est absurde. G est donc
aussi connexe.
2 1
3 6
4 5
Le sommet 1 a un degré sortant égal à 3 et un degré entrant égal à 0, tandis que le sommet 2 a un degré entrant
et un degré sortant tous deux égaux à 2.
Les notions de chemin et de distance s’étendent sans difficulté aucune au cas des graphes orientés.
Un graphe orienté est dit fortement connexe lorsque pour tout couple de sommets (a, b) il existe un chemin
reliant a à b et un chemin reliant b à a. Un graphe orienté peut être décomposé en composantes fortement
connexes (des sous-graphes fortement connexes maximaux).
Par exemple, le graphe orienté suivant possède quatre composantes fortement connexes :
2. Un graphe acyclique mais non connexe est appelé une forêt, chacune de ses composantes connexes étant un arbre.
3. Dans le cas d’un graphe orienté, on parle souvent d’arc plutôt que d’arêtes.
http://info-llg.fr
2.4 option informatique
6
7 3
2
9
4 1 8 5
• Arbre enraciné
Considérons de nouveau un arbre, c’est-à-dire, rappelons-le, un graphe connexe acyclique non orienté, et
prouvons le résultat suivant :
Théorème. — Si a et b sont deux sommets distincts d’un arbre G, il existe un unique chemin reliant a et b.
Preuve. G est connexe, ce qui assure l’existence d’un tel chemin. Et s’il en existait un deuxième, nous pourrions
former un cycle en empruntant le premier entre a et b puis le second entre b et a et ainsi contredire le caractère
acyclique de G.
Une conséquence de ce résultat est qu’il est possible de choisir arbitrairement un sommet r d’un arbre G puis
d’orienter les arêtes de ce graphe de sorte qu’il existe un chemin reliant r à tous les autres sommets. On obtient
alors un arbre enraciné correspondant au sens qu’on lui accorde usuellement en informatique.
5
1
2 4 5 6 7 =⇒ 4 6
3
1 2 3 7
Cette représentation est la plus souple : elle présente l’avantage de permettre d’ajouter ou de supprimer
facilement des sommets et des arêtes, mais l’inconvénient de ne pas permettre un accès rapide à un sommet ou
une arête particulière.
En général, les sommets de chaque liste d’adjacence sont rangés selon un ordre arbitraire.
On peut observer que ces fonctions ne s’appliquent qu’à des graphes orientés. Pour des graphes non orientés, il
faut ajouter/supprimer à la fois l’arête reliant a et b et l’arête reliant b et a.
L’ajout et la suppression d’un sommet, toujours pour le type ’a graph, se réalisent ainsi :
http://info-llg.fr
2.6 option informatique
Cette nouvelle représentation des listes d’adjacence est toujours aussi économique en espace (Θ(n+p)) et permet
encore l’ajout ou la suppression d’une arête, mais plus l’ajout ou la suppression d’un sommet.
let desoriente g =
let n = vect_length g in
let aux i j = if not mem i g.(j) then g.(j) <− i::g.(j) in
for i = 0 to n−1 do
do_list (aux i) g.(i)
done ;;
• Matrices d’adjacence
Si on veut en plus accéder en coût constant à chacune des arêtes, il devient nécessaire d’ordonner les sommets
V = {v1 , v2 , . . . , vn } et utiliser une matrice M ∈ Mn ({0, 1}) pour représenter les arêtes :
1 si (vi , vj ) ∈ E
mij =
0 sinon
Cette représentation a des avantages : l’ajout et la suppression d’une arête a un coût constant ; déterminer si
un graphe est orienté est chose aisée (il suffit de vérifier que la matrice est symétrique), mais elle présente
l’inconvénient d’occuper beaucoup plus d’espace mémoire, en particulier lorsque le nombre d’arêtes est réduit
vis-à-vis du nombre de sommets : si n = |V| et p = |E|, le coût spatial de la représentation par matrice d’adjacence
est un Θ(n2 ), contre un Θ(n + p) pour une liste d’adjacence.
Graphes 2.7
0 1 0 1 0 1
1 0
0 0 0 1 1 0
0 0 0 1 0 0
2 5 M =
0 0 0 0 1 0
0 0 0 0 0 0
3 4 0 1 0 0 0 0
Il peut être utile de passer d’une représentation à une autre, aussi allons-nous écrire une fonction qui calcule la
matrice d’adjacence d’un graphe. Pour en simplifier l’écriture, nous supposerons que les sommets du graphe
sont les entiers de 0 jusqu’à n − 1 et nous utiliserons le type graphe pour représenter le graphe.
let graphe_to_mat g =
let n = vect_length g in
let m = make_matrix n n 0 in
for a = 0 to n−1 do
do_list (function b −> m.(a).(b) <− 1) g.(a)
done ;
m ;;
À l’inverse, la fonction qui suit détermine le graphe associée à une matrice d’adjacence donnée :
let mat_to_graphe m =
let n = vect_length m in
let g = make_vect n [] in
for a = 0 to n−1 do
for b = 0 to n−1 do
if m.(a).(b) = 1 then g.(a) <− b::g.(a)
done
done ;
g ;;
http://info-llg.fr
2.8 option informatique
Coût du parcours
Lors d’un parcours, chaque sommet entre au plus une fois dans la liste des sommets à traiter, et n’en sort donc
aussi qu’au plus une fois. Si ces opérations d’entrée et de sortie dans la liste sont de coût constant (ce qui sera
effectivement le cas dans la suite), le coût total des manipulations de la liste « àTraiter » est un O(n), avec n = |V|.
Chaque liste d’adjacence est parcourue au plus une fois donc le temps total consacré à scruter les listes de
voisinage est un O(p) avec p = |E|, à condition de déterminer si un sommet a déjà été vu en coût constant. Dans ce
cas, le coût total d’un parcours est un O(n + p).
Pour réaliser cette condition, la solution que nous adopterons consistera à utiliser un tableau booléen pour
représenter « déjàVus », destiné à marquer chaque sommet au moment où il entre dans la liste « àTraiter ».
Nous allons maintenant nous intéresser à deux types de parcours, qui diffèrent seulement par la façon d’extraire
les sommets de la liste en cours de traitement : les parcours en largeur et en profondeur. Dans ce qui suit, nous
considérerons un graphe défini par liste d’adjacence (avec le type graphe) ainsi qu’une fonction traitement de
type int −> unit.
let bfs g s =
let dejavu = make_vect (vect_length g) false
and atraiter = new() in
add s atraiter ; dejavu.(s) <− true ;
let rec ajoute_voisin = function
| [] −> ()
| t::q when dejavu.(t) −> ajoute_voisin q
| t::q −> add t atraiter ; dejavu.(t) <− true ;
ajoute_voisin q
in
try while true do
let s = take atraiter in
traitement s ;
ajoute_voisin g.(s)
done
with Empty −> () ;;
Illustrons cette fonction à partir du sommet s0 = 2 du graphe présenté figure 5, en appliquant print_int en
guise de traitement :
2
0 1
2 0 3
3 4 5
1 7
6 7 8 4 5 6 8
4. FIFO, pour First In, First Out, voir cours de première année.
Graphes 2.9
# bfs g 2 ;;
203174568 − : u n i t = ( )
2 =⇒ 0 3 =⇒ 3 1 =⇒ 1 7 =⇒ 7 4 5 =⇒ 4 5 6 8 =⇒ 5 6 8 =⇒ 6 8 =⇒ 8
On constate que le parcours en largueur correspond à un parcours hiérarchique dans l’arborescence associée.
let dfs g s =
let dejavu = make_vect (vect_length g) false
and atraiter = new() in
push s atraiter ; dejavu.(s) <− true ;
let rec ajoute_voisin = function
| [] −> ()
| t::q when dejavu.(t) −> ajoute_voisin q
| t::q −> push t atraiter ; dejavu.(t) <− true ;
ajoute_voisin q
in
try while true do
let s = pop atraiter in
traitement s ;
ajoute_voisin g.(s)
done
with Empty −> () ;;
2
0 1
2 3 0
3 4 5
7 1
6 7 8 8 6 5 4
# dfs g 2 ;;
237865140 − : u n i t = ( )
http://info-llg.fr
2.10 option informatique
8
6 6
7 5 5 5
3 1 1 1 1 1 4
2 =⇒ 0 =⇒ 0 =⇒ 0 =⇒ 0 =⇒ 0 =⇒ 0 =⇒ 0 =⇒ 0
On constate que le parcours en profondeur correspond à un parcours préfixe dans l’arborescence associée.
Notons enfin que l’usage d’une pile laisse présager l’existence d’un algorithme récursif pour le DFS :
let dfs_rec g s =
let dejavu = make_vect (vect_length g) false in
let rec aux = function
| s when dejavu.(s) −> ()
| s −> dejavu.(s) <− true ;
traitement s ;
do_list aux g.(s)
in aux s ;;
Nous allons pour finir donner deux applications du parcours en profondeur : le calcul des composantes connexes
d’un graphe connexe non orienté et le tri topologique d’un graphe orienté acyclique.
Pour réaliser cet algorithme, on utilise la version récursive du parcours en profondeur en remplaçant le
traitement par un accumulateur transportant la liste des sommets rencontrés :
let liste_composantes g =
let dejavu = make_vect (vect_length g) false in
let rec dfs lst = function
| s when dejavu.(s) −> lst
| s −> dejavu.(s) <− true ;
it_list dfs (s::lst) g.(s)
and aux comp = function
| s when s = vect_length g −> comp
| s when dejavu.(s) −> aux comp (s+1)
| s −> aux ((dfs [] s)::comp) (s+1)
in aux [] 0 ;;
lst est un accumulateur qui transporte les sommets faisant partie de la composante connexe en cours d’explo-
ration, comp est un accumulateur qui transporte les composantes connexes déjà trouvées.
2 1
3 0
4 5
3 2 4 1 5 0
Figure 7 – Un exemple d’ordre topologique d’un graphe orienté acyclique. On notera que lorsque le graphe est
dessiné suivant l’ordre topologique les arcs sont tous orientés de gauche à droite.
let tri_topologique g =
let dejavu = make_vect (vect_length g) false in
let rec dfs lst = function
| s when dejavu.(s) −> lst
| s −> dejavu.(s) <− true ;
s::(it_list dfs lst g.(s))
and aux ord = function
| s when s = vect_length g −> ord
| s when dejavu.(s) −> aux ord (s+1)
| s −> aux (dfs ord s) (s+1)
in aux [] 0 ;;
Lemme. — S’il existe un arc reliant le sommet a au sommet b alors a rentre après b dans la liste chaînée.
Preuve. Lors du parcours en profondeur vient forcément un moment où a est vu pour la première fois et ses
voisins examinés. À ce moment a n’est pas encore entré dans la liste chaînée.
– Si b n’a pas encore été vu, le parcours en profondeur se poursuit à partir de b ; une fois ce dernier achevé b
rentre dans la liste chaînée, et a y rentrera plus tard.
– Si b a déjà été vu et est déjà rentré dans la liste chaînée, le résultat est acquis.
– Reste à examiner le cas où b a déjà été vu mais n’est pas encore entré dans la liste chaînée. Ceci signifie
que l’algorithme est en train de parcourir une branche issue de b, avec pour conséquence l’existence d’un
chemin reliant b à a. Mais puisque b est voisin de a, ceci implique l’existence d’un cycle dans G, ce qui est
exclu.
http://info-llg.fr
2.12 option informatique
passer par Londres, Amsterdam, Berlin et Varsovie (ou par Madrid, Paris, Berlin et Varsovie). Mais on peut
aussi prendre en compte la durée associée à chaque trajet ; dans ce cas le chemin le plus rapide ne sera pas
forcément égal au trajet précédent : il est plus intéressant de passer par Madrid, Barcelone, Lyon, Munich,
Prague et Budapest.
12 7
5
18 8
12 4
7
3 3 7 4
4 5
6 3 9
2 5 15
24
7 3
9 7 6
6
9
4 8 5 3
6 3
36 9
Le poids d’un chemin est la somme des poids des arêtes qui le composent. On notera δ(a, b) le poids du plus
court chemin allant de a à b, s’il en existe. Dans le cas contraire, on posera δ(a, b) = +∞.
Remarque. En présence de poids négatifs il convient de prendre quelques précautions pour assurer l’existence
de ce minimum. En particulier, s’il existe un chemin menant de a et b et comprenant un circuit fermé de poids
strictement négatif, il convient de poser δ(a, b) = −∞ car dans ce cas on peut faire indéfiniment décroitre le
poids de ce chemin en multipliant les passages par cette boucle. On peut éviter cette situation en supposant par
exemple que tous les poids sont positifs.
Il existe trois problème de plus courts chemins :
(i) calculer le chemin de poids minimal entre une source a et une destination b ;
(ii) calculer les chemins de poids minimal entre une source a et tout autre sommet du graphe ;
(iii) calculer tous les chemins de poids minimal entre deux sommets quelconques du graphe.
Le troisième problème est le plus simple à résoudre : l’algorithme de Floyd-Warshall nous en donnera une
solution. En revanche et de manière surprenante il n’existe pas à l’heure actuelle d’algorithme qui donne la
solution du premier problème sans résoudre le second. Nous donnerons une solution de ces deux problèmes en
étudiant l’algorithme de Dijkstra. Il faut cependant noter qu’il existe de multiples algorithmes de plus courts
chemins, souvent adaptés à un type particulier de graphe.
Graphes 2.13
On notera enfin que les algorithmes que nous allons étudier sont basés sur le résultat suivant :
Lemme (principe de sous-optimalité). — Si a b est un plus court chemin qui passe par c, alors a c et c b
sont eux aussi des plus courts chemins.
Preuve. S’il existait un chemin plus court entre par exemple a et c, il suffirait de le suivre lors du trajet entre a
et b pour contredire le caractère minimal du trajet a b.
1 1 2 2 3
0 +∞ +∞ +∞ −1 +∞
−1 2 1
0 +∞ 2 +∞ +∞
+∞ 2 0 +∞ +∞ 6
−3 7 5 6 −4 M =
−3 +∞ +∞ 0 +∞ +∞
+∞ 7 +∞ 4 0 +∞
+∞ 5 −4 +∞ +∞ 0
4 4 5 6
Si n désigne l’ordre du graphe G, l’algorithme de Floyd-Warshall consiste à calculer la suite finie de matrices
M(k) , 0 6 k 6 n avec M(0) = M et :
(k+1)
(k) (k) (k)
∀k < n, ∀(i, j) ∈ N2 , mij = min mij , mi,k+1 + mk+1,j .
(k)
Théorème. — Si G ne contient pas de cycle de poids strictement négatif, alors mij est égal au poids du chemin
minimal reliant vi à vj et ne passant que par des sommets de la liste v1 , v2 , . . . , vk .
(n)
De ceci il découle immédiatement que mij est le poids minimal d’un chemin reliant vi à vj .
ce qui nous permet de représenter un graphe pondéré par le type poids vect vect.
On définit la somme et le minimum de deux objets de type poids :
http://info-llg.fr
2.14 option informatique
Il est possible de calculer les différentes valeurs de la suite (M(k) ) en utilisant une seule matrice car la formule :
(k+1)
(k) (k) (k)
mij = min mij , mi,k+1 + mk+1,j
let floydwarshall w =
let n = vect_length w in
let m = make_matrix n n Inf in
for i = 0 to n−1 do
for j = 0 to n−1 do
m.(i).(j) <− w.(i).(j)
done
done ;
for k = 0 to n−1 do
for i = 0 to n−1 do
for j = 0 to n−1 do
m.(i).(j) <− mini m.(i).(j) (som m.(i).(k) m.(k).(j))
done
done
done ;
m ;;
0 6 +∞ 3 +∞
−1
−1
0 +∞ 2 −2 +∞
1 2 0 4 0 6
(6)
M =
−3 3 +∞ 0 −4 +∞
1 7 +∞ 4 0 +∞
−3 −2 −4 0 −4 0
On observe que les sommets 3 et 6 ne sont pas accessibles à partir des sommets 1, 2, 4 et 5. En revanche, on peut
aller du sommet 6 au sommet 1 pour un coût total égal à −3 (il n’est pas difficile de deviner qu’il faut passer par
les sommets 3, 2 et 4) ou du sommet 3 au sommet 5 pour un coût total nul (en passant par les sommets 2, 4 et 1).
let pluscourtschemins w =
let n = vect_length w in
let m = make_matrix n n Inf
and c = make_matrix n n [] in
for i = 0 to n−1 do
for j = 0 to n−1 do
m.(i).(j) <− w.(i).(j)
done
done ;
for k = 0 to n−1 do
for i = 0 to n−1 do
for j = 0 to n−1 do
let l = som m.(i).(k) m.(k).(j) in
if inferieur l m.(i).(j) then
(m.(i).(j) <− l ; c.(i).(j) <− c.(i).(k)@[k+1]@c.(k).(j))
done
done
done ;
for i = 0 to n−1 do
for j = 0 to n−1 do
if i <> j && m.(i).(j) <> Inf then c.(i).(j) <− [i+1]@c.(i).(j)@[j+1]
done
done ;
c ;;
La matrice c contient la liste des sommets intermédiaires par lesquels passer pour obtenir le chemin de poids
minimal. La fin du code ci-dessus ne sert qu’à y rajouter les extrémités.
Appliqué au graphe donné en exemple figure 9, on obtient la matrice des plus courts chemins suivante :
On peut observer que parmi tous ces chemins de poids minimal, le plus long relie le sommet 6 au sommet 5 en
passant par les sommets 3, 2, 4 et 1, pour un poids total égal à −4.
(k+1) (k)
(k) (k)
mij = mij ou mi,k+1 et mk+1,j .
(k)
Il n’est pas difficile de prouver que le booléen mij dénote l’existence d’un chemin reliant les sommets vi et vj en
ne passant que par les sommets v1 , v2 , . . . , vk et que par voie de conséquence la matrice M(n) résout le problème
de la fermeture transitive.
http://info-llg.fr
2.16 option informatique
let warshall w =
let n = vect_length w in
let m = make_matrix n n false in
for i = 0 to n−1 do
for j = 0 to n−1 do
m.(i).(j) <− w.(i).(j) = 1
done
done ;
for k = 0 to n−1 do
for i = 0 to n−1 do
for j = 0 to n−1 do
m.(i).(j) <− m.(i).(j) || (m.(i).(k) && m.(k).(j))
done
done
done ;
m ;;
4 5
1 2 4
7 1
0 5 3
1 2
2 7 5
6. L’exercice 12 présente un algorithme qui s’affranchit de cette hypothèse.
Graphes 2.17
S 0 1 2 3 4 5
{0} · 7 1 ∞ ∞ ∞
{0, 2} · 6 · ∞ 3 8
{0, 2, 4} · 5 · 8 · 8
{0, 2, 4, 1} · · · 8 · 6
{0, 2, 4, 1, 5} · · · 8 · ·
{0, 2, 4, 1, 5, 3} · · · · · ·
∀u ∈ S, du = δ(s, u)
n o
∀v ∈ S, dv = min du + w(u, v) u ∈ S
– Lorsque |S| = 1 nous avons S = {s}, ds = 0 et ∀v , s, dv = w(s, v) donc le résultat annoncé est bien vrai.
– Lorsque |S| > 1, supposons l’invariant acquis à l’étape précédente et distinguons trois cas :
(i) u est déjà dans S : dans ce cas du = δ(s, u) et cette valeur n’est pas modifiée.
(ii) u entre dans S : dans ce cas u ∈ S vérifie ∀v ∈ S, du 6 dv et il s’agit de prouver que du = δ(s, u).
Considérons un plus court chemin s u et notons v le premier sommet de ce parcours qui ne soit
pas dans S (éventuellement on peut avoir v = u). D’après le principe de sous-optimalité, s v et
v u sont des plus courts chemins.
Puisque toutes les pondérations
n sont
positives,
o n δ(s, u) > δ(s, v). Or par
o hypothèse de récurrence
appliquée à v, dv = min dx + w(x, v) x ∈ S = min δ(s, x) + w(x, v) x ∈ S = δ(s, v) (n’oublions pas que
s v est un plus court chemin qui ne passe que par des sommets de S).
De ceci il résulte que δ(s, u) > dv . Par ailleurs, le choix de u impose du 6 dv donc δ(s, u) > du . Mais du
est le poids d’un chemin menant de s à u donc en définitive du = δ(s, u).
(iii) v reste dans S. Dans ce cas, si v n’est pas voisin de l’élément qui entre dans S la valeur de dv n’est
pas modifiée,
n et dans le cas
o contraire sa valeur est modifiée pour continuer à respecter l’égalité
dv = min du + w(u, v) u ∈ S .
Étude de la complexité
Étudier la complexité de l’algorithme de Dijkstra est délicat car tributaire du choix de la représentation des
structures de données. Grossièrement, on effectue n − 1 transferts de S vers S en ayant à chaque fois déterminé
le plus petit élément d’une partie du tableau d puis en ayant modifié certaines de ses valeurs en conséquence.
Tout ceci a un coût linéaire, donc la complexité totale est a priori un O(n2 ).
Cette complexité est optimale pour des graphes denses, c’est-à-dire pour lesquels les sommets ont de nombreux
voisins (le pire des cas correspondant aux graphes complets dont le nombre d’arêtes, égal à n2 , impose de toute
façon un coût au moins quadratique). En revanche, lorsque le graphe est creux, il est possible d’améliorer cette
complexité en utilisant une file de priorité (autrement dit un tas) pour représenter S.
On sait qu’un tas-min permet la récupération de l’élément de priorité minimale (ici la valeur di ) pour un coût
en O(log n) (correspondant à la reformation du tas) donc le coût total du transfert de S à S est un O(n log n). Par
ailleurs, chaque mise a jour d’une valeur du tableau d a lui aussi un coût en O(log n) (là encore pour reformer le
tas S), mais chaque arête ne va intervenir qu’au plus une fois dans ces modifications de d, donc si p désigne le
nombre d’arêtes du graphe le coût total de ces modifications est un O(p log n).
Au final, ceci permet d’envisager un coût total en O((n + p) log n).
n2
Pour qu’il soit intéressant d’utiliser un tas, il faut donc que p log n = O(n2 ), autrement dit que p = O .
log n
http://info-llg.fr
2.18 option informatique
let dijkstra g w s =
let n = vect_length g in
let d = make_vect n Inf
and t = make_vect n 0 and m = make_vect n 0 in
for k = 0 to n−1 do
d.(k) <− w.(s).(k) ;
if k <> s then ajoute d t m k ;
done ;
let rec modif i = function
| [] −> ()
| j::q −> let x = som d.(i) w.(i).(j) in
if inferieur x d.(j) then (d.(j) <− x ; remonte d t m m.(j)) ;
modif i q
in
for k = 0 to n−1 do
let i = extrait d t m in
modif i g.(i)
done ;
d ;;
Illustrons cette fonction à l’aide de l’exemple qui nous a servi à décrire l’algorithme de Dijkstra :
# let g = [| [1; 2]; [3; 5]; [1; 4; 5]; []; [1; 3]; [4] |] ;;
# dijkstra g w 0 ;;
− : poids vect = [ | P 0; P 5; P 1; P 8; P 3; P 6 | ]
Calculons par exemple le plus court chemin reliant les sommets 0 et 5 dans le graphe donné en exemple :
# pluscourtchemin g w 0 5 ;;
− : int l i s t = [ 0 ; 2; 4; 1; 5]
http://info-llg.fr
2.20 option informatique
5
8
4
3 3 4 5
3
2 5
3
6
4
5 3
6 3
Dans toute cette partie, G = (V, E) est un graphe non orienté connexe muni d’une pondération w : E → R à
valeurs strictement positives.
Un sous-graphe G0 = (V0 , E0 ) de G est un graphe tel que V0 ⊂ V et E0 ⊂ E muni de la pondération w|E0 . Il est dit
couvrant lorsque il est lui-aussi connexe et lorsque V0 = V. Le problème que nous allons étudier est la recherche
d’un sous-graphe couvrant de poids minimal.
Mais tout d’abord, prouvons un résultat qui justifie le vocabulaire utilisé :
Théorème. — Si w est à valeurs dans R∗+ , alors G possède un sous-graphe couvrant minimal, et ce dernier est un
arbre (c’est-à-dire rappelons-le un graphe acyclique connexe).
Preuve. L’ensemble des sous-graphes couvrants est non vide puisqu’il contient G, et il est fini, ce qui justifie
l’existence d’un sous-graphe G0 couvrant minimal.
Si jamais ce sous-graphe contenait un cycle, on pourrait supprimer une arête de ce cycle et le sous-graphe
obtenu serait toujours couvrant et de poids strictement inférieur, ce qui est absurde ; G0 est donc bien un
arbre.
Il existe deux algorithmes célèbres pour résoudre le problème de l’arbre couvrant de poids minimum. Chacun
de ces deux algorithmes utilise plus particulièrement une des caractérisations des arbres que nous avons établies
page 3 : le premier, l’algorithme de Prim, utilise le fait qu’un arbre est un graphe connexe à n − 1 arêtes ; le
second, l’algorithme de Kruskal, qu’un arbre est un graphe acyclique à n − 1 arêtes.
A ← A ∪ {(a, b)}
return (S, A)
Un exemple d’application de l’algorithme de Prim illustrant l’évolution de S est présenté figure 11.
2 3 3
2
4 2 1 4 2
1 6 3 6 6
1 4 1 1 1 1
5
5 4
5
3 2 3 3
2 2 3
2 2 2 1
1 1
6 6 1 6
1 1 4 1 1 4 1 4
4
5
Preuve. Notons tout d’abord que le graphe construit par cet algorithme est un graphe connexe couvrant. De
plus, il possède par construction n sommets et n − 1 arêtes donc il s’agit bien d’un arbre. Il reste à prouver qu’il
est de poids minimal.
Nous allons prouver par récurrence sur |S| qu’à chaque étape de l’algorithme il existe un arbre couvrant de
poids minimal qui contient le graphe (S, A), ce qui suffira à prouver le résultat souhaité.
– C’est bien évident lorsque |S| = 1 : A = ∅.
– Si |S| > 1, supposons l’existence d’un arbre couvrant T de poids minimal contenant l’arbre (S, A), et notons
(a, b) l’arête que l’algorithme de Prim ajoute à A.
Si cette arête appartient à T, nous en avons terminé. Dans le cas contraire, ajoutons cette arête à T. Les
caractérisations des arbres que nous avons énoncées page 3 montrent que nous créons nécessairement
un cycle. Ce cycle parcours à la fois des éléments de S (parmi eux, a) et des éléments de V \ S (parmi
eux, b). Il existe donc nécessairement une autre arête (a0 , b0 ) , (a, b) de ce cycle tel que a0 ∈ S et b0 ∈ V \ S.
Considérons alors l’arbre T0 obtenu en supprimant cette arête. Il s’agit de nouveau d’un arbre couvrant et
il est de poids minimal car par choix de (a, b) on a w(a, b) 6 w(a0 , b0 ).
Étude de la complexité
Si p dénote le nombre d’arêtes du graphe G, la recherche naïve de l’arête de poids minimal (a, b) est un O(p) et le
coût total un O(np). On peut néanmoins faire mieux en procédant à un pré-traitement des sommets consistant
à déterminer pour chacun d’eux l’arête incidente de poids minimal qui le relie à un sommet de S. Dès lors, le
coût de la recherche de l’arête de poids minimal devient un O(n), et une fois le nouveau sommet ajouté à S, il
suffit de mettre à jour les voisins de celui-ci. Sachant que le coût du pré-traitement est un O(p) = O(n2 ), le coût
total de l’algorithme est un O(n2 ).
Notons enfin que l’utilisation d’un tas pour stocker les différents sommets n’appartenant pas à S permet de
réduire le coût, qui devient dès lors un O(p log n).
http://info-llg.fr
2.22 option informatique
2 3 3 2 3 2 3
4 2 1 4 1
1 6 3 1 6 1 6
1 4 1 4 1 4
5
5 4
5 5 5
2 3 2 3 3 2 3 3
2 1 2 1 2 1
1 6 1 6 1 6
1 4 1 4 1 4
4
5 5 5
On peut noter qu’ainsi écrit, cet algorithme s’applique à un graphe non nécessairement connexe et retourne
dans ce cas la forêt couvrante de poids minimal de G, c’est-à-dire une forêt dont chaque composante connexe
est un arbre couvrant minimal d’une composante connexe de G. Si on applique cet algorithme à un graphe
qu’on sait être connexe, on peut stopper cet algorithme dès lors que |A| = |V| − 1 pour obtenir l’arbre couvrant
de poids minimal.
La preuve de validité de cet algorithme repose sur le résultat préliminaire suivant :
Lemme. — Toutes les forêts couvrantes d’un graphe G ont même nombre d’arêtes.
Preuve. Notons G = (V, E), et C1 , . . . , Cp les composantes connexes de G. Le nombre d’arêtes d’une forêt cou-
p
X
vrante de G est alors égale à : |Ci | − 1 = |G| − p.
i=1
Preuve. Notons tout d’abord que le graphe ainsi construit est bien une forêt couvrante puisqu’on ajoute des
arêtes sans jamais créer de cycle.
Il reste à établir que cette forêt est de poids minimal. Pour ce faire, on note A = (e1 , . . . , ek ) les arêtes choisies
par l’algorithme de Kruskal, rangées par ordre de poids croissant, et on considère une autre forêt couvrante
F = (V, A0 ) dont les arêtes A0 = (e10 , . . . , ek0 ) sont elles aussi rangées par ordre de poids croissant. Nous allons
montrer que pour tout i ∈ ~1, k on a w(ei ) 6 w(ei0 ), ce qui permettra de conclure.
Graphes 2.23
0
Raisonnons par l’absurde en supposant n qu’il existe un entier i ∈ ~1, k tel que w(ei ) < w(ei ), et considérons alors
0 0 0 0
le graphe G = (V, E ), avec E = e ∈ E w(e) 6 w(ei )}.
Appliqué à G0 , l’algorithme de Kruskal se déroule comme sur G et retourne un ensemble d’arêtes inclus dans
{e1 , . . . , ei−1 }, autrement dit une forêt couvrante de G0 comportant au plus i − 1 arêtes. Or la forêt F0 = (V, A00 )
avec A00 = (e10 , . . . , ei0 ) est une forêt couvrante de G0 qui comporte i arêtes, ce qui contredit le résultat du lemme
précédent.
Étude de la complexité
L’algorithme de Kruskal consiste avant tout à trier les arêtes puis à les énumérer par ordre croissant ; clairement
l’usage d’un tas s’impose. Si on note p le nombre d’arêtes de G, le coût de la formation du tas est un O(p).
Par ailleurs, il existe des structures de données qui permettent de gérer efficacement une partition d’objets et
qui permettent ici de représenter l’évolution des différentes composantes connexes 10 . Si on utilise une telle
structure, le coût total de l’algorithme de Kruskal devient un O(p log p). Enfin, sachant que p = O(n2 ) on peut
simplifier le coût en O(p log n) et retrouver ainsi le même coût que l’algorithme de Prim.
5. Exercices
5.1 Combinatoire des graphes
Exercice 1
a) Montrer qu’un graphe simple non orienté a un nombre pair de sommets de degré impair.
b) Montrer que dans une assemblée de n personnes (n > 2), il y a toujours au moins deux personnes qui ont le
même nombre d’amis présents (on considère que la relation d’amitié est symétrique).
c) Est-il possible de créer un réseau de 15 ordinateurs de sorte que chaque machine soit reliée avec exactement
trois autres ?
Exercice 2 Une suite finie décroissante (au sens large) est dite graphique s’il existe un graphe simple dont les
degrés des sommets correspondent à cette suite.
a) Les suites suivantes sont-elles graphiques ?
Exercice 3 Considérons un arbre G = (V, E) avec V = {1, 2, . . . , n} et n > 2. Le codage de Prüfer permet de
décrire précisément cet arbre à l’aide d’une suite finie de n − 2 entiers de l’intervalle ~1, n. Il se déroule de la
façon suivante :
function prufer(arbre : G = (V, E))
L=∅
while |V| > 2n do o
i ← min j ∈ V deg(j) = 1
Soit j ∈ V (i, j) ∈ E
L ← L ∪ {j}
V ← V \ {i}, E ← E \ {(i, j)}
return L
a) Déterminer le codage de Prüfer de l’arbre ci-dessous :
10. Cette structure de données s’appelle Union-Find.
http://info-llg.fr
2.24 option informatique
4 7
6 2 1 3 5 8
b) On appelle stable un sous-ensemble de V dans lequel ne figure aucun couple de voisins. Prouver avec soin
que la fonction α calcule le cardinal maximal d’un stable de G (le nombre de stabilité de G).
c) Quel est le coût de cet algorithme ?
Exercice 9 On appelle diamètre d’un graphe non orienté la longueur du plus long chemin acyclique qu’il est
possible de tracer.
a) Proposer un algorithme naïf pour calculer le diamètre d’un graphe, et analyser son temps d’exécution.
b) Proposer un algorithme efficace pour calculer le diamètre d’un arbre, et analyser son temps d’exécution.
1 1 3 1 5 2 7
4 2 1 4 2
0 7 4 3 8
3 4 6 3
2 6
Exercice 12 L’algorithme de Bellman-Ford permet de déterminer le plus court chemin à partir d’une source
dans un graphe G = (V, E) pondéré par une fonction w : E → R pouvant prendre des valeurs négatives. Il se
déroule de la façon suivante :
function Bellman-Ford(sommet : s)
for all v ∈ V \ {s} do
dv ← +∞
ds ← 0
for k ∈ ~1, |V| − 1 do
for all (u, v) ∈ E do
dv ← min dv , du + w(u, v)
for all (u, v) ∈ E do
if du + w(u, v) < dv then
return Faux
return Vrai
a) Appliquer cet algorithme au graphe présenté ci-dessous, à partir du sommet s = 1.
5
2 3
−2
6 −4 −3
1 8 7
7
2
5 9 4
http://info-llg.fr
2.26 option informatique
b) Montrer que si le graphe ne présente pas de cycle de poids négatif, alors à la fin de l’algorithme on a
dv = δ(s, v) pour tout v ∈ V et la valeur retournée est « Vrai ».
c) Montrer que si le graphe possède un cycle de poids strictement négatif la valeur retournée est « Faux ».
d) Faire une analyse du coût de cet algorithme.
1 3 1 3 3 6 2 6 2
5 1 6 3 7 5 8 2 9
Exercice 14 Soit G = (V, E) un graphe non orienté connexe, et A un ensemble d’arêtes acyclique. Montrer
l’existence d’un arbre couvrant contenant toutes les arêtes de A.
Exercice 15 Soit G = (V, E) un graphe non orienté connexe, et 0w : E → R+ une pondération des arêtes. On
considère deux arbres couvrants minimaux distincts (V, A) et (V, A ), et on choisit une arête e de poids minimal
dans A 4 A0 = (A \ A0 ) ∪ (A0 \ A). On suppose par exemple e ∈ A \ A0 .
Montrer qu’il existe une arête e0 ∈ A0 \ A telle que w(e0 ) = w(e), et en déduire que si w est injective il existe un
unique arbre couvrant minimal.
Exercice 16 Soit G = (V, E) un graphe non orienté connexe muni d’une pondération injective w : E → R.
L’exercice précédent a montré l’unicité de l’arbre couvrant minimum.
a) Montrer qu’en revanche, le second arbre couvrant par ordre croissant de poids n’est pas nécessairement
unique (donner un exemple).
b) Soit (V, A) l’arbre couvrant minimum, et (V, B) un arbre couvrant second par ordre croissant de poids.
Montrer qu’il existe deux arêtes e ∈ A et e0 < A tel que B = A \ {e} ∪ {e0 }.
c) Soit (V, B) un arbre couvrant quelconque. Pour tout (u, v) ∈ V2 on note maxB (u, v) une arête de poids
maximal sur le chemin (unique) qui relie u et v dans (V, B).
Décrire un algorithme qui, à partir de B, calcule toutes valeurs maxB (u, v) pour un temps total en O(n2 ), avec
n = |V|.
d) En déduire un algorithme efficace permettant de calculer un arbre couvrant second par ordre croissant de
poids.
Exercice 17 On considère un ensemble de villes V = {v1 , v2 , . . . , vn } ainsi que les distances d(vi , vj ) séparant
ces villes. On rappelle qu’une distance vérifie l’inégalité triangulaire :
let swap v i j =
let temp = v.(i) in v.(i) <− v.(j) ; v.(j) <− temp ;;
Commençons par les deux fonctions qui permettent de remonter un élément dans le tas, ou au contraire de le
descendre.
jk k
Le père de l’élément d’indice k > 1 a pour indice , ce qui conduit à la fonction :
2
let rec remonte d t m = function
| 1 −> ()
| k when inferieur d.(t.(k)) d.(t.(k/2)) −> swap m t.(k) t.(k/2) ; swap t k (k/2) ;
remonte d t m (k/2)
| _ −> () ;;
À l’inverse, les deux fils (s’ils existent) de l’élément d’indice k ont pour indices 2k et 2k + 1. Sachant qu’on stocke
dans t.(0) l’indice du dernier élément du tas, ceci conduit à la définition :
let rec descend d t m = function
| k when 2*k > t.(0) −> ()
| k −> let j = if t.(0) = 2*k || inferieur d.(t.(2*k)) d.(t.(2*k+1))
then 2*k else 2*k+1 in
if inferieur d.(t.(j)) d.(t.(k)) then
(swap m t.(k) t.(j) ; swap t k j ; descend d t m j) ;;
let ajoute d t m k =
t.(0) <− t.(0) + 1 ;
t.(t.(0)) <− k ;
m.(k) <− t.(0) ;
remonte d t m t.(0) ;;
Enfin, pour extraire l’élément minimal d’un tas (qui se trouve nécessairement à la racine) on le permute avec le
dernier et on fait descendre celui-ci :
let extrait d t m =
let k = t.(1) in
swap m t.(1) t.(t.(0)) ;
swap t 1 t.(0) ;
t.(0) <− t.(0) − 1 ;
descend d t m 1 ;
k ;;
Reportez-vous à la partie consacrée aux tas binaires dans le premier chapitre de ce cours pour une meilleure
compréhension de ces fonctions.
http://info-llg.fr