Cosa c'è di meglio, elenchi di adiacenza o matrici di adiacenza per problemi di grafici in C ++?


129

Cosa c'è di meglio, elenchi di adiacenza o matrice di adiacenza, per problemi di grafici in C ++? Quali sono i vantaggi e gli svantaggi di ciascuno?


21
La struttura che usi non dipende dalla lingua ma dal problema che stai cercando di risolvere.
avakar,

1
Intendevo un uso generale come l'algoritmo djikstra, ho posto questa domanda perché non so che vale la pena provare l'implementazione dell'elenco collegato perché è più difficile codificare rispetto alla matrice di adiacenza.
magiix,

Gli elenchi in C ++ sono facili come scrivere std::list(o meglio ancora std::vector).
avakar,

1
@avakar: oppure std::dequeoppure std::set. Dipende dal modo in cui il grafico cambierà nel tempo e dagli algoritmi che intendi eseguire su di essi.
Alexandre C.,

Risposte:


125

Dipende dal problema.

Matrice di adiacenza

  • Utilizza la memoria O (n ^ 2)
  • È rapido cercare e verificare la presenza o l'assenza di un bordo specifico
    tra due nodi qualsiasi O (1)
  • È lento iterare su tutti i bordi
  • È lento aggiungere / eliminare un nodo; un'operazione complessa O (n ^ 2)
  • È veloce aggiungere un nuovo bordo O (1)

Elenco di adiacenza

  • L'utilizzo della memoria dipende dal numero di spigoli (non dal numero di nodi),
    che potrebbe risparmiare molta memoria se la matrice di adiacenza è scarsa
  • Trovare la presenza o l'assenza di un bordo specifico tra due nodi qualsiasi
    è leggermente più lento rispetto alla matrice O (k); dove k è il numero di nodi vicini
  • È veloce iterare su tutti i bordi perché è possibile accedere direttamente a tutti i nodi vicini
  • È veloce aggiungere / eliminare un nodo; più semplice della rappresentazione matriciale
  • È veloce aggiungere un nuovo bordo O (1)

gli elenchi collegati sono più difficili da codificare, pensi che valga la pena dedicare un po 'di tempo all'apprendimento?
magiix,

11
@magiix: Sì, penso che dovresti capire come codificare le liste collegate se necessario, ma è anche importante non reinventare la ruota: cplusplus.com/reference/stl/list
Mark Byers

qualcuno può fornire un link con un codice pulito per dire Breadth prima ricerca in formato elenchi collegati ??
magiix


78

Questa risposta non è solo per C ++ poiché tutto quanto menzionato riguarda le strutture di dati stesse, indipendentemente dalla lingua. E, la mia risposta è supporre che tu conosca la struttura di base delle liste di adiacenza e delle matrici.

Memoria

Se la memoria è la tua preoccupazione principale, puoi seguire questa formula per un semplice grafico che consenta loop:

Una matrice di adiacenza occupa n 2 /8 byte di spazio (un bit per voce).

Un elenco di adiacenza occupa 8e spazio, dove e è il numero di spigoli (computer a 32 bit).

Se definiamo la densità del grafico come d = e / n 2 (numero di bordi diviso per il numero massimo di bordi), possiamo trovare il "punto di interruzione" in cui un elenco occupa più memoria di una matrice:

8e> n 2 /8 quando d> 1/64

Quindi con questi numeri (ancora specifici a 32 bit) il breakpoint atterra a 1/64 . Se la densità (e / n 2 ) è maggiore di 1/64, è preferibile una matrice se si desidera risparmiare memoria.

Puoi leggere questo su Wikipedia (articolo sulle matrici di adiacenza) e molti altri siti.

Nota a margine : è possibile migliorare l'efficienza dello spazio della matrice di adiacenza utilizzando una tabella hash in cui i tasti sono coppie di vertici (solo non indirizzati).

Iterazione e ricerca

Gli elenchi di adiacenza sono un modo compatto di rappresentare solo i bordi esistenti. Tuttavia, questo ha un costo per una possibile ricerca lenta di bordi specifici. Poiché ogni elenco è lungo quanto il grado di un vertice, il tempo di ricerca nel caso peggiore del controllo di un bordo specifico può diventare O (n), se l'elenco non è ordinato. Tuttavia, cercare i vicini di un vertice diventa banale e per un grafico scarso o piccolo il costo dell'iterazione attraverso gli elenchi di adiacenza potrebbe essere trascurabile.

Le matrici di adiacenza invece usano più spazio per fornire un tempo di ricerca costante. Poiché esiste ogni possibile voce, è possibile verificare l'esistenza di uno spigolo in tempo costante utilizzando gli indici. Tuttavia, la ricerca del vicino richiede O (n) poiché è necessario controllare tutti i possibili vicini. L'ovvio svantaggio dello spazio è che per i grafici sparsi viene aggiunta molta imbottitura. Vedi la discussione di memoria sopra per ulteriori informazioni al riguardo.

Se non sei ancora sicuro di cosa usare : la maggior parte dei problemi del mondo reale produce grafici sparsi e / o grandi, che sono più adatti per le rappresentazioni dell'elenco di adiacenza. Potrebbero sembrare più difficili da implementare, ma ti assicuro che non lo sono, e quando scrivi un BFS o DFS e vuoi recuperare tutti i vicini di un nodo, sono solo una riga di codice. Tuttavia, tieni presente che non sto promuovendo elenchi di adiacenza in generale.


9
+1 per informazioni dettagliate, ma questo deve essere corretto dall'attuale struttura dati utilizzata per memorizzare gli elenchi di adiacenza. Potresti voler memorizzare per ciascun vertice il suo elenco di adiacenza come una mappa o un vettore, nel qual caso i numeri effettivi nelle tue formule devono essere aggiornati. Inoltre, calcoli simili possono essere utilizzati per valutare i punti di pareggio per la complessità temporale di determinati algoritmi.
Alexandre C.,

3
Sì, questa formula è per uno scenario specifico. Se vuoi una risposta approssimativa, vai avanti e usa questa formula o modificala secondo le tue specifiche secondo necessità (ad esempio, la maggior parte delle persone ha un computer a 64 bit al giorno d'oggi :))
keyser

1
Per coloro che sono interessati, la formula per il punto di rottura (numero massimo di bordi medi in un grafico di n nodi) è e = n / s, dove si strova la dimensione del puntatore.
decelerato

33

Ok, ho compilato le complessità del Tempo e dello Spazio delle operazioni di base sui grafici.
L'immagine seguente dovrebbe essere autoesplicativa.
Nota come la matrice di adiacenza è preferibile quando prevediamo che il grafico sia denso e come è preferibile l'elenco di adiacenza quando prevediamo che il grafico sia sparso.
Ho formulato alcune ipotesi. Chiedimi se una complessità (Tempo o Spazio) necessita di chiarimenti. (Ad esempio, per un grafico sparse, ho preso En come una piccola costante, dato che ho ipotizzato che l'aggiunta di un nuovo vertice aggiungerà solo pochi spigoli, perché prevediamo che il grafico rimarrà scarso anche dopo aver aggiunto che vertice.)

Per favore dimmi se ci sono errori.

inserisci qui la descrizione dell'immagine


Nel caso in cui non sia noto se il grafico sia denso o sparso, sarebbe giusto dire che la complessità dello spazio per un elenco di adiacenza sarebbe O (v + e)?

Per gli algoritmi più pratici, una delle operazioni più importanti è l'iterazione attraverso tutti i bordi che escono da un dato vertice. Potresti aggiungerlo al tuo elenco: è O (grado) per AL e O (V) per AM.
max

@johnred non è meglio dire che l'aggiunta di un vertice (tempo) per AL è O (1) perché invece di O (en) perché non aggiungiamo realmente bordi all'aggiunta di un vertice. L'aggiunta di un bordo può essere gestita come un'operazione separata. Per AM ha senso tenere conto, ma anche lì dobbiamo solo inizializzare le righe e le colonne pertinenti del nuovo vertice a zero. L'aggiunta di bordi anche per AM può essere contabilizzata separatamente.
Usman,

Come si aggiunge un vertice ad AL O (V)? Dobbiamo creare una nuova matrice, copiare i valori precedenti in essa. Dovrebbe essere O (v ^ 2).
Alex_ban,

19

Dipende da cosa stai cercando.

Con le matrici di adiacenza è possibile rispondere rapidamente alle domande riguardanti se un bordo specifico tra due vertici appartiene al grafico e si possono anche avere inserimenti rapidi ed eliminazioni di bordi. Il rovescio della medaglia è che devi usare uno spazio eccessivo, specialmente per i grafici con molti vertici, il che è molto inefficiente soprattutto se il tuo grafico è scarso.

D'altra parte, con gli elenchi di adiacenza è più difficile verificare se un determinato bordo si trova in un grafico, perché è necessario cercare l'elenco appropriato per trovare il bordo, ma sono più efficienti in termini di spazio.

In genere, tuttavia, gli elenchi di adiacenza sono la struttura di dati corretta per la maggior parte delle applicazioni dei grafici.


cosa succede se si utilizzano dizionari per memorizzare l'elenco di adiacenza, che ti darà la presenza di un bordo nel tempo ammortizzato O (1).
Rohith Yeravothula,

10

Supponiamo di avere un grafico che ha n numero di nodi e m numero di spigoli,

Grafico di esempio
inserisci qui la descrizione dell'immagine

Matrice di adiacenza: stiamo creando una matrice che ha n numero di righe e colonne in modo che in memoria occupi spazio proporzionale a n 2 . Per verificare se due nodi denominati u e v hanno uno spigolo tra loro richiederà Θ (1) tempo. Ad esempio, il controllo di (1, 2) è che un bordo avrà il seguente aspetto nel codice:

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

Se si desidera identificare tutti i bordi, è necessario scorrere sopra la matrice in questo caso saranno necessari due cicli nidificati e occorrerà Θ (n 2 ). (Puoi semplicemente usare la parte triangolare superiore della matrice per determinare tutti i bordi ma sarà di nuovo Θ (n 2 ))

Elenco di adiacenza: stiamo creando un elenco che ciascun nodo punta anche a un altro elenco. Il tuo elenco avrà n elementi e ogni elemento punterà a un elenco che ha un numero di elementi uguale al numero di vicini di questo nodo (cerca l'immagine per una migliore visualizzazione). Quindi ci vorrà spazio nella memoria che è proporzionale a n + m . Controllare se (u, v) è un fronte richiederà il tempo O (deg (u)) in cui deg (u) è uguale al numero di vicini di te. Perché al massimo, è necessario scorrere l'elenco che è indicato dalla u. L'identificazione di tutti i bordi richiederà Θ (n + m).

Elenco di adiacenza del grafico di esempio

inserisci qui la descrizione dell'immagine
Dovresti fare la tua scelta in base alle tue esigenze. A causa della mia reputazione non ho potuto mettere l'immagine della matrice, mi dispiace per quello


7

Se stai analizzando l'analisi dei grafici in C ++, probabilmente il primo punto da cui iniziare è la libreria dei grafici boost , che implementa una serie di algoritmi tra cui BFS.

MODIFICARE

Questa precedente domanda su SO probabilmente aiuterà:

how-to-create-ac-boost-undirected-graph-and-cross-it-in-depth-first-searc h


Grazie controllerò questa libreria
magiix

+1 per il grafico boost. Questa è la strada da percorrere (tranne ovviamente se è a scopo educativo)
Tristram Gräbener

5

Questa è la migliore risposta con esempi.

Pensa ad esempio a Floyd-Warshall . Dobbiamo usare una matrice di adiacenza, altrimenti l'algoritmo sarà asintoticamente più lento.

O se fosse un grafico denso su 30.000 vertici? Quindi una matrice di adiacenza potrebbe avere senso, dato che memorizzerai 1 bit per coppia di vertici, anziché i 16 bit per fronte (il minimo necessario per un elenco di adiacenza): sono 107 MB, anziché 1,7 GB.

Ma per algoritmi come DFS, BFS (e quelli che lo usano, come Edmonds-Karp), la ricerca per priorità (Dijkstra, Prim, A *), ecc., Un elenco di adiacenza è buono come una matrice. Bene, una matrice potrebbe avere un leggero margine quando il grafico è denso, ma solo per un fattore costante irrilevante. (Quanto? Si tratta di sperimentare.)


2
Per algoritmi come DFS e BFS, se si utilizza una matrice, è necessario controllare l'intera riga ogni volta che si desidera trovare nodi adiacenti, mentre si hanno già nodi adiacenti in un elenco adiacente. Perché pensi an adjacency list is as good as a matrixin quei casi?
realUser404

@ realUser404 Esattamente, la scansione di un'intera riga di matrice è un'operazione O (n). Gli elenchi di adiacenza sono migliori per i grafici sparsi quando è necessario attraversare tutti i bordi in uscita, possono farlo in O (d) (d: grado del nodo). Le matrici hanno prestazioni della cache migliori rispetto agli elenchi di adiacenza, tuttavia, a causa dell'accesso sequenziale, quindi per grafici piuttosto densi, la scansione di una matrice può avere più senso.
Jochem Kuijpers,

3

Da aggiungere alla risposta di keyser5053 sull'utilizzo della memoria.

Per qualsiasi grafico diretto, una matrice di adiacenza (a 1 bit per fronte) consuma n^2 * (1)bit di memoria.

Per un grafico completo , un elenco di adiacenza (con puntatori a 64 bit) consuma n * (n * 64)bit di memoria, escluso l'overhead dell'elenco.

Per un grafico incompleto, un elenco di adiacenza consuma 0bit di memoria, escluso l'overhead dell'elenco.


Per un elenco di adiacenza, è possibile utilizzare la seguente formula per determinare il numero massimo di spigoli ( e) prima che una matrice di adiacenza sia ottimale per la memoria.

edges = n^2 / sper determinare il numero massimo di spigoli, dov'è sla dimensione del puntatore della piattaforma.

Se il tuo grafico si aggiorna in modo dinamico, puoi mantenere questa efficienza con un conteggio dei bordi medio (per nodo) di n / s.


Alcuni esempi con puntatori a 64 bit e grafico dinamico (Un grafico dinamico aggiorna la soluzione di un problema in modo efficiente dopo le modifiche, piuttosto che ricalcolarlo da capo ogni volta che è stata apportata una modifica.)

Per un grafico diretto, dove nè 300, il numero ottimale di bordi per nodo utilizzando un elenco di adiacenza è:

= 300 / 64
= 4

Se lo inseriamo nella formula di keyser5053, d = e / n^2(dove si etrova il conteggio dei bordi totale), possiamo vedere che siamo al di sotto del punto di interruzione ( 1 / s):

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

Tuttavia, 64 bit per un puntatore possono essere eccessivi. Se invece utilizzi numeri interi a 16 bit come offset del puntatore, possiamo adattare fino a 18 spigoli prima del punto di rottura.

= 300 / 16
= 18

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

Ognuno di questi esempi ignora l'overhead degli elenchi di adiacenza stessi ( 64*2per un vettore e puntatori a 64 bit).


Non capisco la parte d = (4 * 300) / (300 * 300), non dovrebbe essere d = 4 / (300 * 300)? Dal momento che la formula è d = e / n^2.
Saurabh,

2

A seconda dell'implementazione della matrice di adiacenza, la 'n' del grafico dovrebbe essere nota in precedenza per un'implementazione efficiente. Se il grafico è troppo dinamico e richiede di tanto in tanto l'espansione della matrice, può anche essere considerato un aspetto negativo?


1

Se si utilizza una tabella di hash invece della matrice di adiacenza o dell'elenco, si otterrà lo stesso tempo di esecuzione e lo spazio di O-big migliori o uguali per tutte le operazioni (verificare che sia un bordo O(1), ottenere tutti i bordi adiacenti O(degree), ecc.).

C'è comunque un certo sovraccarico di fattori sia per il runtime che per lo spazio (la tabella hash non è veloce come la lista collegata o la ricerca di array e occupa una discreta quantità di spazio extra per ridurre le collisioni).


1

Ho intenzione di toccare il superamento del compromesso della normale rappresentazione dell'elenco di adiacenza, poiché altre risposte hanno coperto altri aspetti.

È possibile rappresentare un grafico nell'elenco di adiacenza con la query EdgeExists in tempo costante ammortizzato, sfruttando le strutture di dati Dictionary e HashSet . L'idea è di mantenere i vertici in un dizionario e, per ogni vertice, manteniamo un set di hash che fa riferimento ad altri vertici con cui ha bordi.

Un piccolo compromesso in questa implementazione è che avrà complessità spaziale O (V + 2E) invece di O (V + E) come nella normale lista di adiacenza, poiché i bordi sono rappresentati due volte qui (perché ogni vertice ha il proprio set di hash dei bordi). Ma operazioni come AddVertex , AddEdge , RemoveEdge possono essere eseguite nel tempo ammortizzato O (1) con questa implementazione, ad eccezione di RemoveVertex che prende O (V) come matrice di adiacenza. Ciò significherebbe che, oltre alla semplicità di implementazione, la matrice di adiacenza non presenta alcun vantaggio specifico. Possiamo risparmiare spazio sul grafico sparso con quasi le stesse prestazioni in questa implementazione dell'elenco di adiacenza.

Dai un'occhiata alle implementazioni di seguito nel repository Github C # per i dettagli. Si noti che per il grafico ponderato utilizza un dizionario nidificato anziché la combinazione di set dizionario-hash in modo da contenere il valore di peso. Allo stesso modo per il grafico diretto ci sono set di hash separati per i bordi in e out.

Advanced-Algoritmi

Nota: credo che usando la cancellazione lenta possiamo ottimizzare ulteriormente l' operazione di RemoveVertex su O (1) ammortizzata, anche se non ho testato quell'idea. Ad esempio, dopo l'eliminazione, contrassegnare il vertice come eliminato nel dizionario, quindi eliminare pigramente i bordi orfani durante altre operazioni.


Per la matrice di adiacenza, rimuovere il vertice prende O (V ^ 2) non O (V)
Saurabh

Sì. Ma se usi un dizionario per tracciare gli indici dell'array, allora scenderà a O (V). Dai un'occhiata a questa implementazione di RemoveVertex .
justcoding121
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.