Gráfok mélységi bejárása

Az előző bejegyzésben gráfok szélességi bejárásával foglalkoztunk. Most gráfok mélységi bejárását (depth first traversal) fogjuk megvalósítani.

A mélységi bejárás alapelve: úgy járjuk be a gráfot, hogy egy kiinduló csúcstól kezdve először is az éppen aktuális csúcs szomszédait határozzuk meg, majd ezek közül a még meg nem látogatottakat vesszük nyilvántartásba egy LIFO konténerben, vagyis egy veremben. Utána e verem tetejéről vesszük a csúcsot (ha van) és innen haladunk tovább, vagyis ennek ismét megállapítjuk a szomszédait, azokból a még nemlátogatottakat betesszük a verembe. Mindezt addig folytatjuk, amíg van nem felderített (nem meglátogatott) csúcs.

Az eljárásból következik, hogy kétszer ugyanazt a csúcsot nem derítjük fel (látogatjuk meg), azaz nem fogjuk újra felkutatni a szomszédait, hiszen egyszer már megtettük.

Az alapelv ismeretében a mélységi bejárás algoritmusa a következő:

  1. Kiválasztjuk a gráf kezdőcsúcsát, ahonnan be akarjuk járni a gráfot.
  2. Egy verem tárolóban nyilvántartást vezetünk a még nem vizsgált (nem meglátogatott) csúcsokról, amelynek kezdetben egyetlen eleme a kiinduló csúcs.
  3. Egy listában nyilvántartást vezetünk a már vizsgált (meglátogatott) csúcsokról, amely kezdetben nem tartalmaz csúcsot.
  4. Amennyiben van vizsgálatra váró csúcs a még nem vizsgált csúcsokat tartalmazó veremben, akkor
    • 4.1. Kivesszük a tetejéről a soron következő csúcsot és ez lesz az éppen aktuálisan vizsgált és feldolgozott csúcs.
    • 4.2. Ha ez az aktuálisan feldolgozott csúcs még nem szerepel meglátogatottként, akkor
      • 4.2.1. Ezt az aktuálisan vizsgált csúcsot hozzáadjuk a már vizsgált csúcsok listájához.
      • 4.2.2. Összegyűjtjük az aktuálisan vizsgált csúcs szomszédait.
      • 4.2.3. Kiválogatjuk a szomszédok közül a még nem vizsgáltakat.
      • 4.2.4. Ezeket betesszük a vizsgálatra váró csúcsokat nyilvántartó verembe.
      • 4.2.5. Folytatjuk az eljárást a 4. ponttól
  5. Az eljárás eredménye a meglátogatott csúcsok listája, amely a bejárási sorrendet is mutatja.

Az eljárás eredményeképpen nem feltétlenül szükséges az 5. pontban előálló sorozatot egy listában megkapni. Helyette megvalósíthajuk az algoritmust iterátorként, ahol minden egyes meglátogatott csúcsot egy-egy kérésre kiadunk. Ha iterátorral valósítjuk meg, akkor a már meglátogatott és feldolgozott csúcsokat lista helyett halmazban is nyilvántarthatjuk, hiszen ilyenkor nem számít a sorrend.

Ha összehasonlítjuk a fenti leírást az előző bejegyzésben szereplő szélességi bejárás leírásával és algoritmikus lépéseivel, akkor látható, hogy lényegében ugyanazokat a lépéseket kell megtenni, azzal a lényeges eltéréssel, hogy a még nem vizsgált csúcsok tárolója mélységi bejárásnál egy LIFO konténer, szemben a szélességi bejárással, ahol ez FIFO működésű.

Ez a hasonlóság a megvalósításban is tükröződik, amit alább láthatunk a depth_first_iterator nevű, generátorfüggvényt megvalósító metódusban. A működést a részletes kommentek írják le összhangban az előbbi eljárási lépésekkel. /A Graph osztály egyéb metódusait, amelyeket az előző bejegyzésekben dolgozunk ki itt most nem jelenítettük meg. Hasonlóan, a Vertex osztály definíciója sem jelenik meg itt. /

Az előző bejegyzésben a szélességi bejárás kapcsán a példányt hívhatóvá tettük a __call__ speciális metódus definiálásával, ami meghíváskor egy iterátort eredményez, amely a gráf teljes bejárását végzi. Bejárási algoritmusnak akkor csak a szélességi állt rendelkezésre. Most, hogy már van mélységi bejárást végző metódus is, a __call__ metódust továbbfejleszthetjük úgy, hogy annak második argumentumaként választhatunk a szélességi és mélységi bejárási algoritmus között. Az így kiegészített __call__ metódus definícióját szintén a fenti kódban látjuk.

A mélységi bejárást gyakran rekurzív módon valósítják meg. Tegyük mi is ezt meg. A következő kódsorokban három metódusimplementációt is látunk. Ezek már nem csak a teljes bejárást végzik, hanem megadható, hogy milyen kezdőcsúcstól mely végcsúcsig történjen a bejárás. Az 1. és 2. változat között az eltérés, hogy while-ciklus helyett for-ciklus működik. A 3. változat pedig abban tér el a 2. változattól, hogy az eddig paraméterek alapértelmezett értékeként szereplő változtatható konténereket „elrejtettük” a metódustörzsben. Ezt úgy tudtuk megtenni, hogy a rekurzívan hívott függvényt beágyaztuk a metódustörzsben.

Mindezek után, a teszteléshez fogaljuk össze a Vertex és Graph osztályok teljes definícióját, amik így néznek ki:

A mélységi bejárás működését egy labirintus bejárásának, illetve útkeresésének példáján keretében teszteljük. Az ábrán egy négyszög alakú labirintus (maze) látható, amelynek egymást követő mezőin, cellái lehet haladni, amennyiben két mezőt nem választ el egy fal. Ha a mezőket azonosítókkal látjuk el, pl. számokkal, ahogy az ábrán, akkor a labirintus modellezhető egy gráffal. A gráf csúcsai az egyes mezők. Két csúcs között pedig akkor van él, ha a megfelelő, oldalukkal szomszédos mezők között lehetéséges haladás. Az így kialakított gráfot is mutatja az ábra.

Alább láthatók a tesztsorok. Itt a labirintus mint gráfpéldány létrehozása után egy listában definiáljuk a mezők közötti egymást követő járható kapcsolatokat. Ezek alapján létrehozzuk a gráf éleit.

Első tesztként meghívjuk a labirintus példányt, megadva egy kezdőcsúcs azonosítót és a második argumentumban jelezzük, hogy mélységi bejárást szeretnénk. A bejárás eredményét a csúcsok sorszámának egymást követő sorozatával megjelenítjük.

Második tesztként a rekurzív megvalósítást ellenőrizzük. Itt meghívjuk a depth_first_recursive metódust megadva egy tetszőleges kiinduló mezőt (csúcsot) és egy célmezőt. Utána a kezdőmezőtől a célmezőig vezető utat kiírjuk. Mivel a gráf megvalósításában a szomszédos csúcsok egy halmazban vannak, ami nem sorrendtartó, így az útkeresést többször futtatva eltérő utakat kaphatunk. Ezt mutatják az eredményben a vagylagos sorok.

E bejegyzés témájához a Python tudásépítés lépésről lépésre című e-könyv következő részei kapcsolódnak különösen: a „ „Kérem a következőt!” – iterálható objektumok” fejezet, a „Iterátorok és elemeik kinyerésének nyelvi megvalósítása” fejezet, a „Kifogyhatatlan sorozatlövők – generátorfüggvények” fejezet, a „Műveletek” fejezet „Konténerépítés” alfejezete, a „Beépített típusok nyilvános metódusai”, valamint a „Mágikus metódusok egyedi igényre szabott osztályokban” fejezet „Osztálypéldány hívhatóvá tétele” alfejezete, a „Különleges függvénydefiníciók” fejezetben az „Amikor a kígyó a farkába harap – függvényrekurzió” és „Álom az álomban – egymásba ágyazott függvények” alfejezetek.

Érdekel a Python tudásépítés lépésről lépésre az alapoktól az első asztali alkalmazásig című e-könyv.