Adloun

Parcours de graphes

Cours · informatique (tronc commun des prépas scientifiques), chapitre 13 · prépas scientifiques, tronc commun · le début du chapitre, la suite sur Adloun

Le parcours en largeur d'un graphe : les sommets sont visités par distances croissantes depuis la source.

13.1 Introduction et motivation

Un graphe en mémoire ne livre rien de lui-même : pour savoir si deux stations de métro sont reliées, si un labyrinthe a une sortie, si un logiciel a des dépendances circulaires, il faut explorer — partir d'un sommet et visiter, de proche en proche, tout ce qui est accessible. C'est le parcours de graphe, l'algorithme fondamental du domaine, dont presque tout le reste (connexité, cycles, plus courts chemins) n'est qu'une instrumentation.

Or il n'y a essentiellement que deux façons d'explorer, et elles diffèrent par un seul choix : quand plusieurs directions s'offrent, traite-t-on d'abord la dernière découverte — on s'enfonce, c'est le parcours en profondeur — ou la première — on rayonne par cercles, c'est le parcours en largeur ? Ce choix est exactement celui d'une structure de données : une pile ou une file, que ce chapitre introduit pour l'occasion — avec un détour obligé par leur implémentation, car la file naïve en liste Python cache un piège de complexité que le programme tient à montrer. À l'arrivée : la connexité et ses composantes, la détection de cycles, et les distances en nombre d'arêtes — le plus court chemin non pondéré, marchepied du chapitre 14.

13.2 Piles et files

13.2.1 Deux disciplines d'attente

Définition 13.1Pile

Une pile (stack) est une collection où l'on insère et retire du même côté — le sommet. Discipline LIFO (last in, first out) : le dernier entré sort le premier, comme une pile d'assiettes. Opérations : empiler, dépiler, tester la vacuité — toutes exigées en . En Python, la liste fait une pile parfaite :


pile = []
pile.append(x)        # empiler : O(1)
x = pile.pop()        # dépiler (le DERNIER ajouté) : O(1)
len(pile) == 0        # vide ?
Définition 13.2File

Une file (queue) insère d'un côté et retire de l'autre. Discipline FIFO (first in, first out) : le premier entré sort le premier — la file d'attente du guichet. Opérations : enfiler, défiler, vacuité, en aussi.

AttentionLe piège de la file en liste Python

La tentation est d'écrire la file avec une liste : file.append(x) pour enfiler, file.pop(0) pour défiler. C'est correct — et c'est un piège de complexité : pop(0) retire la première case, et la liste décale toutes les autres — coût à chaque défilement ! Un parcours qui défile sommets devient en silence. La solution du programme : le module dédié collections.deque (double-ended queue), conçu pour des insertions et retraits en aux deux bouts :


from collections import deque
file = deque()
file.append(x)            # enfiler à droite : O(1)
x = file.popleft()        # défiler à gauche : O(1)

C'est le « choix éclairé des collections » du chapitre 10, version structure : même contrat, complexités opposées — et l'écart se mesure (exercice 4).

13.3 Le parcours en profondeur

13.3.1 S'enfoncer d'abord

Définition 13.3Parcours en profondeur

Le parcours en profondeur (DFS, depth-first search) explore depuis un sommet source en suivant un chemin aussi loin que possible, et ne revient en arrière que bloqué. Version itérative, avec une pile et un ensemble de sommets visités :


def parcours_profondeur(G: dict, source) -> list:
    """Renvoie la liste des sommets accessibles depuis source,
    dans l'ordre de leur visite."""
    visites = []
    vus = {source}                  # marqués : dans la pile ou déjà visités
    pile = [source]
    while pile:                      # variant : nb de sommets jamais empilés... voir preuve
        s = pile.pop()
        visites.append(s)
        for v in G[s]:
            if v not in vus:
                vus.add(v)
                pile.append(v)
    return visites

La version récursive, équivalente, confie la pile à la machine (chapitre 6 — c'est la pile d'appels) :


def dfs(G: dict, s, vus: set, visites: list) -> None:
    vus.add(s)
    visites.append(s)
    for v in G[s]:
        if v not in vus:
            dfs(G, v, vus, visites)

Exercices du chapitre

La suite du cours · Tous les exercices du chapitre