Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
193 changes: 191 additions & 2 deletions paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ func TestUndirectedShortestPath(t *testing.T) {
sourceHash string
targetHash string
isWeighted bool
isDirected bool
expectedShortestPath []string
shouldFail bool
}{
Expand Down Expand Up @@ -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]{
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down