Legrövidebb út kiszámítása gráfokban

Használható segédanyagok: Haskell könyvtárainak dokumentációja, (lokális!) Hoogle, a tárgy honlapja és a BE-AD rendszerbe feltöltött beadandók. Ha bármilyen kérdés, észrevétel felmerül, azt a felügyelőknek kell jelezni, nem a diáktársaknak!

A feladatok egymásra épülnek ezért érdemes ezeket a megadásuk sorrendjében megoldani, de legalább megérteni az aktuális feladatot megelőző feladatokat! A függvények definíciójában lehet, sőt javasolt is alkalmazni a korábban definiált függvényeket (függetlenül attól, hogy sikerült-e azokat megadni).

Tekintve, hogy a tesztesetek, bár odafigyelés mellett íródnak, nem fedik le minden esetben a függvény teljes működését, határozottan javasolt még külön próbálgatni a megoldásokat beadás előtt, vagy megkérdezni a felügyelőket!

Részpontszámokat csak az elégséges szint elérése után lehet kapni!

A feladat összefoglaló leírása

Ebben a feladatban irányított, súlyozott gráfokkal fogunk dolgozni. A gráfok objektumok és a köztük levő kapcsolatok leírására alkalmas ábrázolási mód. A gráfban szereplő objektumokat csúcsoknak (vertex) nevezzük, a köztük levő kapcsolatokat pedig éleknek (edge).

A gráfok csúcspontjait egész számokkal azonosítjuk, nullától kezdve. A csúcsok által lefedett tartomány folytonos, tehát ha van a gráfban 2-es csúcs, akkor van 0-ás és 1-es sorszámú is. A függvények könnyebb megértése érdekében a csúcsokra vezessük be az alábbi típusszinonimát:

type Vertex = Int

Egy él a két végpontjából és a súlyából alkotott hármassal van ábrázolva. Az élek irányítottak, tehát a (3,2,4) él a 3-as csúcsból a 2-esbe vezet, és az élköltség, avagy súly pedig 4.

type Edge = (Vertex, Vertex, Weight)

Az élssúlyokat szintén egész számokkal adjuk meg. Ezek pozitív értékek lesznek minden tesztesetben.

type Weight = Int

A Dijkstra-algoritmus az egy csúcsból kiinduló legrövidebb utak keresésére szolgál irányított gráfokon. Mivel csak olyan esetekben működik, amikor a keresett útvonalban a súlyok összege nem negatív, ezért most kizárólag olyan eseteket fogunk vizsgálni, ahol az élsúlyok pozitív egész számok.

A gráfokat éleikből álló listájukkal adjuk meg. Feltehetjük, hogy legalább egy él mindig van a gráfban.

type Graph = [Edge]

Az algoritmust lépésenként fogjuk összeállítani, így a részletes leírás az egyes részfeladatoknál található.

Az alábbi gráfokat használjuk példaként:

Háromszög:

triangle :: Graph
triangle = [ (0, 1, 2), (1, 2, 3), (2, 0, 1) ]

Dijkstra782c3900e1b2013041eacf3de41e338f.png

Egy egyszerűbb szabálytalan alakú:

basic :: Graph
basic = [ (0, 1, 1), (2, 0, 3), (2, 1, 9), (2, 3, 11), (1, 3, 1) ]

Dijkstra4497ca41e2b17922c26800b5d3474781.png

És egy bonyolultabb gráf:

complex :: Graph
complex = [ (0, 4, 5), (1, 2, 1), (2, 3, 4)
          , (3, 2, 6), (1, 4, 2), (4, 1, 3)
          , (4, 3, 2), (4, 2, 9), (3, 0, 7), (0, 1, 10) ]

Dijkstraecbd1e13ed71da17d052b65c6d130baa.png

Csúcsok száma (1 pont)

Készítsük el azt a függvényt, amely megadja a csúcsok számát egy gráfban! Ez a nullától kezdett folytonos csúcs számozás miatt lényegében a legnagyobb sorszámú csúcs értékénél egyel több.

numVertices :: Graph -> Int

Test>
3 :: Int
Test>
4 :: Int
Test>
5 :: Int

Csúcs szomszédai élsúlyokkal (1 pont)

Adjuk meg egy kiválasztott csúcs szomszédait, a hozzájuk vezető élsúlyukkal együtt (szomszéd, élsúly) elemek listájának formájában! Ha valahonnan nem vezet ki él, akkor a lista üres, tehát ekkor az élek irányítottságát is figyelembe kell venni!

neighbours :: Graph -> Vertex -> [(Vertex, Weight)]

Test>
[(1, 2)] :: [(Vertex, Weight)]
Test>
[(0, 3), (1, 9), (3, 11)] :: [(Vertex, Weight)]
Test>
[] :: [(Vertex, Weight)]
Test>
[(1, 3), (3, 2), (2, 9)] :: [(Vertex, Weight)]

Adott indexű elem frissítése listában (2 pont)

Írjuk meg az alábbi függvényt, amely úgy állít elő egy új listát, hogy a kapott listában az adott indexű elemet kicseréli az új elemre! Ha az index túl kicsi vagy nagy, adjunk megfelelő hibaüzenetet az error függvénnyel! Ne feledjük, a lista nullától indexelődik!

update :: Int -> a -> [a] -> [a]

Test>
[6, 1, 2, 3, 4, 5] :: [Integer]
Test>
[1, 4, 3] :: [Integer]
Test>
⊥₁ :: [Integer]
⊥₁: update: invalid index
CallStack (from HasCallStack):
  error, called at ./Dijkstra.lhs:172:31 in main:Dijkstra
Test>
⊥₁ :: [Integer]
⊥₁: update: invalid index
CallStack (from HasCallStack):
  error, called at ./Dijkstra.lhs:172:31 in main:Dijkstra
Test>
⊥₁ :: [Integer]
⊥₁: update: invalid index
CallStack (from HasCallStack):
  error, called at ./Dijkstra.lhs:172:31 in main:Dijkstra

Beszúrás prioritásos sorba (2 pont)

Az algoritmusban szükségünk lesz csúcsok egy prioritásos sorára. Ezt egy olyan listával fogjuk megvalósítani, ahol minden csúcshoz tartozik egy prioritás, amely egy egész számérték. Vezessük be az alábbi jelöléseket:

type Priority = Int
type Item     = (Priority, Vertex)
type Queue    = [Item]

A sort úgy fogjuk bővíteni, hogy a legkisebb prioritású elemek a sor elején, a nagyobbak a végén legyenek. Azonos prioritású elemek közül elsőként azt fogjuk majd kivenni, amelyet leghamarabb tettek a sorba. Ha egy csúcs már a sorban van, töröljük ki, és szúrjuk be az új prioritással! Ennek megfelelően az alábbi példákat írhatjuk fel:

Készítsük el azt a függvényt, amely egy sorba képes egy csúcsot adott prioritással beszúrni:

insert :: Queue -> Priority -> Vertex -> Queue

Test>
[(9, 2)] :: Queue
Test>
[(9, 2), (9, 1)] :: Queue
Test>
[(5, 3), (9, 2), (9, 1)] :: Queue
Test>
[(5, 3), (6, 1), (9, 2)] :: Queue
Test>
[(0, 0)] :: Queue

Megjegyzés: hatékonyabb implementációt kaphatunk, ha a sort egy fával ábrázoljuk. Ezt most mellőzzük a feladat egyszerűsítése végett.

Elem kivétele prioritásos sorból (1 pont)

Az előző ábrázolás alapján a legkisebb prioritású, azon belül először beszúrt csúcs mindig a sor elején található. Adjuk vissza ezt a csúcsot, illetve az elem kivétele után keletkező maradék sort. Amennyiben a sor üres, úgy az error függvény segítségével ezt jelezzük egy hibaüzenettel!

remove :: Queue -> (Vertex, Queue)

Test>
(2, []) :: (Vertex, Queue)
Test>
(2, [(9, 1)]) :: (Vertex, Queue)
Test>
(1, [(9, 2)]) :: (Vertex, Queue)
Test>
⊥₁ :: (Vertex, Queue)
⊥₁: remove: empty queue
CallStack (from HasCallStack):
  error, called at ./Dijkstra.lhs:236:15 in main:Dijkstra

Elem sokszorozása gráf csúcsaihoz (1 pont)

Készítsünk egy függvényt, amely egy listát állít elő úgy, hogy annyiszor ismétel egy e elemet, ahány csúcsa van az adott g gráfnak!

forVerticesOf :: a -> Graph -> [a]

Test>
["hello", "hello", "hello"] :: [[Char]]
Test>
"aaaa" :: [Char]
Test>
[42, 42, 42, 42, 42] :: [Integer]

Az algoritmus kezdőállapota (3 pont)

A Dijkstra-algoritmus alapvetően két leképezést számol ki a megadott kezdőcsúcsból indulva, minden további csúcshoz:

Ezt a két leképezést egy-egy listában tároljuk. A lista indexe mondja meg, hogy melyik csúcsra vagyunk kíváncsiak, tehát az előző példában a listáknak a 4-es indexű elemét kell vizsgálnunk. A megelőző csúcsok és legkisebb távolságok nyilvántartására vezessük be az alábbi jelölésket:

type Previous = [Vertex]
type Distance = [Weight]

Az algoritmus kezdőállapota egy olyan hármas, két alapértékekkel feltöltött leképezés, illetve egy sor van.

type State = (Previous, Distance, Queue)

Kezdetben a megelőző csúcs ismeretlen minden csúcs számára, hiszen nem ismerjük még a legrövidebb utakat. Ezt úgy jelöljük, hogy minden eleme (-1) lesz, ilyen indexű csúcs ugyanis biztos nem szerepel a gráfban. Természetesen annyi eleme kell, hogy legyen a listának, ahány csúcs a gráfban van, ugyanis később a csúcsok sorszámával indexelve fogjuk lekérdezni.

Például a triangle gráf 3 csúcsot tartalmaz, így kezdetben a megelőző csúcsok leképezése [-1, -1, -1] lesz.

A legkisebb ismert távolság kezdetben pedig végtelen minden csúcs között, ezt a legnagyobb ábrázolható értékre választjuk a maxBound konstans használatával. Van azonbal egy kivétel: maga a kezdőcsúcs, amelynek önmagához vett távolsága nulla. Ennek a leképezésnek szintén annyi eleme van, ahány csúcs a gráfban található.

Például a triangle gráf 3 csúcsot tartalmaz, így kezdetben a távolságok a 2-es kezdőcsúcsot feltételezve a [maxBound, maxBound, 0] leképezést adják.

Ezek előállításához használjuk az előző forVerticesOf függvényt. A kezdőcsúcsra vonatkozó kivételt az update függvénnyel beállíthatjuk.

A sor úgy áll elő, hogy a gráf minden csúcsát beszúrjuk a távolságok leképezésében szereplő értékkel mint prioritással.

Például a triangle gráf 3 csúcsot tartalmaz, így kezdetben a sor 2-es kezdőcsúccsal így alakul: [(0, 2), (maxBound, 0), (maxBound, 1)]. Tehát a 2-es csúcs 0 prioritású, mivel magához vett távolsága nulla. Az összes többi csúcs esetén is a távolság leképezés kezdeti értékeit használjuk prioritásnak.

start :: Graph -> Vertex -> State

Test>
([-1, -1, -1], [0, 9223372036854775807, 9223372036854775807], [(0, 0), (9223372036854775807, 1), (9223372036854775807, 2)]) :: State
Test>
([-1, -1, -1], [9223372036854775807, 9223372036854775807, 0], [(0, 2), (9223372036854775807, 0), (9223372036854775807, 1)]) :: State
Test>
([-1, -1, -1, -1], [9223372036854775807, 0, 9223372036854775807, 9223372036854775807], [(0, 1), (9223372036854775807, 0), (9223372036854775807, 2), (9223372036854775807, 3)]) :: State
Test>
([-1, -1, -1, -1, -1], [9223372036854775807, 9223372036854775807, 9223372036854775807, 0, 9223372036854775807], [(0, 3), (9223372036854775807, 0), (9223372036854775807, 1), (9223372036854775807, 2), (9223372036854775807, 4)]) :: State

Az algoritmus egy lépése (3 pont)

Az algoritmus minden lépésben frissíti a leképezéseket és a sort. Ezt úgy teszi, hogy kivesz egy u csúcsot a sorból (remove). Ennek a csúcsnak minden v szomszédjára (neighbours) a következőket teszi, folyamatosan frissítve az állapotot:

Összeadja a u csúcshoz jelenleg feljegyzett távolságot és hozzáadja az u és v közti él hosszát. Ezt hívjuk d1-nek. Vesszük a leképezésből v csúcs távolságát is, jelöljük ezt d2-vel. Ha d1 < d2, azaz olyan élet találtunk, amely rövidebb távolságot eredményez, mint az eddig ismertek, akkor az állapot így változik meg:

Ezek elvégezhetők az update és insert függvényekkel. Ha d1 >= d2, akkor az állapot nem változik, csak a sor lett rövidebb kezdetben u kivétele miatt. Arra viszont ügyeljünk, ha az u és v csúcsok távolsága már eleve végtelen, vagyis értéke maxBound, akkor ne növeljük tovább, mert túl fog csordulni!

step :: Graph -> State -> State

Test>
([-1, 0, -1], [0, 2, 9223372036854775807], [(2, 1), (9223372036854775807, 2)]) :: State
Test>
([-1, 0, 1], [0, 2, 5], [(5, 2)]) :: State
Test>
([-1, 0, 1], [0, 2, 5], []) :: State
Test>
([-1, -1, -1, -1], [9223372036854775807, 9223372036854775807, 9223372036854775807, 0], [(9223372036854775807, 0), (9223372036854775807, 1), (9223372036854775807, 2)]) :: State
Test>
([-1, -1, -1, -1], [9223372036854775807, 9223372036854775807, 9223372036854775807, 0], [(9223372036854775807, 1), (9223372036854775807, 2)]) :: State
Test>
([-1, -1, -1, -1], [9223372036854775807, 9223372036854775807, 9223372036854775807, 0], [(9223372036854775807, 2)]) :: State
Test>
([-1, -1, -1, -1], [9223372036854775807, 9223372036854775807, 9223372036854775807, 0], []) :: State

A Dijkstra-algoritmus (2 pont)

A végső algoritmust úgy kapjuk meg, hogy egy gráfhoz és kezdőcsúcshoz kiszámítjuk a kezdőállapotot (start). Ezen utána addig ismételjük az algoritmus egy lépését (step), amíg a benne lévő sor ki nem ürül. Ekkor visszaadjuk a távolságokat és megelőző csúcsokat tartalmazó leképezéseket.

dijkstra :: Graph -> Vertex -> (Previous, Distance)

Test>
([-1, 0, 1], [0, 2, 5]) :: (Previous, Distance)
Test>
([2, 0, -1, 1], [3, 4, 0, 5]) :: (Previous, Distance)
Test>
([-1, -1, -1, -1], [9223372036854775807, 9223372036854775807, 9223372036854775807, 0]) :: (Previous, Distance)
Test>
([-1, 4, 1, 4, 0], [0, 8, 9, 7, 5]) :: (Previous, Distance)
Test>
([3, 4, 1, 4, -1], [9, 3, 4, 2, 0]) :: (Previous, Distance)

Útrekonstrukció (2 pont)

Hogy megkapjuk egy u és v csúcs közti legrövidebb utat, az alábbiakat kell tennünk:

Az útvonal csúcspontok listája lesz:

type Path = [Vertex]

A következő függvény tehát egy gráfban két csúcs között adja meg a legrövidebb útvonalat a csomópontok listájával és annak hosszával együtt.

shortestPath :: Graph -> Vertex -> Vertex -> (Path, Weight)

Test>
([0, 1], 2) :: (Path, Weight)
Test>
([1], 0) :: (Path, Weight)
Test>
([2, 0, 1, 3], 5) :: (Path, Weight)
Test>
(⊥₁, 9223372036854775807) :: (Path, Weight)
⊥₁: shortestPath: no path found
CallStack (from HasCallStack):
  error, called at ./Dijkstra.lhs:432:20 in main:Dijkstra
Test>
([0, 4, 1, 2], 9) :: (Path, Weight)
Test>
([1, 4, 3], 4) :: (Path, Weight)

Pontozás