Gegeven is een graaf \((V,E)\) met toppen \(V\) en gerichte, gewogen bogen \(E\). We zoeken hierin het kortste pad van een gegeven starttop naar een gegeven bestemming.

Het algoritme van Dijkstra gaat er van uit dat alle gewichten positief zijn. Indien er ook negatieve gewichten voorkomen, garandeert de gretige aanpak van Dijkstra immers niet dat het correcte kortste pad gevonden wordt. Het algoritme van Bellman en Ford is een variant van Dijkstra die wel met negatieve gewichten omkan. De prijs die daarvoor betaald wordt, is een verhoogde tijdscomplexiteit.

In een graaf met sommige negatieve gewichten kan er eventueel een negatieve cykel voorkomen, d.w.z. een cykel waarvan de som van de gewichten van de bevatte bogen negatief is. Indien zo’n cykel bereikbaar is vanuit de starttop, is het kortste pad ongedefinieerd. We kunnen immers het totale gewicht van elk pad dat deze cykel bevat arbitrair klein maken door herhaaldelijk de cykel te doorlopen. Het algoritme van Bellman en Ford kan dergelijke negatieve cykels ook detecteren en geeft dan een gepaste foutmelding.

Net zoals bij Dijkstra, wordt de afstand van de starttop tot elke andere top benaderd en iteratief bijgestuurd tot ze convergeert naar de effectieve kortste afstand. Het bijsturen gebeurt hier bovendien ook via hetzelfde mechanisme: we nemen een boog \((u,v)\) en kijken of we een kortere weg naar \(v\) gevonden hebben door via \(u\) te gaan. Bij Dijkstra worden stap voor stap alle uitgaande bogen verwerkt van de tot dan toe nog niet bezochte top met minimale afstand (gretig). Elke boog wordt dus slechts eenmaal bekeken. Dit is echter niet voldoende wanneer er negatieve gewichten voorkomen.

Bij Bellman-Ford worden de updates \(\lvert V \lvert -1\) keer toegepast voor alle bogen (in arbitraire volgorde). Na de \(i\)-de update, hebben we zo reeds alle kortste paden gevonden bestaande uit hoogstens \(i\) bogen. Aangezien elk mogelijk pad zonder cykel hoogstens \(\lvert V \lvert -1\) bogen bevat, moeten we dus precies zoveel keer alles updaten om zeker te zijn dat het kortste pad gevonden is. Vervolgens updaten we nog een laatste keer en als dit resulteert in enige wijziging, hebben we een negatieve cykel gevonden (denk erover na waarom dit zo is).

De pseudocode ziet er als volgt uit:

function BellmanFord(graph, source, destination)
   
   distance = []
   predecessor = []

   // stap 1: initialisatie
   for each vertex v in graph.vertices:
      predecessor[v] = null
      if v is source:
         distance[v] = 0
      else:
         distance[v] = inf

   // stap 2: herhaaldelijk updaten
   for i from 1 to size(graph.vertices)-1:
      for each edge (u,v) in graph.edges:
         w = edge.weight
         if distance[u] + w < distance[v]:
            distance[v] = distance[u] + w
            predecessor[v] = u

   // stap 3: check negatieve cykel
   for each edge (u,v) in graph.edges:
      w = edge.weight
      if distance[u] + w < distance[v]:
         error "Graaf bevat een negatieve cykel"
           
   // stap 4: reconstrueer korste pad
   vertex = destination
   path = [destination]
   while vertex != source:
      vertex = predecessor[vertex]
      if vertex == null:
         error "Bestemming onbereikbaar uit starttop"
      path = [vertex, path]
   		
   return path

Implementeer dit algoritme in een klasse BellmanFordKortstePaden die de gegeven interface KortstePaden1 implementeert door de methode public List<Node> kortstePad(DirectedGraph graaf, Node start, Node bestemming) te schrijven. Deze methode neemt een graaf, de starttop en de eindtop als input. Als output geeft het algoritme een lijst terug waarin de bezochte toppen langs het kortste pad, in de juiste volgorde, worden opgeslagen. Indien er tussen start en bestemming geen pad bestaat, wordt er een UnreachableDestinationException opgegooid. Indien er een negatieve cykel aanwezig is in de graaf, wordt er een NegativeCycleException opgegooid. Beide excepties zijn gedefinieerd in de interface KortstePaden2.

We maken bij deze oefening gebruik van een eenvoudige grafenbibliotheek graphlib. Deze bevat onder meer de klassen DirectedGraph en Node. De API van deze bibliotheek vind je hier3. Een andere interessante klasse is DirectedEdge, die ook van pas kan komen tijdens het oplossen van de oefening. Neem eerst en vooral eens de API door en stel eventueel vragen hierover voor je begint te programmeren.

Opmerkingen:

Extra: het is mogelijk om de berekeningen soms vroegtijdig te stoppen. Denk er over na wanneer dit kan en voer de nodige wijzigingen door in het algoritme. Wat is de asymptotische tijdscomplexiteit van het originele en gewijzigde algoritme?

Gebruik eventueel de testklasse SimpleTest6 om je oplossing lokaal te testen. Je kan hierin eenvoudig extra testgevallen toevoegen.