Séance 5 — Graphes

Parcourir, ordonner, trouver le plus court chemin et l’arbre couvrant minimal

Dr. El Hadji Bassirou TOURÉ

Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar

Objectifs de la séance

Ce que la séance construit

Les graphes représentent des relations : des villes reliées par des routes, des cours liés par leurs prérequis. La séance construit leurs deux représentations, les parcours en largeur et en profondeur, le tri topologique, les plus courts chemins de Dijkstra et l’arbre couvrant minimal de Kruskal, chacun montré sur le code Java du cours, pas à pas.

  • Modéliser un problème par un graphe (orienté ou non, pondéré) et en lire le vocabulaire : degré, chemin, cycle, connexité.
  • Construire la matrice et les listes d’adjacence d’un graphe et comparer leurs coûts en mémoire et en temps.
  • Dérouler un parcours en largeur (file, distances en nombre d’arêtes, arbre de parcours) et un parcours en profondeur (pile d’appels, ordres préfixe et postfixe).
  • Détecter un cycle dans un graphe orienté et produire un tri topologique par l’ordre postfixe inversé.
  • Dérouler l’algorithme de Dijkstra avec le tas de la Séance 4 et expliquer pourquoi un poids négatif le met en défaut.
  • Construire un arbre couvrant minimal par l’algorithme de Kruskal et mesurer le coût de son test de cycle.

Des villes reliées par des routes

Un réseau routier est fait de relations : huit villes du centre et du sud, dix tronçons.

Graphe

Un graphe \(G=(V,E)\) : un ensemble \(V\) de sommets et un ensemble \(E\) d’arêtes, chacune reliant deux sommets. Ici \(|V|=8\) villes, \(|E|=10\) routes ; le poids d’une arête est sa longueur en km.

Lecture. Les numéros 0 à 7, en gris, sont ceux du code Java ; les noms ne servent qu’à l’affichage.

Le vocabulaire des graphes

Arêtes et degrés

  • non orienté : l’arête \(v\)–\(w\) se parcourt dans les deux sens ; orienté : l’arc \(v\to w\) dans un seul (un prérequis).
  • pondéré : un poids par arête (km, FCFA).
  • degré \(\deg(v)\) : nombre d’arêtes en \(v\) (orienté : degré sortant et entrant).

Somme des degrés

\(\sum_{v\in V}\deg(v)=2\,|E|\) : chaque arête compte pour ses deux extrémités.

Chemins

  • chemin : sommets reliés deux à deux ; longueur : nombre d’arêtes (ou somme des poids).
  • cycle : chemin qui revient à son départ sans repasser par une arête.
  • connexe : un chemin relie deux sommets quelconques ; sinon, plusieurs composantes connexes.
  • arbre : graphe connexe sans cycle, à \(V-1\) arêtes. Orienté sans cycle : DAG.

Lecture. \(\deg(\text{Kolda})=4\) ; \(3+2+3+1+2+4+2+3=20=2\times10\) ; Kolda–Sédhiou–Ziguinchor–Kolda est un cycle.

Matrice d’adjacence et listes d’adjacence

Matrice : a[v][w] vaut 1 si l’arête \(v\)–\(w\) existe, symétrique pour un graphe non orienté. Listes : adj[v] contient les voisins de \(v\), dans l’ordre des ajouts.

Lecture. La ligne 5 (Kolda) de la matrice contient quatre 1, comme la liste 5 : le degré. La matrice range aussi les 44 zéros.

La classe Graphe

private final int V;                // sommets 0..V-1
private final boolean oriente;
private int E;                      // aretes (ou arcs)
private final List<Integer>[] adj;  // adj[v] : voisins de v

public void addEdge(int v, int w) {
    adj[v].add(w);                // ajout en queue
    if (!oriente) adj[w].add(v);  // non oriente : aussi dans adj[w]
    E++;
}

public List<Integer> adj(int v) {
    return adj[v];
}

Lecture. Constructeur non montré : \(V\) listes vides. Non orienté, chaque arête entre dans deux listes : \(2E\) entrées, ajoutées en queue. degre(v) rend adj[v].size().

Les listes d’adjacence, pas à pas

Sortie \(\rightarrow\) 

8 10
[0, 4, 6, 7]

Lecture. Ligne 1 : 8 listes vides, une par sommet (les villes sont numérotées de 0 à 7).

Lecture. Ligne 2 : addEdge(0, 1), Kaolack–Kaffrine : 1 en queue de la liste 0, 0 en queue de la liste 1.

Lecture. Ligne 2 : addEdge(0, 7), Kaolack–Ziguinchor : 7 en queue de la liste 0, 0 en queue de la liste 7.

Lecture. Ligne 2 : addEdge(0, 5), Kaolack–Kolda : 5 en queue de la liste 0, 0 en queue de la liste 5.

Lecture. Ligne 2 (suite) : addEdge(1, 2), Kaffrine–Tambacounda : une entrée dans chacune des deux listes.

Lecture. Ligne 2 (suite) : addEdge(2, 3), Tambacounda–Kédougou : une entrée dans chacune des deux listes.

Lecture. Ligne 2 (suite) : addEdge(2, 4), Tambacounda–Vélingara : une entrée dans chacune des deux listes.

Lecture. Ligne 2 (suite) : addEdge(4, 5), Vélingara–Kolda : une entrée dans chacune des deux listes.

Lecture. Ligne 2 (suite) : addEdge(5, 6), Kolda–Sédhiou : une entrée dans chacune des deux listes.

Lecture. Ligne 2 (suite) : addEdge(5, 7), Kolda–Ziguinchor : une entrée dans chacune des deux listes.

Lecture. Ligne 2 (suite) : dixième arête, Sédhiou–Ziguinchor : 10 arêtes, 20 entrées.

Lecture. Ligne 3 : 8 sommets, 10 arêtes ; non orienté, chaque arête figure dans deux listes.

Lecture. Ligne 4 : les voisins de Kolda, dans l’ordre des ajouts : Kaolack, Vélingara, Sédhiou, Ziguinchor. Son degré est 4.

Les coûts des deux représentations

opération matrice listes
mémoire \(\Theta(V^2)\) \(\Theta(V+E)\)
l’arête \(v\)–\(w\) existe-t-elle ? \(\Theta(1)\) \(\mathcal{O}(\deg v)\)
parcourir les voisins de \(v\) \(\Theta(V)\) \(\Theta(\deg v)\)
parcourir tout le graphe \(\Theta(V^2)\) \(\Theta(V+E)\)

Le choix suit la densité

Un réseau routier est creux : \(E\) est de l’ordre de \(V\). Pour \(V=10\,000\) villes et \(E=30\,000\) routes, la matrice compte \(10^8\) cases, les listes \(60\,000\) entrées. La matrice convient à un graphe dense (\(E\) proche de \(V^2\)) ou à des tests d’arête répétés.

Lecture. Le cours utilise les listes, comme Sedgewick et Wayne (§ 4.1).

Ce que coûte un parcours selon la représentation

Random r = new Random(221);
for (int V : new int[] {1000, 2000, 4000}) {
    Graphe g = new Graphe(V, false);
    GrapheMatrice m = new GrapheMatrice(V);
    for (int[] a : Generateur.aretes(V, 4 * V, r)) {  // E = 4V
        g.addEdge(a[0], a[1]);
        m.addEdge(a[0], a[1]);
    }
    ParcoursLargeur p = new ParcoursLargeur(g, 0);
    long cases = m.parcoursLargeur(0);
    System.out.printf("V = %d, E = %d : listes %d lues, matrice %d lues%n",
        V, g.E(), p.examines, cases);
}

Résultat \(\rightarrow\)

V = 1000, E = 4000 : listes 8000 lues, matrice 1000000 lues
V = 2000, E = 8000 : listes 16000 lues, matrice 4000000 lues
V = 4000, E = 16000 : listes 32000 lues, matrice 16000000 lues

Lecture. \(E=4V\) : les listes lisent \(2E\) entrées, la matrice \(V^2\) cases : 125 fois plus à \(V=1\,000\), 500 fois à \(4\,000\).

Exercice : deux représentations

Un graphe de six sommets

Arêtes, dans cet ordre : A–B, A–C, B–C, B–D, C–E, D–E, D–F, E–F.

  1. La matrice d’adjacence : combien de 1 ?
  2. Les listes d’adjacence (ajout en queue).
  3. Les degrés et leur somme.
  4. Pour \(V=10\,000\), \(E=30\,000\) : cases de la matrice, entrées des listes.

Correction

  1. Symétrique, deux 1 par arête : 16 sur 36 cases.
  2. A : B, C ; B : A, C, D ; C : A, B, E ; D : B, E, F ; E : C, D, F ; F : D, E.
  3. \(2+3+3+3+3+2=16=2\times8\).
  4. \(10^8\) cases contre \(2E=60\,000\) entrées.
sommet A B C D E F
degré 2 3 3 3 3 2

Le parcours en largeur

Parcours en largeur {(breadth-first search)}

Depuis une source \(s\) : marquer \(s\) et la mettre dans une file. Tant que la file n’est pas vide, en retirer le sommet \(v\) de tête, puis marquer chaque voisin \(w\) non marqué, avec \(\texttt{distTo[w]}=\texttt{distTo[v]}+1\) et \(\texttt{edgeTo[w]}=v\), et le mettre en queue.

Invariant de distTo

La file ne contient que des sommets à distance \(d\), suivis de sommets à distance \(d+1\) : les sommets sortent par distance croissante, et distTo[v] est le plus petit nombre d’arêtes d’un chemin de \(s\) à \(v\). Chaque sommet entre une fois dans la file et chaque liste est lue une fois : \(\Theta(V+E)\).

Lecture. La file est l’ArrayDeque du JDK (Séance 1, § 6.2) : add en queue, remove en tête. Les arêtes edgeTo forment l’arbre de parcours.

Le parcours en largeur, pas à pas

Sortie \(\rightarrow\) 

KaolackKaolack KaffrineKaolack Kaffrine ZiguinchorKaolack Kaffrine Ziguinchor KoldaKaolack Kaffrine Ziguinchor Kolda TambacoundaKaolack Kaffrine Ziguinchor Kolda Tambacounda SedhiouKaolack Kaffrine Ziguinchor Kolda Tambacounda Sedhiou VelingaraKaolack Kaffrine Ziguinchor Kolda Tambacounda Sedhiou Velingara Kedougou

Lecture. Lignes 1–3 : Kaolack (0) est marqué et entre dans la file ; en gras, distTo : distTo[0] vaut 0.

Lecture. Lignes 5–12 : Kaolack sort ; ses voisins 1, 7, 5 sont marqués à distance 1 et entrent en file.

Lecture. Lignes 5–12 : Kaffrine sort ; Tambacounda est marqué à distance 2 et entre en file.

Lecture. Lignes 5–12 : Ziguinchor sort ; Sédhiou est marqué à distance 2 et entre en file.

Lecture. Lignes 5–12 : Kolda sort ; Vélingara est marqué à distance 2 et entre en file.

Lecture. Lignes 5–12 : Tambacounda sort ; Kédougou est marqué à distance 3 et entre en file.

Lecture. Lignes 5–8 : Sédhiou sort ; tous ses voisins sont déjà marqués : la file ne reçoit rien.

Lecture. Lignes 5–8 : Vélingara sort ; tous ses voisins sont déjà marqués : la file ne reçoit rien.

Lecture. Lignes 5–8 : Kédougou (distance 3) sort, la file est vide ; en foncé, l’arbre de parcours.

Le chemin le plus court en tronçons

public List<Integer> chemin(int v) {
    if (!marque[v]) return null;
    LinkedList<Integer> c = new LinkedList<>();
    for (int x = v; x != s; x = edgeTo[x]) c.addFirst(x);
    c.addFirst(s);  // puis la source, en tete
    return c;
}
// dans main :
String[] nom = Reseau.VILLES;
ParcoursLargeur p = new ParcoursLargeur(Reseau.routes(), 0);
for (int v : new int[] {3, 4}) {
    System.out.println(nom[v] + " : " + p.distance(v) + " troncons, "
        + Reseau.noms(p.chemin(v), nom));
}

Résultat \(\rightarrow\)

Kedougou : 3 troncons, Kaolack Kaffrine Tambacounda Kedougou
Velingara : 2 troncons, Kaolack Kolda Velingara

Lecture. chemin(v) remonte l’arbre de parcours de \(v\) à \(s\) par edgeTo : Kédougou à 3 tronçons, Vélingara à 2.

Exercice : un parcours en largeur

Le graphe de six sommets, depuis A

Listes : A : B, C ; B : A, C, D ; C : A, B, E ; D : B, E, F ; E : C, D, F ; F : D, E.

  1. Le sommet qui sort à chaque tour et la file après le tour.
  2. distTo et l’arbre de parcours.
  3. Le chemin de A à F.

tour sort marqués file après
1 A B, C B C
2 B D C D
3 C E D E
4 D F E F
5 E --- F
6 F --- vide

Correction

  1. Ci-dessus : les sommets sortent par distance croissante.
  2. A 0, B 1, C 1, D 2, E 2, F 3 ; arbre A–B, A–C, B–D, C–E, D–F.
  3. A, B, D, F : 3 arêtes.

Le parcours en profondeur

Parcours en profondeur {(depth-first search)}

dfs(v) marque \(v\), puis appelle dfs(w) sur chaque voisin \(w\) non marqué, avant de passer au voisin suivant : le parcours s’enfonce aussi loin que possible, puis revient en arrière. La pile est celle des appels récursifs.

Deux ordres de visite

préfixe : \(v\) à l’entrée de dfs(v) ; postfixe : \(v\) à la sortie, après tous les sommets atteints depuis lui — comme pour les arbres de la Séance 4. Chaque sommet est marqué une fois, chaque liste lue une fois : \(\Theta(V+E)\).

Lecture. Le parcours en profondeur sert à trouver les composantes connexes, les cycles et, sur un graphe orienté, un tri topologique.

Le parcours en profondeur, pas à pas

Sortie \(\rightarrow\) 

Kaolack Kaffrine Tambacounda Kedougou Velingara Kolda Sedhiou Ziguinchor
Kedougou Ziguinchor Sedhiou Kolda Velingara Tambacounda Kaffrine Kaolack

Lecture. Lignes 10, 2–3 : dfs(0) marque Kaolack et l’ajoute au préfixe ; la pile d’appels contient dfs(0). Les nombres en gras donnent l’ordre préfixe.

Lecture. Lignes 4–5, 2–3 : le premier voisin de Kaolack, 1 (Kaffrine), n’est pas marqué : dfs(1) le marque, avant d’examiner les autres voisins.

Lecture. Lignes 4–5, 2–3 : Kaffrine examine 0 (marqué), puis 2 : dfs(2), Tambacounda.

Lecture. Lignes 4–5, 2–3 : Tambacounda examine 1 (marqué), puis 3 : dfs(3), Kédougou. Le parcours s’enfonce.

Lecture. Lignes 4–5, 7 : Kédougou n’a qu’un voisin, déjà marqué : dfs(3) se termine, 3 entre dans le postfixe et l’appel est dépilé. Retour dans dfs(2).

Lecture. Lignes 4–5, 2–3 : Tambacounda passe à son voisin suivant, 4 : dfs(4), Vélingara.

Lecture. Lignes 4–5, 2–3 : Vélingara examine 2 (marqué), puis 5 : dfs(5), Kolda.

Lecture. Lignes 4–5, 2–3 : Kolda examine 0 et 4 (marqués), puis 6 : dfs(6), Sédhiou.

Lecture. Lignes 4–5, 2–3 : Sédhiou examine 5 (marqué), puis 7 : dfs(7), Ziguinchor. La pile contient 7 appels.

Lecture. Lignes 4–5, 7 : les voisins de Ziguinchor (0, 5, 6) sont tous marqués : dfs(7) se termine, puis dfs(6) ; 7 et 6 entrent dans le postfixe.

Lecture. Lignes 4–5, 7 : en remontant, chaque appel trouve ses voisins restants marqués et se termine : 5, 4, 2, 1, puis 0. Les 7 arêtes rouges et foncées forment l’arbre de parcours.

Lecture. Ligne 11 : l’ordre préfixe, celui des entrées dans dfs : chaque ville est visitée avant ses descendants.

Lecture. Ligne 12 : l’ordre postfixe, celui des sorties : une ville n’y entre qu’après toutes celles atteintes depuis elle. Kaolack, la racine, sort en dernier.

Quoi, pourquoi, quand ça échoue : les deux parcours

Quoi

Largeur : une file ; les sommets sortent par distance croissante. Profondeur : la pile des appels ; un chemin aussi long que possible, puis le retour.

Pourquoi on les utilise

Les deux coûtent \(\Theta(V+E)\). La largeur donne les plus courts chemins en nombre d’arêtes ; la profondeur donne les ordres préfixe et postfixe, d’où les cycles et le tri topologique.

Quand ça échoue

La largeur compte des tronçons, pas des kilomètres : Kaolack–Kolda–Vélingara a 2 tronçons et 395 km, Kaolack–Kaffrine–Tambacounda–Vélingara 3 tronçons et 376 km. La profondeur récursive sur une chaîne de quelques milliers de sommets dépasse la pile par défaut de la machine virtuelle (StackOverflowError, sauf avec -Xss).

Exercice : un parcours en profondeur

Le même graphe, depuis A

Listes : A : B, C ; B : A, C, D ; C : A, B, E ; D : B, E, F ; E : C, D, F ; F : D, E.

  1. Les appels successifs et la pile la plus haute.
  2. Les ordres préfixe et postfixe.
  3. L’arbre de parcours, comparé à celui de la largeur.

rang 1 2 3 4 5 6
préfixe A B C E D F
postfixe F D E C B A

Correction

  1. dfs(A), dfs(B) (A marqué), dfs(C), dfs(E), dfs(D), dfs(F) : pile A B C E D F, six appels.
  2. F n’a plus de voisin libre : il sort le premier ; A sort en dernier.
  3. Une chaîne A–B–C–E–D–F, de hauteur 5 ; la largeur donnait un arbre de hauteur 3.

Des prérequis entre cours

Graphe orienté et tri topologique

Un arc \(v\to w\) : le cours \(v\) est un prérequis du cours \(w\). Un tri topologique est un ordre des sommets où tout arc va d’un sommet vers un sommet placé après lui. Il en existe un si et seulement si le graphe est sans cycle (DAG).

Lecture. Algo : algorithmique ; ProgC : programmation C ; Maths : mathématiques discrètes ; Java : programmation Java ; BD : bases de données ; SDA : structures de données ; GL : génie logiciel ; IA : intelligence artificielle.

Le tri topologique, pas à pas

Sortie \(\rightarrow\) 

Maths Algo BD ProgC Java GL SDA IA

Lecture. Lignes 9–10, 2 : \(v=0\) : Algo n’est pas marqué, dfs(0) le marque. Ses successeurs sont 1 (ProgC) et 4 (BD).

Lecture. Lignes 3–4, 2 : le premier successeur, 1 (ProgC), n’est pas marqué : dfs(1).

Lecture. Lignes 3–4, 2 : ProgC a pour successeur 3 (Java) : dfs(3).

Lecture. Lignes 3–4, 2 : Java a pour successeurs 5 (SDA) et 6 (GL) ; le premier, 5, est appelé.

Lecture. Lignes 3–4, 2, 6 : dfs(7) : IA, marqué, n’a aucun successeur, l’appel se termine et 7 est empilé en tête de ordre, le premier.

Lecture. Lignes 2–4, 6 : SDA se termine (5 en tête, devant 7) ; Java passe à 6 (GL), sans successeur, aussitôt empilé.

Lecture. Ligne 6 : Java puis ProgC se terminent : 3, puis 1, en tête. Chaque cours entre devant tous ceux qu’il précède.

Lecture. Lignes 2–4, 6 : retour dans Algo : dfs(4), BD, dont le successeur GL est déjà marqué ; BD, puis Algo, sont empilés.

Lecture. Lignes 9–10, 2–4, 6 : \(v=1\) est marqué ; \(v=2\), Maths, ne l’est pas : ses successeurs SDA et IA sont marqués, il est empilé en tête. Les sommets 3 à 7 sont marqués.

Lecture. Ligne 12 : l’ordre de la pile, du sommet vers le fond : l’ordre postfixe inversé. Chaque arc va d’un cours vers un cours placé après lui.

Justesse de l’ordre postfixe inversé

Un arc \(v\to w\) examiné pendant dfs(v)

  1. \(w\) n’est pas marqué : dfs(w) est appelé et se termine avant dfs(v).
  2. \(w\) est marqué et son appel est terminé : \(w\) est déjà dans le postfixe.
  3. \(w\) est marqué et son appel est en cours : un chemin \(w\to\dots\to v\) existe, et l’arc \(v\to w\) ferme un cycle, impossible dans un DAG.

Dans les cas 1 et 2, \(w\) sort avant \(v\) : dans l’ordre inversé, \(v\) précède \(w\).

Détecter un cycle

Le cas 3 est le test : un arc vers un sommet dont l’appel est en cours (surPile) révèle un cycle.

Détecter un cycle en Java

private void dfs(Graphe g, int v) {
    marque[v] = true;
    surPile[v] = true;
    for (int w : g.adj(v)) {
        if (cycle != null) return;
        if (!marque[w]) {
            edgeTo[w] = v;
            dfs(g, w);
        } else if (surPile[w]) {  // arc vers la pile
            cycle = remonte(v, w);
        }
    }
    surPile[v] = false;
}
Graphe g = Reseau.prerequis();
CycleOriente c = new CycleOriente(g);
System.out.println(c.cycle());
g.addEdge(5, 1);  // SDA -> ProgC
c = new CycleOriente(g);
String s = Reseau.cours(c.cycle());
System.out.println(s);

Résultat \(\rightarrow\)

null
SDA ProgC Java SDA

Lecture. remonte(v, w), non montrée, suit edgeTo de \(v\) à \(w\). Les prérequis forment un DAG : null ; un arc SDA\(\to\)ProgC saisi par erreur ferme un cycle.

Exercice : un tri topologique

Une inscription

Tâches 0 à 5 et arcs, dans cet ordre : 0\(\to\) 1, 1\(\to\) 3, 2\(\to\) 3, 1\(\to\) 4, 3\(\to\) 4, 3\(\to\) 5.

  1. La boucle sur \(v=0..5\) et les appels : l’ordre postfixe.
  2. L’ordre topologique.
  3. On ajoute l’arc Codification\(\to\)Paiement : le cycle.

rang 1 2 3 4 5 6
postfixe 4 5 3 1 0 2
ordre 2 0 1 3 5 4

Correction

  1. dfs(0), dfs(1), dfs(3), dfs(4) sort, dfs(5) sort, puis 3, 1, 0 ; \(v=2\) : Visite sort seule.
  2. Visite, Dossier, Paiement, Carte, Bibliothèque, Codification.
  3. L’arc 4\(\to\) 1 vise Paiement, dont l’appel est en cours : cycle 4, 1, 3, 4.

Chemin le plus court et relâchement

Le problème

Un graphe pondéré, à poids positifs ou nuls ; la longueur d’un chemin est la somme de ses poids, et la distance \(d(s,v)\) la plus petite longueur d’un chemin de \(s\) à \(v\).

Relâcher l’arête \(v\)–\(w\)

\[ \text{si } \texttt{distTo[v]}+p(v,w)<\texttt{distTo[w]}~: \texttt{distTo[w]}\leftarrow\texttt{distTo[v]}+p(v,w), \texttt{edgeTo[w]}\leftarrow v\text{--}w \]

Dijkstra : à chaque tour, extraire le sommet non définitif de plus petite distance provisoire (le tas de la Séance 4), le déclarer définitif et relâcher ses arêtes.

Lecture. La largeur choisissait Kaolack–Kolda–Vélingara : 2 tronçons, \(272+123=395\) km. Le plus court en kilomètres passe par Kaffrine et Tambacounda : 376 km.

Le relâchement en Java

private void relacher(Arete e, int v) {
    int w = e.autre(v);
    if (distTo[v] + e.poids < distTo[w]) {  // meilleur chemin par v
        distTo[w] = distTo[v] + e.poids;
        edgeTo[w] = e;
        tas.insert(new Entree(w, distTo[w]));
        insertions++;
    }
}

Une entrée par amélioration

Une Entree est un couple (sommet, distance), comparé par distance puis par numéro. Le tas de la Séance 4 ne sait pas diminuer une clé déjà rangée : on insère une nouvelle entrée, et l’ancienne, périmée, est ignorée à sa sortie (if (fait[v]) continue). Sedgewick et Wayne utilisent un tas indexé (IndexMinPQ), qui la remplace.

L’algorithme de Dijkstra, pas à pas

Sortie \(\rightarrow\) 

00 630 63 2510 63 251 2720 63 251 272 2730 63 251 272 273 3570 63 251 272 273 357 3760 63 251 272 273 357 376 506
[0, 63, 273, 506, 376, 272, 357, 251]

Lecture. Lignes 1–3 : distTo vaut \(\infty\), sauf distTo[0] = 0 ; le tas reçoit 0:0.

Lecture. Lignes 5–9 : Kaolack sort ; Kaffrine reçoit 63, Ziguinchor 251, Kolda 272.

Lecture. Lignes 5–9 : Kaffrine sort à 63 ; Tambacounda reçoit 273. Trait épais foncé : les arêtes edgeTo.

Lecture. Lignes 5–9 : Ziguinchor sort à 251 ; Sédhiou reçoit 414, Kolda reste à 272.

Lecture. Lignes 5–9 : Kolda sort à 272 ; Sédhiou passe à 357, Vélingara reçoit 395.

Lecture. Lignes 5–9 : Tambacounda sort (273) ; Kédougou reçoit 506, Vélingara 376.

Lecture. Lignes 5–9 : Sédhiou sort à 357, définitif ; aucune distance n’est améliorée.

Lecture. Lignes 5–9 : Vélingara sort à 376, définitif ; aucune distance n’est améliorée.

Lecture. Lignes 5–6 : 395:4 sort, mais Vélingara est déjà définitif : entrée périmée.

Lecture. Lignes 5–6 : 414:6 sort, mais Sédhiou est déjà définitif : entrée périmée.

Lecture. Lignes 5–9 : Kédougou sort à 506, définitif ; aucune distance n’est améliorée.

Lecture. Ligne 11 : distances définitives ; en foncé, l’arbre des plus courts chemins.

Le chemin le plus court en Java

public List<Integer> chemin(int v) {
    if (distTo[v] == Integer.MAX_VALUE) return null;
    LinkedList<Integer> c = new LinkedList<>();
    for (int x = v; x != s; x = edgeTo[x].autre(x)) c.addFirst(x);
    c.addFirst(s);
    return c;
}
// dans main :
String[] nom = Reseau.VILLES;
Dijkstra d = new Dijkstra(Reseau.routesKm(), 0);
System.out.println(d.distance(4) + " km : " + Reseau.noms(d.chemin(4), nom));
System.out.println(d.extractions + " " + d.perimees + " " + d.insertions);

Résultat \(\rightarrow\)

376 km : Kaolack Kaffrine Tambacounda Velingara
10 2 10

Lecture. 376 km par Kaffrine et Tambacounda (la largeur passait par Kolda). 10 extractions, dont 2 entrées périmées : Sédhiou 414, Vélingara 395.

Quoi, pourquoi, quand ça échoue : Dijkstra

Quoi

Un tas des sommets atteints, rangés par distance provisoire ; à chaque tour, le plus proche devient définitif et ses arêtes sont relâchées.

Justesse et coût

Quand \(v\) sort, \(\texttt{distTo[v]}=d(s,v)\) : un chemin plus court quitterait les sommets définitifs par un sommet \(u\) avec \(\texttt{distTo[u]}\geq\texttt{distTo[v]}\), et des poids positifs ou nuls ne le raccourciraient plus. Au plus \(E+1\) entrées dans le tas, chacune insérée et extraite en \(\mathcal{O}(\log E)=\mathcal{O}(\log V)\) : \(\mathcal{O}((V+E)\log V)\).

Quand ça échoue

Avec un poids négatif, un sommet déjà définitif peut encore être amélioré, et ses successeurs gardent une distance fausse. L’algorithme de Bellman-Ford (CLRS, § 22.1) traite ce cas.

Un poids négatif, pas à pas

Sortie \(\rightarrow\) 

00 20 2 40 2 4 4
[0, 1, 4, 4]

Lecture. Lignes 1–3 : le même code, sur quatre sommets reliés par des arcs ; l’arc \(2\to1\) pèse \(-3\).

Lecture. Lignes 5–9 : 0 sort à 0 : le sommet 1 reçoit 2, le sommet 2 reçoit 4.

Lecture. Lignes 5–9 : 1 sort à 2 et devient définitif : le sommet 3 reçoit \(2+2=4\).

Lecture. Lignes 5–9 : 2 sort à 4 (égalité avec 3 : le plus petit numéro d’abord) ; l’arc \(2\to1\) donne \(4-3=1<2\) : distTo[1] passe à 1, bien que 1 soit définitif.

Lecture. Lignes 5–6 : l’entrée 1:1 sort, mais le sommet 1 est déjà définitif : elle est ignorée, et sa nouvelle distance n’est jamais propagée au sommet 3.

Lecture. Lignes 5–9 : 3 sort à 4, distance calculée depuis l’ancienne distance du sommet 1.

Lecture. Ligne 11 : [0, 1, 4, 4] : le chemin \(0\to2\to1\to3\) coûte \(4-3+2=3\), et distTo[3] vaut 4. Avec un poids négatif, un sommet extrait n’a pas forcément sa distance définitive.

Le coût mesuré

{.t-scriptsize}Classe Dijkstra du cours, graphes connexes au hasard (Random(2026)), \(E=4V\), poids de 1 à 1 000.

Lecture. Les comparaisons restent entre \(0{,}64\) et \(0{,}68\) fois \((V+E)\log_2V\) : le coût suit la borne. Entrées insérées : \(1\,848\) pour \(4\,000\) arêtes à \(V=1\,000\).

Exercice : l’algorithme de Dijkstra

Six sommets, depuis A

  1. À chaque tour : le sommet extrait et distTo.
  2. Les entrées périmées.
  3. Le chemin de A à F, l’arbre des plus courts chemins.

extrait A B C D E F
A (0) 0 4 3 \(\infty\) \(\infty\) \(\infty\)
C (3) 0 4 3 11 10 \(\infty\)
B (4) 0 4 3 9 10 \(\infty\)
D (9) 0 4 3 9 10 15
E (10) 0 4 3 9 10 15
D:11 périmée, ignorée
F (15) 0 4 3 9 10 15

Correction

  1. Les distances extraites croissent : 0, 3, 4, 9, 10, 15.
  2. D:11, posée par C, puis améliorée à 9 par B.
  3. A, B, D, F : 15 ; arbre A–B, A–C, B–D, C–E, D–F.

L’arbre couvrant minimal

Arbre couvrant minimal

Dans un graphe connexe pondéré non orienté, un arbre couvrant relie tous les sommets par \(V-1\) arêtes, sans cycle. Un arbre couvrant de poids minimal {(minimum spanning tree)}, ou arbre couvrant minimal, a le plus petit poids total : relier les huit villes par de la fibre optique, le long des routes, au moindre coût.

Propriété de la coupe, algorithme de Kruskal

Pour tout partage des sommets en deux groupes, l’arête la plus légère qui relie les deux groupes appartient à un arbre couvrant minimal. Kruskal : examiner les arêtes par poids croissant et garder chacune qui ne ferme pas de cycle avec les arêtes déjà gardées.

Lecture. Une arête gardée relie deux morceaux de la forêt : toutes les arêtes plus légères ont déjà été examinées, elle est donc la plus légère entre son morceau et le reste.

L’algorithme de Kruskal, pas à pas

Sortie \(\rightarrow\) 

6363 8563 85 10363 85 103 12363 85 103 123 16363 85 103 123 163 21063 85 103 123 163 210 233
total 980

Lecture. Lignes 1–3 : les 10 arêtes entrent dans un tas rangé par poids ; forêt vide.

Lecture. Lignes 5–10 : Kaolack–Kaffrine (63 km) sort ; relies(foret, 0, 1) est faux : retenue.

Lecture. Lignes 5–10 : Kolda–Sédhiou (85 km) sort ; villes non reliées : retenue.

Lecture. Lignes 5–10 : Tambacounda–Vélingara (103 km) sort ; villes non reliées : retenue.

Lecture. Lignes 5–10 : Vélingara–Kolda (123 km) sort ; villes non reliées : retenue.

Lecture. Lignes 5–10 : Sédhiou–Ziguinchor (163 km) sort ; villes non reliées : retenue.

Lecture. Lignes 5–6 : Kolda–Ziguinchor (188 km) sort ; déjà reliées par Sédhiou : rejetée.

Lecture. Lignes 5–10 : Kaffrine–Tambacounda (210 km) sort ; villes non reliées : retenue.

Lecture. Lignes 5–10 : Tambacounda–Kédougou (233 km) retenue : 7 arêtes, \(V-1\), fin.

Lecture. Ligne 12 : 980 km relient les huit villes ; aucun arbre couvrant n’est plus court.

Le test de cycle et son coût

Choix de cette séance : relies(foret, v, w) lance un ParcoursLargeur sur la forêt depuis \(v\) et teste atteint(w) ; le compteur visites additionne les sommets atteints.

Random r = new Random(221);
for (int V : new int[] {500, 1000, 2000, 4000}) {
    GraphePondere g = new GraphePondere(V, false);
    for (int[] a : Generateur.aretes(V, 4 * V, r)) g.addEdge(a[0], a[1], a[2]);
    Kruskal k = new Kruskal(g);
    System.out.printf(Locale.ROOT, "%5d : %6d examinees, %6d rejets, "
        + "%9d visites, %.3f V^2%n", V, k.examinees, k.rejets,
        k.visites, k.visites / ((double) V * V));
}

Résultat \(\rightarrow\)

  500 :   1718 examinees,   1219 rejets,    611838 visites, 2.447 V^2
 1000 :   2971 examinees,   1972 rejets,   1986671 visites, 1.987 V^2
 2000 :   5681 examinees,   3682 rejets,   7389527 visites, 1.847 V^2
 4000 :  13589 examinees,   9590 rejets,  38476757 visites, 2.405 V^2

Lecture. Jusqu’à \(V\) sommets par arête examinée : les visites restent vers \(2V^2\) : elles sont multipliées par 3 à 5 quand \(V\) double. Avec Union-Find (Séance 6), Kruskal coûte \(\mathcal{O}(E\log E)\), le tri des arêtes.

Exercice : l’algorithme de Kruskal

Six sommets pondérés

  1. Les arêtes par poids croissant, la décision et sa raison.
  2. L’arbre obtenu et son poids.
  3. Le comparer à l’arbre des plus courts chemins depuis A.

arête poids décision
D--E 1 gardée
B--C 2 gardée
A--C 3 gardée
A--B 4 rejetée : A--C--B
B--D 5 gardée
D--F 6 gardée, 5 arêtes : fin

Correction

  1. C–E (7), C–D (8), E–F (9) ne sont jamais examinées.
  2. \(1+2+3+5+6=17\).
  3. Depuis A, l’arbre des plus courts chemins A–B, A–C, B–D, C–E, D–F pèse 25 : un autre problème, un autre arbre.

Les algorithmes côte à côte

question algorithme structure coût sur le réseau du cours
moins d’arêtes largeur file \(\Theta(V+E)\) Kédougou à 3 tronçons
ordres, cycles profondeur pile d’appels \(\Theta(V+E)\) préfixe, postfixe
ordre des prérequis postfixe inversé pile \(\Theta(V+E)\) 8 cours ordonnés
plus courts en km Dijkstra tas (Séance 4) \(\mathcal{O}((V+E)\log V)\) Vélingara à 376 km
arbre couvrant minimal Kruskal tas, test de cycle \(\mathcal{O}(EV)\) ici 980 km sur 1 691

Lecture. Tous sur les listes d’adjacence. Avec Union-Find (Séance 6), Kruskal passe à \(\mathcal{O}(E\log E)\). Les dix routes totalisent \(1\,691\) km ; l’arbre couvrant minimal en garde sept, 980 km.

À retenir

À retenir

  • Graphe \(G=(V,E)\) : orienté ou non, pondéré ou non ; \(\sum\deg(v)=2|E|\).
  • Représentations : matrice \(\Theta(V^2)\), test d’arête en \(\Theta(1)\) ; listes \(\Theta(V+E)\), voisins en \(\Theta(\deg v)\), le choix des graphes creux.
  • Largeur : une file ; distTo = plus petit nombre d’arêtes ; edgeTo = arbre de parcours ; \(\Theta(V+E)\).
  • Profondeur : la pile des appels ; ordres préfixe et postfixe ; \(\Theta(V+E)\).
  • Tri topologique : l’ordre postfixe inversé d’un DAG ; un arc vers un appel en cours révèle un cycle.
  • Dijkstra : tas des distances provisoires, relâchement, poids \(\geq0\) ; \(\mathcal{O}((V+E)\log V)\).
  • Kruskal : arêtes par poids croissant, rejet de celles qui ferment un cycle ; \(V-1\) arêtes gardées.

Prochaine séance

Séance 6 — Ensembles disjoints et tris

Le test de cycle de Kruskal parcourt la forêt à chaque arête. La Séance 6 construit :

  • les ensembles disjoints (Union-Find) : QuickFind, QuickUnion, union pondérée, compression de chemin ;
  • les tris par insertion, par fusion et rapide, et la borne \(\Omega(n\log n)\) des tris par comparaison.

À préparer : le TP de la Séance 5 (Graphe, parcours, Dijkstra et Kruskal, tests JUnit et mesures).

Références

  • Sedgewick, Wayne — Algorithms, 4e éd., Addison-Wesley, 2011 (§ 4.1 : graphes non orientés ; § 4.2 : graphes orientés et tri topologique ; § 4.3 : arbres couvrants minimaux ; § 4.4 : plus courts chemins).
  • Cormen, Leiserson, Rivest, Stein — Introduction to Algorithms, 4e éd., MIT Press, 2022 (ch. 20 : parcours et tri topologique ; ch. 21 : arbres couvrants minimaux ; ch. 22 : plus courts chemins à source unique).
  • Skiena — The Algorithm Design Manual, 3e éd., Springer, 2020 (ch. 7 : parcours de graphes ; ch. 8 : graphes pondérés).
  • Dijkstra — « A note on two problems in connexion with graphs », Numerische Mathematik, 1, 1959.
  • Kruskal — « On the shortest spanning subtree of a graph and the traveling salesman problem », Proceedings of the AMS, 7(1), 1956.
  • Logistics Cluster — Logistics Capacity Assessment, Senegal, § 2.3 (réseau routier, distances entre villes).