lang: ‘pl’
title: ‘Dijkstra’s algorithm’
author: ‘Jerry Sky’
date: ‘2020-05-18’


Concept

Algorytm Dijkstry pozwala na znajdowanie najkrótszych ścieżek w grafie G=(V,E,l)G = (V,E,l), gidze l:ER+l: E\to \mathbb{R}_+. Jest on prostą modyfikacją procedury BFS i w gruncie rzeczy polega na zastąpieniu kolejki FIFO w algorytmie BFS, kolejką priorytetową. Implementacje kolejki priorytetowej na bazie kopca minimalnego zostały przedstawione na poprzednich wykładach.

Implementacja

W algorytmie Dijkstry zostaną użyte następujące operacje na kolejce priorytetowej:

Dijkstra(G,s)(G,s):

  1. for all vVv \in V:
    1. v.distv.\mathrm{dist} \gets \infty
    2. v.prevv.\mathrm{prev} \gets null
  2. s.dist0s.\mathrm{dist} \gets 0
  3. s.prevss.\mathrm{prev} \gets s
  4. QQ \gets MakeQueue(V)(V)
    # używa v.distv.\mathrm{dist} jako kluczy w kolejce priorytetowej; im mniejsza wartość dist\mathrm{dist} tym wyższy priorytet
  5. while Q>0|Q| > 0:
    1. uu \gets ExtractMin(Q)(Q)
    2. for all (u,v)E(u,v) \in E
      1. if v.dist>u.dist+l(u,v)v.\mathrm{dist} > u.\mathrm{dist} + l(u,v)
        1. v.distu.dist+l(u,v)v.\mathrm{dist} \gets u.\mathrm{dist} + l(u,v)
        2. v.prevuv.\mathrm{prev} \gets u
        3. DecreaseKey(Q,v)(Q,v)

Dla każdego vVv\in V algorytm Dijkstry zapisuje w v.distv.\mathrm{dist} długość najkrótszej ściezki od wierzchołka startwoeg sVs \in V (jeśli do jakiegoś wierzchołka nie da się dojść od wierzchołka ss to długość ta jest ustawiona na \infty). Dodatkowo, dla każdego vVv \in V w polach v.prevv.\mathrm{prev} znajduje się wierzchołek, z którego bezpośrednio dojdziemy po najkrótszej ścieżce od ss do vv. Pozwala nam to na odtworzenie najkrótszych ścieżek od ss do dowolnego innego wierzchołka grafu GG (jeśli do jakiegoś wierzchołka nie możemy dojść z ss to wartość prev\mathrm{prev} pozostanie nullem).

Analogia do budzików w książce Algorithms DPV~ Chapter 4.4.1

Przykład

example

Alternatywna interpretacja

Możemy popatrzeć na problem znajdowania najkrótszej ścieżki w grafie G=(V,E,l)G = (V,E,l) od wierzchołka startowego sVs \in V jak na powiększanie podzbioru wierzchołków RR, dla których znamy już najkrótsze ścieżki.

Poprawność działania algorytmu

W celu formalnego udowodnienia poprawności działania algorytmu Dijkstry powinniśmy przeprowadzić dowód indukcyjny (opierający się na rozumowaniu z powyższych punktów) z następującym założeniem indukcyjnym:
Na końcu każdej iteracji pętli while w pseudokodzie algorytmu spełnione są następujące własności:

  1. Istnieje wartość dd, taka że wierzchołki należące do zbioru RR (czyli elementy, których nie ma już w kolejce QQ), mają odległość d\le d od wierzchołka startowego ss.
  2. Dla każdego wierzchołka uVu \in V wartość u.distu.\mathrm{dist} jest długością najkrótszej ścieżki od ss do uu, gdzie na ścieżce tej znajdują się tylko wierzchołki należące do zbioru RR (jeśli taka ścieżka nei istnieje to u.distu.\mathrm{dist} \gets \infty)

Złożoność obliczeniowa

Zatem w sumie dostajemy górne ograniczenie na złożoność obliczeniową w postaci V|V|\cdot Insert +V+ |V|\cdot ExtractMin +E+ |E|\cdot DecreaseKey.

Porównanie złożoności obliczeniowej algorytmu Dijkstry dla różnych implementacji kolejki priorytetowej:

Struktura Złożoność ExtractMin Złożoność Insert i DecreaseKey Złożoność Dijkstry
Tablica O(V)O(\lvert V\rvert) O(1)O(1) O(V2)O(\lvert V\rvert^2)
Kopiec binarny O(logV)O(\log \lvert V\rvert) O(logV)O(\log \lvert V\rvert) O((V+E)logV)O((\lvert V\rvert + \lvert E\rvert)\cdot \log\lvert V\rvert)
Kopiec dd-arny O(dlogVlogd)O\left(\frac{d\log \lvert V\rvert}{\log d}\right) O(dlogVlogd)O\left(\frac{d\log \lvert V\rvert}{\log d}\right) O((Vd+E)logVlogd)O\left((\lvert V\rvert \cdot d + \lvert E\rvert)\cdot \frac{\log \lvert V\rvert}{\log d}\right)
Kopiec Fibonacciego O(logV)O(\log \lvert V\rvert) O(1)O(1) (zł. zamortyzowana) O(VlogV+E)O(\lvert V\rvert \log \lvert V\rvert +\lvert E\rvert)

Warto zauważyć, że złożoność ta ostatecznie zależy od gęstości grafu (liczby krawędzi do liczby wierzchołków). Zauważmy, że jeśli E=Ω(V2)|E| = \Omega(|V|^2) (graf gęsty) to implementacja kolejki priorytetowej na prostej tablicy ma asymptotycznie najlepszą złożoność. Implementacja przy pomocy kopca binarnego ma lepszą niż przy pomocy tablicy jeśli E<V2logV|E| < \frac{|V|^2}{\log |V|}. Kopiec dd-arny będący uogólnieniem kopca binarnego ma złożoność zależną od dd. Jeśli ustalimy dEVd \approx \frac{|E|}{|V|}, czyli na średni stopień wierzchołka w grafie wejściowym otrzymujemy implementację, która dla grafów gęstych (czyli takich gdzie E=Ω(V2)|E| = \Omega(|V|^2)) jest asymptotycznie równie dobra jak tablica (złożoność Dijkstry O(V2)O(|V|^2)), a dla grafów rzadkich (czyli takich gdzie E=O(V)|E| = O(|V|)) jest asymptotycznie równie dobra jak kopiec binarny (złożoność Dijkstry O(VlogV)O(|V|\log |V|)). Dodatkowo dla grafów będących pomiędzy (czyli takich gdzie E=V1+δ|E| = |V|^{1+\delta}, δ(0,1)\delta \in (0,1)) złożoność Dijkstry wynosi asymptotycznie O(E)O(|E|), czyli jest liniowa względem wielkości grafu. W celu implementacji kolejki priorytetowej można użyć również np. kopca Fibonacciego. Asymptotyczna złożoność zamortyzowana będzie w tym przypadku najlepsza, ale należy pamiętać, że kopiec Fibonacciego jest znacznie bardziej skomplikowaną strukturą, wymaga więcej pracy podczas implementacji i w praktyce często przegrywa z prostszymi strukturami.

More