diff --git a/paths.go b/paths.go index e6f16e3f..472ba373 100644 --- a/paths.go +++ b/paths.go @@ -75,6 +75,11 @@ func CreatesCycle[K comparable, T any](g Graph[K, T], source, target K) (bool, e // from the source vertex, ErrTargetNotReachable will be returned. If there are multiple shortest // paths, an arbitrary one will be returned. func ShortestPath[K comparable, T any](g Graph[K, T], source, target K) ([]K, error) { + + if g.Traits().IsDirected { + return bellmanFord(g, source, target) + } + weights := make(map[K]float64) visited := make(map[K]bool) @@ -142,6 +147,62 @@ func ShortestPath[K comparable, T any](g Graph[K, T], source, target K) ([]K, er return path, nil } +// bellmanFord is a helper function for ShortestPath that uses the Bellman-Ford algorithm to +// compute the shortest path between a source and a target vertex using the edge weights and returns +// the hash values of the vertices forming that path. This search runs in O(|V|*|E|) time. +// +// The returned path includes the source and target vertices. If the target cannot be reached +// from the source vertex, ErrTargetNotReachable will be returned. If there are multiple shortest +func bellmanFord[K comparable, T any](g Graph[K, T], source, target K) ([]K, error) { + + if !g.Traits().IsDirected { + return nil, errors.New("Bellman-Ford algorithm can only be used on directed graphs") + } + + dist := make(map[K]int) + prev := make(map[K]K) + + adjacencyMap, err := g.AdjacencyMap() + if err != nil { + return nil, fmt.Errorf("could not get adjacency map: %w", err) + } + for key := range adjacencyMap { + dist[key] = math.MaxInt32 + } + dist[source] = 0 + + for i := 0; i < len(adjacencyMap)-1; i++ { + for key, edges := range adjacencyMap { + for _, edge := range edges { + if newDist := dist[key] + edge.Properties.Weight; newDist < dist[edge.Target] { + dist[edge.Target] = newDist + prev[edge.Target] = key + } + } + } + } + + for _, edges := range adjacencyMap { + for _, edge := range edges { + if newDist := dist[edge.Source] + edge.Properties.Weight; newDist < dist[edge.Target] { + return nil, errors.New("graph contains a negative-weight cycle") + } + } + } + + path := []K{} + u := target + for u != source { + if _, ok := prev[u]; !ok { + return nil, ErrTargetNotReachable + } + path = append([]K{u}, path...) + u = prev[u] + } + path = append([]K{source}, path...) + return path, nil +} + type sccState[K comparable] struct { adjacencyMap map[K]map[K]Edge[K] components [][]K diff --git a/paths_test.go b/paths_test.go index a7771c36..2ae88873 100644 --- a/paths_test.go +++ b/paths_test.go @@ -304,6 +304,7 @@ func TestUndirectedShortestPath(t *testing.T) { sourceHash string targetHash string isWeighted bool + isDirected bool expectedShortestPath []string shouldFail bool }{ @@ -367,6 +368,22 @@ func TestUndirectedShortestPath(t *testing.T) { targetHash: "B", expectedShortestPath: []string{"B"}, }, + "can process negative weights": { + vertices: []string{"A", "B", "C", "D", "E"}, + edges: []Edge[string]{ + {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 1}}, + {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 2}}, + {Source: "B", Target: "C", Properties: EdgeProperties{Weight: 2}}, + {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 2}}, + {Source: "C", Target: "E", Properties: EdgeProperties{Weight: 2}}, + {Source: "D", Target: "E", Properties: EdgeProperties{Weight: -1}}, + }, + isWeighted: true, + isDirected: true, + sourceHash: "A", + targetHash: "E", + expectedShortestPath: []string{"A", "B", "D", "E"}, + }, "target not reachable in a disconnected graph": { vertices: []string{"A", "B", "C", "D"}, edges: []Edge[string]{ @@ -382,8 +399,12 @@ func TestUndirectedShortestPath(t *testing.T) { } for name, test := range tests { - graph := New(StringHash) - graph.(*undirected[string, string]).traits.IsWeighted = test.isWeighted + var graph Graph[string, string] + if test.isDirected { + graph = New(StringHash, Directed(), Weighted()) + } else { + graph = New(StringHash, Weighted()) + } for _, vertex := range test.vertices { _ = graph.AddVertex(vertex) @@ -413,6 +434,174 @@ func TestUndirectedShortestPath(t *testing.T) { } } +func Test_BellmanFord(t *testing.T) { + tests := map[string]struct { + vertices []string + edges []Edge[string] + sourceHash string + targetHash string + isWeighted bool + IsDirected bool + expectedShortestPath []string + shouldFail bool + }{ + "graph as on img/dijkstra.svg": { + vertices: []string{"A", "B", "C", "D", "E", "F", "G"}, + edges: []Edge[string]{ + + {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 3}}, + {Source: "A", Target: "F", Properties: EdgeProperties{Weight: 2}}, + {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 4}}, + {Source: "C", Target: "E", Properties: EdgeProperties{Weight: 1}}, + {Source: "C", Target: "F", Properties: EdgeProperties{Weight: 2}}, + {Source: "D", Target: "B", Properties: EdgeProperties{Weight: 1}}, + {Source: "E", Target: "B", Properties: EdgeProperties{Weight: 2}}, + {Source: "E", Target: "F", Properties: EdgeProperties{Weight: 3}}, + {Source: "F", Target: "G", Properties: EdgeProperties{Weight: 5}}, + {Source: "G", Target: "B", Properties: EdgeProperties{Weight: 2}}, + }, + isWeighted: true, + IsDirected: true, + sourceHash: "A", + targetHash: "B", + expectedShortestPath: []string{"A", "C", "E", "B"}, + }, + "diamond-shaped graph": { + vertices: []string{"A", "B", "C", "D"}, + edges: []Edge[string]{ + {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, + {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, + {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 2}}, + {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 2}}, + }, + isWeighted: true, + IsDirected: true, + sourceHash: "A", + targetHash: "D", + expectedShortestPath: []string{"A", "B", "D"}, + }, + "unweighted graph": { + vertices: []string{"A", "B", "C", "D", "E", "F", "G"}, + edges: []Edge[string]{ + {Source: "A", Target: "B", Properties: EdgeProperties{}}, + {Source: "A", Target: "C", Properties: EdgeProperties{}}, + {Source: "B", Target: "D", Properties: EdgeProperties{}}, + {Source: "C", Target: "F", Properties: EdgeProperties{}}, + {Source: "D", Target: "G", Properties: EdgeProperties{}}, + {Source: "E", Target: "G", Properties: EdgeProperties{}}, + {Source: "F", Target: "E", Properties: EdgeProperties{}}, + }, + IsDirected: true, + sourceHash: "A", + targetHash: "G", + expectedShortestPath: []string{"A", "B", "D", "G"}, + }, + "source equal to target": { + vertices: []string{"A", "B", "C", "D"}, + edges: []Edge[string]{ + {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, + {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, + {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 2}}, + {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 2}}, + }, + isWeighted: true, + IsDirected: true, + sourceHash: "B", + targetHash: "B", + expectedShortestPath: []string{"B"}, + }, + "target not reachable in a disconnected graph": { + vertices: []string{"A", "B", "C", "D"}, + edges: []Edge[string]{ + {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, + {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, + }, + isWeighted: true, + IsDirected: true, + sourceHash: "A", + targetHash: "D", + expectedShortestPath: []string{}, + shouldFail: true, + }, + "negative weights graph": { + vertices: []string{"A", "B", "C", "D", "E"}, + edges: []Edge[string]{ + {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 1}}, + {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 2}}, + {Source: "B", Target: "C", Properties: EdgeProperties{Weight: 2}}, + {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 2}}, + {Source: "C", Target: "E", Properties: EdgeProperties{Weight: 2}}, + {Source: "D", Target: "E", Properties: EdgeProperties{Weight: -1}}, + }, + isWeighted: true, + IsDirected: true, + sourceHash: "A", + targetHash: "E", + expectedShortestPath: []string{"A", "B", "D", "E"}, + }, + "fails on negative cycles": { + vertices: []string{"A", "B", "C", "D", "E"}, + edges: []Edge[string]{ + {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 1}}, + {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, + {Source: "B", Target: "C", Properties: EdgeProperties{Weight: 2}}, + {Source: "B", Target: "D", Properties: EdgeProperties{Weight: 6}}, + {Source: "C", Target: "D", Properties: EdgeProperties{Weight: 3}}, + {Source: "C", Target: "E", Properties: EdgeProperties{Weight: 2}}, + {Source: "D", Target: "E", Properties: EdgeProperties{Weight: -3}}, + {Source: "E", Target: "C", Properties: EdgeProperties{Weight: -3}}, + }, + isWeighted: true, + IsDirected: true, + sourceHash: "A", + targetHash: "E", + expectedShortestPath: []string{}, + shouldFail: true, + }, + "fails if not directed": { + vertices: []string{"A", "B", "C", "D"}, + edges: []Edge[string]{ + {Source: "A", Target: "B", Properties: EdgeProperties{Weight: 2}}, + {Source: "A", Target: "C", Properties: EdgeProperties{Weight: 4}}, + }, + isWeighted: true, + sourceHash: "A", + targetHash: "D", + expectedShortestPath: []string{}, + shouldFail: true, + }, + } + for name, test := range tests { + graph := New(StringHash, Directed(), Weighted()) + + for _, vertex := range test.vertices { + _ = graph.AddVertex(vertex) + } + + for _, edge := range test.edges { + if err := graph.AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { + t.Fatalf("%s: failed to add edge: %s", name, err.Error()) + } + } + + shortestPath, err := bellmanFord(graph, test.sourceHash, test.targetHash) + + if test.shouldFail != (err != nil) { + t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, (err != nil), err) + } + + if len(shortestPath) != len(test.expectedShortestPath) { + t.Fatalf("%s: path length expectancy doesn't match: expected %v, got %v", name, len(test.expectedShortestPath), len(shortestPath)) + } + + for i, expectedVertex := range test.expectedShortestPath { + if shortestPath[i] != expectedVertex { + t.Errorf("%s: path vertex expectancy doesn't match: expected %v at index %d, got %v", name, expectedVertex, i, shortestPath[i]) + } + } + } +} + func TestDirectedStronglyConnectedComponents(t *testing.T) { tests := map[string]struct { vertices []int