Imaginez que vous contrôliez un robot placé à un certain endroit dans un graphe comportant n sommets et e arêtes. Vous voulez partir d’un sommet et parcourir progressivement tout le graphe en visitant tous les sommets accessibles depuis votre point de départ. L’algorithme DFS vous permet de faire exactement cela : il part d’un certain sommet, explore un chemin jusqu’à ce qu’il n’y ait plus de nouveau sommet à découvrir sur ce chemin, puis revient en arrière (backtrack) vers les sommets précédents afin de poursuivre l’exploration à partir de là.
Vidéo par <a href="https://www.youtube.com/@Reducible">Reducible</a> – Depth First Search (DFS) Explained: Algorithm, Examples, and Code
Une bonne façon de visualiser l’algorithme DFS est de s’imaginer un robot qui commence à explorer un graphe. Il ne voit que les voisins du sommet courant ainsi que tous les sommets déjà visités (le robot garde donc en mémoire tout le chemin parcouru). C’est un processus récursif : à chaque itération, le robot se déplace vers un nouveau sommet qui n’a pas encore été exploré et répète l’opération jusqu’à ce qu’il n’y ait plus de nouveaux sommets à visiter sur ce chemin. Penser à ce petit robot qui explore le graphe facilite grandement l’implémentation.
L’implémentation de l’algorithme DFS peut se faire avec une fonction récursive où l’on garde la trace du sommet courant (noté v) et de tous les sommets déjà visités (notés used) :
import sys
sys.setrecursionlimit(10 ** 6) # Augmenter la limite de récursion pour que DFS puisse s'exécuter correctement
n, e = map(int, input().split()) # n : sommets, e : arêtes
g = [[] for _ in range(n)] # graphe - liste d'adjacence
used = [False] * n # sommets déjà visités
for _ in range(e): # lire les arêtes
a, b = map(int, input().split()) # a -> b et b -> a
g[a].append(b)
g[b].append(a)
def dfs(v): # recherche en profondeur
used[v] = True # marquer le sommet comme visité
print(v, end=' ') # afficher le sommet
for to in g[v]: # pour chaque sommet connecté à v
if not used[to]: # si le sommet n'est pas visité
dfs(to) # appeler dfs sur ce sommet
start = int(input()) # sommet de départ
dfs(start) # appeler dfs sur le sommet de départ
Nous commençons par initialiser le graphe puis nous lançons le parcours à partir du sommet start. Au départ, toutes les valeurs de used sont définies à False. À chaque visite d’un sommet, on le marque immédiatement comme visité avec used[v] = True, ce qui signifie qu’il est désormais exploré, puis on parcourt tous les sommets voisins de v.
Observez comment la récursion joue le rôle de l’exploration d’un chemin et ensuite du retour en arrière (backtrack) vers le sommet précédent afin de continuer à parcourir les voisins de ce sommet. Quand on remonte dans la récursion, on revient tout simplement au sommet précédent.
Faisons une simulation de l’algorithme sur plusieurs entrées :
n = 4, e = 4
Les paires a, b en entrée sont : [(0, 1), (0, 2), (1, 2), (3, 2)]
Le sommet de départ est 3
v = 3 → used = [False, False, False, True]
explorer tous les voisins de 3 ⇒ [2]
v = 2 → used = [False, False, True, True]
explorer tous les voisins de 2 ⇒ [0, 1]
v = 0 → used = [True, False, True, True]
explorer tous les voisins de 0 ⇒ [1, 2]
v = 1 → used = [True, True, True, True]
explorer tous les voisins de 1 ⇒ [0, 2] ⇒ tous visités
retour en arrière depuis 1
retour en arrière depuis 0
retour en arrière depuis 2
retour en arrière depuis 3 → fin du DFS
Terminé
n = 5, e = 4
Les paires a, b en entrée sont : [(0, 1), (0, 2), (1, 2), (3, 4)]
Le sommet de départ est 0
v = 0 → used = [True, False, False, False, False]
explorer tous les voisins de 0 ⇒ [1, 2]
v = 1 → used = [True, True, False, False, False]
explorer tous les voisins de 1 ⇒ [0, 2]
v = 2 → used = [True, True, True, False, False]
explorer tous les voisins de 2 ⇒ [0, 1] ⇒ tous visités
retour en arrière depuis 2
retour en arrière depuis 1
retour en arrière depuis 0 → fin du DFS
Terminé
Notice how nodes 3 and 4 were never visited.
They were not connected to the initial node 0.
n = 10, e = 11 (exemple de la vidéo)
Les paires a, b en entrée sont : [(0, 1), (0, 2), (1, 2), (1, 4), (1, 3), (3, 5), (6, 5), (5, 8), (5, 7), (7, 8), (8, 9)]
Le sommet de départ est 0
v = 0 → used = [T, F, F, F, F, F, F, F, F, F] ⇒ explorer [1, 2]
v = 1 → used = [T, T, F, F, F, F, F, F, F, F] ⇒ explorer [0, 2, 3, 4]
v = 2 → used = [T, T, T, F, F, F, F, F, F, F] ⇒ explorer [0, 1]
retour depuis 2
v = 1 → used = [T, T, T, F, F, F, F, F, F, F] ⇒ explorer [x, x, 3, 4]
v = 3 → used = [T, T, T, T, F, F, F, F, F, F] ⇒ explorer [1, 5]
v = 5 → used = [T, T, T, T, F, T, F, F, F, F] ⇒ explorer [3, 6, 7, 8]
v = 6 → used = [T, T, T, T, F, T, T, F, F, F] ⇒ explorer [5]
retour depuis 6
v = 7 → used = [T, T, T, T, F, T, T, T, F, F] ⇒ explorer [5, 8]
v = 8 → used = [T, T, T, T, F, T, T, T, T, F] ⇒ explorer [5, 7, 9]
v = 9 → used = [T, T, T, T, F, T, T, T, T, T] ⇒ explorer [8]
retour depuis 8
retour depuis 7
retour depuis 6
retour depuis 5
retour depuis 3
v = 1 → used = [T, T, T, T, F, T, T, T, T, T] ⇒ explorer [x, x, x, 4]
v = 4 → used = [T, T, T, T, T, T, T, T, T, T] ⇒ explorer [1]
retour depuis 4
retour depuis 1
retour depuis 0
Terminé
Défi : compter le nombre de composantes connectées dans un graphe
Étant donné un graphe non orienté avec v sommets et e arêtes, vous devez calculer le nombre de composantes connectées du graphe. Un ensemble de sommets forme une composante connectée s’il est possible de se déplacer de l’un à l’autre au sein de cet ensemble.
Entrée
La première ligne de l’entrée contient deux entiers v (1 ≤ v ≤ 100 000) et e (1 ≤ e ≤ 100 000).
Les e lignes suivantes contiennent des paires d’entiers v1, v2 (1 ≤ v1, v2 ≤ v) qui indiquent que le sommet v1 est connecté au sommet v2, et inversement.
Sortie
Le programme doit afficher le nombre de composantes connectées dans le graphe.