Académique Documents
Professionnel Documents
Culture Documents
I. Introduction
Initiée par Euler, avec le célèbre problème des 7 ponts de Königsberg, les applications de la théorie des
graphes et de la recherche opérationnelle sont aujourd'hui immenses tant au plan civil que militaire :
aide à la décision, stratégie, optimisation (plus court chemin, GPS, coût minimal), réseaux de
transports: chemins de fer, métropolitain, lignes aériennes, électricité, gaz, oléoducs (transport de
l'énergie), Internet (réseau de l'information), ports et aéroports, ordonnancement des tâches, etc.
La théorie des graphes n'est pas une branche indépendante des mathématiques, elle se rattache à
la programmation linéaire, la programmation convexe (où le concept plus général de fonction
convexe remplace les fonctions linéaires et affines), la topologie, le calcul des probabilités.
II. Définitions :
Définition 2.1
Un graphe non orienté G est la donnée d'un couple G = (S, A) tel que :
Une paire {si , sj} est appelée une arête, et est représentée graphiquement par si-sj . On dit que les
sommets si et sj sont adjacents. L'ensemble des sommets adjacents au sommet si ∈ S est noté
Adj(si) = {sj ∈ S, {si , sj} ∈ A}.
Par exemple :
G = (S, A) avec S = {1, 2, 3, 4, 5, 6} et A = {{1, 2}, {1, 5}, {5, 2}, {3, 6}}.
1
Définition 2.2
• Une boucle est une arête reliant un sommet à lui-même.
• Un graphe non-orienté est dit simple s'il ne comporte pas de boucle, et s'il ne comporte
jamais plus d'une arête entre deux sommets.
• Un graphe non orienté qui n'est pas simple est un multi-graphe. Dans le cas d'un multi-
graphe, A n'est plus un ensemble mais un multi-ensemble d'arêtes.
Définition 2.3
On appelle ordre d'un graphe le nombre de ses sommets, i.e c'est card(S).
On appelle taille d'un graphe le nombre de ses arêtes, i.e c'est card(A).
2. Graphes orientés
Définition 2.4
Un graphe orienté G est la donnée d'un couple G = (S, A) tel que :
Par exemple :
G = (S, A) avec S = {1, 2, 3, 4, 5, 6} et A = {(1, 2),(2, 4),(2, 5),(4, 1),(4, 4),(4, 5),(5, 4),(6, 3)}.
Définition 1.5
• Une boucle est un arc reliant un sommet à lui-même.
• Un graphe orienté est dit élémentaire s'il ne contient pas de boucle.
• Un graphe orienté est un p-graphe s'il comporte au plus p arcs entre deux sommets. Le plus
souvent, on étudiera des 1-graphes.
2
3. Degré dans un graphe
Tout d'abord, on peut vouloir attribuer des valeurs aux arcs ou arêtes pour tenir compte de contraintes
comme : distance, coût . . .
Exemples :
3
5. Notions de chemin, chaîne, cycle et circuit
• Un chemin d'un sommet u vers un sommet v est une séquence < s0, s1, s2, . . . , sk > de sommets
tels que u = s0, v = sk et (si−1, si) ∈ A pour tout i ∈ {1, . . . , k}. On dira que le chemin contient les
sommets s0, s1, . . . , sk et les arcs (s0, s1),(s1, s2), . . . ,(sk−1, sk).
• La longueur du chemin est le nombre d'arcs dans le chemin, c'est-à-dire k.
• S'il existe un chemin de u à v, on dira que v est accessible à partir de u.
• Un chemin est élémentaire si les sommets qu'il contient sont tous distincts.
• Un chemin < s0, s1, s2, . . . , sk > forme un circuit si s0 = sk et si le chemin comporte au moins un
arc (k ≥ 1). Ce circuit est élémentaire si, en plus, les sommets s1, s2, . . . , sk sont tous distincts.
• Une boucle est un circuit de longueur 1.
Lemme (de König) Dans un graphe, s'il existe un chemin/chaîne d'un sommet u vers un sommet v,
alors il existe un chemin élémentaire de u vers v. La notion de longueur de chemin nous permet ensuite
de définir la notion de distance dans un graphe.
Définition 1.11
Soit un graphe G = (S, A). On appelle :
• Distance d'un sommet à un autre la longueur du plus court chemin/chaîne entre ces deux
sommets, ou ∞ s'il n'y a pas un tel chemin/chaîne :
𝑘 si le plus court chemin de 𝑥 vers 𝑦 est de longueur 𝑘
∀𝑥, 𝑦 ∈ 𝑆, 𝑑(𝑥, 𝑦) = {
∞ sinon
• Diamètre du graphe la plus grande distance entre deux sommets.
4
Définition 2.11 (Chaîne Hamiltonien)
Une chaîne Hamiltonienne : est une chaîne qui passe une et une seule fois par chacun des sommets
d’un graphe non orienté.
Exemple : dans le graphe ci-dessus ABCD, ABDC, ACBD, ACDB sont des chaîne Hamiltoniennes.
Une Cycle hamiltonien : c’est une chaîne passant une seule fois par tous les sommets d’un graphe et
revenant au sommet de départ.
Remarque :
Un Cycle eulérien : est une chaîne passant une seule fois par toutes les arêtes d’un graphe et revenant
au sommet de départ.
5
Exemple : ABECDBCA est une chaîne eulérienne.
Un graphe est eulérien si tous les sommets du graphe ont un degré pair
Soit le graphe G = (S, A) d'ordre n. On suppose que les sommets de S sont numérotés de 1 à n. La
représentation par listes d'adjacence de G consiste en un dictionnaire D de n listes, une pour chaque
sommet de S.
Exemple :
D={
1 : [2, 5],
2 : [1, 5],
3 : [5],
4 : [ ],
5 : [1, 2, 3]
}
G={
'A' : ['B', 'C'],
'B' : ['A', 'C'],
'C' : ['A', 'B', 'D'],
'D' : ['C', 'E'],
'E' : ['D']
}
6
G={
'A' : [(3,'B'), (9,'C')],
'B' : [(3,'A'), (4,'C')],
'C' : [(9,'A'), (4,'B’),(12,'D')],
'D' : [(7,’E'), (12,'C')],
'E' : [(7,'D')]
}
Soit le graphe G = (S, A) d'ordre n. On suppose que les sommets de S sont numérotés de 1 à n. La
représentation par matrice d'adjacence de G consiste en une matrice binaire M de taille n × n telle que
M[i][j] = 1 si (i, j) ∈ A, et M[i][j] = 0 sinon.
Dans le cas de graphes non orientés, la matrice est symétrique par rapport à sa diagonale descendante.
Dans ce cas, on peut ne mémoriser que la composante triangulaire supérieure de la matrice
d'adjacence.
Exemple :
0 1 0 0 1 0 0 0 0 0
1 0 0 0 1 1 0 0 0 0
0 0 0 0 1 𝑂𝑢 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
(1 1 1 0 0) (1 1 1 0 0)
0 3 9 0 1
4 0 3 0 0
9 4 0 12 1
0 0 12 0 7
(0 0 0 7 0)
Ou
∞ 3 9 ∞ 1
4 ∞ 3 ∞ ∞
9 4 ∞ 12 1
∞ ∞ 12 ∞ 7
(∞ ∞ ∞ 7 ∞)
7
On utilise ∞ s’il peut y avoir des arêtes avec un coût nul.
Opérations sur les matrices d'adjacence : le test de l'existence d'un arc ou d'une arête avec une
représentation par matrice d'adjacence est immédiat (il suffit de tester directement la case
correspondante de la matrice). En revanche, connaître le degré d'un sommet nécessite le parcours de
toute une ligne (ou toute une colonne) de la matrice. D'une façon plus générale, le parcours de
l'ensemble des arcs/arêtes nécessite la consultation de la totalité de la matrice, et prendra un temps
de l'ordre de n2. Si le nombre d'arcs est très inférieur à n2, cette représentation est donc loin d'être
optimale.
Beaucoup de problèmes sur les graphes nécessitent que l'on parcoure l'ensemble des sommets et des
arcs/arêtes du graphe. Nous allons étudier les deux principales stratégies d'exploration :
• Le parcours en largeur consiste à explorer les sommets du graphe niveau par niveau, à partir
d'un sommet donné ;
• Le parcours en profondeur consiste, à partir d'un sommet donné, à suivre un chemin le plus
loin possible, puis à faire des retours en arrière pour reprendre tous les chemins ignorés
précédemment.
Un parcours en largeur débute à partir d'un nœud source. Puis il liste tous les voisins de la source,
pour ensuite les explorer un par un. Ce mode de fonctionnement utilise donc une file dans laquelle il
prend le premier sommet et place en dernier ses voisins non encore explorés. Les nœuds déjà visités
sont marqués afin d'éviter qu'un même nœud soit exploré plusieurs fois. Dans le cas particulier
d'un arbre, le marquage n'est pas nécessaire. Voici les étapes de l'algorithme :
1. mettre le nœud source dans la file ;
2. retirer le nœud du début de la file pour le traiter ;
3. mettre tous ses voisins non explorés dans la file (à la fin) ;
4. si la file n'est pas vide reprendre à l'étape 2.
8
def parcours_largeur(G, d):
visited = []
file_to_visite = [d]
while file_to_visite!=[]:
s = file_to_visite.pop(0)
if s in visited :
continue
visited.append(s)
voisins = G[s]
for dv, v in voisins:
if v not in visited :
file_to_visite.append(v)
return visited
Le parcours d’un graphe en profondeur se réalise en partant d’un sommet arbitraire v à visiter, et en
parcourant d’abord un de ses voisins u et tous ses “descendants” (c’est-à-dire tous les sommets
accessibles à partir de u) avant de traiter le voisin suivant de v. Une fois ces descendants explorés, on
applique le même traitement au prochain voisin de v non encore visité, et ainsi de suite jusqu’à ce que
tous les sommets aient été couverts. On manipulera tout au long de notre parcours une liste résultat,
enregistrant les identifiants des sommets au fur et à mesure qu’on les découvre.
# Pour tester :
L=[]
parcours_profondeur(GRAPHE, 'A', L)
print(L)
9
V. Chemin le plus court entre deux sommets
1. Algorithme de Dijkstra
En théorie des graphes, l'algorithme de Dijkstra sert à résoudre le problème du plus court chemin. Il
permet, par exemple, de déterminer le plus court chemin pour se rendre d'une ville à une autre
connaissant le réseau routier d'une région. Il s'applique à un graphe connexe dont le poids lié aux
arêtes est positif.
L'algorithme porte le nom de son inventeur, l'informaticien néerlandais Edsger Dijkstra et a été publié
en 1959.
Il s'agit de construire progressivement, à partir des données initiales, un sous-graphe dans lequel sont
classés les différents sommets par ordre croissant de leur distance minimale au sommet de départ. La
distance correspond à la somme des poids des arêtes empruntées.
La première étape consiste à mettre de côté le sommet de départ et à repérer la distance du sommet
de départ aux autres sommets du graphe. Cette distance est infinie si aucun arc ne relie le sommet au
sommet de départ, elle est de n s'il existe un arc reliant ce sommet au sommet de départ et que le
poids le plus faible (s'il existe plusieurs arcs) est de n.
La seconde étape consiste à repérer le sommet qui possède alors la plus courte distance au sommet
de départ et à le mettre de côté. Pour tous les sommets restants, on compare alors la distance trouvée
précédemment à celle que l'on obtiendrait via le sommet que l'on vient de mettre de côté et on ne
conserve que la plus petite des valeurs. Et on continue ainsi jusqu'à épuisement des sommets ou
jusqu'à sélection du sommet d'arrivée.
La fonction voisins renvoie la liste des voisins d’un graphe G (Dictionnaire ou matrice) :
noeud_visites = []
file_priorite = PriorityQueue()
10
file_priorite.put((0, depart))
distance_min = {}
distance_min[depart] = 0
precedents = {}
# Calculer le chemin
chemin = []
x = arrivee
while x!=depart :
chemin.insert(0, x)
x = precedents[x]
chemin.insert(0, x)
NB : L'algorithme de Dijkstra ne marche pas toujours quand le graphe contient des arcs dont les coûts
sont négatifs. On peut utiliser l’algorithme de Bellman-Ford.
2. L’algorithme A*
L’algorithme A* est un algorithme heuristique qui permet de trouver très rapidement un plus court
chemin entre deux points avec d’éventuels obstacles. Outre sa vitesse, cet algorithme est réputé
pour garantir une solution en sortie.
NB : Un algorithme heuristique est un procédé qui permet d’obtenir une solution réalisable très
rapidement à un problème d’optimisation complexe. La solution fournie en sortie de l’algorithme n’est
cependant pas garantie d’être optimale.
Comment fonctionne-t-il ?
11
La situation est représentée par les schémas ci-dessous :
Les cases vertes représentent le point de départ et l’arrivée. Les cases foncées représentent les
obstacles, et les bleues celles où il est possible de se déplacer. Sur le schéma numéro 2, les cases violet
clair représentent un chemin théorique éligible pour modéliser l’heuristique, aussi représenté avec la
flèche verte.
L’enjeu ici est de trouver le plus court chemin menant à B, l’individu ne pouvant pas emprunter les
cases noires.
Un algorithme heuristique traditionnel évaluerait tous les chemins possibles et comparerait leur
distance, tandis que l’algorithme A* nous renvoie directement le premier chemin qu’il a déterminé.
Le caractère optimal de ce chemin dépendra du paramètre d’heuristique choisi. Dans notre cas,
l’heuristique sert à fournir une estimation de la distance restante à chaque étape de l’algorithme.
Cette estimation dépend de la métrique choisie dans notre problème d’optimisation, et n’a pas à
rendre une distance exacte.
Dans notre cas, on pourrait choisir comme heuristique la distance de Manhattan séparant A et B, ou
la distance en ligne droite entre ces deux points, calculée à l’aide du théorème de Pythagore.
L’algorithme A* essaye par conséquent de minimiser la somme F = G+H, où G est la distance parcourue
depuis le point de départ et H l’estimation de la distance restante à parcourir jusqu’à l’arrivée, en
fonction de l’heuristique définie au départ de l’algorithme. Ainsi, en entrée, l’algorithme prend :
12
Celle des grandeurs G, représentant la distance réelle parcourue depuis le point A
Ainsi, l’algorithme choisit à chaque étape la case lui permettant de se rapprocher de l’arrivée, et stocke
dans une autre liste d’autres options moins optimales aux premiers abords.
Cette liste secondaire permet de choisir une autre option au cas où l’algorithme rencontre un obstacle,
ce qui garantit la justesse de l’algorithme.
Le choix de l’heuristique d’estimation est crucial pour l’optimalité du chemin renvoyé. Si l’on surestime
le chemin restant à parcourir à chaque étape, l’algorithme risque de ne pas renvoyer le chemin le plus
court. En revanche, si l’on sous-estime l’heuristique, la complexité de l’algorithme augmentera, ce qui
lui fera perdre son atout principal qu’est sa vitesse (l’heuristique nulle représente l’algorithme de
Dijsktra). Si l’estimation est juste, l’algorithme n’explorera pas toutes les options possibles et renverra
une solution optimale. On dit dans ce cas que l’heuristique fournie est admissible.
En théorie des graphes, la coloration de graphe consiste à attribuer une couleur à chacun de ses
sommets de manière que deux sommets reliés par une arête soient de couleur différente. On cherche
souvent à utiliser le nombre minimal de couleurs, appelé nombre chromatique.
Des étudiants A, B, C, D, E et F doivent passer des examens dans différentes disciplines, chaque examen
occupant une demi-journée :
– Algorithmique : étudiants A et B.
– Compilation : étudiants C et D.
– Java : étudiants A, E, F et H.
13
– Architecture : étudiants B, F, G et H.
La solution consiste à modéliser le problème par un graphe où les sommets représente les matières et
on ajoute une arête entre les matières qui ont au moins un étudiant commun.
Une première approche pour colorer le graphe est de prendre ses sommets les uns après les autres
afin de leur affecter une couleur, tout en veillant à ce que deux sommets adjacents n’aient jamais la
même couleur : c’est l’algorithme de coloration séquentielle. Nous obtenons ainsi une coloration
propre du graphe, pas forcément optimale, et qui dépend fortement de l’ordre de visite des sommets.
def coloration_sequentielle(G):
keys = list(G.keys())
n = len(G)
couleurs = {k:-1 for k in G} # Couleurs
for x in G:
# Recherche de la plus petite couleur non utilisée#
Libre = [True]*n
voisins = G[x]
for y in voisins:
iy = keys.index(y)
if couleurs[y]!=-1:
Libre[iy] = False
index = 0
while Libre[index]==False :
index += 1
# Affecter la couleur donnée par index à x #
couleurs[x] = index
# On retourne les les couleurs des sommets et le nombre chromatique
return couleurs , max(couleurs.values())+1
14
2. Deux heuristiques différentes
Il s’agit maintenant d’utiliser l’algorithme de coloration séquentiel avec un ordre judicieux, en vue
d’obtenir une coloration propre la plus "acceptable" possible. L’algorithme de Welsh & Powell consiste
ainsi à colorer séquentiellement le graphe en visitant les sommets par ordre de degré décroissant.
L’idée est que les sommets ayant beaucoup de voisins seront plus difficiles à colorer, et donc il faut les
colorer en premier. La complexité de l’algorithme devient O(n ln(n) + m) en utilisant un tri par
comparaison, mais reste O(n + m) avec un tri par dénombrement.
L’algorithme :
1. On note L la liste des sommets si classes suivant l’ordre décroissant de leur degré :
d(s1) d(s2) d(s3) … d(sn).
2. Initialisation :
3. L : liste des sommets dans l’ordre décroissant du degré
4. couleur = 0
5. Tant que L ≠ ∅ ; faire
6. couleur=couleur+1
7. couleur(s) = couleur
8. V = voisins(s)
9. Pour tout t dans L faire
10. Si t ∉ V
11. couleur(t)=couleur
12. V=V∪t
13. Fin si
14. Fin faire
15. Retirer les sommets colorés de L
16. Fin faire
Cependant on peut parfois aboutir aux pires colorations possibles (n/2 au lieu de 2). L’heuristique
DSATUR propose une amélioration du principe de l’algorithme de Welsh & Powell afin d’éviter ce
problème.
b) DSATUR
Ici, l’idée est que les sommets difficiles à colorer sont ceux qui ont le plus de voisins de couleurs
différentes, puis ceux qui ont le plus de voisins tout courts. Concrètement, on définit le degré de
saturation d’un sommet a, noté DSAT (a) comme suit :
15
Après initialisation de DSAT (a) à d (a) pour tout a, on répète alors les opérations suivantes :
La complexité temporelle de l’algorithme dépend de l’implémentation effectuée. Elle peut être O(n2)
ou O(n2 + nm) selon la structure utilisée.
16