Skip to content

Commit

Permalink
Add topological sort algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandrepiveteau committed Apr 25, 2023
1 parent 313c2c5 commit a9d57ce
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 5 deletions.
3 changes: 3 additions & 0 deletions api/kotlin-graphs.api
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ public final class io/github/alexandrepiveteau/graphs/VertexArrayKt {
public static final fun copyOf-ONBMYxs ([II)[I
public static final fun copyOf-iaUUKik ([I)[I
public static final fun copyOfRange-R4BdYes ([III)[I
public static final fun toTypedArray-iaUUKik ([I)[Lio/github/alexandrepiveteau/graphs/Vertex;
public static final fun toVertexArray ([Lio/github/alexandrepiveteau/graphs/Vertex;)[I
}

public abstract class io/github/alexandrepiveteau/graphs/VertexIterator : java/util/Iterator, kotlin/jvm/internal/markers/KMappedMarker {
Expand Down Expand Up @@ -266,6 +268,7 @@ public final class io/github/alexandrepiveteau/graphs/algorithms/Algorithms {
public static final fun shortestPathFasterAlgorithm-9scIZJg (Lio/github/alexandrepiveteau/graphs/Network;I)Lio/github/alexandrepiveteau/graphs/DirectedNetwork;
public static final fun shortestPathFasterAlgorithm-SPb1_vQ (Lio/github/alexandrepiveteau/graphs/Network;II)[I
public static final fun stronglyConnectedComponentsKosaraju (Lio/github/alexandrepiveteau/graphs/DirectedGraph;)Lkotlin/Pair;
public static final fun topologicalSort (Lio/github/alexandrepiveteau/graphs/DirectedGraph;)[I
}

public final class io/github/alexandrepiveteau/graphs/algorithms/Traversals {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public inline fun VertexArray(
init: (Int) -> Vertex,
): VertexArray = VertexArray(IntArray(size) { init(it).index })

/** Transforms the given [Array] of [Vertex] to a [VertexArray]. */
public fun Array<Vertex>.toVertexArray(): VertexArray = VertexArray(size) { this[it] }

/** Transforms the given [VertexArray] to an [Array] of [Vertex]. */
public fun VertexArray.toTypedArray(): Array<Vertex> = Array(size) { this[it] }

/** Returns a [VertexArray] for the corresponding [IntArray]. */
public fun IntArray.asVertexArray(): VertexArray = VertexArray(this)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
@file:JvmName("Algorithms")
@file:JvmMultifileClass

package io.github.alexandrepiveteau.graphs.algorithms

import io.github.alexandrepiveteau.graphs.DirectedGraph
import io.github.alexandrepiveteau.graphs.VertexArray
import io.github.alexandrepiveteau.graphs.asVertexArray
import io.github.alexandrepiveteau.graphs.internal.collections.IntDequeue
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName

/**
* Returns a [VertexArray] containing the vertices of the graph, sorted in topological order. If the
* graph contains a cycle, an [IllegalArgumentException] is thrown.
*
* ## Asymptotic complexity
* - **Time complexity**: O(|N| + |E|), where |N| is the number of vertices in this graph and |E| is
* the number of edges in this graph.
* - **Space complexity**: O(|N|), where n is the number of vertices in this graph.
*
* @return a [VertexArray] containing the vertices of the graph, sorted in topological order.
* @receiver the [DirectedGraph] to sort topologically.
* @throws IllegalArgumentException if the graph contains a cycle.
*/
public fun DirectedGraph.topologicalSort(): VertexArray {

// 0. Set up a queue of vertices to add to the topological sort, and a boolean array to keep track
// of the vertices that have already been sorted.
val queue = IntDequeue()
val sorted = BooleanArray(size)
val result = IntDequeue()

// 1. Compute the number of incoming edges for each vertex.
val edges = IntArray(size)
forEachArc { (_, to) -> edges[get(to)]++ }

// 2. Add all the vertices with no incoming edges to the queue.
for (i in edges.indices) {
if (edges[i] == 0) {
queue.addLast(i)
sorted[i] = true
}
}

// 3. While there are some vertices to add to the topological sort, add them and remove their
// outgoing edges. If a vertex has no more outgoing edges and has not been sorted yet, add it to
// the queue.
while (queue.size > 0) {
val vertex = queue.removeFirst()
result.addLast(vertex)

forEachNeighbor(get(vertex)) {
val to = get(it)
edges[to]--
if (edges[to] == 0 && !sorted[to]) {
queue.addLast(to)
sorted[to] = true
}
}
}

// 4. If there are still some vertices that have not been sorted, then there is a cycle in the
// graph. Throw an exception.
if (result.size != size) throw IllegalArgumentException("The graph contains a cycle.")

// 5. Return the result.
return result.toIntArray().asVertexArray()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package io.github.alexandrepiveteau.graphs.algorithms

import io.github.alexandrepiveteau.graphs.Vertex
import io.github.alexandrepiveteau.graphs.VertexArray
import io.github.alexandrepiveteau.graphs.arcTo
import io.github.alexandrepiveteau.graphs.builder.buildDirectedGraph
import io.github.alexandrepiveteau.graphs.toVertexArray
import io.github.alexandrepiveteau.graphs.util.Repeats
import io.github.alexandrepiveteau.graphs.util.assertContentEquals
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class TopologicalSortTests {

@Test
fun emptyGraphHasEmptyTopologicalSort() {
val graph = buildDirectedGraph {}
val order = graph.topologicalSort()
assertContentEquals(VertexArray(0), order)
}

@Test
fun singletonGraphHasSingletonTopologicalSort() {
val graph = buildDirectedGraph { addVertex() }
val order = graph.topologicalSort()
assertContentEquals(VertexArray(1) { Vertex(0) }, order)
}

@Test
fun loopHasNoTopologicalSort() {
val graph = buildDirectedGraph {
val a = addVertex()
addArc(a arcTo a)
}
assertFailsWith<IllegalArgumentException> { graph.topologicalSort() }
}

@Test
fun bidirectionalGraphHasNoTopologicalSort() {
val graph = buildDirectedGraph {
val (a, b) = addVertices()
addArc(a arcTo b)
addArc(b arcTo a)
}
assertFailsWith<IllegalArgumentException> { graph.topologicalSort() }
}

@Test
fun disjointGraphHasTopologicalSort() {
for (count in 0 until Repeats) {
val graph = buildDirectedGraph { repeat(count) { addVertex() } }
val order = graph.topologicalSort()
assertEquals(graph.size, order.size)
}
}

@Test
fun lineGraphHasIdenticalTopologicalSort() {
for (count in 2 until Repeats) {
val graph = buildDirectedGraph {
val vertices = VertexArray(count) { addVertex() }
repeat(count - 1) { addArc(vertices[it] arcTo vertices[it + 1]) }
}
val order = graph.topologicalSort()
assertContentEquals(VertexArray(graph.size) { graph[it] }, order)
}
}

@Test
fun geeksForGeeks() {
val graph = buildDirectedGraph {
val (v0, v1, v2, v3, v4, v5) = addVertices()
addArc(v5 arcTo v2)
addArc(v5 arcTo v0)
addArc(v4 arcTo v0)
addArc(v4 arcTo v1)
addArc(v2 arcTo v3)
addArc(v3 arcTo v1)
}
val order = graph.topologicalSort()
val expected = arrayOf(graph[4], graph[5], graph[0], graph[2], graph[3], graph[1])
assertContentEquals(expected.toVertexArray(), order)
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package io.github.alexandrepiveteau.graphs.util

import io.github.alexandrepiveteau.graphs.DirectedNetwork
import io.github.alexandrepiveteau.graphs.Graph
import io.github.alexandrepiveteau.graphs.Network
import io.github.alexandrepiveteau.graphs.Vertex
import io.github.alexandrepiveteau.graphs.*
import io.github.alexandrepiveteau.graphs.algorithms.forEachArc
import kotlin.test.assertEquals
import io.github.alexandrepiveteau.graphs.util.assertEquals as assertEqualsGraph
import kotlin.test.assertEquals

/**
* Asserts that two graphs are strictly equal, i.e. that they have the same size, and that they
Expand Down Expand Up @@ -74,3 +71,16 @@ fun assertFlowValid(
}
assertEquals(total, outgoing[source.index], "Total flow is different.")
}

/**
* Asserts that the contents of the two [VertexArray]s are equal.
*
* @param expected the expected [VertexArray].
* @param actual the actual [VertexArray].
*/
fun assertContentEquals(expected: VertexArray, actual: VertexArray) {
assertEquals(expected.size, actual.size)
for (i in 0 until expected.size) {
assertEquals(expected[i], actual[i])
}
}

0 comments on commit a9d57ce

Please sign in to comment.