web-dev-qa-db-fra.com

Quoi de mieux, des listes de contiguïté ou des matrices de contiguïté pour les problèmes de graphes en C ++?

Quoi de mieux, listes de contiguïté ou matrice de contiguïté, pour les problèmes de graphes en C++? Quels sont les avantages et les inconvénients de chacun?

108
magiix

Cela dépend du problème.

Matrice d'adjacence

  • Utilise la mémoire O (n ^ 2)
  • Il est rapide de rechercher et de vérifier la présence ou l'absence d'un Edge spécifique
    entre deux nœuds quelconques O (1)
  • Il est lent à parcourir tous les bords
  • Il est lent d'ajouter/supprimer un nœud; une opération complexe O (n ^ 2)
  • Il est rapide d'ajouter un nouveau Edge O (1)

Liste des Adjacences

  • L’utilisation de la mémoire dépend du nombre de bords (et non du nombre de nœuds),
    qui pourrait économiser beaucoup de mémoire si la matrice d'adjacence est rare
  • Recherche de la présence ou de l'absence d'un Edge spécifique entre deux nœuds quelconques
    est légèrement plus lent qu'avec la matrice O (k); où k est le nombre de nœuds voisins
  • Il est rapide d'itérer sur toutes les arêtes car vous pouvez accéder directement aux nœuds voisins
  • Il est rapide d'ajouter/supprimer un nœud; plus facile que la représentation matricielle
  • Il est rapide d'ajouter un nouveau Edge O (1)
104
Mark Byers

Cette réponse ne concerne pas uniquement le C++, car tout ce qui est mentionné concerne les structures de données elles-mêmes, indépendamment du langage. Et, ma réponse est de supposer que vous connaissez la structure de base des listes et matrices d'adjacence.

Mémoire

Si la mémoire est votre préoccupation principale, vous pouvez suivre cette formule pour un graphe simple qui permet des boucles:

Une matrice d'adjacence occupe n2/ Espace de 8 octets (un bit par entrée).

Une liste d'adjacence occupe 8e espace, où e est le nombre d'arêtes (ordinateur 32 bits).

Si nous définissons la densité du graphe comme d = e/n2 (nombre d'arêtes divisé par le nombre maximal d'arêtes), nous pouvons trouver le "point d'arrêt" où une liste occupe plus de mémoire qu'une matrice:

8e> n2/ 8 quand d> 1/64

Donc, avec ces chiffres (toujours spécifiques à 32 bits), le point d'arrêt atterrit à 1/64 . Si la densité (e/n2) est plus grand que 1/64, alors un matrice est préférable si vous voulez économiser de la mémoire.

Vous pouvez lire à ce sujet sur wikipedia (article sur les matrices de contiguïté) et sur de nombreux autres sites.

Note latérale : On peut améliorer l'efficacité spatiale de la matrice d'adjacence en utilisant une table de hachage où les clés sont des paires de sommets (non orienté uniquement).

Itération et recherche

Les listes d’adjacence constituent un moyen compact de ne représenter que les arêtes existantes. Cependant, cela se fait au prix d’une éventuelle lenteur de la recherche d’arêtes spécifiques. Étant donné que chaque liste est aussi longue que le degré d'un sommet, le temps de recherche dans le pire des cas pour la vérification d'un bord spécifique peut devenir O (n), si la liste n'est pas ordonnée. Cependant, rechercher les voisins d'un sommet devient trivial et, pour un graphe peu dense ou de petite taille, le coût d'une itération dans les listes de contiguïté peut être négligeable.

Les matrices d’adjacence, d’autre part, utilisent plus d’espace afin de fournir un temps de recherche constant. Étant donné que chaque entrée possible existe, vous pouvez vérifier l'existence d'un Edge en temps constant à l'aide d'index. Cependant, la recherche de voisin prend O(n) puisque vous devez vérifier tous les voisins possibles. L’inconvénient évident de l’espace est que, pour les graphes fragmentés, un remplissage important est ajouté. Voir la discussion sur la mémoire ci-dessus pour plus de détails. informations à ce sujet.

Si vous ne savez toujours pas quoi utiliser : la plupart des problèmes du monde réel produisent des graphiques clairsemés ou de grande taille, mieux adaptés aux représentations de liste de contiguïté. Ils peuvent sembler plus difficiles à implémenter, mais je vous assure que ce n’est pas le cas. Lorsque vous écrivez un fichier BFS ou DFS et souhaitez extraire tous les voisins d’un nœud, il ne vous reste plus qu’une ligne de code. Cependant, notez que je ne fais pas la promotion des listes de contiguïté en général.

72
keyser

D'accord, j'ai compilé les complexités temps-espace des opérations de base sur les graphiques.
L’image ci-dessous devrait s’expliquer d'elle-même.
Remarquez comment la matrice d'adjacence est préférable lorsque nous nous attendons à ce que le graphique soit dense et comment la liste d'adjacence est préférable lorsque nous nous attendons à ce que le graphique soit peu dense.
J'ai fait certaines hypothèses. Demandez-moi si une complexité (temps ou espace) nécessite une clarification. (Par exemple, pour un graphe fragmenté, j'ai pris En comme une petite constante, car j’ai supposé que l’ajout d’un nouveau sommet ajouterait seulement quelques arêtes, car nous nous attendions à ce que le graphe reste éparse même après l’ajout de sommet.)

S'il vous plaît dites-moi s'il y a des erreurs.

enter image description here

30
John Red

Cela dépend de ce que vous recherchez.

Avec matrices d'adjacence , vous pouvez répondre rapidement aux questions concernant le fait de savoir si un bord spécifique entre deux sommets appartient au graphique et vous pouvez également avoir des insertions rapides et des suppressions de bords. L'inconvénient est que vous devez utiliser un espace excessif, en particulier pour les graphes comportant de nombreux sommets, ce qui est très inefficace, surtout si votre graphique est peu dense.

Par contre, avec listes de contiguïté , il est plus difficile de vérifier si un bord donné est dans un graphique, car vous avez pour rechercher dans Edge la liste appropriée, mais ils occupent moins d'espace.

En règle générale, les listes de contiguïté constituent la structure de données appropriée pour la plupart des applications de graphes.

16
Alex

Supposons que nous avons un graphe qui a n nombre de nœuds et m nombre d'arêtes,

Exemple de graphique
enter image description here

Matrice d'adjacence: Nous créons une matrice qui a n nombre de lignes et de colonnes, donc en mémoire, cela prendra de l'espace. c'est proportionnel à n2. Vérifier si deux nœuds portant le nom u et v a un bord entre eux prendra (1) fois. Par exemple, si vous recherchez (1, 2) un Edge, celui-ci se présentera comme suit dans le code:

if(matrix[1][2] == 1)

Si vous voulez identifier toutes les arêtes, vous devez itérer sur la matrice car cela nécessitera deux boucles imbriquées et il faudra (n2). (Vous pouvez simplement utiliser la partie triangulaire supérieure de la matrice pour déterminer toutes les arêtes, mais ce sera à nouveau (n2))

Liste des Adjacences: Nous créons une liste que chaque nœud pointe également vers une autre liste. Votre liste comportera des éléments n et chaque élément indiquera une liste comportant un nombre d'éléments égal au nombre de voisins de ce nœud (regardez l'image pour une meilleure visualisation). Il faudra donc un espace mémoire proportionnel à n + m. Vérifier si (u, v) est un bord prendra O(deg(u)) le temps pendant lequel deg (u) est égal au nombre de voisins de u. Parce qu'au plus, vous devez itérer sur la liste indiquée par le symbole U. L'identification de toutes les arêtes prendra (n + m).

Liste des exemples de graphes adjacents

enter image description here
Vous devez faire votre choix en fonction de vos besoins. En raison de ma réputation, je ne pouvais pas mettre d'image de matrice, désolé pour cela

7
Muhammed Kadir

Si vous envisagez l’analyse de graphes en C++, le premier endroit à commencer serait probablement le bibliothèque de graphes boostés , qui implémente un certain nombre d’algorithmes, notamment BFS.

[~ # ~] éditer [~ # ~]

Cette question précédente sur SO aidera probablement:

comment-créer-un-c-boost-undirected-graphe-et-le-traverse-en-profondeur-première-recherche h

7
Binary Nerd

Ceci est mieux répondu avec des exemples.

Pensez à Floyd-Warshall par exemple. Nous devons utiliser une matrice d'adjacence, sinon l'algorithme sera plus lent asymptotiquement.

Ou que se passe-t-il s'il s'agit d'un graphe dense sur 30 000 sommets? Ensuite, une matrice d'adjacence peut sembler logique, car vous stockerez 1 bit par paire de sommets, au lieu des 16 bits par bord (minimum requis pour une liste d'adjacence): 107 Mo au lieu de 1,7 Go.

Mais pour les algorithmes tels que DFS, BFS (et ceux qui l'utilisent, comme Edmonds-Karp), la recherche avec priorité (Dijkstra, Prim, A *), etc., une liste d'adjacence est aussi efficace qu'une matrice. Une matrice peut avoir un léger contour lorsque le graphique est dense, mais uniquement par un facteur constant non remarquable. (Combien? C'est une question d'expérimentation.)

4
Evgeni Sergeev

Selon l'implémentation de la matrice d'adjacence, le 'n' du graphique doit être connu plus tôt pour une implémentation efficace. Si le graphique est trop dynamique et nécessite une expansion de la matrice de temps en temps, cela peut également être considéré comme un inconvénient?

2
ChrisOdney

Pour ajouter à la réponse de keyser5053 sur l'utilisation de la mémoire.

Pour tout graphe dirigé, une matrice d'adjacence (à 1 bit par bord) consomme n^2 * (1) bits de mémoire.

Pour un graphe complet , une liste de contiguïté (avec des pointeurs sur 64 bits) consomme n * (n * 64) bits de mémoire, sans surcoût pour la liste.

Pour un graphe incomplet, une liste d'adjacence consomme 0 bits de mémoire, à l'exclusion de la surcharge de la liste.


Pour une liste d'adjacence, vous pouvez utiliser la formule suivante pour déterminer le nombre maximal d'arêtes (e) avant qu'une matrice d'adjacence ne soit optimale pour la mémoire.

edges = n^2 / s pour déterminer le nombre maximum d’arêtes, où s est la taille du pointeur de la plate-forme.

Si votre graphique se met à jour de manière dynamique, vous pouvez maintenir cette efficacité avec un nombre de bords moyen (par nœud) de n / s.


Quelques exemples (avec des pointeurs 64 bits).

Pour un graphe orienté, où n est égal à 300, le nombre optimal d'arêtes par nœud à l'aide d'une liste d'adjacence est le suivant:

= 300 / 64
= 4

Si nous connectons ceci à la formule de keyser5053, d = e / n^2 (où e est le nombre total d’Edge), nous pouvons voir que nous sommes en dessous du point de rupture (1 / s):

d = (4 * 300) / (300 * 300)
d < 1/64
aka 0.0133 < 0.0156

Cependant, 64 bits pour un pointeur peuvent être excessifs. Si vous utilisez plutôt des entiers 16 bits comme décalages de pointeur, vous pouvez ajuster jusqu'à 18 arêtes avant le point de rupture.

= 300 / 16
= 18

d = ((18 * 300) / (300^2))
d < 1/16
aka 0.06 < 0.0625

Chacun de ces exemples ignore la surcharge des listes d’adjacence elles-mêmes (64*2 pour un vecteur et des pointeurs 64 bits).

2
deceleratedcaviar

Nous allons simplement aborder le problème du compromis entre la représentation régulière de la liste de contiguïté puisque d’autres réponses ont couvert d’autres aspects.

Il est possible de représenter un graphique dans une liste de contiguïté avec une requête EdgeExists en temps constant amorti en tirant parti du dictionnaire et HashSet structures de données. L'idée est de conserver les sommets dans un dictionnaire et pour chaque sommet, nous conservons un ensemble de hachage faisant référence à d'autres sommets avec lesquels il a des arêtes.

Un inconvénient mineur de cette implémentation est qu’elle aura la complexité de l’espace O (V + 2E) au lieu de O (V + E) comme dans la liste de contiguïté régulière car les arêtes sont représentées deux fois ici (car chaque sommet a son propre ensemble de hachage de bords). Mais des opérations telles que AddVertex , AddEdge , RemoveEdge peut être effectué en temps amorti O(1) avec cette implémentation, sauf pour RemoveVertex qui prend O(V) comme une matrice d'adjacence. Cela signifierait qu'en dehors de la simplicité de mise en œuvre, la matrice d'adjacence ne présente pas d'avantage spécifique. Nous pouvons économiser de l'espace sur des surfaces clairsemées. graphique avec presque les mêmes performances dans cette implémentation de la liste d’adjacence.

Jetez un œil aux implémentations ci-dessous dans le référentiel Github C # pour plus de détails. Notez que pour les graphes pondérés, il utilise un dictionnaire imbriqué au lieu de la combinaison dictionnaire-hachage afin de prendre en compte la valeur de poids. De même, pour les graphes dirigés, il existe des jeux de hachage distincts pour les arêtes d'entrée et de sortie.

Advanced-Algorithms

Remarque: Je pense que l’utilisation de la suppression paresseuse nous permet d’optimiser davantage l'opération RemoveVertex en O(1) amorti même si je N'ayez pas testé cette idée. Par exemple, lors de la suppression, marquez simplement le sommet comme étant supprimé dans le dictionnaire, puis effacez les bords orphelins par la suite lors d'autres opérations.

1
justcoding121

Si vous utilisez une table de hachage au lieu d'une matrice ou d'une liste d'adjacence, vous obtiendrez un meilleur temps d'exécution Big-O ou un espace identique pour toutes les opérations (la recherche d'un bord est O(1), obtenant tous les bords adjacents est O(degree), etc.).

Il existe cependant un surcoût constant lié aux facteurs, tant pour l'exécution que pour l'espace (la table de hachage n'est pas aussi rapide que la liste chaînée ou la recherche de tableau, et prend un espace supplémentaire décent pour réduire les collisions).

1
max