(patch) WIP - tests
This commit is contained in:
parent
80284f8013
commit
48c88e800d
5 changed files with 151 additions and 28 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
val scala3: String = "3.8.2"
|
val scala3: String = "3.8.3"
|
||||||
|
|
||||||
ThisBuild / scalaVersion := scala3
|
ThisBuild / scalaVersion := scala3
|
||||||
ThisBuild / versionScheme := Some("semver-spec")
|
ThisBuild / versionScheme := Some("semver-spec")
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,51 @@ import scala.collection.mutable.ListBuffer
|
||||||
import scala.collection.mutable.Stack
|
import scala.collection.mutable.Stack
|
||||||
|
|
||||||
/** Represents a _Strongly Connected Component_ (SCC).
|
/** Represents a _Strongly Connected Component_ (SCC).
|
||||||
|
*
|
||||||
|
* Each SCC is guaranteed to be non-empty.
|
||||||
*/
|
*/
|
||||||
opaque type SCC = Vector[Vertex]
|
final class SCC private (val value: Set[Vertex]):
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def equals(obj: Any): Boolean =
|
||||||
|
obj match
|
||||||
|
case other: SCC => value.iterator.sameElements(other.value)
|
||||||
|
case _ => false
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def hashCode(): Int = value.hashCode()
|
||||||
|
|
||||||
|
/** @inheritDocs
|
||||||
|
*/
|
||||||
|
override def toString(): String =
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.append("[")
|
||||||
|
sb.append(value.mkString(","))
|
||||||
|
sb.append("]")
|
||||||
|
sb.toString()
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The number of vertices contained by this SCC.
|
||||||
|
*/
|
||||||
|
def size: Int = value.size
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* True if this SCC contains a single vertex.
|
||||||
|
*/
|
||||||
|
def isSingle: Boolean = value.size == 1
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* The set of vertices in the SCC.
|
||||||
|
*/
|
||||||
|
def vertices: Set[Vertex] = value
|
||||||
|
|
||||||
|
/** Retrieve an arbitrary SCC member as the root vertex.
|
||||||
|
*/
|
||||||
|
lazy val root: Vertex = value.toList.head
|
||||||
|
|
||||||
|
end SCC
|
||||||
|
|
||||||
object SCC:
|
object SCC:
|
||||||
|
|
||||||
|
|
@ -20,32 +63,38 @@ object SCC:
|
||||||
* @return
|
* @return
|
||||||
* The new SCC.
|
* The new SCC.
|
||||||
*/
|
*/
|
||||||
def apply(value: Seq[Vertex]): SCC = value.distinct.toVector
|
def apply(value: Vertex*): SCC =
|
||||||
|
if value.isEmpty then
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Empty strongly connected components (SCC) are not allowed."
|
||||||
|
)
|
||||||
|
else new SCC(value.toSet)
|
||||||
|
|
||||||
|
def of(value: Int*): SCC =
|
||||||
|
if value.isEmpty then
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Empty strongly connected components (SCC) are not allowed."
|
||||||
|
)
|
||||||
|
else new SCC(value.map(Vertex.apply).toSet)
|
||||||
|
|
||||||
|
/** Instantiate a new SCC that contains a single [[Vertex]].
|
||||||
|
*
|
||||||
|
* @param value
|
||||||
|
* The input vertex.
|
||||||
|
* @return
|
||||||
|
* The new SCC.
|
||||||
|
*/
|
||||||
|
def single(value: Vertex): SCC = new SCC(Set(value))
|
||||||
|
|
||||||
given CanEqual[SCC, SCC] = CanEqual.derived
|
given CanEqual[SCC, SCC] = CanEqual.derived
|
||||||
|
|
||||||
extension (scc: SCC)
|
|
||||||
/** @return
|
|
||||||
* The underlying value of this SCC.
|
|
||||||
*/
|
|
||||||
def unwrap(): Vector[Vertex] = scc
|
|
||||||
|
|
||||||
/** @return
|
|
||||||
* The vertices contained by this SCC.
|
|
||||||
*/
|
|
||||||
def vertices: Vector[Vertex] = scc
|
|
||||||
|
|
||||||
/** @return
|
|
||||||
* The number of vertices contained by this SCC.
|
|
||||||
*/
|
|
||||||
def size: Int = scc.size
|
|
||||||
|
|
||||||
/** @return
|
|
||||||
* True if this SCC contains a single vertex.
|
|
||||||
*/
|
|
||||||
def isSingle: Boolean = scc.size == 1
|
|
||||||
|
|
||||||
/** Alias for `findAll`.
|
/** Alias for `findAll`.
|
||||||
|
*
|
||||||
|
* Implementation of Tarjan's Algorithm for finding all strongly connected
|
||||||
|
* components for some graph.
|
||||||
|
*
|
||||||
|
* This algorithm captures directed cycles. Any vertex not part of some
|
||||||
|
* directed cycle (e.g. every node in a DAG) forms an SCC by itself.
|
||||||
*
|
*
|
||||||
* @param g
|
* @param g
|
||||||
* The graph to analyze.
|
* The graph to analyze.
|
||||||
|
|
@ -57,6 +106,9 @@ object SCC:
|
||||||
/** Implementation of Tarjan's Algorithm for finding all strongly connected
|
/** Implementation of Tarjan's Algorithm for finding all strongly connected
|
||||||
* components for some graph.
|
* components for some graph.
|
||||||
*
|
*
|
||||||
|
* This algorithm captures directed cycles. Any vertex not part of some
|
||||||
|
* directed cycle (e.g. every node in a DAG) forms an SCC by itself.
|
||||||
|
*
|
||||||
* @param g
|
* @param g
|
||||||
* The graph to analyze.
|
* The graph to analyze.
|
||||||
* @return
|
* @return
|
||||||
|
|
@ -143,14 +195,22 @@ object SCC:
|
||||||
sccVertex = s.pop()
|
sccVertex = s.pop()
|
||||||
|
|
||||||
scc.addOne(sccVertex)
|
scc.addOne(sccVertex)
|
||||||
output.addOne(SCC(scc.toVector))
|
if !scc.isEmpty then output.addOne(new SCC(scc.toSet))
|
||||||
|
else println("ERROR")
|
||||||
else ()
|
else ()
|
||||||
|
|
||||||
final private class Counter(var i: Int = 0):
|
/** Mutable state wrapper around an integer.
|
||||||
|
*
|
||||||
|
* @param i
|
||||||
|
* The integer value.
|
||||||
|
*/
|
||||||
|
final private class Counter(var i: Int):
|
||||||
|
|
||||||
def increment(): Unit =
|
def increment(): Unit =
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
def get(): Int = i
|
def get(): Int = i
|
||||||
|
|
||||||
|
end Counter
|
||||||
|
|
||||||
end SCC
|
end SCC
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class UndirectedGraph(
|
||||||
SCC.findAll(this)
|
SCC.findAll(this)
|
||||||
|
|
||||||
private def oneRootFromEachSCC(sccs: List[SCC]): List[Vertex] =
|
private def oneRootFromEachSCC(sccs: List[SCC]): List[Vertex] =
|
||||||
sccs.map(_.vertices.apply(0))
|
sccs.map(_.root)
|
||||||
|
|
||||||
end UndirectedGraph
|
end UndirectedGraph
|
||||||
|
|
||||||
|
|
|
||||||
63
modules/core/src/test/scala/gs/graph/v0/TarjansTests.scala
Normal file
63
modules/core/src/test/scala/gs/graph/v0/TarjansTests.scala
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package gs.graph.v0
|
||||||
|
|
||||||
|
import gs.graph.v0.directed.Digraph
|
||||||
|
import munit.*
|
||||||
|
|
||||||
|
class TarjansTests extends FunSuite:
|
||||||
|
|
||||||
|
test("should provide basic SCC representation") {
|
||||||
|
val v0 = Vertex.Zero
|
||||||
|
val v1 = Vertex(1)
|
||||||
|
val v2 = Vertex(2)
|
||||||
|
val s1 = SCC(v0)
|
||||||
|
val s2 = SCC(v0, v1, v2)
|
||||||
|
val s3 = SCC(v0)
|
||||||
|
|
||||||
|
assertEquals(s1, s3)
|
||||||
|
assertNotEquals(s1, s2)
|
||||||
|
assertEquals(s2.value, Set(v0, v1, v2))
|
||||||
|
assertEquals(s2.vertices, Set(v0, v1, v2))
|
||||||
|
assertEquals(s2.size, 3)
|
||||||
|
assertEquals(s2.isSingle, false)
|
||||||
|
assertEquals(s1.isSingle, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
test("should produce an empty list of SCCs for an empty graph") {
|
||||||
|
val g = UndirectedGraph.Empty
|
||||||
|
val sccs = SCC.tarjan(g)
|
||||||
|
val alt = SCC.findAll(g)
|
||||||
|
assertEquals(sccs, Nil)
|
||||||
|
assertEquals(alt, Nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
test(
|
||||||
|
"should produce a list of once SCC with one vertex for a graph with one vertex"
|
||||||
|
) {
|
||||||
|
val g = Graph.undirected(Size.One)
|
||||||
|
val sccs = SCC.findAll(g)
|
||||||
|
val expected = SCC.single(Vertex.Zero)
|
||||||
|
assertEquals(sccs, List(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Each vertex of a DAG is an SCC.
|
||||||
|
*/
|
||||||
|
test("should identify a single SCC for a single 'line' of vertices") {
|
||||||
|
val n = Size(3)
|
||||||
|
|
||||||
|
// 0 -> 1 -> 2
|
||||||
|
val g = Digraph.fromAdjacency(
|
||||||
|
Adjacency.fromEdges(
|
||||||
|
n,
|
||||||
|
List(
|
||||||
|
Edge(0, 1),
|
||||||
|
Edge(1, 2)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val sccs = SCC.findAll(g)
|
||||||
|
assertEquals(g.roots, Vector(Vertex.Zero))
|
||||||
|
assertEquals(sccs.toSet, Set(SCC.of(0), SCC.of(1), SCC.of(2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
end TarjansTests
|
||||||
|
|
@ -1 +1 @@
|
||||||
sbt.version=1.12.8
|
sbt.version=1.12.9
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue