Algorithmes de parcours d'un graphe

30 Apr 2019 30 Apr 2019 21434 vues ESSADDOUKI Mostafa 14 min de lecture
Complexité algorithmique
1 Introduction à l'analyse des algorithmes 2 Notations asymptotiques 3 Opérations élémentaires et modèles de coût 4 Complexité temporelle et spatiale 5 Méthode de comptage des pas 6 Méthode de comptage des pas pour les boucles
Diviser pour régner & algorithmes de tri
7 Rappel sur l'approche récursive 8 Diviser pour régner 9 la recherche dichotomique 10 Tri par fusion 11 Tri rapide 12 Analyse des fonctions récursives
Les arbres binaires
13 Introduction aux arbres binaires 14 Définitions récursives des arbres 15 Propriétés des arbres binaires 16 Types d'arbre binaire 17 Parcours en profondeur des arbres binaires 18 Parcours en largeur des arbres binaires 19 Exercices corrigés sur les arbres - TD 1 20 Exercices corrigés sur les arbres - TD 2 21 Exercices corrigés sur les arbres - TD 3 22 DS - Arbres binaires de recherche équilibrés (AVL) 23 DS - Codage de Huffman - Compression de données
Algorithmes gloutons
24 Introduction aux algorithmes gloutons 25 Problème de la sélection d'activités 26 Problème de séquencement des tâches 27 Problème du Sac à Dos fraction
Programmation dynamique
28 Introduction à la programmation dynamique 29 Le concept de sous-structure optimale 30 Le concept de sous-problèmes superposés 31 Méthodes de la programmation dynamique 32 Différence entre la programmation dynamique, l'approche diviser pour régner, et les algorithmes gloutons 33 Calculer les nombres de catalan en C++ et Python 34 Calculer le coefficient binomial 35 Le nombre de façons pour construire un mur de dimension 4*N 36 Défi de conversion de mots 37 Décomposition de phrases à partir d'un dictionnaire 38 La collection de pièces dans un labyrinthe 39 Nombre de façons de regrouper les étudiants 40 Compter tous les chemins possibles dans une grille MxN
Méta heuristique
41 Algorithmes heuristiques et métaheuristiques 42 Algorithme de Recuit simulé 43 Algorithme Colonies de fourmis
Théorie des graphes
44 Introduction et notions fondamentales 45 Chemins, cycles et connexité 46 Représentations des graphes 47 Parcours de graphes 48 Algorithme de Dijkstra 49 Algorithme A* 50 Algorithme de Bellman-Ford 51 Algorithme Floyd-Warshall 52 Coloration des graphes
Base de données et SQL
53 Introduction au langage SQL 54 Concepts de base de SGBDR 55 Syntaxes de différentes instructions SQL 56 Création et suppression d'une base de données 57 Opérateurs SQL 58 Les contraintes en SQL 59 Création et suppression des tables en SQL 60 Insertion et modifications des enregistrements 61 Extraction des données - SELECT 62 Filtrer les données - WHERE 63 Recherche de motifs - LIKE 64 Trier les données - ORDER BY 65 Les jointures en SQL - JOIN 66 Fonctions d'agrégation en SQL - SUM, COUNT, AVG, MIN et MAX 67 Organiser des données identiques en groupes - GROUP BY et HAVING 68 Les sous-requêtes en SQL 69 Les fonctions SQL de manipulation de date 70 DS - langage SQL 71 Exercices corrigés de langage SQL
Introduction à l'intelligence artificielle
Introduction à la théorie des jeux
Préparation aux concours
72 Réseau de distribution d'eau 73 Arbre d'expression arithmétique 74 Exploration d'une grotte souterraine

Parcours de graphes

Du parcours d'arbres au parcours de graphes

Un arbre, étant un graphe connexe et acyclique, impose une structure stricte avec trois propriétés fondamentales :

  • Chemin unique entre la racine et tout nœud.
  • Aucun cycle : on ne peut revenir au point de départ sans faire demi-tour.
  • Exploration linéaire : les parcours progressent toujours vers de nouveaux nœuds.
Conséquence

Cette structure rend inutile la mémorisation ou le marquage des nœuds visités, car aucun nœud ne peut être rencontré deux fois par des chemins différents.

Dans un graphe général (par opposition à un arbre), la structure n'impose plus de contraintes strictes, ce qui entraîne deux défis majeurs :

  • Les cycles : Il est possible de partir d'un sommet A, d'en visiter d'autres, et de revenir à A sans emprunter le même chemin à l'envers. Un algorithme simple peut ainsi tourner en boucle infinie.
  • Convergence des chemins : Plusieurs chemins distincts peuvent mener au même sommet. Sans contrôle, le même nœud peut être traité plusieurs fois, provoquant de la redondance, des inefficacités, ou des résultats incorrects.

Pour explorer un graphe, il est essentiel de mémoriser les nœuds déjà visités.

AstuceAvant de visiter un voisin, il faut systématiquement vérifier s'il a déjà été exploré.
  • S'il est inconnu, on le marque comme visité avant de poursuivre.
  • S'il est déjà connu, on l'ignore pour éviter toute boucle inutile ou traitement redondant.
Cette logique de marquage agit comme un fil conducteur pour ne pas se perdre dans la complexité du graphe.

Parcours en largeur (BFS)

Le BFS explore un graphe par niveaux successifs, du plus proche au plus éloigné.

Mécanisme de base

Il utilise une file (FIFO) pour traiter les nœuds dans l'ordre de leur découverte. Un tableau de marquage est indispensable pour ne pas revisiter un nœud et éviter les boucles infinies.

Avant d'enfiler un voisin dans la file, on vérifie s'il est déjà marqué. S'il l'est, on l'ignore. C'est ce simple test qui transforme un algorithme potentiellement infini en un algorithme robuste.

StructureRôle TechniqueImpact Algorithmique
File (FIFO)Gère l'ordre de priorité des sommets à explorer.Garantit l'exploration par distance croissante.
Tableau de MarquageMémorise l'historique des découvertes.Empêche les cycles et les redondances.
Atout principal

Dans un graphe non pondéré, le BFS garantit de trouver le chemin le plus court (en nombre d'arêtes) entre la source et tout autre sommet. Le "niveau" de découverte correspond exactement à cette distance minimale.

Structure produite

Le parcours génère un arbre BFS, une arborescence qui retrace pour chaque sommet le chemin le plus court depuis la racine. Les arêtes du graphe original qui ne font pas partie de cet arbre sont ignorées, car elles correspondent à des chemins plus longs ou forment des cycles.

  Exemple

On considère le graphe suivant et on effectue un parcours en largeur à partir du sommet A.
Les voisins sont traités par ordre alphabétique.

   
Liste d'adjacence Python
graphe = {
    "A": ["B", "C", "D"],
    "B": ["A", "E"],
    "C": ["A", "D", "F"],
    "D": ["A", "C", "E", "F", "G"],
    "E": ["B", "D", "G"],
    "F": ["C", "D", "G"],
    "G": ["D", "E", "F"],
}
Initialisation du BFS

Avant toute exploration :

  • Le sommet de départ est A
  • A est immédiatement marqué visité
  • La file contient uniquement A

Visites = \(\{A\}\), File = \([A]\)

Étape 1 — Traitement du sommet A
  • Sommet défilé : \(A\)
  • Voisins de \(A\) : \(B, C, D\)
  • Tous sont non visités → on les marque et on les enfile

Visites = \(\{A,B,C,D\}\) File = \([B, C, D]\)

Étape 2 — Traitement du sommet B
  • Sommet défilé : \(B\)
  • Voisins de \(B\) : \(A, E\)
  • \(A\) est déjà visité
  • \(E\) est découvert et ajouté à la file

Visites = \(\{A,B,C,D,E\}\) File = \([C, D, E]\)

Étape 3 — Traitement du sommet C
  • Sommet défilé : \(C\)
  • Voisins de \(C\) : \(A, D, F\)
  • \(A\) et \(D\) sont déjà visités
  • \(F\) est découvert et ajouté à la file

Visites = \(\{A,B,C,D,E,F\}\) File = \([D, E, F]\)

Étape 4 — Traitement du sommet D
  • Sommet défilé : \(D\)
  • Voisins de \(D\) : \(A, C, E, F, G\)
  • Tous sont visités sauf \(G\)
  • \(G\) est découvert et ajouté à la file

Visites = \(\{A,B,C,D,E,F,G\}\) File = \([E, F, G]\)

Étape 5 — Traitement du sommet E
  • Sommet défilé : \(E\)
  • Tous ses voisins sont déjà visités

Visites = \(\{A,B,C,D,E,F,G\}\) File = \([F, G]\)

Étape 6 — Traitement du sommet F
  • Sommet défilé : \(F\)
  • Tous ses voisins sont déjà visités

Visites = \(\{A,B,C,D,E,F,G\}\) File = \([G]\)

Étape 7 — Traitement du sommet G
  • Sommet défilé : \(G\)
  • Tous ses voisins sont déjà visités
  • La file devient vide → fin du parcours

Visites = \(\{A,B,C,D,E,F,G\}\) File = \([]\)

Ordre de visite

\(A \rightarrow B \rightarrow C \rightarrow D \rightarrow E \rightarrow F \rightarrow G\)

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define MAX_SOMMETS 100

// Structure pour la file d'attente
typedef struct {
    int elements[MAX_SOMMETS];
    int debut;
    int fin;
} File;

void initFile(File* f) {
    f->debut = 0;
    f->fin = 0;
}

void enfiler(File* f, int val) {
    f->elements[f->fin++] = val;
}

int defiler(File* f) {
    return f->elements[f->debut++];
}

bool fileVide(File* f) {
    return f->debut == f->fin;
}

// Structure pour le graphe (liste d'adjacence)
typedef struct Noeud {
    int sommet;
    struct Noeud* suivant;
} Noeud;

typedef struct {
    Noeud* tete;
} ListeAdj;

typedef struct {
    int n; // nombre de sommets
    ListeAdj* listes;
} Graphe;

// Créer un graphe avec n sommets
Graphe* creerGraphe(int n) {
    Graphe* graphe = (Graphe*)malloc(sizeof(Graphe));
    graphe->n = n;
    graphe->listes = (ListeAdj*)malloc(n * sizeof(ListeAdj));
    for (int i = 0; i < n; i++) {
        graphe->listes[i].tete = NULL;
    }
    return graphe;
}

// Ajouter une arête non orientée
void ajouterArete(Graphe* graphe, int u, int v) {
    Noeud* nouveau = (Noeud*)malloc(sizeof(Noeud));
    nouveau->sommet = v;
    nouveau->suivant = graphe->listes[u].tete;
    graphe->listes[u].tete = nouveau;
    
    // Pour graphe non orienté, ajouter aussi l'arête inverse
    nouveau = (Noeud*)malloc(sizeof(Noeud));
    nouveau->sommet = u;
    nouveau->suivant = graphe->listes[v].tete;
    graphe->listes[v].tete = nouveau;
}

// BFS
void bfs(Graphe* graphe, int source) {
    bool visites[MAX_SOMMETS] = {false};
    File file;
    initFile(&file);
    
    visites[source] = true;
    enfiler(&file, source);
    
    printf("Ordre BFS : ");
    
    while (!fileVide(&file)) {
        int u = defiler(&file);
        printf("%d ", u);
        
        Noeud* voisin = graphe->listes[u].tete;
        while (voisin != NULL) {
            int v = voisin->sommet;
            if (!visites[v]) {
                visites[v] = true;
                enfiler(&file, v);
            }
            voisin = voisin->suivant;
        }
    }
    printf("\n");
}

int main() {
    // Création du graphe de l'exemple
    // A=0, B=1, C=2, D=3, E=4, F=5, G=6
    Graphe* graphe = creerGraphe(7);
    
    ajouterArete(graphe, 0, 1); // A-B
    ajouterArete(graphe, 0, 2); // A-C
    ajouterArete(graphe, 0, 3); // A-D
    ajouterArete(graphe, 1, 4); // B-E
    ajouterArete(graphe, 2, 3); // C-D
    ajouterArete(graphe, 2, 5); // C-F
    ajouterArete(graphe, 3, 5); // D-F
    ajouterArete(graphe, 3, 4); // D-E
    ajouterArete(graphe, 3, 6); // D-G
    ajouterArete(graphe, 4, 6); // E-G
    ajouterArete(graphe, 5, 6); // F-G
    
    bfs(graphe, 0);
    
    return 0;
}
def parcours_largeur(graphe, source):
    # Initialise la file (FIFO) et le suivi des sommets visités
    file = [source]            
    visites = [source]         
    ordre = [source]

    # Tant qu'il reste des sommets à explorer
    while file:
        u = file.pop(0)        # Récupère le premier sommet de la file

        # Explore chaque voisin non encore visité
        for v in graphe[u]:
            if v not in visites:
                visites.append(v)
                ordre.append(v)
                file.append(v) # Ajoute à la file pour exploration future

    return ordre

graphe = {
    "A": ["B", "C", "D"],
    "B": ["A", "E"],
    "C": ["A", "D", "F"],
    "D": ["A", "C", "E", "F", "G"],
    "E": ["B", "D", "G"],
    "F": ["C", "D", "G"],
    "G": ["D", "E", "F"],
}

ordre = parcours_largeur(graphe, "A")
print(ordre)
Sortie
['A', 'B', 'C', 'D', 'E', 'F', 'G']

Parcours en profondeur (DFS)

Le DFS explore un graphe en s'enfonçant le plus loin possible le long d'un chemin avant de revenir en arrière (backtracking).
Contrairement au BFS qui progresse par niveaux, le DFS privilégie l'exploration complète d'une branche avant d'envisager les autres.

Mécanisme de base

Le DFS repose soit sur la récursion, soit sur une pile explicite (LIFO) pour mémoriser les sommets en attente d'exploration.

Atout principal

Le DFS permet d'analyser finement la structure interne du graphe. Il est particulièrement adapté à la détection de propriétés globales telles que :

  • l'existence de cycles,
  • la connexité,
  • la bipartition,
  • la structure hiérarchique induite par le graphe.

Contrairement au BFS, le DFS ne garantit pas la découverte d'un plus court chemin, mais il offre une exploration plus profonde et plus structurante.

  Exemple

On considère le même graphe. Dans toute la suite, l'ordre des voisins est celui donné par la liste d'adjacence. Le DFS est réalisé ici sous sa forme récursive, ce qui revient conceptuellement à utiliser une pile (LIFO).

Initialisation du DFS

Avant toute exploration :

  • Le sommet de départ est \(A\)
  • \(A\) est immédiatement marqué visité
  • La pile d'appels contient implicitement \(A\)

Visites = \(\{A\}\), Pile (appels récursifs) = \([A]\)

Étape 1 — Traitement du sommet A
  • Sommet courant : \(A\)
  • Voisins de \(A\) : \(B, C, D\)
  • Le premier voisin non visité est \(B\)

On marque \(B\) et on descend récursivement.

Visites = \(\{A, B\}\), Pile = \([A, B]\)

Étape 2 — Traitement du sommet B
  • Sommet courant : \(B\)
  • Voisins de \(B\) : \(A, E\)
  • \(A\) est déjà visité
  • \(E\) est non visité

On marque \(E\) et on continue l'exploration en profondeur.

Visites = \(\{A, B, E\}\), Pile = \([A, B, E]\)

Étape 3 — Traitement du sommet E
  • Sommet courant : \(E\)
  • Voisins de \(E\) : \(B, D, G\)
  • \(B\) est déjà visité
  • \(D\) est non visité

On marque \(D\) et on descend encore.

Visites = \(\{A, B, E, D\}\), Pile = \([A, B, E, D]\)

Étape 4 — Traitement du sommet D
  • Sommet courant : \(D\)
  • Voisins de \(D\) : \(A, C, E, F, G\)
  • \(A\) et \(E\) sont déjà visités
  • \(C\) est non visité

On marque \(C\) et on poursuit.

Visites = \(\{A, B, E, D, C\}\), Pile = \([A, B, E, D, C]\)

Étape 5 — Traitement du sommet C
  • Sommet courant : \(C\)
  • Voisins de \(C\) : \(A, D, F\)
  • \(A\) et \(D\) sont déjà visités
  • \(F\) est non visité

On marque \(F\) et on poursuit.

Visites = \(\{A, B, E, D, C, F\}\), Pile = \([A, B, E, D, C, F]\)

Étape 6 — Traitement du sommet F
  • Sommet courant : \(F\)
  • Voisins de \(F\) : \(C, D, G\)
  • \(C\) et \(D\) sont déjà visités
  • \(G\) est non visité

On marque \(G\) et on poursuit.

Visites = \(\{A, B, E, D, C, F, G\}\), Pile = \([A, B, E, D, C, F, G]\)

Étape 7 — Traitement du sommet G
  • Sommet courant : \(G\)
  • Voisins de \(G\) : \(D, E, F\)
  • Tous sont déjà visités

Aucun nouvel appel récursif possible → retour en arrière (dépilement).

Ordre possible de visite

\(A \rightarrow B \rightarrow E \rightarrow D \rightarrow C \rightarrow F \rightarrow G\)

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define MAX_SOMMETS 100

// Structure pour le graphe (liste d'adjacence)
typedef struct Noeud {
    int sommet;
    struct Noeud* suivant;
} Noeud;

typedef struct {
    Noeud* tete;
} ListeAdj;

typedef struct {
    int n; // nombre de sommets
    ListeAdj* listes;
} Graphe;

Graphe* creerGraphe(int n) {
    Graphe* graphe = (Graphe*)malloc(sizeof(Graphe));
    graphe->n = n;
    graphe->listes = (ListeAdj*)malloc(n * sizeof(ListeAdj));
    for (int i = 0; i < n; i++) {
        graphe->listes[i].tete = NULL;
    }
    return graphe;
}

void ajouterArete(Graphe* graphe, int u, int v) {
    Noeud* nouveau = (Noeud*)malloc(sizeof(Noeud));
    nouveau->sommet = v;
    nouveau->suivant = graphe->listes[u].tete;
    graphe->listes[u].tete = nouveau;
    
    nouveau = (Noeud*)malloc(sizeof(Noeud));
    nouveau->sommet = u;
    nouveau->suivant = graphe->listes[v].tete;
    graphe->listes[v].tete = nouveau;
}

// Fonction récursive pour DFS
void dfsRecursif(Graphe* graphe, int sommet, bool visites[]) {
    visites[sommet] = true;
    printf("%d ", sommet);
    
    Noeud* voisin = graphe->listes[sommet].tete;
    while (voisin != NULL) {
        int v = voisin->sommet;
        if (!visites[v]) {
            dfsRecursif(graphe, v, visites);
        }
        voisin = voisin->suivant;
    }
}

void dfs(Graphe* graphe, int source) {
    bool visites[MAX_SOMMETS] = {false};
    printf("Ordre DFS : ");
    dfsRecursif(graphe, source, visites);
    printf("\n");
}

int main() {
    Graphe* graphe = creerGraphe(7); // A=0, B=1, C=2, D=3, E=4, F=5, G=6
    
    ajouterArete(graphe, 0, 1); // A-B
    ajouterArete(graphe, 0, 2); // A-C
    ajouterArete(graphe, 0, 3); // A-D
    ajouterArete(graphe, 1, 4); // B-E
    ajouterArete(graphe, 2, 3); // C-D
    ajouterArete(graphe, 2, 5); // C-F
    ajouterArete(graphe, 3, 5); // D-F
    ajouterArete(graphe, 3, 4); // D-E
    ajouterArete(graphe, 3, 6); // D-G
    ajouterArete(graphe, 4, 6); // E-G
    ajouterArete(graphe, 5, 6); // F-G
    
    dfs(graphe, 0);
    
    return 0;
}
def parcours_profondeur(graphe, sommet, visites):
    # Ajouter le sommet actuel à la liste des nœuds visités
    visites.append(sommet)
    
    # Affichage du sommet pour suivre l'ordre de passage
    print(sommet, end=" ")

    # Parcourir tous les sommets adjacents (voisins) du sommet actuel
    for voisin in graphe[sommet]:
        # Si le voisin n'a pas encore été visité, on explore sa branche
        if voisin not in visites:
            # Appel récursif : on plonge plus profondément dans le graphe
            parcours_profondeur(graphe, voisin, visites)

graphe = {
    "A": ["B", "C", "D"],
    "B": ["A", "E"],
    "C": ["A", "D", "F"],
    "D": ["A", "C", "E", "F", "G"],
    "E": ["B", "D", "G"],
    "F": ["C", "D", "G"],
    "G": ["D", "E", "F"],
}

print("Ordre DFS : ", end="")
parcours_profondeur(graphe, "A", [])
Sortie
Ordre DFS : A B E D C F G

Comparaison BFS vs DFS

CritèreBFSDFS
Structure de donnéesFile (FIFO)Pile (LIFO) ou récursion
Ordre d'explorationPar niveaux (distance croissante)En profondeur (descend d'abord)
Chemin le plus courtOui (graphe non pondéré)Non
Complexité mémoire\(O(\text{largeur})\)\(O(\text{profondeur})\)
Détection de cyclesPossibleTrès adapté

Sortie
// La sortie apparaîtra ici…
Prêt · Ctrl+Entrée pour exécuter

Discussion (0)

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Votre commentaire sera visible après modération.