cursus.

Cours 5 · Graphes et paradigmesLeçon 1 sur 2

Graphes

5 h de lecture9 sections Version PDF

À la fin de cette leçon, vous saurez

Matrice et listes d'adjacence, parcours en profondeur et en largeur, connexité, détection de cycle, plus court chemin en nombre d'arêtes, Dijkstra en introduction.

Combien de correspondances au minimum entre deux stations de métro ? Un ami commun existe-t-il entre deux personnes d'un réseau social ? Quelles pages web sont atteignables depuis celle-ci ? Ces trois questions n'ont ni hiérarchie ni ordre : elles portent sur des objets reliés arbitrairement, et aucune structure vue jusqu'ici ne les modélise.

Le graphe est cet objet, et il généralise tout le semestre. Une liste est un graphe où chaque sommet a un successeur ; un arbre, un graphe sans cycle avec une racine. Retirer ces contraintes ouvre un champ immense — et introduit une difficulté nouvelle : on peut revenir sur ses pas.

Vocabulaire

Un graphe est un ensemble de sommets et un ensemble d'arêtes reliant des paires de sommets.

Il est orienté quand les liens ont un sens — on parle alors d'arcs : les liens hypertexte, les dépendances entre tâches, les relations « suit » d'un réseau social. Il est non orienté quand ils n'en ont pas : un réseau routier, une amitié réciproque.

Il est pondéré quand chaque arête porte un nombre — distance, durée, coût, capacité. C'est ce qui distinguera le chapitre en deux moitiés : sans poids, la longueur d'un chemin est son nombre d'arêtes ; avec poids, c'est la somme.

TermeDéfinition
Degré d'un sommetnombre d'arêtes qui y aboutissent
Cheminsuite de sommets reliés deux à deux
Cyclechemin qui revient à son point de départ
Connexetout sommet est atteignable depuis tout autre
Composante connexemorceau maximal connexe d'un graphe qui ne l'est pas
Acycliquesans aucun cycle

Deux notations utiles : VV pour le nombre de sommets, EE pour le nombre d'arêtes. Un graphe est dit dense quand EE approche V2V^2 — presque tout est relié — et creux quand EE est de l'ordre de VV. Cette distinction décide de la représentation.

Deux représentations

La matrice d'adjacence est un tableau V×VV \times V où la case (i,j)(i,j) vaut 1 s'il existe une arête de ii vers jj — ou le poids, s'il y en a un.

        A  B  C  D              A ── B     A  0  1  1  0              │    │     B  1  0  1  0              C ───┘     C  1  1  0  1              │     D  0  0  1  0              D

Tester l'existence d'une arête est en O(1)O(1), ce qui est imbattable. Mais la mémoire est O(V2)O(V^2) quel que soit le nombre d'arêtes, et surtout, énumérer les voisins d'un sommet demande de parcourir toute une ligne, soit O(V)O(V) — même s'il n'a que deux voisins.

Les listes d'adjacence associent à chaque sommet la liste de ses voisins.

A : B, CB : A, CC : A, B, DD : C

La mémoire est O(V+E)O(V + E), et énumérer les voisins d'un sommet coûte exactement son degré. En revanche, tester une arête précise demande de parcourir une liste.

MatriceListes
MémoireO(V2)O(V^2)O(V+E)O(V + E)
Tester une arêteO(1)O(1)O(deg)O(\deg)
Énumérer les voisinsO(V)O(V)O(deg)O(\deg)

Le choix se tranche par la densité, et par ce que fait l'algorithme. Les parcours de ce chapitre énumèrent les voisins en boucle et ne testent presque jamais une arête isolée : sur un graphe creux, les listes d'adjacence sont donc le bon choix — et les graphes réels sont massivement creux. Un réseau social de dix millions de comptes n'a pas 101410^{14} relations ; il en a peut-être un milliard, et la matrice serait de toute façon impossible à loger.

Parcourir : la difficulté nouvelle

Les parcours du chapitre 7 se transposent presque tels quels — avec une modification qui n'est pas optionnelle.

Dans un arbre, on ne peut pas revenir sur un nœud déjà visité : il n'y a qu'un chemin depuis la racine. Dans un graphe, un cycle ramène sur ses pas, et un parcours naïf boucle indéfiniment. Il faut donc marquer les sommets visités, et tester ce marquage avant chaque descente. C'est la seule différence de fond avec le chapitre 7, et c'est l'oubli qui produit les boucles infinies du TD.

Le parcours en profondeur (DFS) s'enfonce aussi loin que possible avant de revenir en arrière. Il s'écrit récursivement en cinq lignes — la pile d'appels du bloc I fait le travail — ou itérativement avec une pile explicite.

fonction profondeur(s)    marquer s    traiter s    pour chaque voisin v de s        si v n'est pas marqué alors profondeur(v)

Le parcours en largeur (BFS) explore par couches : d'abord les voisins immédiats, puis leurs voisins, et ainsi de suite. Il emploie une file, exactement comme au chapitre 7.

fonction largeur(s)    marquer s ; enfiler s    tant que la file n'est pas vide        u ← défiler        traiter u        pour chaque voisin v de u non marqué            marquer v ; enfiler v

Un détail qui compte : on marque à l'enfilement, pas au défilement. Sinon un sommet accessible par deux chemins serait enfilé deux fois.

Les deux parcours coûtent O(V+E)O(V + E) avec des listes d'adjacence : chaque sommet est traité une fois, chaque arête examinée une fois (deux en non orienté).

Ce qu'un parcours résout

La connexité et les composantes. Un parcours depuis un sommet atteint exactement sa composante connexe. Pour toutes les trouver, on relance un parcours depuis chaque sommet non encore marqué, en comptant les relances : c'est le nombre de composantes, obtenu en O(V+E)O(V+E).

La détection de cycle. En non orienté, un cycle existe si le parcours rencontre un sommet déjà marqué qui n'est pas le père immédiat — la nuance est essentielle, sans quoi toute arête serait vue comme un cycle. En orienté, c'est plus subtil : il faut repérer un arc vers un sommet encore en cours de traitement dans la pile de récursion, et non simplement déjà visité.

Le plus court chemin en nombre d'arêtes. C'est la propriété remarquable du BFS : il donne le plus court chemin, sans effort supplémentaire. La raison tient à l'ordre de visite — il explore tous les sommets à distance 1, puis tous ceux à distance 2, et ainsi de suite. Quand il atteint un sommet pour la première fois, il l'a donc fait par le chemin le plus court, et il suffit de mémoriser le prédécesseur pour reconstituer l'itinéraire. C'est la réponse à la question des correspondances de métro — à condition que toutes les arêtes se valent.

Le DFS n'a pas cette propriété : il peut atteindre un sommet voisin par un long détour, parce qu'il plonge avant d'explorer les alternatives.

Quiz · vérifiez votre compréhension Sans réponse

Un étudiant écrit un parcours en profondeur sans marquer les sommets visités. Que se passe-t-il, et pourquoi la question ne se posait-elle pas au chapitre 7 ?

Dijkstra, en introduction

Le BFS répond aux arêtes de poids égal. Dès qu'une route est deux fois plus longue qu'une autre, il ne convient plus : le chemin ayant le moins d'arêtes n'est pas forcément le plus court en distance.

L'algorithme de Dijkstra (1959) traite le cas pondéré, à une condition : tous les poids doivent être positifs. Son principe est glouton, au sens du chapitre suivant — il fait à chaque étape le choix qui paraît le meilleur sur le moment, sans jamais revenir dessus.

distance[départ] ← 0, toutes les autres ← infinitant qu'il reste des sommets non traités    u ← le sommet non traité de plus petite distance      ← le choix glouton    marquer u comme traité    pour chaque voisin v de u        si distance[u] + poids(u,v) < distance[v] alors            distance[v] ← distance[u] + poids(u,v)        ← relâchement

Deux remarques suffisent à ce niveau.

Le sommet extrait est définitif. Quand on choisit le sommet non traité le plus proche, aucun chemin passant par les sommets restants ne pourra faire mieux — puisqu'ils sont tous plus loin et que les poids sont positifs, tout détour ne peut qu'allonger. C'est ce qui justifie de ne jamais revenir en arrière, et c'est aussi ce qui s'effondre avec un poids négatif : un arc de poids 5-5 rencontré plus tard pourrait raccourcir un chemin déjà figé.

Le coût dépend de la structure choisie. Chercher le minimum en parcourant tous les sommets donne O(V2)O(V^2). Le prendre dans une file de priorité — le tas du chapitre 8 — donne O((V+E)logV)O((V+E)\log V), bien meilleur sur un graphe creux. C'est le plus bel emploi du tas du semestre : Dijkstra a besoin, à chaque étape, du minimum d'un ensemble qui change, ce qui est exactement le contrat de cette structure.

Quiz · vérifiez votre compréhension Sans réponse

Pourquoi le parcours en largeur donne-t-il le plus court chemin en nombre d'arêtes, et pourquoi ne suffit-il plus sur un graphe pondéré ?

À vous

L'exercice construit un petit graphe en listes d'adjacence, puis enchaîne les applications : DFS et BFS, comptage des composantes connexes, détection de cycle, et plus court chemin en nombre d'arêtes avec reconstitution de l'itinéraire — c'est le tableau des prédécesseurs qui fait le travail, et c'est la partie qu'on oublie le plus souvent.

Le squelette contient un DFS sans marquage : lancez-le d'abord sur le graphe cyclique fourni, avec la garde qui l'empêche de tourner à l'infini, et regardez combien de fois chaque sommet est visité. C'est plus convaincant que la mise en garde du cours.

Une dernière partie, facultative, implémente Dijkstra avec une recherche linéaire du minimum, puis vous invite à le rebrancher sur le tas du chapitre 8.

Exercice · JavaScript · à vous de jouer

Écrivez DFS et BFS avec marquage, comptez les composantes, détectez un cycle, puis lancez Dijkstra.

En attente
// Listes d'adjacence. Deux composantes connexes, et un cycle dans la première.
const G = {
  A: ["B", "C"],
  B: ["A", "D"],
  C: ["A", "D"],
  D: ["B", "C"],      // A-B-D-C-A : un cycle
  E: ["F"],
  F: ["E"],           // seconde composante
};

// ── Le parcours SANS marquage, pour voir ──────────────────────────────────
function dfsSansMarquage(g, s, visites = {}, garde = { n: 0 }) {
  if (garde.n++ > 60) return visites;         // sans cette garde : pile pleine
  visites[s] = (visites[s] ?? 0) + 1;
  for (const v of g[s]) dfsSansMarquage(g, v, visites, garde);
  return visites;
}

// ── Parcours en profondeur ────────────────────────────────────────────────
function dfs(g, depart, vus = new Set(), ordre = []) {
  // ← à écrire : marquer, traiter, puis descendre chez les voisins non vus
  return ordre;
}

// ── Parcours en largeur, avec prédécesseurs ───────────────────────────────
function bfs(g, depart) {
  const vus = new Set([depart]);
  const pere = { [depart]: null };
  const file = [depart];
  const ordre = [];
  while (file.length > 0) {
    const u = file.shift();
    ordre.push(u);
    for (const v of g[u]) {
      // ← à écrire : marquer À L'ENFILEMENT, noter le père, enfiler
    }
  }
  return { ordre, pere };
}

// Reconstitue le chemin en remontant les pères depuis l'arrivée.
function chemin(pere, arrivee) {
  if (!(arrivee in pere)) return null;
  const c = [];
  for (let s = arrivee; s !== null; s = pere[s]) c.unshift(s);
  return c;
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez dfs() et complétez bfs().
// 2. Comptez les composantes connexes de G.
// 3. Détectez le cycle : un voisin déjà vu qui n'est PAS le père immédiat.

console.log("sans marquage :", JSON.stringify(dfsSansMarquage(G, "A")));

Console de sortie
Le résultat s'affiche dans la console

En travaux pratiques

Travaux pratiques 9 · sur machine

Le calculateur d'itinéraires

Faire converger toute la bibliothèque du semestre sur un problème réel : charger un réseau de transport, le parcourir, et calculer le plus court chemin avec la file de priorité du TP 8.

4 h
Avant de commencer
  • Les TP 5 à 8 : liste, file, tas, file de priorité
  • Un fichier de réseau : arrêts et liaisons avec durées — le vôtre, ou celui d'une ville ouverte
  1. 1. Choisir la représentation

    Représentez le graphe de deux façons : matrice d'adjacence et listes d'adjacence. Mesurez la mémoire de chacune sur votre réseau, et calculez sa densité.

  2. 2. Charger le réseau

    Lisez le fichier et construisez le graphe. Comptez sommets, arêtes, et vérifiez qu'aucun arrêt cité dans une liaison n'est absent de la liste des arrêts.

  3. 3. Parcours en largeur

    Écrivez le parcours en largeur avec votre file. Utilisez-le pour trouver l'itinéraire en un minimum de CORRESPONDANCES entre deux arrêts, et reconstituez le chemin.

  4. 4. Parcours en profondeur

    Écrivez-le avec votre pile, puis en récursif. Utilisez-le pour compter les composantes connexes du réseau, et identifiez les arrêts isolés.

  5. 5. Détecter un cycle

    Sur un graphe orienté de dépendances — par exemple l'ordre des travaux d'un chantier —, détectez un cycle, puis produisez un ordre topologique quand il n'y en a pas.

  6. 6. Dijkstra

    Implémentez le plus court chemin en TEMPS, avec la file de priorité du TP 8. Comparez le résultat à celui du parcours en largeur et expliquez la différence.

  7. 7. Mesurer ce que le tas apporte

    Écrivez aussi la version de Dijkstra qui cherche le minimum par balayage linéaire. Comparez les deux sur un réseau de mille, puis de cent mille sommets.

  8. 8. Le piège

    Ajoutez au réseau une liaison de durée négative — une correspondance qui ferait gagner du temps. Exécutez Dijkstra et expliquez pourquoi le résultat est faux.

C'est réussi quand
  • Votre chargeur signale un arrêt manquant plutôt que de planter
  • Le parcours en largeur et Dijkstra donnent des chemins DIFFÉRENTS, et vous savez expliquer lequel répond à quelle question
  • La version à tas bat nettement la version linéaire à cent mille sommets
  • Vous savez dire précisément quelle hypothèse de Dijkstra une arête négative viole

Ce que la suite en fait

Le chapitre 10 clôt le semestre en prenant de la hauteur sur les manières de chercher, et Dijkstra y servira d'exemple : c'est un algorithme glouton, dont la correction n'est garantie que sous une hypothèse précise — les poids positifs. C'est exactement ce que le chapitre dira des algorithmes gloutons en général : ils sont rapides et souvent faux, et il faut prouver qu'ils ne le sont pas.

Le parcours en profondeur y reviendra aussi, sous un autre nom : le retour sur trace est un DFS dans un arbre de choix qu'on ne construit jamais explicitement.

À retenir

Flashcards · 1 / 5Toucher pour retourner
Fin de la leçon

Vous avez parcouru les 9 sections.

Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.