Algo Graphe Largeur

Identification

Infoforall

26 - Graphe : parcours en largeur


Logiciel nécessaire pour l'activité : -

Prérequis :

  • Données : les 4 activités sur les Graphes
  • Algo : Parcours en largeur d'un Arbre

Evaluation ✎ : 01-02-03-08-09-10-11-12-13-14-15

1 - Rappel : parcours en largeur d'un arbre

Nous avons déjà vu la notion de parcours en largeur sur les arbres, qui utilise une File pour mémoriser l'ordre des noeuds à explorer.

Le principe général est

  • d'explorer la racine de l'arbre puis
  • d'explorer tous les noeuds à profondeur 1 de la racine.
  • d'explorer tous les noeuds à profondeur 2 de la racine.
  • ect... jusqu'à la plus grande profondeur
Parcours en largeur

L'ordre de lecture des noeuds donne donc Alice - Bob - Clara - Didier - Eleonore...

L'algorithme en quelques phrases :

  1. Enfiler l'Arbre dans une File
  2. Tant que la File n'est pas vide
    1. Défiler le prochain Arbre
    2. Explorer la racine de l'Arbre
    3. Enfiler les enfants gauche et droite de cet arbre dans la File si ce ne sont pas des Arbres VIDES.

Avant de commencer l'activité, allez regarder cette vidéo YouTube (20 minutes) qui vous montre tout l'intérêt de savoir naviguer dans les graphes :

graphe et réseau social

2 - Parcours en largeur d'un graphe

On fait pareil dans les graphes avec plus de subtilité car on risque de retomber sur des sommets déjà rencontrés. Il peut y avoir des cycles.

On commence par le sommet de départ voulu (S5 ici) ;

On lit les sommets à distance 1 autour de ce premier sommet (S1 et S9) ;

On lit les sommets à distance 2, en s'écartant encore d'un cran (S2, S3 et S8) ;

...

Cliquez sur l'image pour lancer l'animation.

Animation du parcours
CLIQUER SUR L'IMAGE pour ANIMER ou STOPPER
2.1 Parcours en largeur d'un graphe

But de l'algorithme du parcours en largeur
L'algorithme du parcours en largeur permet de découvrir un chemin de plus petite distance entre le sommet de départ et chaque sommet accessible.
On découvre un chemin par sommet, pas tous les chemins possibles. Par contre, c'est un chemin de plus petite distance : il n'existe pas de chemin plus court.
Description des attributs nécessaires à l'ALGORITHME

Pour décrire l'algorithme, on utilise souvent les attributs suivants :

  • un attribut couleur qui précise l'état du sommet de traitement de ce sommet :
    1. BLANC : sommet pas encore découvert ;
    2. GRIS : sommet découvert ;
    3. NOIR : sommet découvert et totalement traité. On a exploré toutes ses arêtes ou ses arcs sortants. Tous les sommets adjacents sont donc GRIS ou NOIR.
  • un attribut distance qui contient le nombre d'arêtes entre ce sommet et le sommet de départ.
    INFINI pour les sommets qui n'ont pas encore été découverts.
  • un attribut parent qui contient le sommet depuis lequel on a découvert ce sommet.
    VIDE au départ de l'exploration.
L'algorithme du parcours en largeur d'abord en français

Phase d'initialisation

  1. Pour chaque sommet u appartenant à l'ensemble S :: on place
    • BLANC dans couleur ;
    • INFINI dans distance ;
    • VIDE dans parent.
  2. On choisit un sommet de départ qu'on peint en GRIS et qui possède une distance de 0.
  3. On enfile ce sommet de départ dans une File

Phase de parcours

  1. TANT QUE la File n'est pas vide
    1. on défile le prochain sommet qu'on nomme u
    2. pour chaque sommet v adjacent à u :
      • Si v est BLANC
        • On peint v en GRIS
        • On définit la distance de v comme étant la distance de u + 1
        • On définit le parent de v comme étant u
        • On enfile v
    3. On peint u en NOIR puisqu'on a découvert tous ses sommets adjacents si on arrive ici
  2. FIN : tous les sommets découvrables ont été traités (NOIR).
Points importants

On agit sur un sommet v que s'il est BLANC. Les actions pointées par la flèche rouge  ne sont donc réalisées qu'une fois sur un sommet donné.

Il est possible que certains sommets ne soient pas découverts à la fin de l'algorithme si le graphe n'est pas connexe. Ils sont alors reconnaissables car leur couleur est BLANC.

2.2 Exemple de parcours en largeur

On considère le graphe suivant en prenant le sommet 5 comme point de départ : on cherche donc à déterminer le nombre minimum d'arêtes entre ce sommet et n'importe quel autre sommet du graphe.

Les listes d'adjacence sont considérés être données en suivant les numéros des sommets.

Quelques listes d'adjacence pour que vous comprenniez le principe :

S5 : S1 -> S9

S6 : S2 -> S7 -> S8

Etape initiale

Image montrant la configuration initiale

Le sommet 5 est à distance 0 du sommet 5.

Les autres distances sont initialisées à l'infini.

On place le sommet 5 dans la file et on peut commencer.

Défilement de la File -> S5 ->

La File n'est pas vide, on défile et on obtient le sommet S5.

On fait le traitement sur ce sommet 5 puis on lit sa liste d'adjacence : [S1, S9].

  1. S1 est BLANC, on le peint en GRIS, son parent est S5 et sa distance avec S5 est 0+1, soit 1. On enfile S1.
  2. S9 est BLANC, on le peint en GRIS, son parent est S5 et sa distance avec S5 est 0+1, soit 1. On enfile S9.
Exploration de S5

On peint S5 en NOIR pour indiquer que nous avons totalement fini son traitement.

Défilement de la File -> S9 -> S1 ->

La File n'est pas vide, on défile et on obtient le sommet S1.

On fait le traitement sur ce sommet 1 puis on lit sa liste d'adjacence : [S2, S3, S5].

  1. S2 est BLANC, on le peint en GRIS, son parent est S1 et sa distance avec S5 est 1+1, soit 2. On enfile S2.
  2. S3 est BLANC, on le peint en GRIS, son parent est S1 et sa distance avec S5 est 1+1, soit 2. On enfile S3.
  3. S5 n'est pas BLANC, on ne fait rien.
Exploration de S1

On peint S1 en NOIR pour indiquer que nous avons totalement fini son traitement.

Défilement de la File -> S3 -> S2 -> S9 ->

La File n'est pas vide, on défile et on obtient le sommet S9.

On fait le traitement sur ce sommet 9 puis on lit sa liste d'adjacence : [S5, S8].

  1. S5 n'est pas BLANC, on ne fait rien.
  2. S8 est BLANC, on le peint en GRIS, son parent est S9 et sa distance avec S5 est 1+1, soit 2. On enfile S8.
Exploration de S9

On peint S9 en NOIR pour indiquer que nous avons totalement fini son traitement.

Défilement de la File -> S8 -> S3 -> S2 ->

La File n'est pas vide, on défile et on obtient le sommet S2.

On fait le traitement sur ce sommet 2 puis on lit sa liste d'adjacence : [S1, S3, S4, S6].

  1. S1 n'est pas BLANC, on ne fait rien.
  2. S3 n'est pas BLANC, on ne fait rien.
  3. S4 est BLANC, on le peint en GRIS, son parent est S2 et sa distance avec S5 est 2+1, soit 3. On enfile S4.
  4. S6 est BLANC, on le peint en GRIS, son parent est S2 et sa distance avec S5 est 2+1, soit 3. On enfile S6.
Exploration de S2

On peint S2 en NOIR pour indiquer que nous avons totalement fini son traitement.

Normalement... vous avez compris le principe.

Défilement de la File -> S6 -> S4 -> S8 -> S3 ->

On traite le sommet S3, on ne fait rien des sommets adjacents à S3 qui sont tous GRIS ou NOIR, puis on place S3 en NOIR.

Exploration de S3

Défilement de la File -> S6 -> S4 -> S8 ->

On traite le sommet S8, on ne fait rien des sommets adjacents à S8 qui sont tous GRIS ou NOIR, puis on place S8 en NOIR.

Exploration de S8

Défilement de la File -> S6 -> S4 ->

La File n'est pas vide, on défile et on obtient le sommet S4.

On fait le traitement sur ce sommet 2 puis on lit sa liste d'adjacence : [S2, S3, S7].

  1. S2 n'est pas BLANC, on ne fait rien.
  2. S3 n'est pas BLANC, on ne fait rien.
  3. S7 est BLANC, on le peint en GRIS, son parent est S4 et sa distance avec S5 est 3+1, soit 4. On enfile S7.
Exploration de S4

Défilement de la File -> S7 -> S6 ->

On traite le sommet S6, on ne fait rien des sommets adjacents à S6 qui sont tous GRIS ou NOIR, puis on place S6 en NOIR.

Exploration de S6

Défilement de la File -> S7 ->

On traite le sommet S7, on ne fait rien des sommets adjacents à S7 qui sont tous GRIS ou NOIR, puis on place S7 en NOIR.

Exploration de S7

File VIDE : on stoppe la boucle

Après exécution de l'algorithme, on obtient :

  • Un Arbre dont la racine est le sommet de départ et qui mène à tous les autres sommets : cela permet de connaître UN chemin entre le sommet de départ et les autres.
  • La distance minimale en nombre d'arêtes entre le sommet de départ et n'importe quel autre sommet : c'est la profondeur du sommet (on prend la convention profondeur 0 pour la racine).
Etat final

Et voici le graphe correspondant à l'arbre du parcours, obtenu en supprimant les arêtes que nous n'avons pas utilisés lors de la création de l'arbre :

Etat final

3.3 Questions de compréhension

✎ 01° Sur l'Arbre de parcours obtenu, quel est l'enchaînement de sommets obtenu pour aller du sommet S5 au sommet S6 ?

✎ 02° Compter le nombre d'arêtes utilisées pour aller de S5 à S6. Obtient-on bien une distance de 3 comme indiqué au dessus du sommet 6 sur le graphe ?

✎ 03° Pourquoi la distance indiquée pour S5 sur le graphe est-elle de 0 ?

04° On utilise une fois chaque sommet comme centre de recherches. Combien de fois utilise-t-on les arêtes ?

  • On utilise une fois chaque arête
  • On utilise deux fois chaque arête
  • On utilise trois fois chaque arête
  • J'aime ni les arêtes, ni le poisson

...CORRECTION...

On utilise les arêtes dans les deux sens. Donc deux fois.

05° On cherche la complexité de cet algorithme en prenant comme référence le nombre de fois où on teste si un sommet est BLANC. On considère uniquement un graphe connexe. Notons a le nombre d'arêtes. La complexité de cet algorithme est :

  • Logarithmique en fonction de a ?
  • Linéaire en fonction de a ?
  • Quadratique en fonction de a ?
  • Exponentielle en fonction de a ?

...CORRECTION...

Puisqu'on utilise deux fois les arêtes, on peut noter 𝞗(2a). Comme on ne s'intéresse pas au facteur multiplicatif, on obtient donc 𝞗(a). La bonne réponse est donc linéaire.

Le coût total d'exécution dépend en réalité du nombre de sommets et du nombre d'arêtes. Par contre, si le graphe n'est pas connexe, on peut très bien ne pas rencontrer ni traiter certains sommets et certains arcs ou arêtes.

On peut donc écrire 𝞞(s+a)

06° En prenant le graphe, donner deux chemins de distance 3 allant de S5 à S6.

Etat final

Quel est le parent unique de S6 sur l'Arbre de parcours obtenu ? En déduire le chemin qui va être choisi ici si on suit l'Arbre de parcours.

...CORRECTION...

S5-S1-S2-S6 ou S5-S9-S8-S6.

On voit sur l'Arbre qu'on obtiendrait ici le premier choix car le parent de S6 est S2.

Le parent de S2 est S1.

Le parent de S1 est S5.

07° Peut-on trouver un chemin de moins de 3 arêtes menant de S5 à S6 ?

...CORRECTION...

Non : le parcours en largeur donne 3. Il n'y a donc pas de chemin plus court que 3 arêtes.

3 - Algorithme de parcours en largeur

Nous avons vu l'algorithme décrit en français, voyons son pseudo-code.

3 Pseudo-code du parcours en largeur d'un graphe

Algorithme du parcours en largeur de graphe

BUT

découvrir un chemin de plus petite distance entre le sommet de départ et chaque sommet accessible.

ENTREES

  • Un graphe noté g
  • Un sommet s qui servira de sommet origine

SORTIE

Aucune ici mais on peut facilement modifier l'algorithme pour qu'il renvoie par exemple l'arbre de parcours.

ALGORITHME PARCOURS EN LARGEUR

    Phase 1 d'initialisation

    POUR chaque sommet u appartenant au graphe g

      u.couleur ← BLANC

      u.distance ← INFINI

      u.parent ← VIDE

    Fin Pour

    s.couleur ← GRIS

    s.distance ← 0

    s.parent ← VIDE

    a_traiternouvelleFileVide()

    enfiler(a_traiter, s)

    Phase 2 de parcours

    TANT QUE NON estFileVide(a_traiter)

      udefiler(a_traiter)

      POUR chaque sommet v appartenant à la liste d'adjacence du sommet u

        SI v.couleur est BLANC

          si on arrive ici, c'est que le sommet n'a pas encore été découvert

          v.couleur ← GRIS

          v.distance ← u.distance + 1

          v.parent ← u

          enfiler(a_traiter, v)

        Fin Si

        u.couleur ← NOIR

      Fin Pour

    Fin Tant Que

    Renvoyer VIDE (∅)

4 - Exercices

Maintenant que vous avez vu l'exécution une première fois et que vous avez vu l'algorithme, il est temps de vous l'approprier.

Nous l'utiliserons dans le cadre d'un mini-projet on nous tenterons de faire sortir un personnage d'un labyrinthe, alors que des monstres rodent...

✎ 08° Fournir les listes d'adjacence des sommets ci-dessous. On placera toujours les sommets dans l'ordre alphabétique.

Arbre de l'exercice

✎ 09° Fournir l'arbre de parcours obtenu à partir du sommet c.

✎ 10° Fournir les listes des successeurs des sommets ci-dessous. Attention, cette fois le graphe est orienté. On placera toujours les sommets dans l'ordre alphabétique.

graphe orienté

✎ 11° Fournir l'arbre de parcours obtenu à partir du sommet c. Il faudra bien entendu ne prendre que la liste des successeurs.

✎ 12° Fournir l'arbre de parcours obtenu à partir du sommet a.

✎ 13° Fournir l'arbre de parcours obtenu à partir du sommet d.

✎ 14° Quel semble être le sommet le plus central ?

✎ 15° Le parcours en largeur fonctionne pour trouver le plus court chemin dans un graphe orienté non pondéré. Expliquez pourquoi il ne fonctionne pas pour un graphe pondéré.

5 - FAQ

Et on fait comment sur un graphe pondéré ?

On applique l'algorithme de Dijkstra.

Et avec un traitement naïf en Python, ça donne quoi ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
"""Implémentation du parcours en largeur pour : * un graphe implémenté sous forme d'un graphe-dico {sommet:liste d'adjacence} * une file muable implémentée via la classe Deque du module collections -> nouvelleFile() avec file = collections.deque() -> enfiler() avec file.append(x) -> defiler() avec file.popleft() -> lireAvant() avec file[0] -> estVide() avec file Le module comporte les fonctions : -> creer_graphedico_no_np(g:tuple) -> dict[int, list] -> largeur_d_abord_v1(g:dict[int, list], s:int) -> dict -> donner_chemin(sommet_initial, sommet_final, parcours:dict) -> list """ # Importations ================================================= import collections import math # Constantes =================================================== BLANC = 0 GRIS = 1 NOIR = 2 INFINI = math.inf # Fonctions ===================================================== def creer_graphedico_no_np(g:tuple) -> dict[int, list]: """Renvoie un graphe-dico à partir du graphe-couple (non orienté, non pondéré)""" s, a = g # Désempaquetage de s et a depuis g graphe = {} # Dictionnaire vide for sommet in s: # pour chaque sommet de l'ensemble des sommets graphe[sommet] = [] # on crée une clé correspond au sommet menant à une liste vide for arete in a: # pour chaque arête présente dans l'ensemble a s1, s2 = arete graphe[s1].append(s2) graphe[s2].append(s1) return graphe def largeur_d_abord_v1(g:dict[int, list], s:int) -> dict: """Renvoie un dictionnaire contenant la distance à s et le parent de chaque sommet""" parcours = {} # on initialise tous les sommets for sommet in g.keys(): parcours[sommet] = {} parcours[sommet]['couleur'] = BLANC parcours[sommet]['distance'] = INFINI parcours[sommet]['parent'] = None # on initialise le sommet de départ parcours[s]["couleur"] = GRIS parcours[s]["distance"] = 0 # on crée la file et on y place le sommet origine a_traiter = collections.deque() # on crée un nouvelle file muable a_traiter.append(s) # on enfile s dans la file muable # on sort et active un sommet à la fois de la file while a_traiter: # tant que la file des sommets à traiter n'est pas vide u = a_traiter.popleft() # on défile le prochain sommet à traiter for v in g[u]: # pour chaque sommet v dans la liste d'adjacence de u if parcours[v]["couleur"] == BLANC: parcours[v]["couleur"] = GRIS parcours[v]["distance"] = parcours[u]["distance"] + 1 parcours[v]["parent"] = u a_traiter.append(v) parcours[u]["couleur"] = NOIR # on renvoie le résultat du parcours return parcours def donner_chemin(sommet_initial, sommet_final, parcours:dict) -> list: """Renvoie une liste des sommets permettant d'aller de initial vers final""" reponse = [] if sommet_initial in parcours.keys() and sommet_final in parcours.keys(): sommet = sommet_final while sommet is not None: reponse.append(sommet) sommet = parcours[sommet]['parent'] inversion = [] while reponse: inversion.append(reponse.pop()) return inversion # Programme principal ================================================= if __name__ == "__main__": s = (0, 1, 2, 3, 4) # Ensemble s des sommets du graphe a = ((0,1), (0,2), (1,3), (2,3), (3,4)) # Ensemble a des arêtes g = (s, a) # Graphe défini comme un couple graphe = creer_graphedico_no_np(g) print("Graphe en tant que dictionnaire :") print(graphe) parcours = largeur_d_abord_v1(graphe, 0) print("\nRésultat du parcours en largeur d'abord") for couple in parcours.items(): print(couple) print("\nRecherche du chemin entre 0 et 3") print(donner_chemin(0, 3, parcours))

Voici le résultat du programme de test précédent :

Graphe en tant que dictionnaire : {0: [1, 2], 1: [0, 3], 2: [0, 3], 3: [1, 2, 4], 4: [3]} Résultat du parcours en largeur d'abord (0, {'couleur': 2, 'distance': 0, 'parent': None}) (1, {'couleur': 2, 'distance': 1, 'parent': 0}) (2, {'couleur': 2, 'distance': 1, 'parent': 0}) (3, {'couleur': 2, 'distance': 2, 'parent': 1}) (4, {'couleur': 2, 'distance': 3, 'parent': 3}) Recherche du chemin entre 0 et 3 [0, 1, 3]

Et avec un bon traitement en Python, ça donne quoi ?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
"""Implémentation du parcours en largeur pour : * un graphe implémenté sous forme d'un graphe-dico {sommet:liste d'adjacence} * une file muable implémentée via la classe deque du module collections -> nouvelleFile() avec file = collections.deque() -> enfiler() avec file.append(x) -> defiler() avec file.popleft() -> lireAvant() avec file[0] -> estVide() avec file Le module comporte les fonctions : -> creer_graphedico_no_np(g:tuple) -> dict[int, list] -> largeur_d_abord_v1(g:dict[int, list], s:int) -> dict -> donner_chemin(sommet_initial, sommet_final, parcours:dict) -> list """ # Importations ================================================= import collections # 2 - déclaration des fonctions ================================ def largeur_d_abord_v2(g:dict[int, list], s:int) -> dict: """Renvoie un dictionnaire contenant la distance à s et le parent de chaque sommet""" decouverts = [] # on initialise la liste des sommets découverts parcours = {} # on initialise le dict des infos sur le parcours # on initialise le sommet de départ decouverts.append(s) # on signale qu'on a découvert ce sommet parcours[s] = {} parcours[s]["distance"] = 0 # et qu'il est à distance 0 de lui-même parcours[s]["parent"] = None # et qu'il est la racine de l'arbre de parcours # on crée la file et on y place le sommet origine a_traiter = collections.deque() # on crée un nouvelle file muable illimitée en taille a_traiter.append(s) # on enfile s dans la file muable # on sort et active un sommet à la fois de la file while a_traiter: # tant que la file des sommets à traiter n'est pas vide u = a_traiter.popleft() # on défile le prochain sommet u à traiter for v in g[u]: # pour chaque sommet v dans la liste d'adjacence de u if v not in decouverts: # si v n'est pas encore decouvert decouverts.append(v) # dire que v est découvert parcours[v] = {} parcours[v]["distance"] = parcours[u]["distance"] + 1 parcours[v]["parent"] = u a_traiter.append(v) # on rajoute v dans la file des sommets à traiter # à partir d'ici u est NOIR car dans decouvert et dans parcours # on renvoie le résultat du parcours return parcours def donner_chemin(sommet_initial, sommet_final, parcours:dict) -> list: """Renvoie une liste des sommets permettant d'aller de initial vers final""" reponse = [] if sommet_initial in parcours.keys() and sommet_final in parcours.keys(): sommet = sommet_final while sommet is not None: reponse.append(sommet) sommet = parcours[sommet]['parent'] inversion = [] while reponse: inversion.append(reponse.pop()) return inversion # Programme de test ================================================= if __name__ == "__main__": graphe_test = {0: [1, 2], 1: [0, 3], 2: [0, 3], 3: [1, 2, 4], 4: [3]} print("Graphe en tant que dictionnaire :") print(graphe_test) parcours = largeur_d_abord_v2(graphe_test, 0) print("\nRésultat du parcours en largeur d'abord") for couple in parcours.items(): print(couple) print("\nRecherche du chemin entre 0 et 3") print(donner_chemin(0, 3, parcours))

Il nous reste à voir le parcours en profondeur et la recherche de cycle.

Nous verrons ensuite en pratique comment mettre tout ceci en machine pour résoudre des problèmes.

Activité publiée le 29 04 2021
Dernière modification : 29 04 2021
Auteur : ows. h.