From 75c49510b49879c736af3b8d21cc942e1a028162 Mon Sep 17 00:00:00 2001 From: Kiarash Hajian <133909368+kiarash8112@users.noreply.github.com> Date: Wed, 8 Nov 2023 07:18:29 +0330 Subject: [PATCH] feat: graph cycle detection (#689) * 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 * hide imp detail for pkg out side graph/cycle.go Co-authored-by: Taj * feat: change FindAllcycles return type to graph * test: update findAllcycles tests * fix: fix HasCycle docs dictation --------- Co-authored-by: user Co-authored-by: Taj --- graph/cycle.go | 110 ++++++++++++++++++++++++++++++++++++++++++++ graph/cycle_test.go | 53 +++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 graph/cycle.go create mode 100644 graph/cycle_test.go diff --git a/graph/cycle.go b/graph/cycle.go new file mode 100644 index 000000000..cf45d1854 --- /dev/null +++ b/graph/cycle.go @@ -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 +} diff --git a/graph/cycle_test.go b/graph/cycle_test.go new file mode 100644 index 000000000..0ac7dfdbe --- /dev/null +++ b/graph/cycle_test.go @@ -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") + } +}