Représenter un graphe en machine
Ce que ce chapitre apporte5 points
- Construire une liste d'adjacence, une matrice d'adjacence et une liste d'arêtes à partir du même graphe.
- Donner le coût en mémoire et en temps de chaque opération courante sur chacune.
- Distinguer un graphe creux d'un graphe dense, et choisir la représentation en conséquence.
- Interpréter les puissances de la matrice d'adjacence.
- Adapter la représentation aux graphes orientés et pondérés.
Le même graphe, trois écritures
Prenons ce graphe et écrivons-le trois fois.
La liste d'adjacence donne, pour chaque sommet, la liste de ses voisins. En Python, un dictionnaire de set.
La matrice d'adjacence est un tableau où si l'arête existe, 0 sinon.
La liste d'arêtes est la simple liste des paires.
| A | B | C | D | E | F | d | |
|---|---|---|---|---|---|---|---|
| A | 0 | 1 | 1 | 0 | 0 | 0 | 2 |
| B | 1 | 0 | 1 | 0 | 0 | 0 | 2 |
| C | 1 | 1 | 0 | 1 | 0 | 0 | 3 |
| D | 0 | 0 | 1 | 0 | 1 | 1 | 3 |
| E | 0 | 0 | 0 | 1 | 0 | 1 | 2 |
| F | 0 | 0 | 0 | 1 | 1 | 0 | 2 |
- A
- : {B, C}
- B
- : {A, C}
- C
- : {A, B, D}
- D
- : {C, E, F}
- E
- : {D, F}
- F
- : {D, E}
- (A, B)
- (A, C)
- (B, C)
- (C, D)
- (D, E)
- (D, F)
- (E, F)
Les trois écritures contiennent exactement la même information : chacune se reconstruit à partir de n'importe laquelle des deux autres. Ce qui les sépare n'est pas ce qu'elles disent, mais ce qu'elles rendent facile.
La diagonale nulle. La case demande « le sommet est-il son propre voisin ». Un 1 sur la diagonale est une boucle ; un graphe simple n'en a aucune.
Les degrés, gratuitement. La somme d'une ligne compte les voisins du sommet : c'est son degré, sans parcourir quoi que ce soit. C'est la colonne
d ajoutée à droite.
A[i][j] sans se demander lequel de ou est le plus grand vaut bien ce gâchis.Sur les très grands graphes, en revanche, ce facteur 2 s'ajoute au et la matrice devient inutilisable bien avant. C'est ce que chiffre la section suivante.
Ce que chacune coûte
| Opération | Liste d'adjacence | Matrice | Liste d'arêtes |
|---|---|---|---|
| et sont-ils voisins ? | avec un set | ||
| Parcourir les voisins de | |||
| Ajouter une arête | |||
| Supprimer une arête | |||
| Mémoire |
La ligne décisive est la deuxième. Tous les algorithmes de ce module passent leur temps à demander « qui sont les voisins de ce sommet ». Avec une liste d'adjacence, la réponse coûte le degré du sommet ; avec une matrice, elle coûte , qu'il y ait deux voisins ou aucun, car il faut lire toute la ligne.
Un graphe est creux quand est de l'ordre de , dense quand approche son maximum .
Pour un graphe creux, la liste d'adjacence gagne sur les deux tableaux à la fois : moins de mémoire et parcours plus rapide. C'est le choix par défaut.
La matrice ne se justifie que sur un graphe dense, sur de très petits graphes, ou quand on veut exploiter ses propriétés algébriques.
Matrice : cases. Même à un bit par case, cela fait plus de cent mille téraoctets. Impossible, et pas approximativement : impossible.
Liste d'adjacence : entrées. Quelques téraoctets, réparties sur un ensemble de machines. C'est ce que font les vrais systèmes.
1.Un réseau social d'un milliard de comptes, 200 relations chacun. Quelle représentation ?
2.Avec une matrice d'adjacence, parcourir les voisins d'un sommet coûte…
3.Sur un graphe non orienté sans boucle, la matrice d'adjacence est…
Ce que la matrice sait faire toute seule
La matrice a un pouvoir que les deux autres représentations n'ont pas : elle se multiplie.
Le coefficient de donne le nombre de chaînes de longueur exactement entre les sommets et .
La raison est simple à voir sur . Le coefficient vaut , et chaque terme de cette somme vaut 1 exactement quand est à la fois voisin de et voisin de , c'est-à-dire quand il y a une chaîne . La somme compte donc ces chaînes.
Le nombre de triangles vaut donc . C'est un résultat qu'on démontre en deux lignes et qui impressionne beaucoup en devoir.
Orienté, pondéré : les mêmes structures, adaptées
Pour un graphe orienté, on ne remplit qu'un sens : la matrice cesse d'être symétrique, et la liste d'adjacence ne contient que les successeurs. Si l'algorithme a besoin des prédécesseurs, il faut construire aussi le graphe inverse.
Pour un graphe pondéré, la case de la matrice contient le poids au lieu de 1, et la liste d'adjacence stocke des paires (voisin, poids).
Et voici sa matrice. Elle se lit comme la précédente, à une différence près qui change tout.
| A | B | C | D | d⁺ | |
|---|---|---|---|---|---|
| A | 0 | 4 | 2 | 0 | 2 |
| B | 0 | 0 | 0 | 5 | 1 |
| C | 0 | 1 | 0 | 8 | 2 |
| D | 0 | 0 | 0 | 0 | 0 |
| d⁻ | 0 | 2 | 1 | 2 |
Il y a désormais deux degrés. Les cases remplies d'une ligne comptent les arcs qui partent, c'est le degré sortant ; celles d'une colonne comptent ceux qui arrivent, c'est le degré entrant . La figure affiche les deux, en marge droite et en bas. Attention à ne pas additionner les poids : un degré compte des arcs, il ne les pèse pas.
Chercher les prédécesseurs coûte cher. Lire une ligne est immédiat, lire une colonne oblige à descendre toute la matrice. C'est pour cela qu'un algorithme qui remonte les arcs, comme la recherche d'un plus court chemin à rebours, construit d'abord le graphe inverse plutôt que de payer ce parcours à chaque fois.
{u, v} demande deux écritures dans la liste d'adjacence. En oublier une donne un graphe à demi orienté, et les parcours donnent alors des résultats incompréhensibles.Utiliser une liste au lieu d'un ensemble. Avec une liste Python, tester l'appartenance coûte au lieu de , et rien n'empêche d'insérer deux fois la même arête. Le
set règle les deux problèmes d'un coup.
Exercices type
Quelle représentation pour un réseau routier de 200 000 carrefours ?
Liste d'adjacence.
Un carrefour a rarement plus de six routes, donc est de l'ordre de : le graphe est très creux. La liste occupe quelques mégaoctets.
La matrice demanderait cases, soit des dizaines de gigaoctets pour stocker presque exclusivement des zéros. Et chaque recherche de voisins lirait 200 000 cases pour en trouver quatre.
Comment vérifier sur la matrice qu'un graphe est non orienté et sans boucle ?
Non orienté : la matrice doit être symétrique, c'est-à-dire pour toute paire.
Sans boucle : la diagonale doit être nulle, car signifierait une arête d'un sommet vers lui-même.
Ces deux contrôles se codent en deux boucles et servent de garde-fou après une lecture de fichier : une matrice non symétrique alors qu'on attendait un graphe non orienté signale presque toujours une arête ajoutée dans un seul sens.
Que vaut le coefficient de , et pourquoi ?
Il vaut le degré du sommet .
Le coefficient de compte les chaînes de longueur 2 allant de à , c'est-à-dire les allers-retours . Il y en a exactement un par voisin .
Donc la diagonale de donne la suite des degrés, ce qui fournit au passage un contrôle de cohérence gratuit sur une matrice construite à la main.
Pourquoi une liste d'arêtes reste-t-elle utile malgré ses coûts ?
Parce que certains algorithmes ne demandent jamais « qui sont les voisins de », mais parcourent simplement toutes les arêtes.
C'est le cas de l'algorithme de Kruskal, qui trie les arêtes par poids croissant et les examine une par une : la liste d'arêtes est exactement la forme dont il a besoin.
C'est aussi le format d'échange le plus courant dans les fichiers, une ligne par arête, parce qu'il est compact et se lit sans connaître le nombre de sommets à l'avance.
Un graphe est stocké en liste d'adjacence avec des listes Python. Quel problème ?
Le test d'appartenance v in adjacence[u] coûte sur une liste, contre sur un set.
Sur un algorithme qui teste l'adjacence dans une boucle interne, cela transforme un en quelque chose de nettement plus lent, sans que rien ne le signale.
Une liste autorise en outre les doublons : ajouter deux fois la même arête passe inaperçu et fausse ensuite tous les degrés. Le set interdit les deux erreurs.
La méthode
- Compter l'ordre de grandeur de et de avant de choisir.
- Liste d'adjacence par défaut, avec des
setet non des listes. - Matrice seulement si le graphe est dense, minuscule, ou si on veut la multiplier.
- Liste d'arêtes quand l'algorithme balaie les arêtes sans jamais chercher un voisinage.
- Écrire les deux sens pour une arête non orientée, ou passe par une fonction d'ajout qui le fait pour soi.
- Vérifier la symétrie et la diagonale après toute construction depuis un fichier.
- Garder le graphe inverse si l'algorithme a besoin des prédécesseurs.
Synthèse
- Trois représentations : liste d'adjacence, matrice, liste d'arêtes.
- La liste d'adjacence coûte en mémoire et donne les voisins en .
- La matrice coûte quel que soit le nombre d'arêtes.
- Les graphes réels sont creux : la liste d'adjacence est le choix par défaut.
- Sur un graphe non orienté, la matrice est symétrique et sa diagonale est nulle.
- La somme d'une ligne de la matrice donne le degré du sommet.
- Le coefficient de compte les chaînes de longueur .
- La trace de vaut six fois le nombre de triangles.
- Pour un graphe orienté, il faut parfois stocker aussi le graphe inverse.
Mettre en pratique
Degrés, matrice d'adjacence et liste d'adjacence.
- Le degré entrant d'un sommetNiveau 1
- De la matrice à la liste d'adjacenceNiveau 2
- Débogage : une arête à sens uniqueNiveau 2