Théorie des graphes¶
Les graphes sont partout — réseaux, dépendances, workflows, routage — et les algorithmes qui les traversent sont le socle invisible de l'infrastructure moderne.
La structure universelle¶
Un réseau de microservices est un graphe. Un arbre de dépendances npm est un graphe. Le routage BGP entre AS est un graphe. Les états d'une machine a états sont un graphe. Un schéma de base de données avec des clés etrangeres est un graphe.
La théorie des graphes fournit un vocabulaire commun et des algorithmes prouvablement corrects pour analyser toutes ces structures. Savoir qu'OSPF utilisé Dijkstra ou que le tri topologique détecté les cycles n'est pas de la culture générale — c'est savoir pourquoi ces outils ont les propriétés qu'ils ont et ou ils échouent.
Définitions fondamentales¶
Graphe non orienté¶
Un graphe G = (V, E) est compose de :
- V (vertices / sommets) : ensemble des nœuds
- E (edges / aretes) : ensemble de paires {u, v} de sommets
Le degré d'un sommet est le nombre d'aretes qui lui sont incidentes.
Graphe orienté (digraphe)¶
Les aretes sont des paires ordonnées (u, v) — on distingue l'origine et la destination. Le degré entrant (in-degree) est le nombre d'aretes pointant vers un sommet. Le degré sortant (out-degree) est le nombre d'aretes partant d'un sommet.
Graphe pondere¶
Chaque arete porte un poids w(u, v) ∈ ℝ — une distance, une latence, un coût, une capacité.
Connexite¶
Un graphe non orienté est connexe si tout sommet est accessible depuis tout autre sommet. Une composante connexe est un sous-graphe connexe maximal.
Un graphe orienté est fortement connexe si tout sommet est accessible depuis tout autre via un chemin orienté. Un graphe est faiblement connexe si le graphe non orienté sous-jacent est connexe.
Représentations en mémoire¶
| Structure | Avantages | Inconvénients | Utilisation |
|---|---|---|---|
| Matrice d'adjacence | Test d'arete en O(1) | O(V²) en espace | Graphes denses |
| Liste d'adjacence | O(V+E) en espace | Test d'arete en O(degré) | Graphes creux (la majorité) |
| Liste d'aretes | Simple, serializabe | Pas de lookup par sommet | Import/export, algorithmique pure |
from collections import defaultdict
# Liste d'adjacence : la representation standard
class Graphe:
def __init__(self, oriente=False):
self.adj = defaultdict(list)
self.oriente = oriente
def ajouter_arete(self, u, v, poids=1):
self.adj[u].append((v, poids))
if not self.oriente:
self.adj[v].append((u, poids))
def voisins(self, u):
return self.adj[u]
def sommets(self):
return set(self.adj.keys())
Parcours : BFS et DFS¶
BFS — Breadth-First Search (parcours en largeur)¶
Le BFS explore le graphe niveau par niveau, en utilisant une file (FIFO). Il trouve le plus court chemin en nombre d'aretes dans un graphe non pondere.
from collections import deque
def bfs(graphe, source):
"""Retourne les distances depuis source (en nombre d'aretes)."""
distances = {source: 0}
file = deque([source])
while file:
u = file.popleft()
for v, _ in graphe.voisins(u):
if v not in distances:
distances[v] = distances[u] + 1
file.append(v)
return distances
# Complexite : O(V + E)
Propriétés du BFS :
- Garantit le chemin le plus court (en aretes non ponderees) vers chaque sommet
- Peut tester la connexite : si
len(distances) < |V|, le graphe n'est pas connexe - Base de l'algorithme de bipartition (un graphe est biparti si et seulement si il n'a pas de cycle impair — détecté par BFS)
DFS — Depth-First Search (parcours en profondeur)¶
Le DFS explore aussi loin que possible avant de revenir en arriere, en utilisant une pile (explicite ou la pile d'appels recursifs).
def dfs(graphe, source, visite=None):
"""DFS recursif — retourne l'ensemble des sommets visites."""
if visite is None:
visite = set()
visite.add(source)
for voisin, _ in graphe.voisins(source):
if voisin not in visite:
dfs(graphe, voisin, visite)
return visite
# Complexite : O(V + E)
Détection de cycles avec DFS¶
Le DFS colorie les sommets en trois états pour détecter les cycles dans un graphe orienté :
- Blanc : non visite
- Gris : en cours de visite (dans la pile d'appels)
- Noir : traitement termine
Un cycle existe si et seulement si on rencontre un sommet gris pendant le DFS.
def contient_cycle(graphe):
"""Detecte un cycle dans un graphe oriente via DFS."""
BLANC, GRIS, NOIR = 0, 1, 2
couleur = {s: BLANC for s in graphe.sommets()}
def dfs_cycle(u):
couleur[u] = GRIS
for v, _ in graphe.voisins(u):
if couleur[v] == GRIS:
return True # Arc arriere -> cycle
if couleur[v] == BLANC and dfs_cycle(v):
return True
couleur[u] = NOIR
return False
return any(
dfs_cycle(u)
for u in graphe.sommets()
if couleur[u] == BLANC
)
Tri topologique¶
Le tri topologique ordonné les sommets d'un DAG (Directed Acyclic Graph) de sorte que pour chaque arete (u, v), u apparaisse avant v. Il n'existe que si le graphe n'a pas de cycle.
Algorithme de Kahn (base sur les degrés entrants) :
from collections import deque
def tri_topologique(graphe):
"""Algorithme de Kahn. Retourne None si un cycle est detecte."""
# Calculer les degres entrants
degre_entrant = defaultdict(int)
for u in graphe.sommets():
for v, _ in graphe.voisins(u):
degre_entrant[v] += 1
# File des sommets sans predecesseur
file = deque(
s for s in graphe.sommets()
if degre_entrant[s] == 0
)
ordre = []
while file:
u = file.popleft()
ordre.append(u)
for v, _ in graphe.voisins(u):
degre_entrant[v] -= 1
if degre_entrant[v] == 0:
file.append(v)
if len(ordre) != len(list(graphe.sommets())):
return None # Cycle detecte
return ordre
Applications directes du tri topologique :
- Résolution de dépendances (npm install, pip, Maven)
- Ordonnancement de tâches (Makefile, DAGs Airflow)
- Compilation (ordre de compilation des modules)
- Migrations de base de données
Plus courts chemins¶
Dijkstra¶
L'algorithme de Dijkstra trouve le plus court chemin depuis un sommet source vers tous les autres sommets dans un graphe a poids positifs.
Principe : à chaque étape, extraire le sommet non encore traite avec la distance minimale connue, puis "relaxer" ses voisins — mettre à jour leur distance si le passage par ce sommet est plus court.
Table de progression (exemple sur un graphe de 5 nœuds) :
| Étape | Sommet traite | A | B | C | D | E |
|---|---|---|---|---|---|---|
| Init | — | 0 | ∞ | ∞ | ∞ | ∞ |
| 1 | A | 0 | 4 | 2 | ∞ | ∞ |
| 2 | C | 0 | 3 | 2 | 7 | ∞ |
| 3 | B | 0 | 3 | 2 | 6 | ∞ |
| 4 | D | 0 | 3 | 2 | 6 | 8 |
| 5 | E | 0 | 3 | 2 | 6 | 8 |
import heapq
def dijkstra(graphe, source):
"""Retourne les distances et predecesseurs depuis source."""
distances = {source: 0}
predecesseur = {}
# Tas min : (distance, sommet)
tas = [(0, source)]
while tas:
dist_u, u = heapq.heappop(tas)
# Si on a deja traite ce sommet avec une meilleure distance
if dist_u > distances.get(u, float('inf')):
continue
for v, poids in graphe.voisins(u):
nouvelle_dist = distances[u] + poids
if nouvelle_dist < distances.get(v, float('inf')):
distances[v] = nouvelle_dist
predecesseur[v] = u
heapq.heappush(tas, (nouvelle_dist, v))
return distances, predecesseur
def reconstituer_chemin(predecesseur, source, cible):
chemin = []
noeud = cible
while noeud != source:
chemin.append(noeud)
noeud = predecesseur[noeud]
chemin.append(source)
return list(reversed(chemin))
# Complexite : O((V + E) log V) avec un tas binaire
Warning
Dijkstra ne fonctionne pas avec des poids negatifs. Un poids negatif signifie qu'un chemin peut être "amélioré" en ajoutant une arete — ce qui viole l'hypothèse que les distances ne font que croitre lors de l'exploration. Utiliser Bellman-Ford si des poids negatifs sont possibles.
Bellman-Ford¶
Bellman-Ford géré les poids negatifs et détecté les cycles de poids negatif (qui rendent le problème du plus court chemin sans solution).
Principe : relacher toutes les aretes V-1 fois (un chemin sans cycle a au plus V-1 aretes). Après V-1 iterations, toute distance qui peut encore être améliorée appartient a un cycle negatif.
def bellman_ford(graphe, source):
"""Retourne None si un cycle de poids negatif est detecte."""
sommets = list(graphe.sommets())
aretes = [
(u, v, poids)
for u in sommets
for v, poids in graphe.voisins(u)
]
distances = {s: float('inf') for s in sommets}
distances[source] = 0
# V - 1 relaxations
for _ in range(len(sommets) - 1):
for u, v, poids in aretes:
if distances[u] + poids < distances[v]:
distances[v] = distances[u] + poids
# Detection de cycle negatif
for u, v, poids in aretes:
if distances[u] + poids < distances[v]:
return None # Cycle negatif detecte
return distances
# Complexite : O(V × E) — plus lent que Dijkstra
| Algorithme | Poids | Complexité | Usage |
|---|---|---|---|
| BFS | Non pondere | O(V+E) | Graphes sans poids |
| Dijkstra | Positifs | O((V+E) log V) | OSPF, GPS, routage |
| Bellman-Ford | Quelconques | O(V×E) | BGP, détection arbitrage |
| Floyd-Warshall | Quelconques | O(V³) | Tous les couples (graphes petits) |
Flot maximal¶
Formulation du problème¶
Dans un graphe orienté pondere, chaque arete a une capacité — le flux maximal qu'elle peut transporter. On cherche le flux maximal de la source s vers le puits t.
Applications : débit maximal d'un réseau, affectation de ressources, correspondance bipartie (matching), découpé d'images en vision par ordinateur.
Ford-Fulkerson et le théorème min-cut/max-flow¶
Algorithme de Ford-Fulkerson : trouver un chemin augmentant (source vers puits avec capacité residuelle positive), augmenter le flux sur ce chemin, répéter jusqu'à ce qu'il n'existe plus de chemin augmentant.
Théorème min-cut/max-flow (Ford-Fulkerson, 1956) : la valeur du flot maximal est egale à la capacité du coupe minimal (l'ensemble d'aretes dont la suppression déconnecté source et puits et dont la somme des capacités est minimale).
Ce théorème a des implications profondes : il etablit une dualite entre un problème de maximisation (flot) et un problème de minimisation (coupe). Il est utilisé dans la théorie des réseaux, la planification de capacité, et la conception de CDN.
Diagramme : graphe de dépendances et chemin critique¶
graph LR
A([Debut]) --> B[Infra: 2j]
A --> C[Schema DB: 1j]
B --> D[Backend API: 3j]
C --> D
C --> E[Migration: 2j]
D --> F[Tests integration: 2j]
E --> F
D --> G[Frontend: 4j]
F --> H[Staging: 1j]
G --> H
H --> I([Fin])
style A fill:#2d6a4f,color:#fff
style I fill:#2d6a4f,color:#fff
style B fill:#e76f51,color:#fff
style D fill:#e76f51,color:#fff
style G fill:#e76f51,color:#fff
style H fill:#e76f51,color:#fff Chemin critique (en rouge) : Infra (2j) → Backend API (3j) → Frontend (4j) → Staging (1j) = 10 jours. Tout retard sur ce chemin retarde la livraison.
Le chemin critique est le plus long chemin du début à la fin dans le graphe de dépendances de tâches — il détermine la durée minimale du projet. L'algorithme de calcul est un tri topologique suivi d'une relaxation des distances (Dijkstra adapté pour la maximisation).
Applications systèmes¶
Graphes de dépendances : npm, Maven, pip¶
Un package manager maintient un DAG de dépendances. L'installation suit un tri topologique. La détection de conflits de version est un problème de satisfiabilite sur un graphe contraint (SAT, dans les cas complexes).
probleme npm : trouver un sous-ensemble de versions
tel que toutes les contraintes (A@>=1.0 ∧ B@<2.0 ∧ ...) soient satisfaites
et que le graphe de dependances resultant soit sans cycle
Les "dependency hell" surgissent quand le système de contraintes est insatisfaisable — deux packages exigent des versions incompatibles d'un même tiers.
Routage réseau : OSPF et Dijkstra¶
OSPF (Open Shortest Path First) est un protocole de routage interne qui utilise exactement l'algorithme de Dijkstra. Chaque routeur :
- Construit une représentation complète de la topologie du réseau (Link State Database)
- Exécuté Dijkstra depuis lui-même comme source
- Installe les prochains sauts dans sa table de routage
Le "poids" d'une arete OSPF est configurable — il peut représenter la bande passante inverse, la latence, ou un coût administratif. Un opérateur peut forcer le trafic vers un lien en abaissant son poids OSPF.
BGP (inter-AS) n'utilise pas Dijkstra mais un algorithme de vecteur de distance (inspiré de Bellman-Ford) avec des attributs de politique — car les AS ont des politiques de routage différentes, pas seulement des coûts.
Ordonnancement de tâches et méthode du chemin critique¶
La méthode PERT/CPM (Critical Path Method) est un tri topologique suivi d'un calcul de chemin critique. En DevOps :
- Les pipelines CI/CD sont des DAGs — Jenkins, GitLab CI, GitHub Actions les représentent explicitement
- Airflow, Prefect, et Dagster modelisent les workflows de données comme des DAGs
- Les Makefiles sont des DAGs de dépendances de fichiers
Note
Les outils modernes de CI/CD detectent automatiquement les cycles dans les définitions de pipeline et refusent de les exécuter. Cette détection est exactement l'algorithme DFS de détection de cycle — la même structure mathématique sous-jacente, quelle que soit la technologie.
Composantes fortement connexes¶
Les composantes fortement connexes (SCC) d'un graphe orienté sont les groupes maximaux de sommets mutuellement accessibles. L'algorithme de Kosaraju ou de Tarjan les calcule en O(V+E).
Application : dans un graphe de microservices, une SCC de plus d'un nœud indique un couplage circulaire — le service A dépend de B qui dépend de A. C'est presque toujours une erreur d'architecture. Un outil comme dependency-cruiser détecté ces cycles en construisant le graphe de dépendances et en calculant les SCC.
Formulaire de référence¶
| Algorithme | Problème résolu | Complexité | Contrainte |
|---|---|---|---|
| BFS | Plus court chemin (aretes) | O(V+E) | Graphe non pondere |
| DFS | Connexite, cycles, tri topo | O(V+E) | — |
| Tri topologique (Kahn) | Ordre sur DAG | O(V+E) | Acyclique |
| Dijkstra | Plus courts chemins | O((V+E) log V) | Poids ≥ 0 |
| Bellman-Ford | Plus courts chemins | O(V×E) | Poids quelconques |
| Floyd-Warshall | Tous les couples | O(V³) | Pas de cycle negatif |
| Ford-Fulkerson | Flot maximal | O(E × | f* |
| Tarjan/Kosaraju | SCC | O(V+E) | Graphe orienté |