Minimalne drzewo rozpinające - algorytmy Prima i Kruskala

Wprowadzenie

Minimalne drzewo rozpinające (ang. Minimum Spanning Tree, MST) to podgraf spójnego grafu ważonego, który: - Jest drzewem (spójny i acykliczny) - Rozpina cały graf (zawiera wszystkie wierzchołki) - Ma minimalną sumę wag krawędzi

MST jest fundamentalnym problemem w teorii grafów z licznymi zastosowaniami praktycznymi.

Zastosowania: - Projektowanie sieci (telekomunikacyjne, elektryczne, wodociągowe) - Minimalizacja kosztów połączeń między miastami - Analiza skupień (clustering w machine learning) - Aproksymacja problemu komiwojażera (TSP) - Optymalizacja tras dostaw - Projektowanie układów scalonych (minimalizacja przewodów)

Własność: Graf z n wierzchołkami ma drzewo rozpinające z dokładnie n-1 krawędziami.


Problem minimalnego drzewa rozpinającego

Definicja formalna

Dany jest graf nieskierowany, spójny i ważony G = (V, E) z wagami w: E → ℝ.

Drzewo rozpinające T = (V, E') to podgraf, gdzie: - E' ⊆ E - |E'| = |V| - 1 - Graf T jest spójny (łączy wszystkie wierzchołki)

Minimalne drzewo rozpinające to drzewo rozpinające o minimalnej sumie wag:

waga(T) = Σ w(e) → minimum
         e∈E'

Wizualizacja

Graf wejściowy:
        1
    A ---- B
    |    / |
   4|  2/  |3
    | /    |
    C ---- D
        1

Wszystkie możliwe drzewa rozpinające:

Drzewo 1: A-B, B-C, C-D    Waga = 1+2+1 = 4  ← MST!
Drzewo 2: A-B, B-D, C-D    Waga = 1+3+1 = 5
Drzewo 3: A-C, B-C, C-D    Waga = 4+2+1 = 7
Drzewo 4: A-B, A-C, B-D    Waga = 1+4+3 = 8
... (inne kombinacje)

MST zawiera krawędzie: {A-B: 1, B-C: 2, C-D: 1}

Uwaga: MST nie musi być unikalne (jeśli są krawędzie o tej samej wadze).


Własności MST - twierdzenia

Twierdzenie 1: Własność cięcia (Cut Property)

Cięcie grafu to podział wierzchołków na dwa rozłączne zbiory S i V-S.

Twierdzenie: Dla dowolnego cięcia (S, V-S), najlżejsza krawędź przecinająca cięcie (łącząca S z V-S) należy do pewnego MST.

Przykład:
        1
    A ---- B
    |      |
   4|      |3
    |      |
    C ---- D
        5

Cięcie: S = {A, C}, V-S = {B, D}
Krawędzie przecinające: A-B (1), C-D (5)
Najlżejsza: A-B (1) → musi być w MST

Algorytm Prima wykorzystuje tę własność.


Twierdzenie 2: Własność cyklu (Cycle Property)

Twierdzenie: Dla dowolnego cyklu w grafie, najcięższa krawędź w cyklu nie należy do żadnego MST.

Przykład:
    A ---- B
    |      |
   2|      |3
    |      |
    C ---- D
        5    (najcięższa w cyklu A-B-D-C-A)

Krawędź C-D (5) nie może być w MST, bo tworzy cykl
i jest najcięższa → usuwamy ją

Algorytm Kruskala wykorzystuje tę własność (unika dodawania krawędzi tworzących cykl).


Algorytm Prima - "rosnące drzewo"

Idea algorytmu

Algorytm Prima działa podobnie do algorytmu Dijkstry, ale zamiast szukać najkrótszej ścieżki, buduje MST:

  1. Zaczynaj od dowolnego wierzchołka
  2. W każdym kroku dodaj najlżejszą krawędź łączącą drzewo z wierzchołkiem spoza drzewa
  3. Kontynuuj, aż wszystkie wierzchołki będą w drzewie

Strategia zachłanna: Zawsze wybieramy najtańsze połączenie z obecnym drzewem.


Pseudokod algorytmu Prima

Prim(graf G, wierzchołek start):
    1. Inicjalizacja:
        - T = {start}           // Drzewo MST (wierzchołki)
        - E_T = ∅               // Krawędzie MST
        - PQ = kolejka priorytetowa

    2. Dla każdego sąsiada v wierzchołka start:
        - Dodaj (waga, start, v) do PQ

    3. Dopóki |T| < |V|:
        a. (waga, u, v) = PQ.extract_min()

        b. Jeśli v już w T:
            - continue  // Pomiń

        c. Dodaj v do T
        d. Dodaj krawędź (u, v) do E_T

        e. Dla każdego sąsiada w wierzchołka v:
            - Jeśli w nie w T:
                - Dodaj (waga(v,w), v, w) do PQ

    4. Zwróć (T, E_T)

Złożoność: - Z binary heap: O(E log V) - Z Fibonacci heap: O(E + V log V)


Implementacja algorytmu Prima

Podstawowa implementacja

Wyjście:

=== Algorytm Prima ===

Krawędzie MST:
  A - B: 1
  B - C: 2
  C - D: 1

Całkowita waga MST: 4

Prim krok po kroku - wizualizacja

Przykładowe wyjście:

=== Prim krok po kroku ===

Start: A
Drzewo: {A}

Kolejka po inicjalizacji: [(1, 'A', 'B'), (4, 'A', 'C')]

Krok 1: Dodaję krawędź A-B (waga: 1)
  Drzewo: {'A', 'B'}
  Dodaję do kolejki: [('B', 'C', 2), ('B', 'D', 3)]
  Kolejka: [(2, 'B', 'C'), (3, 'B', 'D'), (4, 'A', 'C')]

Krok 2: Dodaję krawędź B-C (waga: 2)
  Drzewo: {'A', 'B', 'C'}
  Dodaję do kolejki: [('C', 'D', 1)]
  Kolejka: [(1, 'C', 'D'), (3, 'B', 'D'), (4, 'A', 'C')]

Krok 3: Dodaję krawędź C-D (waga: 1)
  Drzewo: {'A', 'B', 'C', 'D'}
  Kolejka: [(3, 'B', 'D'), (4, 'A', 'C')]

Algorytm Kruskala - "sortowanie krawędzi"

Idea algorytmu

Algorytm Kruskala działa inaczej niż Prim - zamiast budować jedno drzewo, łączy lasy:

  1. Sortuj wszystkie krawędzie rosnąco według wag
  2. Przeglądaj krawędzie w kolejności:
  3. Jeśli krawędź nie tworzy cyklu, dodaj ją do MST
  4. Jeśli tworzy cykl, pomiń
  5. Zakończ po dodaniu n-1 krawędzi

Kluczowe pytanie: Jak sprawdzić czy krawędź tworzy cykl? Odpowiedź: Struktura Union-Find (Disjoint Set Union)!


Pseudokod algorytmu Kruskala

Kruskal(graf G):
    1. Posortuj wszystkie krawędzie E rosnąco według wag

    2. Utwórz strukturę Union-Find dla wszystkich wierzchołków
       (każdy wierzchołek w osobnym zbiorze)

    3. E_T = ∅  // Krawędzie MST

    4. Dla każdej krawędzi (u, v, waga) w posortowanej kolejności:
        a. Jeśli Find(u) ≠ Find(v):  // Różne zbiory (nie ma cyklu)
            i.  Dodaj (u, v) do E_T
            ii. Union(u, v)  // Połącz zbiory

        b. Jeśli |E_T| == |V| - 1:
            - Przerwij (MST kompletne)

    5. Zwróć E_T

Złożoność: - Sortowanie: O(E log E) - Operacje Union-Find: O(E α(V)) gdzie α to odwrotna funkcja Ackermanna (≈ stała) - Całkowita: O(E log E) = O(E log V)


Struktura Union-Find (Disjoint Set Union)

Idea

Union-Find to struktura danych utrzymująca rozłączne zbiory z operacjami: - Find(x) - znajdź reprezentanta (root) zbioru zawierającego x - Union(x, y) - połącz zbiory zawierające x i y

Zastosowanie w Kruskalu: - Każdy wierzchołek zaczyna w osobnym zbiorze - Find(u) == Find(v)u i v są już połączone → dodanie krawędzi tworzy cykl - Union(u, v) → połącz składowe zawierające u i v


Implementacja Union-Find

Wyjście:

=== Union-Find Test ===

A i B połączone? False

Po union(A, B):
A i B połączone? True

Po union(C, D) i union(B, C):
A i D połączone? True
A i E połączone? False

Implementacja algorytmu Kruskala

Wyjście:

=== Algorytm Kruskala ===

Krawędzie MST:
  A - B: 1
  C - D: 1
  B - C: 2

Całkowita waga MST: 4

Kruskal krok po kroku - wizualizacja

Przykładowe wyjście:

=== Kruskal krok po kroku ===

Krawędzie posortowane według wag:
  A-B: 1
  C-D: 1
  B-C: 2
  B-D: 3
  A-C: 4

Krok 1: Sprawdzam krawędź A-B (waga: 1)
  ✓ Dodaję do MST (nie tworzy cyklu)
  MST: [('A', 'B', 1)]
  Komponenty: [['A', 'B'], ['C'], ['D']]

Krok 2: Sprawdzam krawędź C-D (waga: 1)
  ✓ Dodaję do MST (nie tworzy cyklu)
  MST: [('A', 'B', 1), ('C', 'D', 1)]
  Komponenty: [['A', 'B'], ['C', 'D']]

Krok 3: Sprawdzam krawędź B-C (waga: 2)
  ✓ Dodaję do MST (nie tworzy cyklu)
  MST: [('A', 'B', 1), ('C', 'D', 1), ('B', 'C', 2)]
  Komponenty: [['A', 'B', 'C', 'D']]

MST kompletne (3 krawędzi)

Praktyczne zastosowania MST

1. Projektowanie sieci telekomunikacyjnej

Wyjście:

=== Projektowanie sieci telekomunikacyjnej ===

Optymalne połączenia (MST):
  Kraków — Katowice: 80 km
  Wrocław — Poznań: 170 km
  Katowice — Wrocław: 200 km
  Kraków — Wrocław: 270 km
  Warszawa — Kraków: 290 km

Koszt MST: 1010 km kabla
Koszt pełnej sieci: 1670 km kabla
Oszczędności: 660 km (39.5%)

2. Clustering (grupowanie danych)

Wyjście:

=== MST Clustering ===

Przypisanie punktów do 3 klastrów:
  Klaster 0: ['A', 'B', 'C']
  Klaster 1: ['D', 'E', 'F']
  Klaster 2: ['G', 'H']

Porównanie algorytmów Prima i Kruskala

Cecha Prim Kruskal
Strategia Rosnące drzewo Łączenie lasów
Struktura danych Kolejka priorytetowa Union-Find
Złożoność O(E log V) O(E log E) = O(E log V)
Sortowanie ❌ Nie ✅ Tak (krawędzie)
Lepszy dla Grafów gęstych (E ≈ V²) Grafów rzadkich (E << V²)
Implementacja Prostsza (bez Union-Find) Wymaga Union-Find
Równoległość Trudniejsza Łatwiejsza (sortowanie + Union-Find)

Praktyczna rada: - Gęsty graf (dużo krawędzi) → Prim - Rzadki graf (mało krawędzi) → Kruskal - W praktyce różnice są małe dla typowych grafów


Dowód poprawności algorytmów

Dlaczego Prim i Kruskal działają?

Oba algorytmy są zachłanne (greedy) i zawsze dają optymalne rozwiązanie. Dlaczego?

Idea dowodu (dla obu): 1. Algorytm zachowuje niezmiennik: W każdym kroku krawędzie w budowanym zbiorze należą do pewnego MST 2. Własność cięcia/cyklu: Wybór najlżejszej krawędzi gwarantuje, że nowa krawędź też należy do MST 3. Na końcu: Mamy drzewo rozpinające (n-1 krawędzi) należące do MST, więc to musi być MST

Dowód przez sprzeczność: Załóżmy, że algorytm zwraca drzewo T, które nie jest MST. Wtedy: - Istnieje MST T* o mniejszej wadze - Ale algorytm zachłanny zawsze wybiera najtańszą opcję (własność cięcia/cyklu) - Sprzeczność! → T musi być MST


Warianty i rozszerzenia MST

1. Maksymalne drzewo rozpinające

Zamiast minimalnego, szukamy maksymalnego drzewa rozpinającego:


2. Drugie najlepsze MST

Znajdź MST o drugiej najmniejszej wadze:


Złożoność i optymalizacje

Złożoność czasowa

Algorytm Struktura danych Złożoność
Prim (lista sąsiedztwa) Bez kolejki O(V²)
Prim (binary heap) Binary heap O(E log V)
Prim (Fibonacci heap) Fibonacci heap O(E + V log V)
Kruskal Union-Find O(E log E) = O(E log V)

W praktyce: Binary heap wystarczy dla większości zastosowań.


Podsumowanie

Kluczowe cechy MST

Własność Opis
Definicja Drzewo rozpinające o minimalnej sumie wag
Liczba krawędzi n - 1 (gdzie n = liczba wierzchołków)
Niepowtarzalność MST może nie być unikalne
Algorytmy Prim (rosnące drzewo), Kruskal (łączenie lasów)
Złożoność O(E log V) dla obu

Kiedy używać którego algorytmu?

Algorytm Prima: ✅ Grafy gęste (dużo krawędzi) ✅ Reprezentacja: macierz sąsiedztwa ✅ Szukamy MST od konkretnego wierzchołka

Algorytm Kruskala: ✅ Grafy rzadkie (mało krawędzi) ✅ Reprezentacja: lista krawędzi ✅ Proste sortowanie krawędzi ✅ Łatwo równoległe


Co dalej?

Po opanowaniu MST, następne kroki to:

  1. Lekcja 60: Algorytm Bellmana-Forda
  2. Najkrótsza ścieżka z ujemnymi wagami
  3. Wykrywanie cykli ujemnych
  4. Porównanie z Dijkstrą

  5. Algorytm Floyda-Warshalla:

  6. Najkrótsze ścieżki wszystkie pary
  7. Programowanie dynamiczne
  8. Złożoność O(V³)

  9. Przepływy w sieciach:

  10. Algorytm Forda-Fulkersona
  11. Maksymalny przepływ
  12. Minimalne cięcie

Zadania praktyczne

Zadanie 1: Stopień w MST

Udowodnij, że suma stopni wierzchołków w MST wynosi 2(n-1).

Zadanie 2: Liczba MST

Napisz funkcję liczącą wszystkie różne MST w grafie (dla małych grafów).

Zadanie 3: MST z ograniczeniami

Znajdź MST, w którym żaden wierzchołek nie ma stopnia większego niż k.

Zadanie 4: Wizualizacja

Stwórz wizualizację graficzną działania algorytmów Prima i Kruskala krok po kroku.

Zadanie 5: Optymalizacja sieci

Masz sieć miast i możliwe połączenia z kosztami. Znajdź najtańszy sposób połączenia wszystkich miast tak, aby: - Wszystkie miasta były połączone - Każde miasto miało co najmniej 2 połączenia (dla redundancji)


Następna lekcja: Algorytm Bellmana-Forda - najkrótsza ścieżka z ujemnymi wagami