LYCÉE → PRÉPA · L05

Module L05 · Partie F · Coder comme un professionnel

Graphes : tout est réseau.

Une carte routière, les dépendances d’un projet, les amis d’un réseau social, les états d’un robot, les pages du web, les neurones d’un cerveau : des sommets reliés par des arêtes. Une fois qu’un problème est un graphe, une poignée d’algorithmes le résout. Ce module les implémente tous, avec leurs preuves et leurs pièges.

Durée : 3 séances · Prérequis : L03 (files, tas), L04. Objectifs : représentations, BFS/DFS et leurs applications, Dijkstra, A*, Bellman-Ford, tri topologique, composantes fortement connexes, Union-Find et Kruskal, Prim, Floyd-Warshall, une introduction aux flots.

Ce que vous saurez faire à la fin
  • Choisir liste d’adjacence ou matrice selon la densité.
  • Implémenter 8 algorithmes de graphes et connaître leur complexité.
  • Modéliser un problème concret (planification de robot, réseau de capteurs, dépendances) en graphe.

Références : CLRS chapitres 22-26, programme MPI « graphes », Sedgewick Algorithms partie 4.

Fiche de cours · Définitions

Le vocabulaire des graphes

Définition (graphe). G = (S, A) : un ensemble de sommets S (|S| = n) et un ensemble d’arêtes A (|A| = m), paires {u, v} (non orienté) ou couples (u, v) (orienté). Un graphe pondéré associe un poids w(u, v) à chaque arête. Le degré d’un sommet est son nombre d’arêtes incidentes.
Définition (chemin, cycle, connexité). Un chemin est une suite de sommets consécutivement adjacents ; il est simple sans répétition de sommet ; un cycle est un chemin fermé. G est connexe si tout couple de sommets est relié par un chemin ; ses composantes connexes sont les classes de cette relation. Un graphe orienté sans cycle est un DAG.
Définition (arbre). Graphe connexe sans cycle. Propriétés équivalentes pour n sommets : connexe et m = n − 1 ; sans cycle et m = n − 1 ; entre deux sommets il existe un unique chemin simple. Un arbre couvrant de G est un sous-graphe qui est un arbre contenant tous les sommets ; de poids minimal (ACM) si la somme des poids est minimale.
Définition (représentations). Liste d’adjacence (dict sommet → voisins) : O(n + m) mémoire, parcours des voisins en O(deg). Matrice d’adjacence (n×n) : O(n²) mémoire, test d’adjacence en O(1). Les parcours coûtent O(n + m) avec des listes, O(n²) avec une matrice.
Définition (tri topologique). Ordre des sommets d’un DAG tel que toute arête (u, v) ait u avant v. Existe si et seulement si le graphe est sans cycle.

Fiche de cours · Formules

Formules et complexités à connaître

Σv∈S deg(v) = 2m lemme des poignées de main — conséquence : le nombre de sommets de degré impair est pair
Arbre à n sommets : m = n − 1 ; forêt à c composantes : m = n − c
AlgorithmeProblèmeComplexitéStructure clé
BFSPlus court chemin (non pondéré), composantes, bipartiO(n + m)File
DFSCycles, tri topologique, composantes fortement connexesO(n + m)Pile / récursion
DijkstraPlus court chemin, poids ≥ 0O((n + m) log n)Tas de priorité
Bellman-FordPlus court chemin, poids négatifs, détection de cycle négatifO(n·m)n − 1 relaxations de toutes les arêtes
Floyd-WarshallToutes les pairesO(n³)PD sur les sommets intermédiaires
KruskalACMO(m log m)Tri + union-find
PrimACMO((n + m) log n)Tas
A*Plus court chemin avec heuristiquedépend de hTas sur g + h (L17, L21)

Fiche de cours · Théorèmes et démonstrations

Démonstrations à savoir refaire (1/2)

Théorème 1 (BFS calcule les distances). Dans un graphe non pondéré, le parcours en largeur depuis s affecte à chaque sommet v atteint la valeur d(v) = longueur du plus court chemin de s à v.
Notons δ(v) la vraie distance. (i) d(v) ≥ δ(v) : d(v) est la longueur d’un chemin réel (celui par lequel v est découvert). (ii) Invariant sur la file : elle contient des sommets de distances d croissantes, et max − min ≤ 1 (récurrence : on défile un sommet u de d minimale et on enfile ses voisins avec d(u)+1). (iii) Par l’absurde, soit v le sommet avec d(v) > δ(v) et δ(v) minimal ; soit u son prédécesseur sur un plus court chemin : δ(u) = δ(v) − 1 et d(u) = δ(u) par minimalité. Quand u est défilé, v est soit non découvert — il reçoit d(u)+1 = δ(v), contradiction — soit déjà découvert avec d(v) ≤ d(u) + 1 par (ii), contradiction aussi.
Théorème 2 (correction de Dijkstra). Si tous les poids sont ≥ 0, quand un sommet u est extrait du tas (avec la plus petite estimation d), on a d(u) = δ(u), et cette valeur n’est plus jamais modifiée.
Par l’absurde, soit u le premier sommet extrait avec d(u) > δ(u). Considérons un plus court chemin de s à u ; soit y le premier sommet de ce chemin non encore extrait, et x son prédécesseur (extrait, donc d(x) = δ(x)). Lors de l’extraction de x, l’arête (x, y) a été relaxée : d(y) ≤ δ(x) + w(x, y) = δ(y) (sous-structure optimale), donc d(y) = δ(y). Or δ(y) ≤ δ(u) car les poids sont ≥ 0 et y précède u sur le chemin. D’où d(y) = δ(y) ≤ δ(u) < d(u) : y aurait dû être extrait avant u — contradiction. Avec des poids négatifs, l’inégalité δ(y) ≤ δ(u) tombe et l’algorithme peut se tromper.

Fiche de cours · Théorèmes et démonstrations

Démonstrations à savoir refaire (2/2)

Théorème 3 (propriété de la coupe, Kruskal et Prim). Soit (X, S∖X) une partition des sommets (une coupe) et e une arête de poids minimal traversant la coupe. Alors il existe un ACM contenant e.
Soit T un ACM. Si e ∈ T, fini. Sinon, T ∪ {e} contient un cycle, qui traverse la coupe une seconde fois par une arête e′ ≠ e. Alors T′ = T ∪ {e} ∖ {e′} est encore un arbre couvrant (on a cassé le seul cycle) et w(T′) = w(T) + w(e) − w(e′) ≤ w(T) puisque w(e) ≤ w(e′). T′ est donc un ACM contenant e. Kruskal applique ce lemme à la coupe séparant la composante de u du reste quand il ajoute l’arête (u, v) de poids minimal ne créant pas de cycle ; Prim à la coupe (arbre construit, reste). Les deux sont donc corrects, par récurrence sur les arêtes ajoutées (invariant : « les arêtes choisies sont incluses dans un ACM »).
Théorème 4 (tri topologique et cycles). Un graphe orienté admet un tri topologique si et seulement s’il est sans cycle ; l’algorithme de Kahn (retirer répétitivement un sommet de degré entrant 0) le construit en O(n + m).
(⇒) Si (v₁, …, vk, v₁) est un cycle, l’ordre exigerait v₁ avant v₂ … avant v₁ : impossible. (⇐) Un DAG a un sommet de degré entrant 0 : sinon, en remontant indéfiniment un prédécesseur depuis un sommet quelconque, on visite n + 1 sommets parmi n, donc l’un est répété — un cycle. On le retire (le graphe reste un DAG) et on recommence : par récurrence sur n, on obtient un ordre où chaque sommet précède ses successeurs. Complexité : chaque arête est « retirée » une fois lors de la mise à jour des degrés entrants.
Théorème 5 (Euler). Un graphe connexe possède un cycle passant exactement une fois par chaque arête si et seulement si tous ses sommets sont de degré pair.
(⇒) Chaque passage par un sommet consomme deux arêtes. (⇐, idée) Partir d’un sommet et avancer sans réutiliser d’arête : comme les degrés sont pairs, on ne peut rester bloqué qu’au sommet de départ, ce qui donne un cycle. Retirer ses arêtes ; les degrés restent pairs ; par connexité, un sommet du cycle possède encore des arêtes ; on y recommence et on greffe le nouveau cycle dans le premier. Récurrence sur m.

Fiche de cours · Méthodes

Méthodes et pièges

Méthode — modéliser en graphe. Identifier les états (sommets) et les transitions (arêtes) : positions d’un robot sur une grille, configurations d’un jeu, tâches et dépendances, mots et lettres différentes. Puis reconnaître le problème : « moins d’étapes » = BFS ; « moins cher » = Dijkstra ; « toutes les paires » = Floyd ; « connecter à moindre coût » = ACM ; « ordonner » = tri topologique ; « affectation » = couplage.
Méthode — écrire un parcours sans bug. Marquer un sommet comme visité au moment où on l’enfile (BFS), pas quand on le défile — sinon il peut être enfilé plusieurs fois et la complexité explose. En DFS récursif, attention à la limite de récursion Python (1000) : utiliser une pile explicite pour les grands graphes.
Méthode — reconstruire un chemin. Mémoriser pred[v] = u quand v est découvert (ou relaxé) depuis u ; remonter de t à s ; inverser.

Pièges : Dijkstra avec des poids négatifs ; oublier les sommets isolés en itérant seulement sur les arêtes ; matrice d’adjacence pour un graphe creux (mémoire n²) ; graphe orienté traité comme non orienté (arêtes en un seul sens) ; ACM ≠ arbre des plus courts chemins.

Fiche de cours · Exercices corrigés

Exercices corrigés

Exercice 1. Montrer qu’un graphe est biparti si et seulement s’il n’a aucun cycle de longueur impaire, et en déduire un algorithme en O(n + m).
Correction. (⇒) Sur un cycle, les couleurs alternent ; revenir au départ après k arêtes impose k pair. (⇐) Colorier par BFS depuis un sommet s : couleur(v) = parité de d(v). Une arête (u, v) entre deux sommets de même parité fermerait, avec les chemins BFS vers s, un parcours fermé de longueur d(u) + d(v) + 1 impaire, qui contient un cycle impair (un parcours fermé de longueur impaire contient toujours un cycle impair, par récurrence en le découpant à un sommet répété). Algorithme : BFS + vérification des arêtes, O(n + m).
Exercice 2. Un robot doit visiter des salles reliées par des couloirs de longueurs données ; certaines portes sont à sens unique. Il part de A et doit atteindre F au plus vite en passant obligatoirement par C. Comment faire avec Dijkstra ?
Correction. Deux exécutions : Dijkstra depuis A donne δ(A, C) ; Dijkstra depuis C donne δ(C, F). Le chemin optimal via C est la concaténation, de coût δ(A, C) + δ(C, F) — par sous-structure optimale, la portion A→C d’un chemin optimal via C est elle-même optimale, et de même pour C→F. Si le chemin doit être simple (ne pas repasser deux fois par une salle), le problème devient difficile en général ; sans cette contrainte, deux Dijkstra suffisent. Graphe orienté : les couloirs à double sens donnent deux arcs.
Exercice 3. Prouver que Kruskal produit bien un arbre (connexe, sans cycle) quand le graphe est connexe, et donner sa complexité avec union-find.
Correction. Sans cycle : on n’ajoute une arête que si ses extrémités sont dans des composantes différentes (union-find), donc jamais de cycle. Connexe : par l’absurde, s’il reste deux composantes à la fin, le graphe étant connexe, il existe une arête entre elles ; quand elle a été examinée, ses extrémités étaient déjà dans des composantes différentes (les composantes ne font que fusionner), donc elle aurait été ajoutée — contradiction. Complexité : tri des m arêtes O(m log m) ; m opérations find/union quasi constantes (α(n), inverse d’Ackermann, avec compression de chemin et union par rang) : O(m log m) au total, soit O(m log n) car m ≤ n².

01 / Représenter

Listes d’adjacence, matrice, et une classe qui sert pour tout le module

Quelle représentation ?
Liste d’adjacenceMatrice
MémoireO(S + A)O(S²)
« u et v sont-ils voisins ? »O(deg u) ou O(1) avec un dictO(1)
Parcourir les voisins de uO(deg u)O(S)
Adapté àgraphes creux (routes, web, réseaux sociaux)graphes denses, calcul matriciel (Floyd, puissances de matrice)

Un graphe routier de 107 sommets a ~2·107 arêtes : la matrice pèserait 1014 cases. La liste d’adjacence est le choix par défaut.

02 / Parcours

BFS : le plus court chemin en nombre d’arêtes

Pourquoi BFS trouve le plus court chemin : la file traite les sommets par distance croissante (invariant : à tout instant, la file contient des sommets à distance d puis d+1, jamais plus). La première fois qu’on atteint un sommet, c’est donc par un plus court chemin. C’est un argument à savoir rédiger.

02 / Parcours

DFS : composantes, cycles, tri topologique

Gris = « en cours » : la détection de cycle

Un sommet gris est sur la pile d’appels courante. Rencontrer une arête vers un sommet gris, c’est revenir sur un ancêtre : un cycle. Le tri topologique est l’ordre inverse des dates de fin : quand u finit, tous ses descendants ont fini avant lui. Applications : make, gestionnaires de paquets, ordonnancement de calculs (PyTorch construit et trie topologiquement le graphe des opérations pour la rétropropagation, module L14). La version récursive dépasse la limite de pile pour ~1000 sommets : en pratique on écrit une version itérative avec une pile explicite.

03 / Plus courts chemins

Dijkstra : le plus court chemin pondéré

L’invariant de Dijkstra

Quand un sommet u sort du tas avec la distance d, aucune autre voie ne peut être plus courte : tout autre chemin vers u passe par un sommet encore dans le tas, dont la distance est ≥ d, plus des poids ≥ 0. Cette preuve échoue avec des poids négatifs (ici, Dijkstra fixe B à 5 avant de découvrir A→C→B = −2 ; par chance la version « paresseuse » ci-dessus se corrige, mais ce n’est pas garanti en général). Dijkstra est un glouton dont l’argument d’échange repose sur la positivité. Pour un GPS : contraction hierarchies, A* et pré-calculs rendent Dijkstra 1000 fois plus rapide sur un graphe continental.

03 / Plus courts chemins

A* : Dijkstra guidé par une heuristique

Admissible, consistante, et le piège de la sur-estimation

A* trie par f = g + h : coût déjà payé + estimation du reste. Si h ne sur-estime jamais (admissible), le premier chemin trouvé est optimal. Si de plus h(u) ≤ coût(u, v) + h(v) (consistante, inégalité triangulaire), chaque sommet est sorti une seule fois. La distance euclidienne est admissible pour tout déplacement ; l’octile est plus serrée pour 8 directions, donc explore moins. Une heuristique qui sur-estime (h × 1,5) va plus vite mais peut rater l’optimum : c’est le « weighted A* » des jeux vidéo, où la vitesse prime. Sur un robot, A* tourne sur une grille d’occupation (module L20) ; en 3D et avec des contraintes cinématiques, on passe aux RRT (module L21).

04 / Connexité et arbres couvrants

Union-Find et Kruskal : relier au moindre coût

Preuve de Kruskal (propriété de la coupe) : l’arête la plus légère qui traverse une coupe quelconque appartient à un arbre couvrant minimal ; l’argument d’échange du module L04. Union-Find sert aussi à la détection de cycles, aux composantes connexes dynamiques et à la segmentation d’images.

04 / Connexité et arbres couvrants

Floyd-Warshall et composantes fortement connexes

Les composantes fortement connexes regroupent les sommets mutuellement accessibles : dans un graphe de dépendances, ce sont les cycles à casser ; dans le web, les « communautés » ; dans une machine à états, les régions d’où on ne peut plus sortir (un piège à détecter dans un robot !). Le graphe réduit des CFC est toujours acyclique.

05 / Flots

Flot maximal : combien peut passer ?

Max-flot / min-coupe

Le flot maximal égale la capacité de la coupe minimale (théorème de Ford-Fulkerson) : le goulot d’étranglement d’un réseau est exactement son débit maximal. Applications : bande passante, affectation (couplage biparti), segmentation d’images (graph cuts), planification de projets, et beaucoup de problèmes qui ne ressemblent pas à des flots mais s’y réduisent — reconnaître ces réductions est un art de concours.

Cours

Cours 1 — Définitions et propriétés de base des graphes

NotionDéfinitionFait utile
Graphe G = (S, A)Sommets S, arêtes A ⊆ S×S ; orienté (arcs) ou non|A| ≤ |S|(|S|−1)/2 (non orienté simple)
DegréNombre d’arêtes incidentes (entrant/sortant si orienté)Σ deg(v) = 2|A| ⇒ le nombre de sommets de degré impair est pair (« lemme des poignées de main »)
Chemin / cycleSuite d’arêtes consécutives ; fermée pour un cycleUn chemin simple ne repasse pas par un sommet
ConnexeTout couple de sommets est relié par un cheminUn graphe connexe à n sommets a ≥ n−1 arêtes
ArbreConnexe et sans cycleExactement n−1 arêtes ; un unique chemin entre deux sommets ; ajouter une arête crée exactement un cycle
DAGOrienté sans cycleAdmet un ordre topologique ; a au moins un sommet sans prédécesseur
BipartiSommets colorables en 2 couleurs sans arête monochrome⇔ aucun cycle de longueur impaire ; testable par BFS
PlanaireDessinable sans croisementEuler : S − A + F = 2 ; donc A ≤ 3S − 6

Exemple travaillé (poignées de main). Dans un tournoi de robotique, 7 équipes affirment chacune avoir rencontré exactement 3 autres équipes. Possible ? Σ deg = 21, impair : impossible, car chaque rencontre compte deux fois. Ce genre d’argument de parité est la première chose à essayer sur un problème de graphe « existe-t-il… ».

Cours

Cours 2 — Modéliser : reconnaître un graphe dans un énoncé

La compétence clé n’est pas d’implémenter Dijkstra (c’est fait, dans toutes les bibliothèques), mais de voir qu’un problème est un plus court chemin. Cinq modélisations classiques :

  1. Espace d’états : sommet = configuration (position + orientation + objets portés…), arête = action. Le puzzle du taquin, le loup-chèvre-chou, le robot avec cap (défi ★★) : BFS donne le nombre minimal d’actions.
  2. Dépendances : sommet = tâche, arc = « doit précéder ». Tri topologique pour l’ordre ; plus long chemin dans le DAG pour la durée minimale du projet (chemin critique).
  3. Similarité : sommet = objet, arête = « ressemblent » (mots à une lettre près, images proches). Composantes pour regrouper ; BFS pour transformer l’un en l’autre.
  4. Réseau de flux : capacités, source, puits. Affectations, bande passante, couplages.
  5. Grille : cases = sommets, adjacence = 4 ou 8 voisins, poids = coût de traversée. Cartes de robots (L20), images (segmentation).

Cours

Cours 3 — Preuves de correction : tri topologique et Kruskal, rédigées

Tri topologique par DFS. Énoncé : dans un DAG, l’ordre inverse des dates de fin d’un DFS est un ordre topologique. Preuve : soit un arc u → v. Quand le DFS examine cet arc depuis u, trois cas : (a) v est blanc : on visite v, qui finit avant u ; (b) v est noir : v a déjà fini, donc avant u ; (c) v est gris : v est un ancêtre de u sur la pile, donc il existe un chemin v ⇝ u, et u → v ferme un cycle — impossible dans un DAG. Dans tous les cas fin(v) < fin(u), donc u précède v dans l’ordre inverse des fins. ∎ Corollaire : le cas (c) détecte les cycles.

Kruskal (propriété de la coupe). Lemme : soit une partition (X, S∖X) des sommets et e l’arête la plus légère qui la traverse ; alors e appartient à un arbre couvrant minimal (ACM). Preuve : soit T un ACM sans e. Ajouter e à T crée un cycle, qui traverse la coupe au moins une autre fois par une arête e′ avec poids(e′) ≥ poids(e). T + e − e′ est un arbre couvrant de poids ≤ poids(T), donc un ACM contenant e. ∎ Kruskal : chaque arête acceptée est la plus légère traversant la coupe entre sa composante et le reste (toutes les plus légères ont déjà été examinées) ; par le lemme, elle est « sûre » ; par récurrence l’ensemble final est un ACM. ∎

Structure commune : identifier un invariant (« l’ensemble des arêtes choisies est inclus dans un ACM ») et montrer que chaque étape le conserve. C’est le module L09 appliqué aux graphes ; à l’oral, on attend exactement ce niveau de rédaction.

TP guidé

TP — Bibliothèque de graphes et planificateur sur carte réelle (sur PC, 2 h 30)

Exercices

Exercices auto-corrigés — parcours

Exercice 1 — Biparti, et plus courte transformation

a) est_biparti(adj) par BFS 2-coloration (adj : dict sommet → liste de voisins). b) echelle(depart, arrivee, mots) : plus courte suite de mots du dictionnaire où deux mots consécutifs diffèrent d’une lettre (BFS ; renvoyer la liste, ou None).

Correction
def est_biparti(adj):
    couleur = {}
    for s in adj:
        if s in couleur: continue
        couleur[s] = 0; file = deque([s])
        while file:
            u = file.popleft()
            for v in adj[u]:
                if v not in couleur: couleur[v] = 1 - couleur[u]; file.append(v)
                elif couleur[v] == couleur[u]: return False
    return True
def echelle(depart, arrivee, mots):
    D = set(mots) | {depart}; pred = {depart: None}; file = deque([depart])
    while file:
        u = file.popleft()
        if u == arrivee: break
        for i in range(len(u)):
            for c in "abcdefghijklmnopqrstuvwxyz":
                v = u[:i] + c + u[i + 1:]
                if v in D and v not in pred: pred[v] = u; file.append(v)
    if arrivee not in pred: return None
    c, u = [], arrivee
    while u: c.append(u); u = pred[u]
    return c[::-1]

Exercices

Exercices auto-corrigés — chemins et arbres

Exercice 2 — Chemin critique dans un DAG

Tâches avec durées et dépendances. duree_projet(taches) renvoie la durée minimale totale (plus long chemin dans le DAG, calculé en ordre topologique) et la liste des tâches critiques (celles dont tout retard retarde le projet).

Correction
def duree_projet(taches):
    ordre, vus = [], set()
    def visiter(t):
        if t in vus: return
        vus.add(t)
        for p in taches[t][1]: visiter(p)
        ordre.append(t)
    for t in taches: visiter(t)
    debut = {}
    for t in ordre: debut[t] = max((debut[p] + taches[p][0] for p in taches[t][1]), default=0)
    fin_projet = max(debut[t] + taches[t][0] for t in taches)
    tard = {}                                          # au plus tard : en ordre inverse
    for t in reversed(ordre):
        succ = [s for s in taches if t in taches[s][1]]
        tard[t] = min((tard[s] for s in succ), default=fin_projet) - taches[t][0]
    return fin_projet, sorted(t for t in taches if debut[t] == tard[t])

Exercice 3 — Dijkstra avec contrainte et Kruskal de mémoire

a) chemin_moins_de_k_aretes(g, s, t, k) : plus court chemin pondéré utilisant au plus k arêtes (état = (sommet, nombre d’arêtes utilisées)). b) kruskal(n, aretes) de mémoire : renvoie le poids total de l’ACM (arêtes : liste de (u, v, p), sommets 0..n−1) ou None si le graphe n’est pas connexe.

Correction
def chemin_moins_de_k_aretes(adj, s, t, k):
    dist = {(s, 0): 0}; tas = [(0, s, 0)]
    while tas:
        d, u, n = heapq.heappop(tas)
        if u == t: return d
        if d > dist.get((u, n), float("inf")) or n == k: continue
        for v, p in adj[u]:
            if d + p < dist.get((v, n + 1), float("inf")):
                dist[(v, n + 1)] = d + p; heapq.heappush(tas, (d + p, v, n + 1))
    return None
def kruskal(n, aretes):
    parent = list(range(n))
    def trouver(x):
        while parent[x] != x: parent[x] = parent[parent[x]]; x = parent[x]
        return x
    total, nb = 0, 0
    for u, v, p in sorted(aretes, key=lambda a: a[2]):
        ru, rv = trouver(u), trouver(v)
        if ru != rv: parent[ru] = rv; total += p; nb += 1
    return total if nb == n - 1 else None

06 / Défis

Défi ★ — Le graphe des mots

Consigne

Deux mots de même longueur sont voisins s’ils diffrent d’une lettre. Avec le petit dictionnaire fourni, trouvez par BFS la plus courte échelle de froid à chaud (chaque étape est un mot du dictionnaire). Combien de composantes connexes a ce graphe ? Quel est le mot le plus « central » (excentricité minimale) ?

Correction
class G:
    def voisins(self, m): return voisins(m)
    def sommets(self): return mots
g2 = G()
dist, pred = bfs(g2, "froid")
print(chemin(pred, "chaud") if "chaud" in dist else "pas de chemin")
print("composantes :", composantes_connexes(g2))
exc = {m: max(bfs(g2, m)[0].values()) for m in mots if len(bfs(g2, m)[0]) > 1}
print(min(exc, key=exc.get), exc)

Construire le graphe explicitement coûterait O(n²·L) comparaisons ; pour un vrai dictionnaire (300 000 mots), on hache les motifs « fr_id » pour trouver les voisins en O(L) par mot.

06 / Défis

Défi ★★ — Planificateur de robot avec coûts de rotation

Consigne

Sur la grille du labyrinthe (slide BFS), le robot a une orientation (N, E, S, O). Avancer coûte 1, tourner de 90° coûte 2. Modélisez l’état comme (case, orientation), construisez les arêtes à la volée et lancez Dijkstra pour trouver le trajet de coût minimal de S (orienté E) à E. Comparez avec le chemin BFS : le nombre de virages diminue-t-il ?

Correction
def successeurs(etat):
    i, j, d = etat
    di, dj = DIRS[d]
    a, b = i + di, j + dj
    if 0 <= a < len(labyrinthe) and 0 <= b < len(labyrinthe[0]) and labyrinthe[a][b] != "#":
        yield (a, b, d), 1
    k = ORDRE.index(d)
    yield (i, j, ORDRE[(k + 1) % 4]), 2
    yield (i, j, ORDRE[(k - 1) % 4]), 2

def dijkstra_etats(depart, est_but):
    dist, pred, tas = {depart: 0}, {depart: None}, [(0, depart)]
    while tas:
        d, u = heapq.heappop(tas)
        if d > dist[u]: continue
        if est_but(u):
            c = []
            while u: c.append(u); u = pred[u]
            return d, c[::-1]
        for v, p in successeurs(u):
            if d + p < dist.get(v, float("inf")):
                dist[v], pred[v] = d + p, u; heapq.heappush(tas, (d + p, v))

cout, traj = dijkstra_etats((0, 0, "E"), lambda e: e[:2] == (4, 8))
print("coût :", cout, "| virages :", sum(1 for a, b in zip(traj, traj[1:]) if a[2] != b[2]))
print([e[:2] for e in traj if True][::3])

L’espace d’états est 4 fois plus grand que la grille, mais Dijkstra reste instantané. C’est exactement ainsi qu’un planificateur de robot (ROS Nav2, module L21) intègre les contraintes cinématiques : l’état n’est pas la position mais (position, cap, vitesse…).

06 / Défis

Défi ★★★ — Esprit prépa : 2-SAT par les composantes fortement connexes

Consigne

Une formule 2-SAT est une conjonction de clauses à deux littéraux (x ∨ ¬y) ∧ (…). Construisez le graphe d’implication : chaque clause (a ∨ b) donne les arêtes ¬a → b et ¬b → a. La formule est satisfiable si et seulement si aucune variable x n’est dans la même CFC que ¬x. Implémentez, testez sur une formule satisfiable et une insatisfiable, puis extrayez une affectation (ordre topologique des CFC). Complexité : O(n + m), alors que 3-SAT est NP-complet — pourquoi la frontière passe-t-elle entre 2 et 3 ?

Correction
def deux_sat(clauses):
    g = Graphe(oriente=True)
    for a, b in clauses:
        g.ajouter_arete(-a, b); g.ajouter_arete(-b, a)
    for a, b in clauses:
        for x in (a, b, -a, -b): g.adj[x]
    comps = kosaraju(g)
    cfc = {}
    for i, c in enumerate(comps):
        for x in c: cfc[x] = i
    for x in list(cfc):
        if x > 0 and -x in cfc and cfc[x] == cfc[-x]: return None
    # Kosaraju renvoie les CFC en ordre topologique du graphe : x vrai si cfc[x] > cfc[-x]
    return {x: cfc[x] > cfc[-x] for x in cfc if x > 0}

print(deux_sat(clauses_ok)); print(deux_sat(clauses_ko))
aff = deux_sat(clauses_ok)
val = lambda l: aff[abs(l)] if l > 0 else not aff[abs(l)]
print("vérification :", all(val(a) or val(b) for a, b in clauses_ok))

Avec 2 littéraux, une clause est une implication et le problème devient un problème de graphe. Avec 3, il n’y a plus de structure d’implication simple : 3-SAT est le problème NP-complet de référence (Cook-Levin, module L23). Cette frontière — un problème facile, son voisin immédiat difficile — est l’un des phénomènes centraux de l’informatique théorique.

07 / Vérification

Pour trouver le plus court chemin (en nombre d’arêtes), on remplace la file du BFS par une pile. Résultat ?

Deux questions supplémentaires

1. Pourquoi Dijkstra utilise-t-il un tas et non une file ? Parce que les poids diffèrent : il faut toujours traiter le sommet de distance minimale, ce que la file (ordre d’arrivée) ne garantit pas.

2. Une heuristique A* valant toujours 0 donne quoi ? Exactement Dijkstra.

Référence

Les algorithmes à retenir

AlgorithmeProblèmeComplexité
BFSPlus court chemin non pondéré, niveauxO(S + A)
DFSComposantes, cycles, tri topologique, CFCO(S + A)
DijkstraPlus court chemin, poids ≥ 0O((S + A) log S)
Bellman-FordPoids négatifs, cycles négatifsO(S·A)
A*Plus court chemin guidédépend de h
Floyd-WarshallToutes les pairesO(S³)
Kruskal / PrimArbre couvrant minimalO(A log A)
Union-FindEnsembles disjoints~O(1) amorti (α(n))
Kosaraju / TarjanComposantes fortement connexesO(S + A)
Edmonds-KarpFlot maximal, couplageO(S·A²)

Pour continuer

Vous modélisez en graphes

Module suivant : les outils du développeur — terminal, Git, organisation d’un projet, tests automatisés en intégration continue — pour travailler comme une équipe et pas comme un fichier unique.

À faire chez soi

← L04SommaireL06 : Outils du développeur →