Remarque : Il est possible d'affecter des valeurs, des poids (coefficients positifs), des couleurs, etc... aux arêtes (le plus souvent) ou bien aux sommets (parfois) : on parle de graphe valué. Ces valeurs peuvent indiquer des distances, des coûts, etc. Pour être plus précis, on peut utiliser les termes graphes pondérés pour un graphe valué par des nombres et graphe étiqueté pour un graphe valué par des chaînes de caractères.
Un peu de vocabulaire pour fixer ces notions :
Ce qui compte, c'est de se promener sur un graphe... :
Dans un graphe connexe :
Pour un graphe donné, on associe à chaque sommet
la liste de ses voisins (sommets adjacents). Dans le cas
d'un graphe orienté, on peut aussi parler de «liste de successeurs»
(flèches partant du sommet considéré) ; dans le cas orienté, on peut aussi
considérer la «liste de prédécesseurs» (flèches arrivant au sommet
considéré).
Lorsque le graphe est valué, on associe à chaque sommet une liste de
tuples (voisin, dictionnaire), où les clés du dictionnaire (poids,
couleur) permettent d'accéder à la/les donnée(s) figurant sur l'arête. Si
on utilise une seule donnée, le dictionnaire n'est pas nécessaire : on le
remplace dans le tuple directement par cette seule donnée.
{A:[B,D,E], B:[A,E], C:[D,E], D:[A,C], E:[A,B,C]}
{A:[B,E], B:[E], C:[D,E], D:[A], E:[]}
{A:[(B,1), (E,2)], B:[(E,2)], C:[(D,1), (E,3)], D:[(A,3)], E:[]}
La matrice d'adjacence est un tableau carré (à double
entrée). Les lignes et les colonnes correspondent aux sommets (ordonnés
dans l'ordre alphanumérique si rien n'est précisé).
Si le sommet X et adjacent à Y (X-->Y), on remplit dans la matrice, à
l'intersection de la ligne de X et de la colonne de Y, un 1 (ou un V pour
«vrai»), ou bien une donnée si le graphe est valué (il y aura alors une
matrice d'adjacence selon chaque type de donnée). S'il n'y a pas de
relation entre X et Y, on note un 0 (ou un F pour «faux»).
Lorsque le graphe n'est pas orienté, la matrice d'adjacence est symétrique
par rapport à sa diagonale (diagonale principale : de en haut à gauche à
en bas à droite) ; aussi, les boucles apparaissent sur la diagonale.
[ [0, 1, 0, 1, 1], [1, 0, 0, 0, 1], [0, 0, 0, 1, 1], [1, 0, 1, 0, 0], [1, 1, 1, 0, 0] ]
[ [0, 1, 0, 0, 1], [0, 0, 0, 0, 1], [0, 0, 0, 1, 1], [1, 0, 0, 0, 0], [0, 0, 0, 0, 0] ]
[ [0, 1, 0, 0, 2], [0, 0, 0, 0, 2], [0, 0, 0, 1, 3], [3, 0, 0, 0, 0], [0, 0, 0, 0, 0] ]
[ [0, 1, 0, 1, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0, 0], [1, 0, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 1, 0], [0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 1, 0, 1] ]
creer_graphe
construit un graphe vide, non-orienté par défaut et
orienté si le booléen ori
est vrai.est_oriente
: renvoie vrai lorsque le graphe est orienté.ajouter_sommet
: ajoute un sommet s au graphe G .
sommet_existe
: renvoie vrai lorsque le sommet s
est dans G
.ajouter_arc
: ajoute un arc de poids w
entre les sommets sa
et
sd
au graphe G
. (Pour un graphe non-pondéré, w
peut valoir par défaut 1.) w
: sd → sa
et sa → sd
.arc_existe
: renvoie vrai
lorsque l'arc entre sa
et sd
est dans G
.supprimer_sommet
: enlève un sommet s
de G
et supprime aussi ses
arcs incidents.supprimer_arc
: enlève les arcs entre les sommets sa
et sd.Graphe |
---|
creer_graphe(ori) : vide |
est_oriente(G) : booléen |
ajouter_sommet(G, s) [sommet pas encore dans G] : vide |
sommet_existe(G, s) : booléen |
ajouter_arc(G, sd, sa, w) [sa et sd sont dans G, l'arc pas encore] : vide |
arc_existe(G, sd, sa) : booléen |
supprimer_sommet(G, s) [s est dans G] : vide |
supprimer_arc(G, sd, sa) [sa et sd sont dans G] : vide |
voisins(G, s)
qui retourne la liste des voisins du sommet s
dans G
.
voisins_poids(G, s)
qui retourne une liste de tuples (v, p)
avec les voisins d'un sommet s
et le poids de l'arête.
liste_adj
qui retourne la liste d'adjacence et matrice_adj
qui retourne la matrice d'adjacence du graphe.
L'algorithme de parcours en largeur (ou BFS, pour Breadth First Search en anglais) permet le parcours d'un graphe ou d'un arbre de la manière suivante : on commence par explorer un nœud source, puis ses successeurs, puis les successeurs non-explorés des successeurs, etc. L'algorithme de parcours en largeur permet de calculer les distances de tous les nœuds depuis un nœud source dans un graphe non-pondéré (orienté ou non-orienté). Il peut aussi servir à déterminer si un graphe non-orienté est connexe.
Pseudo code : L'algorithme s'implémente à l'aide d'une file.
ParcoursLargeur(Graphe G, Sommet s) f = CreerFile() f.enfiler(s) marquer(s) tant que la file est non-vide s = f.defiler() afficher(s) pour tout voisin t de s dans G si t non-marqué f.enfiler(t) marquer(t)
Complexité : La complexité en temps dans le pire cas est en `O( | S | + | A | )` où `| S |` est le nombre de sommets et `| A |` est le nombre d'arcs. En effet, chaque arc et chaque sommet est visité au plus une seule fois.
Graphe
déjà
construite.
L'exploration d'un parcours en profondeur depuis un sommet S fonctionne
comme suit : on suit un chemin dans le graphe jusqu'à un cul-de-sac ou
alors jusqu'à atteindre un sommet déjà visité. On revient alors sur le
dernier sommet où on pouvait suivre un autre chemin puis on explore un
autre chemin. L'exploration s'arrête quand tous les sommets depuis S ont
été visités. Bref, l'exploration progresse à partir d'un sommet S en
s'appelant récursivement pour chaque sommet voisin de S.
Le nom d'algorithme en profondeur est dû au fait que, contrairement à
l'algorithme de parcours en largeur, il explore en fait « à fond » les
chemins un par un : pour chaque sommet, il marque le sommet actuel, et il
prend le premier sommet voisin jusqu'à ce qu'un sommet n'ait plus de
voisins (ou que tous ses voisins soient marqués), et revient alors au
sommet père.
Si G n'était pas un arbre, l'algorithme pourrait a priori tourner
indéfiniment si on continuait l'exploration depuis un sommet déjà visité.
Pour éviter cela, on marque les sommets que l'on visite, de façon à ne pas
les explorer à nouveau. Dans le cas d'un arbre, le parcours en profondeur
est utilisé pour caractériser l'arbre.
explorer(graphe G, sommet s) marquer le sommet s afficher(s) pour tout sommet t voisin du sommet s si t n'est pas marqué alors explorer(G, t); parcoursProfondeur(graphe G, sommet s) pour tout sommet du graphe G en partant du sommet s si s n'est pas marqué alors explorer(G, s)
Graphe
déjà construite.
set
pour désigner une composante connexe. Graphe
déjà
construite. Chaque composante sera représentée par la liste de ses sommets.
Ces algorithmes sont utilisés pour traiter des problèmes d'optimisation (transports, câblage électrique ou réseau, déplacements de pnj dans un jeu vidéo, ...).
Illustration de ces algorithmes sur une carte réelle avec : Pathfinding.
Graph
déjà construite.
Conseils :
Définir deux fonctions :
def dijkstra(self, sd):
Pour créer le tableau de l'
arbre couvrant (arbre qui recouvre tous les sommets d'un graphe) issu du sommet de départdef chemin_dijkstra(self, sd, sa):
Pour extraire le plus court chemin de ce tableau.Le principe doit être vu à titre culturel et peut être travaillé comme approfondissement.
On pourra à ce titre consulter l'article : A*.
import networkx as nx
(nx
est souvent l'abréviation consacrée).
pip install networkx
ou bien pip3 install networkx
.
Graphe
que nous avons implémentée est petite en regard des 4 classes de graphes (Graph
, DiGraph
, MultiGraph
, MultiDiGraph
) utilisés par networkx
. Dans un premier temps, on peut remarquer que créer une arête crée aussi les sommets mentionnés, même s'ils ne sont pas présents dans le graphe. #!/usr/bin/env python
# -*- coding: utf-8 -*-
import matplotlib.pyplot as plt # pour les dessins
import networkx as nx
# Créer un objet graphe (Graph)
G = nx.Graph()
# G = nx.DiGraph() # pour les graphes orientés
# G = nx.MultiGraph() # avec arêtes parallèles
# G = nx.MultiDiGraph() # avec arêtes parallèles orientées
# Ajouter un sommet / une arête (tout objet hashable sauf None) :
G.add_node("A")
G.add_edge("A", "B") # le sommet "B" est ajouté directement
# Ajouter depuis un conteneur plusieurs sommets/arêtes (ici : list, aussi : dict, set, Graph, fichier, ...)
G.add_nodes_from(["B", "C", "D"])
G.add_edges_from([("B", "C"), ("C", "D"), ("A", "C"), ("E","E")])
# On peut ajouter des étiquettes { : } sur les arêtes :
# (début, fin, {clé : valeur} )
G.add_edges_from([("A", "E", {"info": 7}), ("B", "E", {"info": 8})])
print("sommets : \n", G.nodes())
print("triés : \n", sorted(G.nodes(), key=str))
print("nombre de sommets : \n",G.number_of_nodes())
print("arêtes : \n", G.edges(data=True))
print("nombre d'arêtes : \n", G.number_of_edges())
print("plus court chemin : \n", nx.shortest_path(G, "A", "D"))
print("distance : \n", nx.shortest_path_length(G, "A", "D"))
print("Diamètre : \n", nx.diameter(G))
print("Rayon : \n", nx.radius(G))
print("Centre : \n", nx.center(G))
print("accès à une étiquette : \n", G["A"]["E"]["info"])
# G[u][v]['width'] est identique à G.edges[u, v]['width']
# print("vue selon les étiquettes : \n", [G.edges.data("info")])
# print("liste des voisins d'un sommet : \n",[item for item in G.neighbors("E")])
# --- LISTE ET MATRICE D'ADJACENCE ---
# print("liste d'adjacence : \n",[item for item in G.adjacency()])
MatAdjG = nx.adjacency_matrix(G)
MatAdjG.setdiag(MatAdjG.diagonal()*2) # def : boucles comptent 2
print("matrice d'adjacence : \n", MatAdjG.todense())
networkx
et matplotlib
: