Pr. EL HADIQ Zouhair
Un graphe modĂ©lise un ensemble d'objets (les sommets) reliĂ©s par des relations (les arĂȘtes ou arcs) : rĂ©seaux routiers, rĂ©seaux sociaux, pages web, circuits⊠Ce chapitre dĂ©finit le vocabulaire, montre comment reprĂ©senter un graphe en Python, puis prĂ©sente les algorithmes de parcours et de plus court chemin.
Un graphe est une structure de donnĂ©es qui modĂ©lise des objets et les liens entre eux. Formellement, un graphe G est un couple G = (S, A) oĂč :
De nombreuses situations se modélisent par un graphe :
| Situation | Sommets | Liens |
|---|---|---|
| Réseau routier | Villes / carrefours | Routes |
| Réseau social | Personnes | Relations d'amitié |
| Web | Pages | Liens hypertextes (orientés) |
| Labyrinthe | Cases | Passages entre cases |
On distingue deux grandes familles selon que les liens ont un sens ou non.
Les liens sont des arĂȘtes : une arĂȘte {u, v} relie u et v sans privilĂ©gier de sens. Si u est reliĂ© Ă v, alors v est reliĂ© Ă u (relation symĂ©trique). On dit que u et v sont adjacents (ou voisins) et que l'arĂȘte est incidente Ă u et Ă v.
Les liens sont des arcs : un arc (u, v) va de u vers v. Le sens compte : l'arc (u, v) n'implique pas l'arc (v, u). On dit que u est le sommet de départ (origine) et v le sommet d'arrivée. v est un successeur de u, et u un prédécesseur de v.
| Notion | Non orienté | Orienté |
|---|---|---|
| Lien | arĂȘte {u, v} | arc (u, v) |
| Sens | aucun (symétrique) | de u vers v |
| Suite de sommets | chaĂźne | chemin |
| Boucle fermée | cycle | circuit |
Ordre du graphe. L'ordre d'un graphe est son nombre de sommets, notĂ© n = |S|. On note souvent m = |A| le nombre d'arĂȘtes (ou d'arcs). Pour un graphe simple non orientĂ©, 0 ≤ m ≤ n(n−1)/2 ; pour un graphe simple orientĂ©, 0 ≤ m ≤ n(n−1).
Le degré mesure le nombre de liens attachés à un sommet.
Une chaĂźne (graphe non orientĂ©) ou un chemin (graphe orientĂ©) est une suite de sommets s0, s1, âŠ, sk telle que deux sommets consĂ©cutifs sont reliĂ©s (par une arĂȘte, resp. un arc dans le bon sens). Sa longueur est le nombre de liens parcourus, soit k.
On dit qu'un sommet v est accessible depuis u s'il existe un chemin de u vers v. La distance entre u et v est la longueur du plus court chemin les reliant (â s'il n'en existe aucun).
Un graphe non orientĂ© est connexe s'il existe une chaĂźne entre toute paire de sommets : « on peut aller de n'importe oĂč Ă n'importe oĂč ». Sinon, le graphe se dĂ©compose en composantes connexes, chacune Ă©tant un sous-ensemble maximal de sommets deux Ă deux reliĂ©s.
NumĂ©rotons les sommets de 0 Ă n−1. La matrice d'adjacence M est une matrice \(n \times n\) telle que :
M[i][j] = 1 s'il existe un arc/arĂȘte de i vers j, M[i][j] = 0 sinon.
Exemple â graphe non orientĂ© Ă 4 sommets avec les arĂȘtes {0,1}, {0,2}, {1,2}, {2,3} :
0 1 2 3 0 [ 0 1 1 0 ] 1 [ 1 0 1 0 ] 2 [ 1 1 0 1 ] 3 [ 0 0 1 0 ]
# Matrice d'adjacence en Python : liste de listes
M = [
[0, 1, 1, 0],
[1, 0, 1, 0],
[1, 1, 0, 1],
[0, 0, 1, 0],
]
# Existe-t-il une arĂȘte entre 0 et 2 ?
print(M[0][2] == 1) # True
| Opération | Coût (matrice) |
|---|---|
| Test d'existence d'un arc | \(O(1)\) |
| Liste des voisins d'un sommet | \(O(n)\) |
| Mémoire utilisée | \(O(n^{2})\) |
La matrice est idĂ©ale pour un graphe dense (beaucoup d'arĂȘtes) ou quand on teste souvent l'existence d'un lien, mais elle gaspille de la mĂ©moire pour un graphe creux.
Pour chaque sommet, on stocke la liste de ses voisins. On regroupe ces listes dans une liste (sommets numérotés) ou dans un dictionnaire (sommets quelconques).
# Listes d'adjacence dans une LISTE (sommets 0..n-1)
G = [
[1, 2], # voisins de 0
[0, 2], # voisins de 1
[0, 1, 3], # voisins de 2
[2], # voisins de 3
]
# Listes d'adjacence dans un DICTIONNAIRE
G = {
'A': ['B', 'C'],
'B': ['A', 'C'],
'C': ['A', 'B', 'D'],
'D': ['C'],
}
print(G['C']) # ['A', 'B', 'D'] : voisins de C
| Opération | Coût (listes d'adj.) |
|---|---|
| Test d'existence d'un arc | \(O(deg(u))\) |
| Liste des voisins d'un sommet | \(O(deg(u))\) |
| Mémoire utilisée | \(O(n + m)\) |
Voici les opérations de base sur une représentation par dictionnaire de listes (graphe non orienté).
def ajouter_sommet(G, u):
if u not in G:
G[u] = []
def ajouter_arete(G, u, v):
ajouter_sommet(G, u)
ajouter_sommet(G, v)
if v not in G[u]:
G[u].append(v)
G[v].append(u) # non orienté : on ajoute dans les deux sens
def supprimer_arete(G, u, v):
if v in G[u]:
G[u].remove(v)
G[v].remove(u)
def existe_arete(G, u, v):
return u in G and v in G[u]
def voisins(G, u):
return G[u]
Construire la liste des voisins d'un sommet Ă partir de la matrice d'adjacence est un grand classique :
def voisins_depuis_matrice(M, u):
return [j for j in range(len(M)) if M[u][j] == 1]
M = [[0,1,1,0],[1,0,1,0],[1,1,0,1],[0,0,1,0]]
print(voisins_depuis_matrice(M, 2)) # [0, 1, 3]
Le parcours en largeur (Breadth-First Search) explore le graphe « par cercles concentriques » : d'abord le sommet de départ, puis tous ses voisins (distance 1), puis les voisins des voisins (distance 2), etc. Il utilise une file (FIFO : premier entré, premier sorti).
list Python est lente en tĂȘte (pop(0) est en \(O(n)\)). On utilise deque du module collections : popleft() et append() sont en \(O(1)\).
from collections import deque
def bfs(G, depart):
vus = {depart}
file = deque([depart])
ordre = []
while file:
u = file.popleft() # dĂ©file en tĂȘte (FIFO)
ordre.append(u)
for v in G[u]:
if v not in vus:
vus.add(v)
file.append(v) # enfile en queue
return ordre
G = {'A':['B','C'], 'B':['A','D'], 'C':['A','D'], 'D':['B','C']}
print(bfs(G, 'A')) # ['A', 'B', 'C', 'D']
Le BFS visite chaque sommet une fois et examine chaque arĂȘte une fois : sa complexitĂ© est \(O(n + m)\). PropriĂ©tĂ© fondamentale : dans un graphe non pondĂ©rĂ©, le BFS calcule le plus court chemin (en nombre d'arĂȘtes) depuis le sommet de dĂ©part.
Le parcours en profondeur (Depth-First Search) explore « le plus loin possible » avant de revenir en arriÚre (backtracking). Il s'écrit naturellement de façon récursive, ou de façon itérative avec une pile (LIFO).
# Version récursive
def dfs(G, u, vus=None):
if vus is None:
vus = set()
vus.add(u)
ordre = [u]
for v in G[u]:
if v not in vus:
ordre += dfs(G, v, vus)
return ordre
# Version itérative (pile)
def dfs_iter(G, depart):
vus = set()
pile = [depart]
ordre = []
while pile:
u = pile.pop() # dépile au sommet (LIFO)
if u not in vus:
vus.add(u)
ordre.append(u)
for v in G[u]:
if v not in vus:
pile.append(v)
return ordre
Comme le BFS, le DFS est en \(O(n + m)\). La différence est l'ordre de visite : le BFS privilégie la largeur (file), le DFS la profondeur (pile/récursion).
def est_connexe(G):
if not G:
return True
depart = next(iter(G)) # un sommet quelconque
atteints = set(bfs(G, depart))
return len(atteints) == len(G) # a-t-on atteint TOUS les sommets ?
Lors d'un DFS, si on rencontre un voisin déjà visité qui n'est pas le pÚre dont on vient, c'est qu'il existe un cycle.
def a_un_cycle(G):
vus = set()
def explore(u, pere):
vus.add(u)
for v in G[u]:
if v not in vus:
if explore(v, u):
return True
elif v != pere: # voisin déjà vu, différent du pÚre => cycle
return True
return False
for s in G:
if s not in vus:
if explore(s, None):
return True
return False
Un graphe est pondĂ©rĂ© quand chaque arc/arĂȘte porte une Ă©tiquette numĂ©rique (un poids ou coĂ»t) : distance, temps, prix⊠Le problĂšme du plus court chemin consiste Ă minimiser la somme des poids le long du chemin.
En Python, on stocke le poids à cÎté du voisin :
# dictionnaire de listes de couples (voisin, poids)
G = {
'A': [('B', 4), ('C', 1)],
'B': [('D', 1)],
'C': [('B', 2), ('D', 5)],
'D': [],
}
L'algorithme de Dijkstra calcule les plus courtes distances depuis une source, à condition que tous les poids soient positifs. Idée : à chaque étape, on « fige » le sommet non encore traité de plus petite distance estimée, puis on relùche ses arcs (on met à jour la distance de ses voisins). Une file de priorité (tas, module heapq) sélectionne efficacement ce minimum.
import heapq
def dijkstra(G, source):
dist = {u: float('inf') for u in G}
dist[source] = 0
tas = [(0, source)] # (distance, sommet)
while tas:
d, u = heapq.heappop(tas) # sommet de plus petite distance
if d > dist[u]:
continue # entrée périmée
for v, poids in G[u]:
if dist[u] + poids < dist[v]:
dist[v] = dist[u] + poids # relĂąchement
heapq.heappush(tas, (dist[v], v))
return dist
print(dijkstra(G, 'A')) # {'A': 0, 'B': 3, 'C': 1, 'D': 4}
Avec un tas, Dijkstra s'exĂ©cute en \(O((n + m) \log n)\). Note : le plus court chemin AâB passe par C (1 + 2 = 3) plutĂŽt que par l'arc direct AâB (de poids 4).
Algorithme A*. C'est une variante heuristique de Dijkstra. à la distance déjà parcourue g(v) on ajoute une estimation h(v) du reste à parcourir jusqu'au but (par exemple la distance à vol d'oiseau). On explore en priorité les sommets de plus petit f(v) = g(v) + h(v). Si l'heuristique ne surestime jamais la distance réelle (heuristique admissible), A* trouve le plus court chemin tout en explorant moins de sommets que Dijkstra. TrÚs utilisé en jeux vidéo et GPS.
Floyd-Warshall. Calcule les plus courtes distances entre toutes les paires de sommets, par programmation dynamique. On amĂ©liore progressivement la distance d[i][j] en autorisant comme sommets intermĂ©diaires 0, 1, âŠ, k :
def floyd_warshall(d): # d : matrice n x n des poids, inf si pas d'arc
n = len(d)
for k in range(n):
for i in range(n):
for j in range(n):
if d[i][k] + d[k][j] < d[i][j]:
d[i][j] = d[i][k] + d[k][j]
return d
Sa complexitĂ© est \(O(n^{3})\). La coloration de graphe (attribuer une couleur Ă chaque sommet pour que deux voisins n'aient jamais la mĂȘme) est un autre classique d'application.
deque) et DFS (pile/rĂ©cursion) parcourent un graphe en \(O(n + m)\) ; applications : connexitĂ©, dĂ©tection de cycle, plus court chemin non pondĂ©rĂ© (BFS).CPGE MP / PSI · Informatique â Introduction Ă la thĂ©orie des graphes