Minimális feszítőfák 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ítatlan, 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 éleknek nincs iránya (vagyis nem számít, honnan hova mutat), tehát a ((3,2),4) él a 3-as és 2-es csúcs közti kapcsolatot adja meg, ahol az élköltség, avagy súly pedig 4.

type Edge   = ((Vertex, Vertex), Weight)
type Weight = Int

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]

A Prim-algoritmus egy ún. minimális költségű feszítőfa előállítására szolgál gráfokon. Maga a feszítőfa egy olyan részgráf, amely az eredeti gráf minden csúcsát tartalmazza, de nem tartalmaz kört — tehát ekkor lényegében az eredeti gráfot az élek kihagyásával fává alakítjuk. A minimális jelző pedig arra utal, hogy olyan éleket válogatunk a fába az eredetiek közül, amelyekkel a lehető legkisebb lesz az élek tartozó költségek összege.

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. Azokat az éleket jelöltük meg pirossal, amelyek az adott gráf feszítőfájában szerepelnek majd.

Négyszög:

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

Prim2305c3e93d39eba8f0475487ade3cec6.png

Más élekkel:

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

Prim36aef1897e95cdf1866318a01737f230.png

És egy bonyolultabb gráf:

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

Prima39374c9745c0f6b3298aa0546ec0e39.png

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 valahová él vezet, akkor onnan kifelé is, mivel az élek nem irányítottak!

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

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

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>
4 :: Int
Test>
4 :: Int
Test>
5 :: Int

Beszúrás halmazba (2 pont)

A Prim-algorirtmus leírásához szükségünk lesz halmazműveletekre. A halmazokat olyan listákkal valósítunk meg, amelyek egy elemet csak egyszer tartalmazhatnak, és a beszúrt elemeket rendezve tárolják a gyorsabb lekérdezés érdekében.

Megjegyzés: a halmazokat szokás fák segítségével ábrázolni a nagyobb teljesítmény érdekében. A feladat egyszerűsítése miatt itt most eltekintünk ettől.

Emlékezzünk, az Ord a megkötés arra utal, hogy a olyan típust jelöl, amelynek elemei összehasonlíthatóak, tehát értelmezve van rajtuk a < relációs operátor!

insert :: Ord a => a -> [a] -> [a]

Test>
[5] :: [Integer]
Test>
[5] :: [Integer]
Test>
[5, 6] :: [Integer]
Test>
[5, 6] :: [Integer]
Test>
[5, 6] :: [Integer]
Test>
[5, 6, 7] :: [Integer]
Test>
[4, 5, 6, 7] :: [Integer]

Halmazok uniója (1 pont)

Vegyük két halmaz unióját, tehát állítsuk elő azt a halmazt, amely mindkettő elemeit tartalmazza!

union :: Ord a => [a] -> [a] -> [a]

Test>
[5] :: [Integer]
Test>
[5, 6] :: [Integer]
Test>
[5, 6] :: [Integer]
Test>
[5, 6] :: [Integer]
Test>
[5, 6, 7] :: [Integer]
Test>
[5, 6, 7] :: [Integer]
Test>
[5, 6, 7] :: [Integer]

Törlés halmazból (2 pont)

Töröljük ki a megadott elemet egy halmazból! Ha nem tartalmazta eddig sem, akkor a halmaz nem változik meg. A függvény úgy valósítsuk meg, hogy ne dolgozza fel a teljes halmazt, csak addig az elemig menjen el, amelyiket ki kell törölni. Ha a megvalósítás nem ilyen, a feladat csak 1 pontot ér!

remove :: Ord a => a -> [a] -> [a]

Test>
[] :: [Integer]
Test>
[] :: [Integer]
Test>
[6] :: [Integer]
Test>
[6, 7] :: [Integer]

Az algoritmus kezdőállapota (2 pont)

A Prim-algoritmus három halmazt tart karban. Az első a feszítőfába már bevett csúcsokat tartalmazza, ezt később X-el jelöljük:

type TreeVertices = [Vertex]

A második a gráfból még be nem vett csúcsokat tartalmazza (a továbbiakban Y):

type RemainingVertices = [Vertex]

Az utolsó, F pedig jelölje a feszítőfába bevett élek halmazát:

type TreeEdges = [Edge]

Az algoritmus kezdőállapota a következőképpen reprezentálható a három halmazzal egy hármasként:

type State = (TreeVertices, RemainingVertices, TreeEdges)

Készítsük el azt a függvényt, amely előállítja az algoritmus kezdőállapotát egy tetszőleges gráfból úgy, hogy az első (0-ás sorszámú) u csúcsot használja kiindulási alapnak! Kezdetben X-ben csak az u csúcs szerepel, Y-ban pedig az összes többi, kivéve u-t. Ekkor F legyen üres halmaz.

start :: Graph -> State

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

Legkisebb költségű él keresése (3 pont)

Készítsük el azt a függvényt, amely egy g gráf és egy állapot segítségével megadja azt az élet a gráfból, amire igazak az alábbiak:

Ismét figyeljünk oda arra, hogy az élek nem irányítottak, tehát ha u és v csúcsok között él fut, az ((u,v),e) és ((v,u),e) alakban is szerepelhet a gráfban (ahol e az él súlyát jelölte)!

findEdge :: Graph -> State -> Edge

Test>
((0, 1), 2) :: Edge
Test>
((0, 1), 1) :: Edge
Test>
((0, 4), 5) :: Edge

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

Az algoritmus egy lépése egy állapotból egy másikba visz. Ez a következőképpen történik:

  1. Kiválasztunk egy e élet a findEdge függvénnyel az aktuális, (X, Y, F) hármassal leírható állapotból és a kapott gráfból.

  2. Az élet hozzáadjuk F halmazhoz.

  3. X-hez hozzávesszük az union művelettel azt a halmazt, amely az új él végpontjait tartalmazza.

  4. Y-ból pedig kitöröljük a remove művelettel az él azon végpontját, amely eddig benne volt.

step :: Graph -> State -> State

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

A Prim-algoritmus (2 pont)

A Prim-algoritmus az előző segédfüggvényekkel most már leírható a kövekezőképpen:

  1. Állítsuk elő a kapott g gráfhoz tartozó (X, Y, F) kezőállapotot a start függvény segítségével.

  2. Ismételjük az állapot léptetését a step függvénnyel.

  3. Folytassuk a léptetést, amíg az állapotban X nem tartalmazza a gráf minden csúcsát (mivel halmaz, elég azt megnézni, hogy a számossága azonos g csúcsainak számával).

prim :: Graph -> Graph

Test>
[((0, 1), 2), ((1, 2), 3), ((2, 3), 1)] :: Graph
Test>
[((0, 1), 1), ((1, 3), 1), ((2, 0), 3)] :: Graph
Test>
[((0, 4), 5), ((1, 2), 1), ((1, 4), 2), ((4, 3), 2)] :: Graph

Megjegyzés: ezek az eredémynek éppen azokat a gráfokat adják, amelyeket úgy kapnuk, hogy a fenti példa gráfokban csak a pirosra színezett éleket hagyjuk meg.

Pontozás