From cfd8054c3852997ff82d48ee5e231e995fe79c83 Mon Sep 17 00:00:00 2001 From: norangebit Date: Sun, 19 May 2019 18:21:36 +0200 Subject: [PATCH] add operations on graph - fix DirectedCycle - add DepthFirstOrder - add SCC - add TopologicalOrder --- .../graph/operations/DepthFirstOrder.kt | 71 +++++++++ .../graph/operations/DirectedCycle.kt | 18 +-- .../operations/StronglyConnectedComponents.kt | 65 ++++++++ .../graph/operations/TopologicalOrder.kt | 41 +++++ .../graph/operations/DepthFirstOrderTest.kt | 64 ++++++++ .../StronglyConnectedComponentsTest.kt | 141 ++++++++++++++++++ .../graph/operations/TopologicalOrderTest.kt | 101 +++++++++++++ 7 files changed, 492 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/it/norangeb/algorithms/graph/operations/DepthFirstOrder.kt create mode 100644 src/main/kotlin/it/norangeb/algorithms/graph/operations/StronglyConnectedComponents.kt create mode 100644 src/main/kotlin/it/norangeb/algorithms/graph/operations/TopologicalOrder.kt create mode 100644 src/test/kotlin/it/norangeb/algorithms/graph/operations/DepthFirstOrderTest.kt create mode 100644 src/test/kotlin/it/norangeb/algorithms/graph/operations/StronglyConnectedComponentsTest.kt create mode 100644 src/test/kotlin/it/norangeb/algorithms/graph/operations/TopologicalOrderTest.kt diff --git a/src/main/kotlin/it/norangeb/algorithms/graph/operations/DepthFirstOrder.kt b/src/main/kotlin/it/norangeb/algorithms/graph/operations/DepthFirstOrder.kt new file mode 100644 index 0000000..788501a --- /dev/null +++ b/src/main/kotlin/it/norangeb/algorithms/graph/operations/DepthFirstOrder.kt @@ -0,0 +1,71 @@ +/* + * MIT License + * + * Copyright (c) 2019 norangebit + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package it.norangeb.algorithms.graph.operations + +import it.norangeb.algorithms.graph.UndirectedGraph +import java.util.ArrayDeque + +class DepthFirstOrder(graph: UndirectedGraph) { + private val visited = Array(graph.vertexNumber()) { false } + + private val preOrder: MutableCollection = ArrayDeque() + private val postOrder: MutableCollection = ArrayDeque() + + init { + (0 until graph.vertexNumber()) + .forEach { + if (!visited[it]) + dfs(graph, it) + } + } + + private fun dfs(graph: UndirectedGraph, vertex: Int) { + preOrder.add(vertex) + + visited[vertex] = true + + graph.adjacentVertex(vertex) + .forEach { + if (!visited[it]) + dfs(graph, it) + } + + postOrder.add(vertex) + } + + fun preOrder(): Iterable { + return preOrder + } + + fun postOrder(): Iterable { + return postOrder + } + + fun reversePostOrder(): Iterable { + return postOrder.reversed() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/it/norangeb/algorithms/graph/operations/DirectedCycle.kt b/src/main/kotlin/it/norangeb/algorithms/graph/operations/DirectedCycle.kt index 800af6f..7930a95 100644 --- a/src/main/kotlin/it/norangeb/algorithms/graph/operations/DirectedCycle.kt +++ b/src/main/kotlin/it/norangeb/algorithms/graph/operations/DirectedCycle.kt @@ -36,7 +36,7 @@ class DirectedCycle(graph: DirectedGraph) { init { (0 until graph.vertexNumber()) .forEach { - if (!graphInfo[it].visited) + if (!graphInfo[it].isVisited) dfs(graph, it) } } @@ -44,19 +44,19 @@ class DirectedCycle(graph: DirectedGraph) { fun hasCycle(): Boolean = cycle != null private fun dfs(graph: UndirectedGraph, vertex: Int) { - graphInfo[vertex].visited = true - graphInfo[vertex].onStack = true + graphInfo[vertex].isVisited = true + graphInfo[vertex].isOnStack = true graph.adjacentVertex(vertex) .forEach { when { hasCycle() -> return@forEach - !graphInfo[it].visited -> exploreChild(graph, vertex, it) - graphInfo[it].onStack -> cycle = makeCycle(vertex, it) + !graphInfo[it].isVisited -> exploreChild(graph, vertex, it) + graphInfo[it].isOnStack -> cycle = makeCycle(vertex, it) } } - graphInfo[vertex].onStack = false + graphInfo[vertex].isOnStack = false } private fun makeCycle(start: Int, end: Int): MutableCollection { @@ -80,9 +80,9 @@ class DirectedCycle(graph: DirectedGraph) { dfs(graph, child) } - data class VertexInfo( - var visited: Boolean = false, - var onStack: Boolean = false, + private data class VertexInfo( + var isVisited: Boolean = false, + var isOnStack: Boolean = false, var previously: Int = -1 ) } \ No newline at end of file diff --git a/src/main/kotlin/it/norangeb/algorithms/graph/operations/StronglyConnectedComponents.kt b/src/main/kotlin/it/norangeb/algorithms/graph/operations/StronglyConnectedComponents.kt new file mode 100644 index 0000000..d60219c --- /dev/null +++ b/src/main/kotlin/it/norangeb/algorithms/graph/operations/StronglyConnectedComponents.kt @@ -0,0 +1,65 @@ +/* + * MIT License + * + * Copyright (c) 2019 norangebit + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package it.norangeb.algorithms.graph.operations + +import it.norangeb.algorithms.graph.DirectedGraph +import it.norangeb.algorithms.graph.UndirectedGraph + +class StronglyConnectedComponents(graph: DirectedGraph) { + private val graphInfo = Array(graph.vertexNumber()) { VertexInfo() } + private var label = 0 + + init { + DepthFirstOrder(graph.reverse()) + .reversePostOrder() + .forEach { + if (!graphInfo[it].isVisited) + dfs(graph, it, label++) + } + } + + private fun dfs(graph: UndirectedGraph, vertex: Int, label: Int) { + graphInfo[vertex] = VertexInfo(true, label) + + graph.adjacentVertex(vertex) + .forEach { + if (!graphInfo[it].isVisited) + dfs(graph, it, label) + } + } + + fun count() = label + + fun component(vertex: Int) = graphInfo[vertex].label + + fun stronglyConnected(firstVertex: Int, secondVertex: Int) = + component(firstVertex) == component(secondVertex) + + private data class VertexInfo( + var isVisited: Boolean = false, + var label: Int = -1 + ) +} \ No newline at end of file diff --git a/src/main/kotlin/it/norangeb/algorithms/graph/operations/TopologicalOrder.kt b/src/main/kotlin/it/norangeb/algorithms/graph/operations/TopologicalOrder.kt new file mode 100644 index 0000000..04b7754 --- /dev/null +++ b/src/main/kotlin/it/norangeb/algorithms/graph/operations/TopologicalOrder.kt @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2019 norangebit + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package it.norangeb.algorithms.graph.operations + +import it.norangeb.algorithms.graph.DirectedGraph + +class TopologicalOrder(graph: DirectedGraph) { + var order: Iterable? = null + + init { + val cycle = DirectedCycle(graph) + + if (!cycle.hasCycle()) + order = DepthFirstOrder(graph).reversePostOrder() + } + + fun isDag() = order != null +} \ No newline at end of file diff --git a/src/test/kotlin/it/norangeb/algorithms/graph/operations/DepthFirstOrderTest.kt b/src/test/kotlin/it/norangeb/algorithms/graph/operations/DepthFirstOrderTest.kt new file mode 100644 index 0000000..67cecf9 --- /dev/null +++ b/src/test/kotlin/it/norangeb/algorithms/graph/operations/DepthFirstOrderTest.kt @@ -0,0 +1,64 @@ +/* + * MIT License + * + * Copyright (c) 2019 norangebit + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package it.norangeb.algorithms.graph.operations + +import it.norangeb.algorithms.graph.DiGraph +import org.amshove.kluent.`should equal` +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.gherkin.Feature + +object DepthFirstOrderTest : Spek({ + Feature("order of graph traverse") { + Scenario("traverse linear graph") { + val graph by memoized { DiGraph() } + + Given("graph 0 -> 1, 1 -> 2, 2 -> 3, 3 -> 4") { + graph.addEdge(0, 1) + graph.addEdge(1, 2) + graph.addEdge(2, 3) + graph.addEdge(3, 4) + } + + lateinit var result: DepthFirstOrder + + When("execute a depth first order traver") { + result = DepthFirstOrder(graph) + } + + Then("preOrder should equal listOf(0, 1, 2, 3, 4)") { + result.preOrder().toList() `should equal` listOf(0, 1, 2, 3, 4) + } + + Then("postOrder should equal listOf(4, 3, 2, 1, 0)") { + result.postOrder().toList() `should equal` listOf(4, 3, 2, 1, 0) + } + + Then("reversePostOrder should equal listOf(0, 1, 2, 3, 4)") { + result.reversePostOrder() `should equal` listOf(0, 1, 2, 3, 4) + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/it/norangeb/algorithms/graph/operations/StronglyConnectedComponentsTest.kt b/src/test/kotlin/it/norangeb/algorithms/graph/operations/StronglyConnectedComponentsTest.kt new file mode 100644 index 0000000..dfbcaef --- /dev/null +++ b/src/test/kotlin/it/norangeb/algorithms/graph/operations/StronglyConnectedComponentsTest.kt @@ -0,0 +1,141 @@ +/* + * MIT License + * + * Copyright (c) 2019 norangebit + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package it.norangeb.algorithms.graph.operations + +import it.norangeb.algorithms.graph.DiGraph +import org.amshove.kluent.`should be equal to` +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.gherkin.Feature + +object StronglyConnectedComponentsTest : Spek({ + Feature("compute strongly connected components") { + Scenario("compute strongly connected components on a graph") { + val graph by memoized { DiGraph() } + + Given("a graph with five scc") { + graph.addEdge(0, 1) + graph.addEdge(0, 5) + graph.addEdge(2, 0) + graph.addEdge(2, 3) + graph.addEdge(3, 2) + graph.addEdge(3, 5) + graph.addEdge(4, 2) + graph.addEdge(4, 3) + graph.addEdge(5, 4) + graph.addEdge(6, 0) + graph.addEdge(6, 4) + graph.addEdge(6, 8) + graph.addEdge(6, 9) + graph.addEdge(7, 6) + graph.addEdge(7, 9) + graph.addEdge(8, 6) + graph.addEdge(9, 10) + graph.addEdge(9, 11) + graph.addEdge(10, 12) + graph.addEdge(11, 4) + graph.addEdge(11, 12) + graph.addEdge(12, 9) + } + + lateinit var result: StronglyConnectedComponents + + When("compute the scc") { + result = StronglyConnectedComponents(graph) + } + + Then("components should be 5") { + result.count() `should be equal to` 5 + } + + Then("connected on 0, 1 should be false") { + result.stronglyConnected(0, 1) `should be equal to` false + } + + Then("connected on 3, 4 should be true") { + result.stronglyConnected(3, 4) `should be equal to` true + } + + Then("connected on 4, 3 should be true") { + result.stronglyConnected(4, 3) `should be equal to` true + } + + Then("connected on 9, 12 should be true") { + result.stronglyConnected(9, 12) `should be equal to` true + } + + Then("connected on 9, 8 should be false") { + result.stronglyConnected(9, 8) `should be equal to` false + } + + Then("connected on 1, 1 should be true") { + result.stronglyConnected(1, 1) `should be equal to` true + } + + Then("connected on 7, 6 should be false") { + result.stronglyConnected(7, 6) `should be equal to` false + } + } + + Scenario("compute scc on dag") { + val dag by memoized { DiGraph() } + + Given("a dag") { + dag.addEdge(0, 1) + dag.addEdge(0, 3) + dag.addEdge(3, 1) + dag.addEdge(2, 0) + dag.addEdge(2, 1) + dag.addEdge(2, 3) + } + + lateinit var result: StronglyConnectedComponents + + When("compute the scc") { + result = StronglyConnectedComponents(dag) + } + + Then("count should be equal to number of vertexes") { + result.count() `should be equal to` dag.vertexNumber() + } + + Then("every vertex is sc only with it self") { + (0 until dag.vertexNumber()) + .forEach { from -> + (0 until dag.vertexNumber()) + .forEach { to -> + when (from) { + to -> + result + .stronglyConnected(from, to) `should be equal to` true + else -> result + .stronglyConnected(from, to) `should be equal to` false + } + } + } + } + } + } +}) \ No newline at end of file diff --git a/src/test/kotlin/it/norangeb/algorithms/graph/operations/TopologicalOrderTest.kt b/src/test/kotlin/it/norangeb/algorithms/graph/operations/TopologicalOrderTest.kt new file mode 100644 index 0000000..88336bf --- /dev/null +++ b/src/test/kotlin/it/norangeb/algorithms/graph/operations/TopologicalOrderTest.kt @@ -0,0 +1,101 @@ +/* + * MIT License + * + * Copyright (c) 2019 norangebit + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package it.norangeb.algorithms.graph.operations + +import it.norangeb.algorithms.graph.DiGraph +import org.amshove.kluent.`should be equal to` +import org.amshove.kluent.`should equal` +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.gherkin.Feature + +object TopologicalOrderTest : Spek({ + Feature("topological order") { + Scenario("topological order on DAG") { + val dag by memoized { DiGraph() } + + Given("a dag") { + dag.addEdge(0, 1) + dag.addEdge(0, 2) + dag.addEdge(0, 5) + dag.addEdge(1, 4) + dag.addEdge(3, 2) + dag.addEdge(3, 4) + dag.addEdge(3, 5) + dag.addEdge(3, 6) + dag.addEdge(5, 2) + dag.addEdge(6, 0) + dag.addEdge(6, 4) + } + + lateinit var result: TopologicalOrder + + When("calculate the topological order") { + result = TopologicalOrder(dag) + } + + Then("isDag should be true") { + result.isDag() `should be equal to` true + } + + Then("topological order should equal listOf(3, 6, 0, 5, 2, 1, 4") { + result.order `should equal` listOf(3, 6, 0, 5, 2, 1, 4) + } + } + + Scenario("topological order into cyclic graph") { + val cyclicGraph by memoized { DiGraph() } + + Given("a cyclic graph") { + cyclicGraph.addEdge(0, 1) + cyclicGraph.addEdge(0, 2) + cyclicGraph.addEdge(0, 5) + cyclicGraph.addEdge(1, 4) + cyclicGraph.addEdge(3, 2) + cyclicGraph.addEdge(3, 4) + cyclicGraph.addEdge(3, 5) + cyclicGraph.addEdge(3, 6) + cyclicGraph.addEdge(4, 6) + cyclicGraph.addEdge(5, 2) + cyclicGraph.addEdge(6, 0) + cyclicGraph.addEdge(6, 4) + } + + lateinit var result: TopologicalOrder + + When("compute the topological order") { + result = TopologicalOrder(cyclicGraph) + } + + Then("isDag should be false") { + result.isDag() `should be equal to` false + } + + Then("order should be null") { + result.order `should equal` null + } + } + } +}) \ No newline at end of file