Lukáš sa rozhodol, že prestane používať Linux a nájde si dievča. Keďže je to však informatik, reprezentoval si cestu k dievčaťu grafom. Pomôž Lukášovi nájsť najkratšiu trasu od jeho aktuálneho stavu k jeho vysnívanému dievčaťu.
Lukášov postup vieme zakresliť ako ohodnotený graf. Vrcholy reprezentujú jednotlivé stavy, v ktorých sa môže nachádzať, a hrany predstavujú možné prechody medzi nimi. Každá hrana má priradenú hodnotu, ktorá vyjadruje „náročnosť“ daného kroku (napríklad čas alebo úsilie). Lukáš začína v jednom z vrcholov a zaujíma ho, ako sa čo najefektívnejšie dostať k jednotlivým vrcholom.
a. Pre každý vrchol urč najmenšiu možnú „náročnosť“, za ktorú sa doň Lukáš vie dostať zo štartovacieho vrcholu.

b. Lukáš si vymyslel nasledovný postup:
„Zo svojho aktuálneho vrcholu sa vždy presuniem do susedného vrcholu, do ktorého vedie najmenej náročný prechod. Tento krok budem opakovať.“
Funguje tento postup vždy správne?
Ak nie, vysvetli prečo a uveď konkrétny príklad grafu, na ktorom tento postup zlyhá.
c. Navrhnite všeobecný postup, ktorý pre ľubovoľný ohodnotený graf s nezápornými hranami určí najmenšiu náročnosť cesty zo štartovacieho vrcholu do všetkých ostatných vrcholov.
Tvoj postup popíš dostatočne presne, aby sa dal jednoznačne vykonať.
d. Lukáš má teraz veľa práce, musí chodiť do posilky, riešiť FKS a loviť Julku pokeballmi, takže sa mu nechcelo kódiť hľadanie najkratšej cesty. Tak rozmýšlaľ a potom si uvedomil, že by mohol zrecyklovať kód z BFS, len by si pre každý vrchol pamätal vzdialenosť a vždy keď nájde kratšiu cestu ako má, prepíše ju.
Prečo by bolo takéto riešenie neefektívne prípadne by vôbec nefungovalo? Popíš (alebo nakresli) graf, na ktorom to ukážeš.
Povedzme, že Lukáš začína vo vrchole Linux vpravo dole. Po vyskúšaní rôznych ciest nám môžu vyjsť následovné najmenšie možné náročnosti:
Linux do vrcholu Linux je náročnosť $0$Linux do vrcholu Jablko je náročnosť $6$Linux do vrcholu Okná je náročnosť $4$Linux do vrcholu Auto je náročnosť $6$Linux do vrcholu Posilka je náročnosť $14$Linux do vrcholu Pokemon je náročnosť $16$Linux do vrcholu PRASK je náročnosť $13$Linux do vrcholu Peniaze je náročnosť $15$Linux do vrcholu FKS je náročnosť $13$Linux do vrcholu Dievča je náročnosť $17$Všimnime si, že do troch vrcholov susediacich s Linuxom najmenšiu náročnosť vždy poznáme bez toho aby sme skúšali a porovnávali viaceré možnosti - je to jednoducho náročnosť hrany medzi nimi. Pre ostatné vrcholy však musíme vymyslieť niečo múdrejšie, ak sa nám to nechce len tak skúšať.
Tento postup nefunguje vždy správne. Dá sa totiž chápať dvoma rôznymi spôsobmi a ani jeden z nich nefunguje. Ak si Lukáš nepamätá, ktoré vrcholy už navštívil, môže sa stať, že sa bude stále pohybovať medzi niekoľkými vrcholmi dookola a algoritmus sa nikdy neskončí. Ani keď si navštívené vrcholy pamätá, postup stále nemusí nájsť najkratšiu cestu. To, že je aktuálne jedna hrana najlacnejšia, ešte neznamená, že cesta vedúca cez ňu bude najvýhodnejšia celkovo. Algoritmus robí rozhodnutia iba podľa momentálnej situácie a neberie do úvahy, čo ho čaká ďalej v grafe. Takýto „chamtivý“ (greedy) prístup preto vo všeobecnosti nestačí na hľadanie najkratších ciest v ohodnotených grafoch.
Postup, ktorý použijeme na riešenie takéhoto problému sa nazýva Dijkstrov algoritmus.
Na začiatku si ku každému vrcholu uložíme vzdialenosť od štartu. Štartovací vrchol dostane hodnotu $0$, pretože sa v ňom už nachádzame, a všetky ostatné vrcholy dostanú hodnotu $\infty$, keďže k nim zatiaľ nepoznáme žiadnu cestu.
Potom budeme opakovane vyberať nespracovaný vrchol s najmenšou známou vzdialenosťou. Keď takýto vrchol vyberieme, pozrieme sa na všetkých jeho susedov a skontrolujeme, či sa cez aktuálny vrchol nedá dostať lacnejšie než doteraz. Ak áno, uloženú vzdialenosť prepíšeme na menšiu hodnotu.
Keď vrchol spracujeme, označíme ho ako hotový. Vďaka tomu, že všetky hrany majú nezáporné váhy, vieme, že sme už pre tento vrchol našli najkratšiu možnú cestu a jeho vzdialenosť sa už nikdy nebude meniť.
Tento postup opakujeme dovtedy, kým nespracujeme všetky vrcholy alebo kým už neexistuje ďalší dosiahnuteľný nespracovaný vrchol.
Po skončení algoritmu budeme mať pri každom vrchole uloženú najmenšiu možnú náročnosť cesty zo štartu.
Takto má Dijkstrov algoritmus časovú náročnosť $O(V^2+E)$, ($E$ je počet hrán a $V$ je počet vrcholov). To po prídape vieme zlepšiť tým, že použijeme prioritnú frontu, v ktorej si budeme pamätať vzdialenosti do nespracovaných vrcholov namisto obyčajného poľa. Tak zrýchlime vyhľadávanie nespracovaného vrcholu s najmenšou vzdialenosťou. Takýto Dijkstra má potom časovú náročnosť $O((V^2+E)\log V)$.
Tento prístup môže fungovať, ale môže byť veľmi neefektívny.
BFS funguje dobre pri neohodnotených grafoch, pretože všetky hrany majú rovnakú cenu. Pri rôznych váhach sa však môže stať, že neskôr objavíme oveľa lepšiu cestu do vrcholu, ktorý sme už dávno spracovali. Potom musíme znova aktualizovať všetky vrcholy, ku ktorým z neho vedú cesty.
Toto sa môže opakovať veľakrát. Algoritmus teda môže stále dokola prepisovať vzdialenosti a opakovane prechádzať tie isté časti grafu. Pri väčších grafoch sa tým výrazne spomalí.
Dijkstrov algoritmus sa tomuto problému vyhýba tým, že vrcholy spracúva v poradí podľa aktuálnej najmenšej vzdialenosti. Keď raz spracuje nejaký vrchol, vie, že jeho vzdialenosť už je definitívne správna a nebude ju musieť neskôr meniť.
V najhoršom prípade môže takéto prepisovanie viesť až k časovej náročnosti $O(VE + V + E)$, pretože jeden vrchol aj jeho susedia sa môžu opakovane vracať do prepisovať. Tu vidíme, že ak pri grafoch, kde $E > V$ (ako napríklad aj náš graf Lukášových rozhodnutí), je toto podstatne horšia časová náročnosť ako Dijkstrov $O(V^2+E)$.
Súťaž PRASK zastrešuje občianske združenie Trojsten.
Trojsten, o.z.
FMFI UK, Mlynská dolina
842 48 Bratislava
Programátorská súťaž pre stredoškolákov
Tímová matematicko-fyzikálna súťaž pre základoškolákov
Materiály a úlohy na výučbu programovania