Legrövidebb út meghatározása gráfokban

Az előző bejegyzésben gráfok ábrázolásával és implementációjával foglalkoztunk. Most arra keressük a megoldást, hogy hogyan lehet meghatározni egy gráf egy adott csúcsából a többi csúcsba vezető legkisebb élsúlyösszegű, vagy más szóval legrövidebb utakat. Ez a kérdés számos, gyakorlatban felmerülő, gráffal modellezhető problémában felmerül. Például utazáskor a távolságban vagy időben legrövidebb út megtalálása, vagy számítógéphálózatokban a csomagtovábbítás optimalizálása.

A kérdésre a választ a Dijkstra algoritmussal adjuk meg. Az algoritmus Edsger W. Dijkstra holland informatikusról kapta a nevét, aki 1959-ben publikálta azt.

Az algoritmus elve röviden:

  • Sorra veszünk minden csúcsot, kezdve a megadott kezdőcsúccsal, és megvizsgáljuk az éppen aktuális csúcs még nem vizsgált szomszédait a kiinduló csúcstól vett távolság (össz élsúly) szempontjából. Ha egy szomszéd csúcs eddig ismert legrövidebb úthossza nagyobb, mint az aktuális csúcs eddig ismert legrövidebb úthossza + az aktuális csúcs és a szomszéd között levő út hossza, akkor a szomszéd csúcs eddig ismert legrövidebb úthosszát erre az összegre módosítjuk.
  • Ha ezt az aktuális csúcs minden eddig nem vizsgált szomszédjára elvégeztük, akkor az aktuális csúcsot vizsgáltnak (látogatottnak) minősítjük, és vesszük a következő vizsgálandó csúcsot. Egy olyan új aktuális, vizsgálni kívánt csúcsot választunk a még nem látogatott csúcsok közül, amely ezek közül a legrövidebb útvonallal rendelkezik, és újra elvégezzük az előbbi vizsgálatot és a szomszédos csúcsok legrövidebb távolságadatának szükség szerinti aktualizálását.
  • Mivel kezdetben a csúcsok addig ismert legrövidebb úthosszát nem ismerjük még, ezért ezt kezdőértékként minden csúcsra végtelenre állítjuk. A kiinduló csúcsra viszont e kezdőértéket 0-ra állítjuk, hiszen önmagától nulla távolságra van.
  • Ha már minden csúcsot megvizsgáltunk, azaz nincs már nem látogatottként nyilvántartott csúcs, akkor végeztünk, és ekkor az egyes csúcsok távolságadata lesz a kiinduló csúcstól vett legrövidebb távolság.
  • Ha azt is szeretnénk tudni, hogy a kiinduló csúcstól milyen útvonalon, azaz mely csúcsokon keresztül jutunk el a legrövidebb úton egy adott csúcsig, akkor a fenti lépesekben, amikor egy szomszéd csúcs eddig ismert legrövidebb úthosszát módosítjuk egy kisebb értékre, akkor egyúttal azt is el kell tárolni e csúcshoz, hogy melyik előző csúcstól jutottunk ide e rövidebb úttal. Ha ez minden csúcsra rendelkezésre áll, akkor a végén vissza tudjuk követni a legrövidebb útvonalat.

Ha ez a rövid összefoglaló nem lenne elegendő az algoritmus lényegének megértéséhez, akkor a „Dijkstra algoritmus” keresőkifejezést beírva a böngészőbe az interneten magyarul és angolul is számos részletesebb leírást találunk.

Az alább látható kódban az előző bejegyzésben megalkotott Graph osztályt egészítettük ki egy shortest_paths() nevű metódussal, amely a Dijkstra algoritmus alapján egy szótárban adja vissza a gráf megadott csúcsától minden más csúcshoz vezető legrövidebb úthosszakat és az odavezető utakat, azaz a csúcsok listáját. A megértést a fenti leírás tükrében a részletes kommentek segítik.

Az előző bejegyzésben említettük, hogy a csúcsokat célszerű osztállyal modellezni, mert ilyenkor a csúcs azonosító címkéjén felül egyéb adat is társítható a csúcshoz. Most erre is láthatunk példát, hiszen a legrövidebb úthosszt, és a megelőző csúcsot mint adatpárt a csúcs data attribútuma tárolja.

Az előbb nem említettük, hogy a Dijkstra algoritmus csak akkor működik helyesen, ha az élsúlyok nemnegatív valós számok. (Ha precízebbek akarunk lenni, akkor az élsúlyokra a feltétel, hogy értékkészletük monoton növekvő rendezett legyen, ami nem csak számokra lehet igaz.)

Ezért a metódushíváskor először ellenőrizzük, hogy fennáll-e az élsúlyokra a feltétel. Ahhoz, hogy a shortest_paths() metódus törzsében a logika áttekinthetősége az ellenőrzőkódokkal ne csökkenjen, az élsúlyok megfelelőségét egy dekorátorfüggvénybe szerveztük ki, és ezzel dekoráljuk a shortest_paths() metódust. A dekorátorfüggvény definíciója az alábbi:

A fenti ellenőrzést végző kódsorból látható, hogy nem csak nemnegatív int és float számokat engedünk meg élsúlyként, hanem olyan karakterláncot is, amelyek nemnegatív int vagy float számot reprezentálnak.

Mindezek után a Graph valamint a Vertex osztály teljes definíciója az alábbi lesz:

Tesztelésre egy gyakorlati példát választottunk, amelyben néhány nagyváros mint csúcs és a közöttük levő, közúton mért távolság mint élsúly szerepel kilométerben. Ezt mutatja az ábra:

Az alábbi tesztprogramban egyrészt láthatjuk megjelenítve a szomszédsági viszonyokat, másrészt azt, hogy egy kiválasztott várostól a többi városba milyen hosszú a legrövidebb út, és az milyen városokon keresztül haladva érhető el.

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 „Minden objektum!” fejezet „Objektumok létrehozásának módjai” alfejezet, „Konstruktorok” cím, a „Műveletek” fejezet „Konténerépítés” alfejezet, a „Beépített típusok nyilvános metódusai”, és a „Képességfejlesztés – függvénydekorátorok” fejezet.

É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.