Skip to content

Commit

Permalink
Run TopologicalSort in O(V+E) instead of O(V^2) (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonbrandenburg authored Nov 18, 2023
1 parent 8757b27 commit 3908bcf
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 58 deletions.
65 changes: 33 additions & 32 deletions dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
}
}
Expand All @@ -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])
Expand All @@ -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 {
Expand All @@ -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")
}
Expand Down
209 changes: 183 additions & 26 deletions dag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package graph

import (
"fmt"
"math/rand"
"testing"
"time"
)

func TestDirectedTopologicalSort(t *testing.T) {
Expand All @@ -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]{
Expand All @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
}

0 comments on commit 3908bcf

Please sign in to comment.