Skip to content

Commit

Permalink
feat: graph cycle detection (#689)
Browse files Browse the repository at this point in the history
* feat: add HasCycle algorithm

* feat: add FindAllCycles algorithm

* docs: add comments to findAllCycles and HasCycle algorithm

* test: add test for hasCycle Algorithm

* test: add test for FindAllCycles Algorithm

* hide imp detail for pkg out side graph/cycle.go

Co-authored-by: Taj <[email protected]>

* hide imp detail for pkg out side  graph/cycle.go

Co-authored-by: Taj <[email protected]>

* feat: change FindAllcycles return type to graph

* test: update findAllcycles tests

* fix: fix HasCycle docs dictation

---------

Co-authored-by: user <[email protected]>
Co-authored-by: Taj <[email protected]>
  • Loading branch information
3 people authored Nov 8, 2023
1 parent e33cfa9 commit 75c4951
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 0 deletions.
110 changes: 110 additions & 0 deletions graph/cycle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// cycle.go
// this file handle algorithm that related to cycle in graph
// reference: https://en.wikipedia.org/wiki/Cycle_(graph_theory)
// [kiarash hajian](https://github.com/kiarash8112)

package graph

func (g *Graph) HasCycle() bool {
//this implimetation referred as 3-color too
all := map[int]struct{}{}
visiting := map[int]struct{}{}
visited := map[int]struct{}{}

for v := range g.edges {
all[v] = struct{}{}
}

for current := range all {
if g.hasCycleHelper(current, all, visiting, visited) {
return true
}
}

return false

}

func (g Graph) hasCycleHelper(v int, all, visiting, visited map[int]struct{}) bool {
delete(all, v)
visiting[v] = struct{}{}

neighbors := g.edges[v]
for v := range neighbors {
if _, ok := visited[v]; ok {
continue
} else if _, ok := visiting[v]; ok {
return true
} else if g.hasCycleHelper(v, all, visiting, visited) {
return true
}
}
delete(visiting, v)
visited[v] = struct{}{}
return false
}

// this function can do HasCycle() job but it is slower
func (g *Graph) FindAllCycles() []Graph {
all := map[int]struct{}{}
visiting := map[int]struct{}{}
visited := map[int]struct{}{}

allCycles := []Graph{}

for v := range g.edges {
all[v] = struct{}{}
}

for current := range all {
foundCycle, parents := g.findAllCyclesHelper(current, all, visiting, visited)

if foundCycle {
foundCycleFromCurrent := false
//this loop remove additional vertex from detected cycle
//using foundCycleFromCurrent bool to make sure after removing vertex we still have cycle
for i := len(parents) - 1; i > 0; i-- {
if parents[i][1] == parents[0][0] {
parents = parents[:i+1]
foundCycleFromCurrent = true
}
}
if foundCycleFromCurrent {
graph := Graph{Directed: true}
for _, edges := range parents {
graph.AddEdge(edges[1], edges[0])
}
allCycles = append(allCycles, graph)
}

}

}

return allCycles

}

func (g Graph) findAllCyclesHelper(current int, all, visiting, visited map[int]struct{}) (bool, [][]int) {
parents := [][]int{}

delete(all, current)
visiting[current] = struct{}{}

neighbors := g.edges[current]
for v := range neighbors {
if _, ok := visited[v]; ok {
continue
} else if _, ok := visiting[v]; ok {
parents = append(parents, []int{v, current})
return true, parents
} else if ok, savedParents := g.findAllCyclesHelper(v, all, visiting, visited); ok {
parents = append(parents, savedParents...)
parents = append(parents, []int{v, current})
return true, parents
}
}
delete(visiting, current)
visited[current] = struct{}{}
return false, parents
}
53 changes: 53 additions & 0 deletions graph/cycle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package graph

import (
"testing"
)

func TestHasCycle(t *testing.T) {
graph := Graph{Directed: true}
edges := [][]int{{0, 1}, {1, 2}, {2, 0}, {4, 0}}
for _, edge := range edges {
graph.AddEdge(edge[0], edge[1])
}
if !graph.HasCycle() {
t.Error("answer of hasCycle is not correct")
}

graph = Graph{Directed: true}
edges = [][]int{{0, 1}, {1, 2}, {2, 6}, {4, 0}}
for _, edge := range edges {
graph.AddEdge(edge[0], edge[1])
}
if graph.HasCycle() {
t.Error("answer of hasCycle is not correct")
}
}

func TestFindAllCycles(t *testing.T) {
graph := Graph{Directed: true}
edges := [][]int{{0, 4}, {1, 3}, {2, 3}, {3, 4}, {4, 7}, {5, 2}, {6, 3}, {7, 3}}
for _, edge := range edges {
graph.AddEdge(edge[0], edge[1])
}

res := graph.FindAllCycles()

if len(res) != 1 {
t.Error("number of cycles is not correct")
}

firstCycle := res[0]
if len(firstCycle.edges) != 3 {
t.Error("number of vertex in cycle is not correct")
}
if _, ok := firstCycle.edges[3][4]; !ok {
t.Error("connection in cycle is not correct")
}
if _, ok := firstCycle.edges[4][7]; !ok {
t.Error("connection in cycle is not correct")
}
if _, ok := firstCycle.edges[7][3]; !ok {
t.Error("connection in cycle is not correct")
}
}

0 comments on commit 75c4951

Please sign in to comment.