Compare commits

..

No commits in common. "84d96f50284fc3b232719356f573e41dd07f8995" and "80284f8013dac356f789676a1c12e85ad027fc8d" have entirely different histories.

5 changed files with 28 additions and 190 deletions

View file

@ -1,4 +1,4 @@
val scala3: String = "3.8.3"
val scala3: String = "3.8.2"
ThisBuild / scalaVersion := scala3
ThisBuild / versionScheme := Some("semver-spec")

View file

@ -6,51 +6,8 @@ import scala.collection.mutable.ListBuffer
import scala.collection.mutable.Stack
/** Represents a _Strongly Connected Component_ (SCC).
*
* Each SCC is guaranteed to be non-empty.
*/
final class SCC private (val value: Set[Vertex]):
/** @inheritDocs
*/
override def equals(obj: Any): Boolean =
obj match
case other: SCC => value.equals(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
opaque type SCC = Vector[Vertex]
object SCC:
@ -63,38 +20,32 @@ object SCC:
* @return
* The new SCC.
*/
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))
def apply(value: Seq[Vertex]): SCC = value.distinct.toVector
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`.
*
* 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
* The graph to analyze.
@ -106,9 +57,6 @@ object SCC:
/** 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
* The graph to analyze.
* @return
@ -195,22 +143,14 @@ object SCC:
sccVertex = s.pop()
scc.addOne(sccVertex)
if !scc.isEmpty then output.addOne(new SCC(scc.toSet))
else println("ERROR")
output.addOne(SCC(scc.toVector))
else ()
/** Mutable state wrapper around an integer.
*
* @param i
* The integer value.
*/
final private class Counter(var i: Int):
final private class Counter(var i: Int = 0):
def increment(): Unit =
i += 1
def get(): Int = i
end Counter
end SCC

View file

@ -34,7 +34,7 @@ class UndirectedGraph(
SCC.findAll(this)
private def oneRootFromEachSCC(sccs: List[SCC]): List[Vertex] =
sccs.map(_.root)
sccs.map(_.vertices.apply(0))
end UndirectedGraph

View file

@ -1,102 +0,0 @@
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)))
}
test("should handle a graph with multiple SCCs") {
val n = Size(9)
// Establish discrete cycles
// 0 -> 1 -> 2 -> 0
// 3 -> 4 -> 5 -> 3
// 6 -> 7 -> 8 -> 6
val g = Digraph.fromAdjacency(
Adjacency.fromEdges(
n,
List(
// Cycle 1
Edge(0, 1),
Edge(1, 2),
Edge(2, 0),
// Cycle 2
Edge(3, 4),
Edge(4, 5),
Edge(5, 3),
// Cycle 3
Edge(6, 7),
Edge(7, 8),
Edge(8, 6)
)
)
)
val sccs = SCC.findAll(g)
assertEquals(g.roots, Vector.empty)
assertEquals(
sccs.toSet,
Set(
SCC.of(0, 1, 2),
SCC.of(3, 4, 5),
SCC.of(6, 7, 8)
)
)
}
end TarjansTests

View file

@ -1 +1 @@
sbt.version=1.12.9
sbt.version=1.12.8