Exercices : graphes

Exercice 1 — QCM : vocabulaire des graphes

Pour chaque question, une seule réponse est correcte.

1. Dans un graphe non orienté simple à 5 sommets, le degré maximal d’un sommet est :

  • A. 4
  • B. 5
  • C. 8
  • D. 10
Correction

A. Dans un graphe simple (sans boucle, sans arête multiple), un sommet peut être relié à tous les autres sommets, soit \(n - 1 = 4\) arêtes. C’est le cas du graphe complet \(K_5\). B oublie qu’un sommet ne peut pas être relié à lui-même (pas de boucle). C et D confondent degré et nombre total d’arêtes.

2. Un graphe non orienté a 6 sommets et 9 arêtes. La somme des degrés de tous les sommets est :

  • A. 9
  • B. 12
  • C. 15
  • D. 18
Correction

D. D’après le théorème de la somme des degrés : \(\sum \deg(S) = 2 \times \text{nombre d’arêtes} = 2 \times 9 = 18\). L’erreur A confond la somme avec le nombre d’arêtes. L’erreur B divise au lieu de multiplier.

3. Quelle structure de données utilise-t-on pour implémenter un parcours en largeur (BFS) ?

  • A. une pile
  • B. une file
  • C. un arbre binaire
  • D. un dictionnaire
Correction

B. Le parcours en largeur explore les sommets niveau par niveau : on enfile les voisins non visités et on défile le prochain sommet à traiter. C’est le comportement FIFO d’une file. Une pile (A) donnerait un parcours en profondeur (DFS).

4. On représente un graphe à 4 sommets par la matrice suivante :

$$\begin{pmatrix} 0 & 1 & 0 & 1 \\ 0 & 0 & 1 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 1 & 0 \end{pmatrix}$$

Ce graphe est :

  • A. non orienté et connexe
  • B. orienté car la matrice n’est pas symétrique
  • C. non orienté car la diagonale est nulle
  • D. orienté et complet
Correction

B. La matrice n’est pas symétrique (par exemple, la case (0,1) vaut 1 mais la case (1,0) vaut 0), donc le graphe est orienté. Une diagonale nulle (C) signifie simplement qu’il n’y a pas de boucle, cela ne renseigne pas sur l’orientation. Un graphe complet (D) aurait tous les coefficients non diagonaux à 1.

5. L’algorithme de Dijkstra ne fonctionne pas correctement si :

  • A. le graphe est non orienté
  • B. le graphe contient des arêtes de poids négatif
  • C. le graphe est connexe
  • D. le graphe a plus de 100 sommets
Correction

B. Dijkstra suppose que tous les poids sont positifs ou nuls. Avec des poids négatifs, un sommet déjà marqué pourrait être atteint par un chemin plus court en passant par une arête de poids négatif, ce qui fausse l’algorithme. Le graphe peut être orienté ou non (A, C), et la taille (D) n’est pas un problème.


Exercice 2 — Exercice guidé : passer d’un graphe à ses représentations

On considère le graphe non orienté suivant :

    A --- B
    |   / |
    |  /  |
    C --- D --- E

Arêtes : A–B, A–C, B–C, B–D, C–D, D–E.

  1. Écrire la matrice d’adjacence de ce graphe (sommets dans l’ordre A, B, C, D, E).

  2. Écrire le dictionnaire d’adjacence Python correspondant.

  3. Vérifier que la somme des degrés est bien le double du nombre d’arêtes.

Correction
  1. Matrice d’adjacence (A=0, B=1, C=2, D=3, E=4) :

$$\begin{pmatrix} 0 & 1 & 1 & 0 & 0 \\ 1 & 0 & 1 & 1 & 0 \\ 1 & 1 & 0 & 1 & 0 \\ 0 & 1 & 1 & 0 & 1 \\ 0 & 0 & 0 & 1 & 0 \end{pmatrix}$$

La matrice est bien symétrique (graphe non orienté).

graphe = {
    "A": ["B", "C"],
    "B": ["A", "C", "D"],
    "C": ["A", "B", "D"],
    "D": ["B", "C", "E"],
    "E": ["D"]
}
  1. deg(A) = 2, deg(B) = 3, deg(C) = 3, deg(D) = 3, deg(E) = 1. Somme = 12 = 2 × 6 arêtes. ✓

Exercice 3 — Exercice guidé : parcours en profondeur (DFS) pas à pas

On reprend le graphe de l’exercice 2 et on effectue un parcours en profondeur à partir du sommet A.

Rappel : le parcours en profondeur utilise une pile (ou la récursivité). À chaque étape, on visite le sommet au sommet de la pile, on le marque comme visité, et on empile ses voisins non visités.

  1. Compléter le tableau en choisissant les voisins dans l’ordre alphabétique :
ÉtapeSommet visitéPile (après traitement)Sommets visités
1A{A}
2
3
4
5
  1. Donner l’ordre complet de visite des sommets.

  2. Écrire l’algorithme DFS récursif en Python utilisant le dictionnaire d’adjacence.

Correction
  1. En DFS récursif avec les voisins traités dans l’ordre alphabétique :
ÉtapeSommet visitéAppels récursifs en attenteSommets visités
1Aexplorer B, C{A}
2Bexplorer C, D (A déjà visité){A, B}
3Cexplorer D (A, B déjà visités){A, B, C}
4Dexplorer E (B, C déjà visités){A, B, C, D}
5E(D déjà visité){A, B, C, D, E}
  1. Ordre de visite : A, B, C, D, E.

Le DFS « plonge » d’abord le plus profondément possible (A → B → C → D → E) avant de remonter.

def dfs(graphe, sommet, visites=None):
    if visites is None:
        visites = set()
    visites.add(sommet)
    print(sommet, end=" ")
    for voisin in sorted(graphe[sommet]):
        if voisin not in visites:
            dfs(graphe, voisin, visites)
    return visites

Remarque : sorted() garantit l’ordre alphabétique. Sans tri, l’ordre dépendrait de l’ordre dans la liste d’adjacence.


Exercice 4 — Exercice guidé : parcours en largeur (BFS) pas à pas

On reprend le même graphe et on effectue un parcours en largeur à partir du sommet A.

Rappel : le parcours en largeur utilise une file. On enfile le sommet de départ, puis à chaque étape on défile un sommet, on le traite et on enfile ses voisins non visités.

  1. Compléter le tableau (voisins dans l’ordre alphabétique) :
ÉtapeFile (avant)Sommet défiléVoisins enfilésSommets visités
1[A]A{A}
2
3
4
5
  1. Donner l’ordre complet de visite.

  2. Écrire l’algorithme BFS en Python utilisant une liste comme file.

Correction
ÉtapeFile (avant)Sommet défiléVoisins enfilésSommets visités
1[A]AB, C{A, B, C}
2[B, C]BD (A, C déjà visités){A, B, C, D}
3[C, D]C(A, B, D déjà visités){A, B, C, D}
4[D]DE (B, C déjà visités){A, B, C, D, E}
5[E]E(D déjà visité){A, B, C, D, E}

Remarque : on marque un sommet comme visité au moment où on l’enfile (pas quand on le défile), pour éviter de l’enfiler plusieurs fois.

  1. Ordre de visite : A, B, C, D, E.

Le BFS visite d’abord les voisins directs (B, C à distance 1 de A), puis les voisins à distance 2 (D), puis à distance 3 (E).

def bfs(graphe, depart):
    visites = {depart}
    file = [depart]
    ordre = []
    while file:
        sommet = file.pop(0)
        ordre.append(sommet)
        for voisin in sorted(graphe[sommet]):
            if voisin not in visites:
                visites.add(voisin)
                file.append(voisin)
    return ordre

Exercice 5 — Implémenter DFS et BFS

  1. Écrire une fonction chemin_existe(graphe, depart, arrivee) qui utilise un parcours (au choix DFS ou BFS) pour déterminer s’il existe un chemin entre depart et arrivee.

  2. Écrire une fonction chemin_bfs(graphe, depart, arrivee) qui renvoie un plus court chemin (en nombre d’arêtes) entre depart et arrivee, sous forme de liste de sommets.

    Indication : utiliser un dictionnaire predecesseur qui associe à chaque sommet visité le sommet depuis lequel on l’a découvert. Une fois arrivee atteint, remonter les prédécesseurs.

  3. Tester avec le graphe de l’exercice 2 : chemin_bfs(graphe, "A", "E") doit renvoyer ["A", "B", "D", "E"] ou ["A", "C", "D", "E"].

Correction
def chemin_existe(graphe, depart, arrivee):
    visites = set()
    pile = [depart]
    while pile:
        sommet = pile.pop()
        if sommet == arrivee:
            return True
        if sommet not in visites:
            visites.add(sommet)
            for voisin in graphe[sommet]:
                pile.append(voisin)
    return False
def chemin_bfs(graphe, depart, arrivee):
    if depart == arrivee:
        return [depart]
    visites = {depart}
    file = [depart]
    predecesseur = {depart: None}
    while file:
        sommet = file.pop(0)
        for voisin in sorted(graphe[sommet]):
            if voisin not in visites:
                visites.add(voisin)
                predecesseur[voisin] = sommet
                if voisin == arrivee:
                    # Reconstituer le chemin
                    chemin = []
                    courant = arrivee
                    while courant is not None:
                        chemin.append(courant)
                        courant = predecesseur[courant]
                    return chemin[::-1]
                file.append(voisin)
    return None  # pas de chemin
  1. chemin_bfs(graphe, "A", "E") renvoie ["A", "B", "D", "E"] (chemin de longueur 3).

Pourquoi BFS donne le plus court chemin ? Le BFS explore les sommets par distance croissante depuis le départ. Le premier chemin trouvé vers l’arrivée est donc forcément le plus court (en nombre d’arêtes).


Exercice 6 — Exercice guidé : algorithme de Dijkstra pas à pas

On considère le graphe pondéré suivant :

    A --5-- B --2-- C
    |       |       |
    3       7       4
    |       |       |
    D --1-- E --6-- F

On cherche le plus court chemin de A à F avec l’algorithme de Dijkstra.

  1. Compléter le tableau de Dijkstra. À chaque étape, on sélectionne le sommet non traité de distance minimale.
ÉtapeSommet sélectionnédist(A)dist(B)dist(C)dist(D)dist(E)dist(F)
Init0\(\infty\)\(\infty\)\(\infty\)\(\infty\)\(\infty\)
1A
2
3
4
5
6
  1. Quel est le plus court chemin de A à F ? Quel est son poids ?

  2. Le plus court chemin passe-t-il par le moins d’arêtes possible ? Justifier.

Correction
ÉtapeSommet sélectionnédist(A)dist(B)dist(C)dist(D)dist(E)dist(F)
Init0\(\infty\)\(\infty\)\(\infty\)\(\infty\)\(\infty\)
1A (0)05\(\infty\)3\(\infty\)\(\infty\)
2D (3)05\(\infty\)34\(\infty\)
3E (4)05\(\infty\)3410
4B (5)0573410
5C (7)0573410
6F (10)0573410

Détail des mises à jour :

  • Étape 1 (A) : B ← 0+5=5, D ← 0+3=3.
  • Étape 2 (D) : E ← 3+1=4 (mieux que \(\infty\)).
  • Étape 3 (E) : B ← 4+7=11 (pas mieux que 5), F ← 4+6=10.
  • Étape 4 (B) : C ← 5+2=7, E ← 5+7=12 (pas mieux que 4).
  • Étape 5 (C) : F ← 7+4=11 (pas mieux que 10).
  1. Plus court chemin de A à F : A → D → E → F, de poids 10.

    On reconstitue en remontant les prédécesseurs : F vient de E (étape 3), E vient de D (étape 2), D vient de A (étape 1).

  2. Non. Le chemin A → D → E → F utilise trois arêtes. Le chemin A → B → C → F utilise aussi trois arêtes mais pèse 5 + 2 + 4 = 11 > 10. Le chemin le plus court en poids n’est pas toujours celui qui utilise le moins d’arêtes, et inversement.


Exercice 7 — Détecter un cycle dans un graphe

  1. Rappeler la définition d’un cycle dans un graphe.

  2. Écrire une fonction contient_cycle(graphe) qui renvoie True si le graphe (non orienté, donné par un dictionnaire d’adjacence) contient un cycle, False sinon.

    Indication : adapter le DFS. Un cycle est détecté si, en explorant les voisins d’un sommet, on tombe sur un sommet déjà visité qui n’est pas le prédécesseur direct.

  3. Tester sur les graphes suivants :

    • {"A": ["B"], "B": ["A", "C"], "C": ["B"]} (pas de cycle)
    • {"A": ["B", "C"], "B": ["A", "C"], "C": ["A", "B"]} (cycle A–B–C)
Correction
  1. Un cycle est une chaîne fermée (qui revient à son sommet de départ) dont toutes les arêtes sont distinctes et de longueur au moins 3.

def contient_cycle(graphe):
    visites = set()

    def dfs(sommet, parent):
        visites.add(sommet)
        for voisin in graphe[sommet]:
            if voisin not in visites:
                if dfs(voisin, sommet):
                    return True
            elif voisin != parent:
                return True  # voisin déjà visité et ce n'est pas le parent → cycle
        return False

    for sommet in graphe:
        if sommet not in visites:
            if dfs(sommet, None):
                return True
    return False

Principe : on lance un DFS. Quand on visite un sommet, on transmet l’information de son parent (le sommet d’où on vient). Si on découvre un voisin déjà visité qui n’est pas le parent, c’est qu’on a trouvé un cycle. La boucle externe gère le cas des graphes non connexes.

  1. Tests :
  • contient_cycle({"A": ["B"], "B": ["A", "C"], "C": ["B"]})False ✓ (c’est une chaîne A–B–C sans cycle)
  • contient_cycle({"A": ["B", "C"], "B": ["A", "C"], "C": ["A", "B"]})True ✓ (cycle A–B–C–A)

Exercice 8 — Synthèse : réseau de transport

La ville de Graphville possède cinq stations de métro reliées comme suit :

  Gare --4-- Marché --3-- Hôpital
    |          |              |
    6          2              5
    |          |              |
  Mairie --7-- Stade ---------+

Les poids représentent les temps de trajet en minutes.

  1. Modéliser ce réseau par un dictionnaire d’adjacence pondéré en Python.

    Indication : pour un graphe pondéré, on utilise un dictionnaire de dictionnaires :

    graphe = {"Gare": {"Marché": 4, "Mairie": 6}, ...}
    
  2. Écrire une fonction dijkstra(graphe, depart) qui implémente l’algorithme de Dijkstra et renvoie un dictionnaire {sommet: distance_minimale}.

  3. Trouver le plus court chemin et le temps minimal pour aller de Gare à Hôpital.

  4. Un incident bloque la ligne Marché–Stade. Quel est maintenant le plus court chemin de Gare à Hôpital ? Le temps a-t-il augmenté ?

Correction
graphe = {
    "Gare":     {"Marché": 4, "Mairie": 6},
    "Marché":   {"Gare": 4, "Hôpital": 3, "Stade": 2},
    "Hôpital":  {"Marché": 3, "Stade": 5},
    "Mairie":   {"Gare": 6, "Stade": 7},
    "Stade":    {"Marché": 2, "Hôpital": 5, "Mairie": 7}
}
def dijkstra(graphe, depart):
    distances = {s: float("inf") for s in graphe}
    distances[depart] = 0
    predecesseurs = {s: None for s in graphe}
    non_traites = set(graphe.keys())

    while non_traites:
        # Sélectionner le sommet non traité de distance minimale
        sommet = min(non_traites, key=lambda s: distances[s])
        non_traites.remove(sommet)

        for voisin, poids in graphe[sommet].items():
            nouvelle_dist = distances[sommet] + poids
            if nouvelle_dist < distances[voisin]:
                distances[voisin] = nouvelle_dist
                predecesseurs[voisin] = sommet

    return distances, predecesseurs
  1. Dijkstra depuis Gare :

    • Gare = 0
    • Marché = 4 (via Gare)
    • Stade = 6 (via Gare → Marché → Stade : 4+2)
    • Mairie = 6 (via Gare)
    • Hôpital = 7 (via Gare → Marché → Hôpital : 4+3)

    Plus court chemin : Gare → Marché → Hôpital, temps = 7 minutes.

  2. Sans la ligne Marché–Stade, on recalcule. Le chemin Gare → Marché → Hôpital (7 min) ne passe pas par Stade, donc il n’est pas affecté. Le temps reste 7 minutes. En revanche, pour aller de Gare à Stade, il faudrait passer par Mairie (Gare → Mairie → Stade = 13 min) au lieu de Gare → Marché → Stade = 6 min.