Aller au contenu

Graphes

Nous avons vu que les arbes étaient des graphes particuliers. Les graphes sont des objets fondamentaux en informatique qui ont à la fois un intérêt théorique important, puisque tout un pan des mathématiques leur est dédiée. C'est ce qu'on appelle "La théorie des graphes".

Les graphes sont également massivement utilisées pour résoudre des problèmes d'ingénierie, notamment dans les protocoles de routage de paquets sur internet, ou des problèmes scientifiques comme en biologie.

Histoire

La théorie des graphes est née des travaux de Leonhard Euler sur les septs ponts de Königsberg (aujourd'hui Kaliningrad en Russie) en 1736. Le problème consistait à déterminer s'il était possible de travserser la ville en ne passant par chacun des ponts qu'une seule fois.

image

Euler a démontré mathématiquement que cela était impossible en représentant le problème sous la forme d'un graphe.

Aujourd'hui les graphes sont utilisés dans une très grande variété de domaines.

Exemples d'applications

Biologie

En biologie la théorie des graphes permet par exemple de modéliser le fonctionnement du cerveau :

image

Réseaux sociaux

On utilise les graphes pour représenter les relations "d'amitié" dans les réseaux sociaux ou les interactions entre utilisateurs.

image

Internet

Internet peut être vu comme un immense graphe où les routeurs correspondent aux sommets et les câbles aux arêtes.

image

Web

PageRank

Pour comprendre le fonctionnement de l'algorithme PageRank de Google, il faut s'intéresser aux chaînes de Markov (étudiées en maths expertes). On représente les pages web sous forme de sommets et la navigation d'une page vers une autre par des arcs (le graphe est orienté).

Graphe, non orienté et orienté, pondéré

Graphe

Un graphe est une structure de données composée d’objets: les sommets dans laquelle certaines paires d’objets sont reliées par des arêtes (ou arcs dans le cas de graphes orientés).

On note \(G = (V, E)\)\(V\) correspond à l'ensemble des sommets et \(E\) l'ensemble des arêtes (ou arcs). On notera parfois \(G = (V, A)\) un graphe orienté.

Question

Pour les experts en anglais, pourquoi \(V\) et \(E\) ?

Graphe non orienté et orienté

Dans un graphe non orienté, les arêtes ne possèdent pas d'orientation.

Exemple

image

Dans un graphe orienté, les arêtes possèdent une orientation. Ces "arêtes orientées" sont souvent appelées "arcs". On dira qu'un graphe orienté \(G\) est un couple \(G = (V, A)\) avec \(V\) un ensemble de sommets et \(A\) un ensemble d'arcs.

Exemple

image

Arête

Une arête est désignée par les deux sommets u et v qu'elle relie, sous la forme d'une paire \(\{u, v \}\). Ici, les accolades représentent un ensemble et donc l'ordre n'a pas d'importance, \(\{u, v\}\) est équivalent à \(\{v, u \}\).

Arc

Un arc est désigné par les deux sommets u et v qu'elle relie, sous la forme d'un couple \((u, v)\). Ici l'ordre a une importance \((u, v)\) est différent de \((v, u)\).

Question

D'après vous, doit on représenter le problème des ponts de Königsberg plutôt par un graphe orienté ou un graphe non orienté ?

Graphe pondéré

Un graphe pondéré Un graphe pondéré est un graphe dans lequel chaque arête est associée à un poids (un nombre).

En fonction des problèmes on peut chercher à maximiser ou minimiser la somme des poids.

Encore des définitions

Chaîne

Dans un graphe non orienté, une chaîne reliant \(u\) à \(v\) est définie par une suite finie d'arêtes consécutives, reliant \(u\) à \(v\).

Sommets adjacents

Deux sommets sont adjacents s'ils sont reliés par une arête.

Degré d'un sommet

Le degré (ou valence) d'un sommet d'un graphe est le nombre de liens (arêtes ou arcs) reliant ce sommet, avec les boucles comptées deux fois.

Ordre d'un graphe

L'ordre d'un graphe est le nombre de sommets de ce graphe.

Connexité

Un graphe non orienté est dit connexe lorsque deux sommets quelconques peuvent être reliés par une chaine.

Implémentation des graphes

On représente un graphe sous la forme d'une liste d'adjacence ou d'une matrice d'adjacence. Les deux représentations ont des avantages et des inconvénients. La complexité des algorithmes dépendra de la représentation choisie.

Liste d'adjacence

A chaque sommet \(v\) dans un graphe nous allons associer les sommets connectés à \(v\) sous la forme d'une liste.

Graphes non orientés

image

En utilisant un dictionnaire et des listes Python :

graphe = {
     1 : [2, 3],
     2 : [1, 3],
     3 : [1, 2, 4],
     4 : [3]
}

Graphes orientés

image

En utilisant un dictionnaire et des listes Python :

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

Matrice d'adjacence

On appelle matrice carrée une matrice qui comporte le même nombre de lignes et de colonnes. Les matrices d'adjacences sont des matrices carrées.

A chaque ligne correspond un sommet du graphe et à chaque colonne correspond aussi un sommet du graphe. À chaque intersection, on place un 1 s'il existe une arête entre le sommet \(i\) et le sommet \(j\), et un zéro s'il n'existe pas d'arête entre le sommet \(i\) et le sommet \(j\).

Graphes non orientés

Soit le graphe suivant :

image

Sa Matrice d'ajacence associée :

image

Il est assez simple d'utiliser les matrices d'adjacence en Python grâce aux tableaux de tableaux.

graphe = [
    [0, 1, 1, 1, 0, 0, 0],
    [1, 0, 0, 0, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 0],
    [1, 0, 0, 0, 0, 0, 1],
    [0, 1, 0, 0, 0, 1, 0],
    [0, 1, 0, 0, 1, 0, 0],
    [0, 1, 0, 1, 0, 0, 0]
]

Graphes orientés

image

graphe = [
    [0, 0, 1, 1, 0, 0, 0],
    [1, 0, 0, 0, 0, 1, 1],
    [1, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 1],
    [0, 1, 0, 0, 0, 1, 0],
    [0, 0, 0, 0, 1, 0, 0],
    [0, 1, 0, 1, 0, 0, 0]
]

graphes pondérés

Dans le cas des graphes pondérés pour représenter la matrice d'ajacence, on remplace les 1 par les poids correspondants à chaque arête (ou arc si le graphe est orienté).

Exercice

Faites la matrice d'ajacence du graphe ci-dessous.
image

On peut inclure l'information correspondant aux poids des arêtes dans la liste d'ajacence.

Exercice

Proposez une représentation sous forme de liste d'ajacence du graphe ci-dessous.
image

Comparaison entre les deux représentations

\(|V|\) correspond au nombre de sommets. \(|E|\) correspond au nombre de d'arêtes.

Opérations Matrice d'adjacence Liste d'adjacence
Espace \(O(\lvert V\rvert ^2)\) \(O(\lvert V\rvert + \lvert E\rvert)\) avantageux quand le graphe est peu dense
Vérifier si deux sommets sont adjacents \(O(1)\) \(O(\lvert V \rvert)\)

Algorithmie

Tous comme pour les arbres, il est possible de réaliser deux types de parcours d’un arbre:

  • le parcours en profondeur(Depth-First Search)
  • le parcours en largeur(Breadth First Search)

Cependant, contrairement aux arbres

  • Il n’y a pas de racine, donc on doit choisir à partir de quel noeud on part: le noeud source.
  • Il peut y avoir un nombre quelconque d’arêtes, et il faut donc marquer les chemins déjà empruntés lors du parcours.

Parcours en largeur

Ici, on visite les sommets les plus proches du sommet précédent. Le parcours en largeur visite donc les sommets à distance 1, puis ceux à distance 2, etc

On peut implémenter cet algorithme de manière itérative en utilisant une file.

Les étapes de l'algorithme :

  • 1) mettre le nœud source dans la file
  • 2) retirer le nœud du début de la file pour le traiter
  • 3) mettre tous ses voisins non explorés dans la file (à la fin)
  • 4) si la file n'est pas vide reprendre à l'étape 2.
 parcoursLargeur(Graphe G, Sommet s):
       f = CreerFile();
       f.enfiler(s);
       tant que la file est non vide
                s = f.defiler();
                si s non marqué
                    afficher(s);
                    marquer(s);
                    pour tout voisin v de s dans G
                        f.enfiler(v);

La version Python

def parcours_largeur(graphe, sommet_depart):
   """
   Auteur : 'Monsieur NSI'


   Args:
    graphe (dict): une liste d'ajacence
    sommet_depart (str): le sommet de départ
   """
   # On initialise la liste des sommets déjà visitée
   deja_visite = []
   # On initialise la file
   file = []
   # On enfile le premier sommet
   file.append(sommet_depart)
   # Tant que la file n'est pas vide
   while file:
    # Défiler le sommet se trouvant en tête de la file
    sommet = file.pop(0)
    if sommet not in deja_visite:
        print(f'on visite {sommet}')
        deja_visite.append(sommet)
        # Pour chaque sommet "enfant" adjacent au sommet "sommet"
        for enfant in graphe[sommet]:
                file.append(enfant)

Exercice

Effectuez un parcours en largeur de ce graphe en partant du sommet A : image

Parcours en profondeur

On marque le sommet actuel, et on prend le premier sommet voisin jusqu'à ce qu'un sommet n'ait plus de voisins (ou que tous ses voisins soient visités), et on revient alors au sommet père.

Algorithme récursif

parcoursProfondeur(deja_visite dv, graphe G, sommet s)
      si s n'est pas dans dv
          afficher(s)
          ajouter s dans dv
          pour tout voisin v de s:
                parcoursProfondeur(deja_visite dv, graphe G, sommet v)

La version Python

deja_visite = []

def parcours_profondeur_recursif(deja_visite, graphe, sommet):
    if sommet not in deja_visite:
        print(f'on visite {sommet}')
        deja_visite.append(sommet)
        for voisin in graphe[sommet]:
            parcours_profondeur_recursif(deja_visite, graphe, voisin)

On peut également implémenter cet algorithme de manière itérative en utilisant une pile.

 parcoursProfondeur(Graphe G, Sommet s):
       p = CreerPile();
       p.empiler(s);
       tant que la pile est non vide
                s = p.depiler();
                si s non marqué
                    afficher(s);
                    marquer(s);
                    pour tout voisin v de s dans G
                        p.empiler(v);

La version Python

def parcours_profondeur(graphe, sommet_depart):
   """
   Auteur : 'Monsieur NSI'


   Args:
    graphe (dict): une liste d'ajacence
    sommet_depart (str): le sommet de départ
   """
   # On initialise la liste des sommets déjà visitée
   deja_visite = []
   # On initialise la pile
   pile = []
   # On empile le premier sommet
   pile.append(sommet_depart)
   # Tant que la pile n'est pas vide
   while pile:
    # Dépiler le sommet se trouvant en haut de la pile
    sommet = pile.pop()
    if sommet not in deja_visite:
        print(f'on visite {sommet}')
        deja_visite.append(sommet)
        # Pour chaque sommet "enfant" adjacent au sommet "sommet"
        for enfant in graphe[sommet]:
                pile.append(enfant)

Exercice

Effectuez un parcours en profondeur de ce graphe en partant du sommet A : image

Détection de cycle

Cycle

Un cycle est une suite d’arêtes consécutives dont les deux sommets extrémités sont identiques.

Pour détecter un cycle dans un graphe non orienté, nous allons simplement parcourir le graphe en profondeur et vérifier qu’aucune arête pointe vers un sommet déjà visité. Il faut aussi vérifier que le sommet n'est pas le prédécesseur du dernier voisin visité. On fait cette vérification car sinon entre deux sommets reliés par une arête on détecterait un cycle alors que l'on est simplement revenu "en arrière".

Par exemple si deux sommets A et B sont reliés par une arête, et que l'on va du sommet A pour aller ensuite vers le sommet B, et qu'on revient ensuite dans le "sens inverse" de B vers A alors on ne doit pas détecter de cycle.

 detectionCycle(Graphe G, Sommet s):
       p = CreerPile();
       p.empiler(s);
       tant que la pile est non vide
                s = p.depiler();
                si s non marqué
                    afficher(s);
                    marquer(s);
                    pour tout voisin v de s dans G
                        p.empiler(v);
                sinon
                    si s n'est pas le prédécesseur de v
                        renvoyer Vrai;
        renvoyer Faux;

Dans un graphe orienté on parle de circuit.

Circuit

Dans un graphe orienté, on appelle circuit une suite d'arcs consécutifs (chemin) dont les deux sommets extrémités sont identiques.

Recherche de chaîne et de chemin dans un graphe

Recherche de chaîne dans un graphe

Nous allons maintenant nous intéresser à un algorithme qui permet de trouver une chaine entre 2 sommets (sommet de départ et sommet d'arrivée). Les algorithmes de ce type ont une grande importance et sont très souvent utilisés).

 rechercheChaine(Graphe G, Sommet source, Sommet destination):
       p = CreerPile();
       p.empiler(s);
       predecesseurs = {source : Vide}
       tant que la pile est non vide
                s = p.depiler();
                si s non marqué
                    marquer(s);
                    pour tout voisin v de s dans G
                        p.empiler(v);
                        predecesseurs[v] = s;
                        si v est égal à destination
                            renvoyer predecesseurs;
        renvoyer None;

Question

Sur quel algorithme s'appuie cette recherche de chaîne ?

Exercice

Prenez une feuille et implémenter cet algorithme en Python

Plus court chemin dans un graphe pondéré

Les algorithmes de Dijkstra et de Bellman-Ford permettent de répondre à ce problème. L'algorithme de Bellman-Ford est utilisé avec le protocole RIP.

Nous présentons ici l’algorithme de Dijkstra plus en détail.

Cet algorithme est utilisé dans le protocole OSPF.