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.
Écrire la matrice d’adjacence de ce graphe (sommets dans l’ordre A, B, C, D, E).
Écrire le dictionnaire d’adjacence Python correspondant.
Vérifier que la somme des degrés est bien le double du nombre d’arêtes.
Correction
- 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"]
}
- 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.
- Compléter le tableau en choisissant les voisins dans l’ordre alphabétique :
| Étape | Sommet visité | Pile (après traitement) | Sommets visités |
|---|---|---|---|
| 1 | A | {A} | |
| 2 | |||
| 3 | |||
| 4 | |||
| 5 |
Donner l’ordre complet de visite des sommets.
Écrire l’algorithme DFS récursif en Python utilisant le dictionnaire d’adjacence.
Correction
- En DFS récursif avec les voisins traités dans l’ordre alphabétique :
| Étape | Sommet visité | Appels récursifs en attente | Sommets visités |
|---|---|---|---|
| 1 | A | explorer B, C | {A} |
| 2 | B | explorer C, D (A déjà visité) | {A, B} |
| 3 | C | explorer D (A, B déjà visités) | {A, B, C} |
| 4 | D | explorer E (B, C déjà visités) | {A, B, C, D} |
| 5 | E | (D déjà visité) | {A, B, C, D, E} |
- 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.
- Compléter le tableau (voisins dans l’ordre alphabétique) :
| Étape | File (avant) | Sommet défilé | Voisins enfilés | Sommets visités |
|---|---|---|---|---|
| 1 | [A] | A | {A} | |
| 2 | ||||
| 3 | ||||
| 4 | ||||
| 5 |
Donner l’ordre complet de visite.
Écrire l’algorithme BFS en Python utilisant une liste comme file.
Correction
| Étape | File (avant) | Sommet défilé | Voisins enfilés | Sommets visités |
|---|---|---|---|---|
| 1 | [A] | A | B, C | {A, B, C} |
| 2 | [B, C] | B | D (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] | D | E (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.
- 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
É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 entredepartetarrivee.Écrire une fonction
chemin_bfs(graphe, depart, arrivee)qui renvoie un plus court chemin (en nombre d’arêtes) entredepartetarrivee, sous forme de liste de sommets.Indication : utiliser un dictionnaire
predecesseurqui associe à chaque sommet visité le sommet depuis lequel on l’a découvert. Une foisarriveeatteint, remonter les prédécesseurs.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
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.
- Compléter le tableau de Dijkstra. À chaque étape, on sélectionne le sommet non traité de distance minimale.
| Étape | Sommet sélectionné | dist(A) | dist(B) | dist(C) | dist(D) | dist(E) | dist(F) |
|---|---|---|---|---|---|---|---|
| Init | — | 0 | \(\infty\) | \(\infty\) | \(\infty\) | \(\infty\) | \(\infty\) |
| 1 | A | ||||||
| 2 | |||||||
| 3 | |||||||
| 4 | |||||||
| 5 | |||||||
| 6 |
Quel est le plus court chemin de A à F ? Quel est son poids ?
Le plus court chemin passe-t-il par le moins d’arêtes possible ? Justifier.
Correction
| Étape | Sommet sélectionné | dist(A) | dist(B) | dist(C) | dist(D) | dist(E) | dist(F) |
|---|---|---|---|---|---|---|---|
| Init | — | 0 | \(\infty\) | \(\infty\) | \(\infty\) | \(\infty\) | \(\infty\) |
| 1 | A (0) | 0 | 5 | \(\infty\) | 3 | \(\infty\) | \(\infty\) |
| 2 | D (3) | 0 | 5 | \(\infty\) | 3 | 4 | \(\infty\) |
| 3 | E (4) | 0 | 5 | \(\infty\) | 3 | 4 | 10 |
| 4 | B (5) | 0 | 5 | 7 | 3 | 4 | 10 |
| 5 | C (7) | 0 | 5 | 7 | 3 | 4 | 10 |
| 6 | F (10) | 0 | 5 | 7 | 3 | 4 | 10 |
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).
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).
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
Rappeler la définition d’un cycle dans un graphe.
Écrire une fonction
contient_cycle(graphe)qui renvoieTruesi le graphe (non orienté, donné par un dictionnaire d’adjacence) contient un cycle,Falsesinon.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.
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
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.
- 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.
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}, ...}Écrire une fonction
dijkstra(graphe, depart)qui implémente l’algorithme de Dijkstra et renvoie un dictionnaire{sommet: distance_minimale}.Trouver le plus court chemin et le temps minimal pour aller de Gare à Hôpital.
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
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.
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.