Compare commits
No commits in common. "84d96f50284fc3b232719356f573e41dd07f8995" and "80284f8013dac356f789676a1c12e85ad027fc8d" have entirely different histories.
84d96f5028
...
80284f8013
5 changed files with 28 additions and 190 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
val scala3: String = "3.8.3"
|
val scala3: String = "3.8.2"
|
||||||
|
|
||||||
ThisBuild / scalaVersion := scala3
|
ThisBuild / scalaVersion := scala3
|
||||||
ThisBuild / versionScheme := Some("semver-spec")
|
ThisBuild / versionScheme := Some("semver-spec")
|
||||||
|
|
|
||||||
|
|
@ -6,51 +6,8 @@ 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.
|
|
||||||
*/
|
*/
|
||||||
final class SCC private (val value: Set[Vertex]):
|
opaque type SCC = Vector[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
|
|
||||||
|
|
||||||
object SCC:
|
object SCC:
|
||||||
|
|
||||||
|
|
@ -63,38 +20,32 @@ object SCC:
|
||||||
* @return
|
* @return
|
||||||
* The new SCC.
|
* The new SCC.
|
||||||
*/
|
*/
|
||||||
def apply(value: Vertex*): SCC =
|
def apply(value: Seq[Vertex]): SCC = value.distinct.toVector
|
||||||
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.
|
||||||
|
|
@ -106,9 +57,6 @@ 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
|
||||||
|
|
@ -195,22 +143,14 @@ object SCC:
|
||||||
sccVertex = s.pop()
|
sccVertex = s.pop()
|
||||||
|
|
||||||
scc.addOne(sccVertex)
|
scc.addOne(sccVertex)
|
||||||
if !scc.isEmpty then output.addOne(new SCC(scc.toSet))
|
output.addOne(SCC(scc.toVector))
|
||||||
else println("ERROR")
|
|
||||||
else ()
|
else ()
|
||||||
|
|
||||||
/** Mutable state wrapper around an integer.
|
final private class Counter(var i: Int = 0):
|
||||||
*
|
|
||||||
* @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(_.root)
|
sccs.map(_.vertices.apply(0))
|
||||||
|
|
||||||
end UndirectedGraph
|
end UndirectedGraph
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
sbt.version=1.12.9
|
sbt.version=1.12.8
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue