Topológiai rendezés

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 gráfok csúcsainak topológiai rendezését kell megvalósítani. Egy gráf objektumok, vagy más néven csúcsok halmaza, amelyeket élek segítségével valamilyen módon összekapcsolunk. Egy gráfot irányítottnak tekintünk, ha benne az éleknek van irányultsága, vagyis a csúcsok közti kapcsolatokat csak az egyik irányból vesszük.

Topológiai rendezésnek az irányított gráf csúcsainak egy olyan permutációját tekintjük, ahol először azok a csúcsok szerepelnek, amelyeknek nincsen megelőzője (nem hivatkozik rájuk senki), majd azok rákövetkezői és annak rákövetkezői. Egy irányított gráfhoz több ilyen rendezés is tartozik, ezek közül kell egyet megadnunk egy algoritmus felhasználásával. Továbbá észrevehetjük azt is, hogy topológiai rendezést csak körmentes gráfot esetében tudunk meghatározni, vagyis csak olyan esetekben, amikor egyértelműen megállapíthatóak kezdetben azok a csúcsok, amelyeknek nincsenek megelőzői.

Topológiai rendezéseket az informatikában nagyon sok helyen lehet alkalmazni, például a Haskell fordító ennek alapján tudja megállapítani, hogy milyen sorrendben tudja lefordítani egy több modulból álló Haskell-program egyes részeit (amennyiben a modulok nem hivatkoznak kölcsönösen egymásra, akár közvetve, akár közvetlenül).

Az irányított gráfokat (Graph) a csúcsaikat összekötő élek (Edge) felsorolásával adhatjuk meg, és a csúcsokban tárolt értékek típusával paraméterezzük. Ennek megfelelően a következő típusszinonimákat fogjuk alkalmazni:

type Edge a  = (a, a)
type Graph a = [Edge a]

A feladatban a következő példaként megadott irányított gráfok szerepelnek:

Toposortc3034fd2c621847528636a45273f5e7f.png

graph1 :: Graph Int
graph1 =
  [ (5, 11), (7, 8), (7, 11), (3, 8), (11, 9)
  , (11, 10), (8, 9), (11, 2), (3, 10)
  ]

Láthatjuk, hogy ebben az esetben a csúcsok egyik lehetséges topológiai rendezése az 5, 7, 3, 11, 8, 2, 9, 10 sorozat.

Toposort33a8ee95b7abc427f9c5437400551722.png

graph2 :: Graph [Int]
graph2 =
  [ ([], [1]), ([], [2]), ([], [3])
  , ([1], [1,2]), ([1], [1,3]), ([2], [1,2]), ([2], [2,3])
  , ([3], [1,3]), ([3], [2,3]), ([1,2], [1,2,3]), ([1,3],[1,2,3])
  , ([2,3], [1,2,3])
  ]

Ennek a gráfnak (Hasse-diagramnak) egy topológiai rendezése a {}, { 1 }, { 2 }, { 3 }, { 1, 3 }, { 2, 3 }, { 1, 2 }, { 1, 2, 3 } sorozat.

Végül megadunk egy kört tartalmazó gráfot, amelynek emiatt nem adható meg topológiai rendezése:

Toposort78b0888c3ed70a1ac291467fcf453e39.png

graph3 :: Graph Char
graph3 =
  [ ('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'E')
  , ('E', 'A'), ('B', 'E'), ('D', 'E'), ('C', 'A')
  ]

Egy gráf összes csúcsa (2 pont)

Készítsünk egy olyan függvényt, amely meghatározza egy irányított gráf korábban bevezetett ábrázolásából a gráf csúcsait! Ügyeljünk arra, hogy az eredményben minden csúcs csak egyszer szerepeljen, a sorrend nem számít.

nodesOf :: Eq a => Graph a -> [a]

Test>
[2, 3, 5, 7, 8, 9, 10, 11] :: [Int]
Test>
[[], [1], [1, 2], [1, 2, 3], [1, 3], [2], [2, 3], [3]] :: [[Int]]
Test>
"ABCDE" :: [Char]

Az adott csúcsból közvetlenül elérhető csúcsok (1 pont)

Írjunk egy függvényt, amely a gráf éleinek ismeretében megadja, hogy egy benne szerepelő csúcsból (az élek szerint) mely más csúcsok érhetőek el közvetlenül, vagyis egyetlen lépésben! Ha nincs benne az adott elem a gráfban, akkor az eredmény üres lista. Az eredményben az elemek sorrendje nem számít.

reachableFrom :: Eq a => Graph a -> a -> [a]

Test>
[] :: [Int]
Test>
[8, 10] :: [Int]
Test>
[11] :: [Int]
Test>
[2, 9, 10] :: [Int]
Test>
[] :: [Int]
Test>
[[1], [2], [3]] :: [[Int]]
Test>
[[1, 2, 3]] :: [[Int]]
Test>
"B" :: [Char]
Test>
"A" :: [Char]

Az adott csúcsból elérhető összes csúcs (2 pont)

Írjunk egy függvényt, amely a gráf éleinek ismeretében megadja, hogy egy benne szereplő csúcsból (az élek szerint) mely csúcsok érhetőek el valamilyen módon, vagyis akár több elemen keresztül! Ha nincs benne az adott elem a gráfban, akkor az eredmény üres lista, továbbá az eredményben az elemek sorrendje nem számít. A listában szerepeljen maga az elem is.

allReachableFrom :: Eq a => Graph a -> a -> [a]

Test>
[3, 8, 9, 10] :: [Int]
Test>
[2, 5, 9, 10, 11] :: [Int]
Test>
[2, 9, 10, 11] :: [Int]
Test>
[9] :: [Int]
Test>
[[], [1], [1, 2], [1, 2], [1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 3], [1, 3], [2], [2, 3], [2, 3], [3]] :: [[Int]]
Test>
[[1, 2], [1, 2, 3]] :: [[Int]]
Test>
[[1, 2, 3]] :: [[Int]]

Tartalmaz-e kört a gráf? (2 pont)

Valósítsuk meg azt a függvényt, amely egy irányított gráfról el tudja dönteni, hogy tartalmaz kört vagy sem. Egy irányított gráfra akkor mondjuk, hogy kört tartalmaz (ciklikus), ha van benne legalább egy olyan elem, amely közvetve vagy közvetlenül elérhető elérhető saját magából indulva.

isCyclic :: Eq a => Graph a -> Bool

Test>
False :: Bool
Test>
False :: Bool
Test>
True :: Bool

A kezdőcsúcsok kiszámítása (2 pont)

Készítsünk egy olyan függvényt, amely egy irányított gráf éleiből megadja azokat a csúcsait, amelyeknek nincsen egyetlen megelőzője sem! Vegyük észre, ez kört tartalmazó gráf esetében üres lista lesz. Az eredményben az elemek sorrendje nem számít.

roots :: Eq a => Graph a -> [a]

Test>
[3, 5, 7] :: [Int]
Test>
[[]] :: [[Int]]
Test>
[] :: [Char]

Topológiai rendezés (3 pont)

Írjuk meg a topológiai rendezés függvényét! A megvalósítás során a következő (Arthur Kahn által 1962-ben publikált) algoritmusra támaszkodjunk:

  1. Legyen L egy lista, amely a topológiailag rendezett csúcsokat tartalmazza. Ez a lista kezdetben legyen üres.

  2. Legyen S egy lista, amely kezdetben a gráf azon csúcsait tartalmazza, amelyeknek nincsenek megelőzőik.

  3. Amíg S listában vannak elemek:

    1. Vegyünk ki egy n csúcsot az S listából (úgy, hogy ne maradjon a listában).

    2. Az n csúcsot fűzzük az L végére.

    3. Minden m elemnél, amelyet az n elemmel egy e él összeköti:

      1. Töröljük az e élet a gráfból.

      2. Ha m csúcsnak nincsenek más megelőzői, akkor tegyük bele az S listába.

  4. Az L lista fogja tartalmazni az eredményt.

Ne feledjük, hogy ha a gráf köröket tartalmaz, a topológiai rendezést nem tudjuk kiszámítani. Ebben az esetben a példában látható hibaüzenetet kell adnunk!

toposort :: Eq a => Graph a -> [a]

Test>
[5, 7, 3, 11, 8, 10, 2, 9] :: [Int]
Test>
[[], [1], [2], [3], [1, 2], [1, 3], [2, 3], [1, 2, 3]] :: [[Int]]
Test>
⊥₁ :: [Char]
⊥₁: toposort: cyclic graph
CallStack (from HasCallStack):
  error, called at ./Toposort.lhs:275:29 in main:Toposort

Pontozás (elmélet + gyakorlat)