From 3908bcf283ef308ff0824b0f6a1fa00dca19af03 Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 18 Nov 2023 01:44:20 -0500 Subject: [PATCH] Run TopologicalSort in O(V+E) instead of O(V^2) (#144) --- dag.go | 65 ++++++++-------- dag_test.go | 209 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 216 insertions(+), 58 deletions(-) diff --git a/dag.go b/dag.go index 87b7847c..b5ca4978 100644 --- a/dag.go +++ b/dag.go @@ -27,6 +27,11 @@ func TopologicalSort[K comparable, T any](g Graph[K, T]) ([]K, error) { return nil, fmt.Errorf("failed to get graph order: %w", err) } + adjacencyMap, err := g.AdjacencyMap() + if err != nil { + return nil, fmt.Errorf("failed to get adjacency map: %w", err) + } + predecessorMap, err := g.PredecessorMap() if err != nil { return nil, fmt.Errorf("failed to get predecessor map: %w", err) @@ -37,28 +42,28 @@ func TopologicalSort[K comparable, T any](g Graph[K, T]) ([]K, error) { for vertex, predecessors := range predecessorMap { if len(predecessors) == 0 { queue = append(queue, vertex) + delete(predecessorMap, vertex) } } order := make([]K, 0, gOrder) - visited := make(map[K]struct{}, gOrder) for len(queue) > 0 { currentVertex := queue[0] queue = queue[1:] - if _, ok := visited[currentVertex]; ok { - continue - } - order = append(order, currentVertex) - visited[currentVertex] = struct{}{} - for vertex, predecessors := range predecessorMap { + edgeMap := adjacencyMap[currentVertex] + + for target := range edgeMap { + + predecessors := predecessorMap[target] delete(predecessors, currentVertex) if len(predecessors) == 0 { - queue = append(queue, vertex) + queue = append(queue, target) + delete(predecessorMap, target) } } } @@ -78,23 +83,31 @@ func StableTopologicalSort[K comparable, T any](g Graph[K, T], less func(K, K) b return nil, fmt.Errorf("topological sort cannot be computed on undirected graph") } + gOrder, err := g.Order() + if err != nil { + return nil, fmt.Errorf("failed to get graph order: %w", err) + } + + adjacencyMap, err := g.AdjacencyMap() + if err != nil { + return nil, fmt.Errorf("failed to get adjacency map: %w", err) + } + predecessorMap, err := g.PredecessorMap() if err != nil { return nil, fmt.Errorf("failed to get predecessor map: %w", err) } queue := make([]K, 0) - queued := make(map[K]struct{}) for vertex, predecessors := range predecessorMap { if len(predecessors) == 0 { queue = append(queue, vertex) - queued[vertex] = struct{}{} + delete(predecessorMap, vertex) } } - order := make([]K, 0, len(predecessorMap)) - visited := make(map[K]struct{}) + order := make([]K, 0, gOrder) sort.Slice(queue, func(i, j int) bool { return less(queue[i], queue[j]) @@ -104,28 +117,21 @@ func StableTopologicalSort[K comparable, T any](g Graph[K, T], less func(K, K) b currentVertex := queue[0] queue = queue[1:] - if _, ok := visited[currentVertex]; ok { - continue - } - order = append(order, currentVertex) - visited[currentVertex] = struct{}{} frontier := make([]K, 0) - for vertex, predecessors := range predecessorMap { - delete(predecessors, currentVertex) + edgeMap := adjacencyMap[currentVertex] - if len(predecessors) != 0 { - continue - } + for target := range edgeMap { - if _, ok := queued[vertex]; ok { - continue - } + predecessors := predecessorMap[target] + delete(predecessors, currentVertex) - frontier = append(frontier, vertex) - queued[vertex] = struct{}{} + if len(predecessors) == 0 { + frontier = append(frontier, target) + delete(predecessorMap, target) + } } sort.Slice(frontier, func(i, j int) bool { @@ -135,11 +141,6 @@ func StableTopologicalSort[K comparable, T any](g Graph[K, T], less func(K, K) b queue = append(queue, frontier...) } - gOrder, err := g.Order() - if err != nil { - return nil, fmt.Errorf("failed to get graph order: %w", err) - } - if len(order) != gOrder { return nil, errors.New("topological sort cannot be computed on graph with cycles") } diff --git a/dag_test.go b/dag_test.go index cc21483c..04a448f7 100644 --- a/dag_test.go +++ b/dag_test.go @@ -2,7 +2,9 @@ package graph import ( "fmt" + "math/rand" "testing" + "time" ) func TestDirectedTopologicalSort(t *testing.T) { @@ -25,6 +27,17 @@ func TestDirectedTopologicalSort(t *testing.T) { }, expectedOrder: []int{1, 2, 3, 4, 5}, }, + "graph with many possible topological orders": { + vertices: []int{1, 2, 3, 4, 5, 6, 10, 20, 30, 40, 50, 60}, + edges: []Edge[int]{ + {Source: 1, Target: 10}, + {Source: 2, Target: 20}, + {Source: 3, Target: 30}, + {Source: 4, Target: 40}, + {Source: 5, Target: 50}, + {Source: 6, Target: 60}, + }, + }, "graph with cycle": { vertices: []int{1, 2, 3}, edges: []Edge[int]{ @@ -39,14 +52,9 @@ func TestDirectedTopologicalSort(t *testing.T) { for name, test := range tests { graph := New(IntHash, Directed()) - 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()) - } + err := buildGraph(&graph, test.vertices, test.edges) + if err != nil { + t.Fatalf("%s: failed to construct graph: %s", name, err.Error()) } order, err := TopologicalSort(graph) @@ -59,8 +67,17 @@ func TestDirectedTopologicalSort(t *testing.T) { continue } - if len(order) != len(test.expectedOrder) { - t.Errorf("%s: order length expectancy doesn't match: expected %v, got %v", name, len(test.expectedOrder), len(order)) + if len(order) != len(test.vertices) { + t.Errorf("%s: order length expectancy doesn't match: expected %v, got %v", name, len(test.vertices), len(order)) + } + + if len(test.expectedOrder) <= 0 { + + fmt.Println("topological sort", order) + + if err := verifyTopologicalSort(graph, order); err != nil { + t.Errorf("%s: invalid topological sort - %v", name, err) + } } for i, expectedVertex := range test.expectedOrder { @@ -143,14 +160,9 @@ func TestDirectedStableTopologicalSort(t *testing.T) { for name, test := range tests { graph := New(IntHash, Directed()) - 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()) - } + err := buildGraph(&graph, test.vertices, test.edges) + if err != nil { + t.Fatalf("%s: failed to construct graph: %s", name, err.Error()) } order, err := StableTopologicalSort(graph, func(a, b int) bool { @@ -246,14 +258,9 @@ func TestDirectedTransitiveReduction(t *testing.T) { for name, test := range tests { graph := New(StringHash, Directed()) - 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()) - } + err := buildGraph(&graph, test.vertices, test.edges) + if err != nil { + t.Fatalf("%s: failed to construct graph: %s", name, err.Error()) } reduction, err := TransitiveReduction(graph) @@ -303,6 +310,103 @@ func TestUndirectedTransitiveReduction(t *testing.T) { } } +func TestVerifyTopologicalSort(t *testing.T) { + tests := map[string]struct { + vertices []int + edges []Edge[int] + invalidOrder []int + }{ + "graph with 2 vertices": { + vertices: []int{1, 2}, + edges: []Edge[int]{ + {Source: 1, Target: 2}, + }, + }, + "graph with 2 vertices - reversed": { + vertices: []int{1, 2}, + edges: []Edge[int]{ + {Source: 2, Target: 1}, + }, + }, + "graph with 2 vertices - invalid": { + vertices: []int{1, 2}, + edges: []Edge[int]{ + {Source: 1, Target: 2}, + }, + invalidOrder: []int{2, 1}, + }, + "graph with 3 vertices": { + vertices: []int{1, 2, 3}, + edges: []Edge[int]{ + {Source: 1, Target: 2}, + {Source: 1, Target: 3}, + {Source: 2, Target: 3}, + }, + }, + "graph with 3 vertices - invalid": { + vertices: []int{1, 2, 3}, + edges: []Edge[int]{ + {Source: 1, Target: 2}, + {Source: 1, Target: 3}, + {Source: 2, Target: 3}, + }, + invalidOrder: []int{1, 3, 2}, + }, + "graph with 5 vertices": { + vertices: []int{1, 2, 3, 4, 5}, + edges: []Edge[int]{ + {Source: 1, Target: 2}, + {Source: 1, Target: 3}, + {Source: 2, Target: 3}, + {Source: 2, Target: 4}, + {Source: 2, Target: 5}, + {Source: 3, Target: 4}, + {Source: 4, Target: 5}, + }, + }, + "graph with many possible topological orders": { + vertices: []int{1, 2, 3, 4, 5, 6, 10, 20, 30, 40, 50, 60}, + edges: []Edge[int]{ + {Source: 1, Target: 10}, + {Source: 2, Target: 20}, + {Source: 3, Target: 30}, + {Source: 4, Target: 40}, + {Source: 5, Target: 50}, + {Source: 6, Target: 60}, + }, + invalidOrder: []int{2, 3, 4, 5, 6, 10, 1, 20, 30, 40, 50, 60}, + }, + } + + for name, test := range tests { + graph := New[int, int](IntHash, Directed()) + + err := buildGraph(&graph, test.vertices, test.edges) + if err != nil { + t.Fatalf("%s: failed to construct graph: %s", name, err.Error()) + } + + var order[] int + + if len(test.invalidOrder) > 0 { + order = test.invalidOrder + } else { + order, err = TopologicalSort(graph) + if err != nil { + t.Fatalf("%s: error failed to produce topological sort: %v)", name, err) + } + } + + err = verifyTopologicalSort(graph, order) + + shouldFail := len(test.invalidOrder) > 0 + + if shouldFail != (err != nil) { + t.Errorf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, shouldFail, err != nil, err) + } + } +} + func slicesAreEqualWithFunc[T any](a, b []T, equals func(a, b T) bool) bool { if len(a) != len(b) { return false @@ -322,3 +426,56 @@ func slicesAreEqualWithFunc[T any](a, b []T, equals func(a, b T) bool) bool { return true } + +// Please note that this call is destructive. Make a clone of your graph before calling if you +// wish to preserve the graph. +func verifyTopologicalSort[K comparable, T any](graph Graph[K, T], order []K) error { + + adjacencyMap, err := graph.AdjacencyMap() + if err != nil { + return fmt.Errorf("failed to get adjacency map: %v", err) + } + + for i := range order { + + for _, edge := range adjacencyMap[order[i]] { + err = graph.RemoveEdge(edge.Source, edge.Target) + if err != nil { + return fmt.Errorf("failed to remove edge: %v -> %v : %v", edge.Source, edge.Target, err) + } + } + + err = graph.RemoveVertex(order[i]) + if err != nil { + return fmt.Errorf("failed to remove vertex: %v at index %d: %v", order[i], i, err) + } + } + + return nil +} + +// randomizes the ordering of the edges and vertices to help ferret out any potential bugs +// related to ordering +func buildGraph[K comparable, T any](g *Graph[K, T], vertices []T, edges []Edge[K]) error { + + if g == nil { + return fmt.Errorf("graph must be initialized") + } + + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(vertices), func(i, j int) { vertices[i], vertices[j] = vertices[j], vertices[i] }) + + for _, vertex := range vertices { + _ = (*g).AddVertex(vertex) + } + + rand.Shuffle(len(edges), func(i, j int) { edges[i], edges[j] = edges[j], edges[i] }) + + for _, edge := range edges { + if err := (*g).AddEdge(edge.Source, edge.Target, EdgeWeight(edge.Properties.Weight)); err != nil { + return err + } + } + + return nil +}