Reprezentacje grafów - macierz sąsiedztwa i lista

Wprowadzenie

Aby efektywnie pracować z grafami w programowaniu, musimy wybrać odpowiednią strukturę danych do ich reprezentacji. Wybór reprezentacji wpływa na: - Wydajność operacji (dodawanie, usuwanie, wyszukiwanie) - Zużycie pamięci - Prostotę implementacji algorytmów

W tej lekcji poznamy trzy główne reprezentacje grafów: 1. Macierz sąsiedztwa (Adjacency Matrix) 2. Lista sąsiedztwa (Adjacency List) 3. Lista krawędzi (Edge List)


1. Macierz sąsiedztwa (Adjacency Matrix)

Definicja

Macierz sąsiedztwa to dwuwymiarowa tablica A[n][n], gdzie n = liczba wierzchołków.

Dla grafu nieskierowanego:

A[i][j] = 1   jeśli istnieje krawędź między wierzchołkiem i a j
A[i][j] = 0   w przeciwnym razie

Dla grafu ważonego:

A[i][j] = waga   jeśli istnieje krawędź
A[i][j] = ∞      jeśli nie ma krawędzi (lub 0, w zależności od konwencji)

Przykład - graf nieskierowany

Graf:
    0 --- 1
    |     |
    3 --- 2

Krawędzie: {0,1}, {0,3}, {1,2}, {2,3}

Macierz sąsiedztwa:
     0  1  2  3
  0 [0  1  0  1]
  1 [1  0  1  0]
  2 [0  1  0  1]
  3 [1  0  1  0]

Uwaga: Macierz jest SYMETRYCZNA (A[i][j] = A[j][i])

Przykład - graf skierowany

Graf:
    0 → 1
    ↓   ↓
    3 ← 2

Krawędzie: (0,1), (0,3), (1,2), (2,3)

Macierz sąsiedztwa:
     0  1  2  3
  0 [0  1  0  1]
  1 [0  0  1  0]
  2 [0  0  0  1]
  3 [0  0  0  0]

Uwaga: Macierz NIE jest symetryczna
A[i][j] = 1 oznacza krawędź i → j

Implementacja w Pythonie

Wyjście:

Graf nieskierowany:
      0  1  2  3
 0    0  1  0  1
 1    1  0  1  0
 2    0  1  0  1
 3    1  0  1  0

Sąsiedzi wierzchołka 0: [(1, 1), (3, 1)]
Stopień wierzchołka 1: 2
Czy istnieje krawędź 0-2? False


Graf skierowany:
      0  1  2  3
 0    0  1  0  1
 1    0  0  1  0
 2    0  0  0  1
 3    0  0  0  0

Następniki wierzchołka 0: [(1, 1), (3, 1)]

Zalety macierzy sąsiedztwa

Szybkie sprawdzanie krawędzi: O(1) ✅ Prosta implementacjaŁatwe dodawanie/usuwanie krawędzi: O(1) ✅ Dobre dla algorytmów wymagających szybkiego sprawdzania sąsiedztwa

Wady macierzy sąsiedztwa

Duże zużycie pamięci: O(V²) nawet dla rzadkich grafów ❌ Wolne iterowanie po sąsiadach: O(V) zamiast O(deg(v)) ❌ Trudność z dynamicznym dodawaniem wierzchołków (trzeba realokować całą macierz)


2. Lista sąsiedztwa (Adjacency List)

Definicja

Lista sąsiedztwa to tablica list, gdzie dla każdego wierzchołka v przechowujemy listę jego sąsiadów.

Dla każdego wierzchołka v:
  adj[v] = lista wierzchołków sąsiadujących z v

Dla grafu ważonego:

adj[v] = lista tupli (sąsiad, waga)

Przykład - graf nieskierowany

Graf:
    0 --- 1
    |     |
    3 --- 2

Lista sąsiedztwa:
0 → [1, 3]
1 → [0, 2]
2 → [1, 3]
3 → [0, 2]

Przykład - graf skierowany

Graf:
    0 → 1
    ↓   ↓
    3 ← 2

Lista sąsiedztwa:
0 → [1, 3]
1 → [2]
2 → [3]
3 → []

Implementacja w Pythonie

Wyjście:

Graf nieskierowany:
0 → ['1(w=5)', '3(w=3)']
1 → ['0(w=5)', '2(w=2)']
2 → ['1(w=2)', '3(w=4)']
3 → ['0(w=3)', '2(w=4)']

Sąsiedzi wierzchołka 0: [(1, 5), (3, 3)]
Stopień wierzchołka 1: 2
Czy istnieje krawędź 0-2? False


Graf skierowany:
A → ['B(w=1)', 'C(w=1)']
B → ['C(w=1)']
C → ['D(w=1)']
D → []

Zalety listy sąsiedztwa

Oszczędność pamięci: O(V + E) zamiast O(V²) ✅ Szybkie iterowanie po sąsiadach: O(deg(v)) ✅ Łatwe dodawanie wierzchołków: O(1) ✅ Dobre dla rzadkich grafów (sparse graphs)

Wady listy sąsiedztwa

Wolniejsze sprawdzanie krawędzi: O(deg(v)) zamiast O(1) ❌ Usuwanie krawędzi wymaga przeszukania listy: O(deg(v))


3. Lista krawędzi (Edge List)

Definicja

Lista krawędzi to prosta lista wszystkich krawędzi w grafie.

Dla grafu nieskierowanego: [(u, v), ...]
Dla grafu ważonego: [(u, v, waga), ...]

Implementacja w Pythonie

Wyjście:

Graf jako lista krawędzi:
0 --- 1 (waga: 5)
0 --- 3 (waga: 3)
1 --- 2 (waga: 2)
2 --- 3 (waga: 4)

Wierzchołki: {0, 1, 2, 3}
Liczba krawędzi: 4

Zalety listy krawędzi

Bardzo prosta implementacjaMinimalne zużycie pamięci: O(E) ✅ Dobre dla algorytmów przetwarzających wszystkie krawędzie (np. Kruskal)

Wady listy krawędzi

Bardzo wolne znajdowanie sąsiadów: O(E) ❌ Wolne sprawdzanie krawędzi: O(E) ❌ Nieefektywne dla większości algorytmów grafowych


Porównanie reprezentacji

Tabela złożoności

Operacja Macierz sąsiedztwa Lista sąsiedztwa Lista krawędzi
Pamięć O(V²) O(V + E) O(E)
Dodaj wierzchołek O(V²) O(1) O(1)
Dodaj krawędź O(1) O(1) O(1)
Usuń krawędź O(1) O(V) O(E)
Sprawdź krawędź O(1) O(V) O(E)
Znajdź sąsiadów O(V) O(deg(v)) O(E)
Iteruj po krawędziach O(V²) O(V + E) O(E)

Kiedy używać której reprezentacji?

Macierz sąsiedztwa

Użyj gdy: - Graf jest gęsty (duża liczba krawędzi, E ≈ V²) - Często sprawdzasz czy istnieje krawędź między dwoma wierzchołkami - Implementujesz algorytmy wymagające szybkiego dostępu do wag krawędzi (np. Floyd-Warshall)

Przykłady zastosowań: - Graf pełny lub prawie pełny - Algorytmy najkrótszej ścieżki dla wszystkich par (Floyd-Warshall) - Problemy dopasowania w grafach dwudzielnych


Lista sąsiedztwa

Użyj gdy: - Graf jest rzadki (mała liczba krawędzi, E << V²) - Często iterujesz po sąsiadach wierzchołka - Dynamicznie dodajesz/usuwasz wierzchołki

Przykłady zastosowań: - BFS, DFS (przeszukiwanie grafów) - Algorytm Dijkstry - Większość rzeczywistych grafów (sieci społecznościowe, internet, mapy)


Lista krawędzi

Użyj gdy: - Przetwarzasz wszystkie krawędzie naraz - Sortujesz krawędzie według wagi - Graf jest bardzo prosty

Przykłady zastosowań: - Algorytm Kruskala (MST) - Algorytmy sortujące krawędzie - Proste analizy grafów


Przykład praktyczny: Porównanie wydajności

Przykładowe wyniki:

=== Graf RZADKI ===
Graf: V=100, E=200
Gęstość: 4.04%

Reprezentacja        Budowa (s)      1000 sprawdzeń (s)
-------------------------------------------------------
Macierz sąsiedztwa   0.000234        0.000012
Lista sąsiedztwa     0.000089        0.001234
Lista krawędzi       0.000045        0.023456

Wnioski: Lista sąsiedztwa najszybsza do budowy, macierz najszybsza do sprawdzania


=== Graf GĘSTY ===
Graf: V=100, E=3000
Gęstość: 60.61%

Reprezentacja        Budowa (s)      1000 sprawdzeń (s)
-------------------------------------------------------
Macierz sąsiedztwa   0.002345        0.000012
Lista sąsiedztwa     0.001234        0.003456
Lista krawędzi       0.000678        0.145678

Wnioski: Dla gęstego grafu, macierz jest konkurencyjna

Konwersje między reprezentacjami

Macierz → Lista sąsiedztwa


Lista sąsiedztwa → Macierz


Podsumowanie

Wybór reprezentacji - szybki przewodnik


Co dalej?

Po zrozumieniu reprezentacji grafów, następne kroki to:

  1. Lekcja 56: Przeszukiwanie wszerz (BFS)
  2. Algorytm BFS
  3. Najkrótsza ścieżka w grafie nieważonym
  4. Poziomy grafu

  5. Lekcja 57: Przeszukiwanie w głąb (DFS)

  6. Algorytm DFS
  7. Wykrywanie cykli
  8. Sortowanie topologiczne

  9. Lekcja 58: Algorytm Dijkstry

  10. Najkrótsza ścieżka w grafie ważonym
  11. Implementacja z kolejką priorytetową

Zadania praktyczne

Zadanie 1: Transpozycja grafu

Dla grafu skierowanego, zaimplementuj funkcję zwracającą graf transponowany (wszystkie krawędzie odwrócone).

Zadanie 2: Gęstość grafu

Napisz funkcję obliczającą gęstość grafu i klasyfikującą go jako "rzadki", "średni" lub "gęsty".

Zadanie 3: Komplementarny graf

Zaimplementuj funkcję zwracającą graf komplementarny (krawędzie istnieją tam, gdzie ich nie było, i na odwrót).

Zadanie 4: Benchmark własny

Stwórz własny benchmark porównujący reprezentacje dla konkretnego przypadku użycia (np. sieć społecznościowa, mapa drogowa).


Następna lekcja: Przeszukiwanie wszerz (BFS) - pierwszy algorytm przeszukiwania grafów