Minimal spanning trees

by Jerry Sky

MSTs



Outline

Na wejściu dostajemy graf spójny G=(V,E,l)G = (V,E,l), gdzie VV jest zbiorem wierzchołków, EE zbiorem krawędzi, a l:ER+l: E \to \mathbb{R}_+ funkcją wagi krawędzi (zakładamy, że waga krawędzi jest dodatnia). Celem jest wyznaczenie podzbioru krawędzi tego grafu, który pozostawi graf spójnym oraz suma wag tych krawędzi będzie możliwie najmniejsza.

I/O

Input: nieskierowany graf G=(V,E,l),l:ER+G = (V,E,l), l: E \to \mathbb{R}_+.

Output: drzewo T=(V,E)T = (V,E'), gdzie EEE' \subseteq E, który minimalizuje wagę drzewa waga(T)=eEl(e)\mathrm{waga}(T) = \sum_{e\in E'}l(e).

Drzewa

Nim przejdziemy do budowy algorytmów zachłannych rozwiązujących problem MST przyjrzyjmy się pewnym własnościom grafów (drzew nieukorzenionych):

Algorytm Kruskala

Algorytm ten buduje minimalne drzewo rozpinające dla wejściowego grafu G=(V,E,l)G = (V,E,l) i jest bardzo dobrą ilustracją zachłannego wyboru.

>Do MST TT dodawaj kolejne krawędzie o najmniejszej wadze, które nie tworzą cyklu.

Poprawność algorytmu Kruskala

Zauważmy, że po kilku krokach algorytmu Kruskala mamy częściowe rozwiązanie, które składa się ze zbioru spójnych składowych będących drzewami. Kolejna dodawana krawędź ee będzie łączyć dwie spójne składowe, powiedzmy T1T_1 oraz T2T_2. Skoro ee jest krawędzią o najmniejszej wadze nietworzącą cyklu to jest ona również krawędzią o najmniejszej wadze pomiędzy wierzchołkami należącymi do T1T_1 i VT1V\setminus T_1. A zatem z cut property jest ona częścią MST, co dowodzi poprawności algorytmu Kruskala.

Zbiory rozłączne (disjoint sets)

Ostatnią rzecz, którą pozostaje zrobić aby zaimplementować algorytm Kruskala, to wymyślić w jaki sposób możemy efektywnie stwierdzać, czy wybrana krawędź łączy ze sobą dwie różne komponenty (czyli nie tworzy cyklu). Użyjemy w tym celu struktury zbiorów rozłącznych, która będzie implementować następujące procedury:

Jednym ze sposobów reprezentowania zbiorów jest użycie ukorzenionego drzewa, gdzie:

Implementacja metod struktury dla ukorzenionych drzew:

makeset(x)(x):

  1. x.parentxx.\mathrm{parent} \gets x
  2. x.rank0x.\mathrm{rank} \gets 0

find(x)(x):

  1. while xx.parentx \neq x.\mathrm{parent}:
    1. xx.parentx \gets x.\mathrm{parent}
  2. return xx

union(x,y)(x,y)

  1. rootX\mathrm{rootX} \gets find(x)(x)
  2. rootY\mathrm{rootY} \gets find(y)(y)
  3. if rootX=rootY\mathrm{rootX} = \mathrm{rootY}:
    1. return
  4. \triangleright łączymy drzewa poprzez podłączanie wyższego drzewa do korzenia niższego
  5. if rootX.rank>rootY.rank\mathrm{rootX}.\mathrm{rank} > \mathrm{rootY}.\mathrm{rank}:
    1. rootY.parentrootX\mathrm{rootY}.\mathrm{parent} \gets \mathrm{rootX}
  6. else:
    1. rootX.parentrootY\mathrm{rootX}.\mathrm{parent} \gets \mathrm{rootY}
    2. if rootX.parent=rootY\mathrm{rootX}.\mathrm{parent} = \mathrm{rootY}:
      1. rootY.rankrootY.rank+1\mathrm{rootY}.\mathrm{rank} \gets \mathrm{rootY}.\mathrm{rank} + 1

Procedura union łącząc zbiory buduje drzewa reprezentujące te zbiory w taki sposób, że zachowane są następujące własności:

  1. d1d_1: dla każdego xx, x.rank<x.parent.rankx.\mathrm{rank} < x.\mathrm{parent}.\mathrm{rank};
    Własność zachodzi, ponieważ węzeł z rank\mathrm{rank} równym kk jest tworzony poprzez złączenie dwóch drzew, których korzenie mieli rank\mathrm{rank} równy k1k-1 (rozumowanie to można rozszerzyć do formalnego dowodu indukcyjnego).
  2. d2d_2: każdy korzeń mający rank\mathrm{rank} równy kk jest korzeniem drzewa mającego co najmniej 2k2^k węzłów.
    Jeśli zbiór ma nn elementów, a korzeń jego drzewa ma rank\mathrm{rank} równą kk to n>2kn > 2^k co implikuje, że k<log(n)k < \log(n), co daje logarytmiczne ograniczenie na wysokość drzewa. Zatem procedury find oraz union mają pesymistyczną złożoność obliczeniową O(log(n))O(\log(n)) dla zbiorów wielkości nn.

W sekcji 5.1.4 książki Algorithms jest pokazane jak można jeszcze usprawnić tę implementację.

Pseudokod algorytmu Kruskala

Kruskal(G)(G):

  1. for all vVv \in V:
    1. makeset(v)(v)
  2. X{}X \gets \{\}
  3. posortuj krawędzie ze zbioru E według ich wagi\text{posortuj krawędzie ze zbioru } E \text{ według ich wagi}
  4. for all {u,v}E\{u,v\} \in E w kolejności rosnącej:
    1. if find(u)(u) \neq find(v)(v):
      1. dodaj krawędzˊ {u,v} do X\text{dodaj krawędź } \{u,v\} \text{ do } X
      2. union(u,v)(u,v)

Złożoność obliczeniowa: W pesymistycznym przypadku algorytm Kruskala wykona V×|V| \times makeset, 2E×2|E| \times find oraz (V1)×(|V| - 1) \times union.

Algorytm Prima

Algorytm szukający MST wykorzystując cut property. W przypadku tego algorytmu wybrane krawędzie do zbioru XX (oznaczenia zgodne z wcześniejszymi z cut property) zawsze formują drzewo (czyli graf spójny, a nie jak w przypadku algorytmu Kruskala rozłączne komponenty), a zbiór wierzchołków SS jest po prostu zbiorem węzłów tego drzewa tworzonego przez krawędzie XX.

Prim(G)(G):

  1. for all vVv \in V:
    1. v.costv.\mathrm{cost} \gets \infty
    2. v.prevv.\mathrm{prev} \gets null
  2. wybierzwęzełstartowys\mathrm{wybierz węzeł startowy } s
  3. s.cost0s.\mathrm{cost} \gets 0
  4. s.prevss.\mathrm{prev} \gets s
  5. \triangleright używa v.costv.\mathrm{cost} jako kluczy w kolejce priorytetowej; im mniejsza wartość cost\mathrm{cost} tym wyższy priorytet
  6. QQ \gets MakeQueue(V)(V)
  7. while QQ \neq \emptyset:
    1. vv \gets ExtractMin(Q)(Q)
    2. for all {v,z}E\{v,z\} \in E:
      1. if z.cost>l(v,z)z.\mathrm{cost} > l(v,z):
        1. z.costl(v,z)z.\mathrm{cost} \gets l(v,z)
        2. z.prev=vz.\mathrm{prev} = v
        3. DecreaseKey(Q,z)(Q,z)

Algorytm Prima w każdej iteracji pętli while dodaje krawędź do zbioru XX (realizowane jest to przez ExtractMin). Dodawana krawędź ma najmniejszą wagę spośród krawędzi pomiędzy wierzchołkami należącymi do zbioru SS i VSV \setminus S, co jest realizacją cut property. Z tego wynika poprawność algorytmu Prima.

Złożoność obliczeniowa: Łatwo zauważyć, że algorytm Prima jest bardzo podobny do algorytmu Dijkstry, a główna różnica polega na tym, że priorytetem wierzchołka w algorytmie Prima jest waga najlżejszej krawędzi łączącej ten wierzchołek z wierzchołkami ze zbioru SS (w algorytmie Dijkstry była to łączna długość ścieżki od węzła startowego). Zatem algorytm Prima ma taką samą złożoność jak algorytm Dijkstry i jak już wiemy jest ona zależna od implementacji kolejki priorytetowej.

More