Dans cette partie, on considère le même graphe orienté que dans le chapitre précédent :
graphe = { 'A': ['B','C'], 'B': ['A'], 'C': ['A','B'] }
def matrice(graphe): M = [] sommets = graphe.keys() for i in sommets: ligne = [int(j in graphe[i]) for j in sommets] M.append(ligne) return M
Remarque : pour afficher la matrice, plutôt que
print(matrice)
il vaut mieux faire
for ligne in matrice: print(ligne)
qui affiche les lignes de la matrice l'une en-dessous de l'autre.
Remarque : l'ordre dans lequel les clés du dictionnaire sont ordonnées
par la méthode keys
n'est pas toujours le même. Ce n'est pas
important lorsqu'il s'agit de graphes, mais on peut de toute façon y
remédier en utilisant un orderedDict
.
def grapheor(matrice): g = {} N = len(matrice) for i in range(N): g[chr(65+i)] = [] for i in range(N): adjlist = [] for j in range(N): if matrice[i][j]==1: g[chr(65+i)].append(chr(65+j)) return g
Pour engendrer automatiquement les noms des sommets, on utilise la
fonction chr
qui code l'Ascii
. On rappelle que
le code 65
représente un "A" (ici associé à la ligne 0 de la
matrice).
Dans cette partie on considère des graphes non orientés. On prendra comme exemple, le graphe de Moser :
Ce graphe peut être décrit par cette structure :
{'A': ['B', 'C', 'D'], 'B': ['A', 'C', 'F'], 'C': ['A', 'B', 'D'], 'D': ['A', 'C', 'E', 'G'], 'E': ['D', 'F', 'G'], 'F': ['B', 'E', 'G'], 'G': ['D', 'E', 'F']}
Un parcours de graphe est un ordre d'énumération de ses sommets.
En partant du sommet A, qu'on colorie en vert pour se rappeler qu'on l'a déjà vu :
On construit la liste des sommets déjà visités. Tout d'abord on ajoute à la liste (contenant déjà A), tous les voisins de A (coloriés en cyan) :
Puis on ajoute les voisins des voisins (mais pas A puisqu'il a déjà été visité). Le seul voisin de B non encore visité est F :
Tous les voisins de C ont déjà été visités. On colorie donc les voisins de D (en magenta):
En Python, on a donc besoin de deux listes : visités
qui est la liste
des sommets déjà visités (ou coloriés), et à_voir
qui contient la
liste des sommets encore à voir.
def en_largeur(graphe): visités = [] à_voir = list(graphe.keys()) while à_voir: S = à_voir.pop(0) if S not in visités: visités.append(S) for T in graphe[S]: if T not in visités: à_voir.append(T) return visités
L'instruction S = à_voir.pop(0)
fait que le prochain sommet qu'on
va examiner est celui qui est au début de la liste, donc le moins récent puisque
à_voir.append(T)
fait que la tâche la plus récente est au contraire
reléguée à la fin de la liste. La structure ainsi simulée est une file
qu'on verra plus tard.
Le parcours du graphe de Moser en largeur est ABCDFEG
.
Au lieu de traiter d'abord le plus ancien sommet de la liste à_faire
,
on peut aussi traiter au contraire le sommet le plus récent d'abord. On verra dans
le chapitre sur les arbres que cela revient à aller en priorité vers les régions
les plus profondes de l'arbre.
def en_profondeur(graphe): visités = [] à_voir = list(graphe.keys()) while à_voir: S = à_voir.pop() if S not in visités: for T in graphe[S]: if T not in visités: à_voir.append(T) visités.append(S) return visités
La structure simulée avec à_voir.append(T)
et S = à_voir.pop()
est une pile qui l'on verra également plus tard.
Le parcours en profondeur du graphe de Moser est ABFGEDC
.
Pour effectuer un parcours du graphe en profondeur à partir d'un sommet, on peut, pour chacun des sommets qui lui sont adjacents et pas encore visités, effectuer un parcours en profondeur du graphe à partir de ce nouveau sommet :
def parcours(graphe,sommet,visités=[]): visités.append(sommet) for voisin in graphe[sommet]: if voisin not in visités: visités = parcours(graphe,voisin,visités) return visités
On remarque que la liste visités
est passée en argument à chaque
appel de la fonction. On dit qu'elle a été mémoïsée. Cet algorithme est
un exemple de programmation dynamique : pour trouver un parcours en
profondeur de tout le graphe, on utilise les parcours en profondeur des
successeurs du sommet de départ.
Les parcours en largeur et en profondeur sont tous deux de complexité linéaire, par rapport au nombre de sommets, et par rapport au nombre d'arêtes du graphe.
Un chemin est une suite de sommets tels que chacun est adjacent à la fois au précédent et au suivant.
Un chemin d'un sommet S
à un sommet T
est
ST
si S et T sont adjacents.R
à S
suivi de l'arête RT
.Il est donc possible de programmer récursivement la recherche d'un chemin entre deux sommets dans un graphe :
def chemin(S,T,graphe): if S==T: return S else: for R in graphe[T]: return chemin(S,R,graphe)+T
Un cycle est un chemin dont le départ et l'arrivée coïncident.
On ne considère pas ABA
comme un cycle (c'est une même arête
parcourue dans les deux sens) mais ABC
est un cycle, ainsi que
ABCD
etc.
Pour chercher si un graphe a un cycle, on le parcourt en profondeur et si jamais on trouve, au cours de ce parcours, un sommet
def cycles(graphe,sommet,visités=[]): visités.append(sommet) for voisin in graphe[sommet]: if voisin not in visités: for s in visités: if s in graphe[voisin] and s!=sommet: print(visités+[voisin,s]) visités = cycles(graphe,voisin,visités) return visités
Cet algorithme ne trouve pas tous les cycles du graphe de Moser, mais il trouve
ABCA
, ABCDA
, BCDEFB
, DEFGD
et
EFGE
.
Le parcours en largeur permet également de détecter un cycle :