Compare commits
3 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 180990c934 | |||
| 117bff5242 | |||
| 95e9ec5e08 |
28 changed files with 1800 additions and 314 deletions
11
build.sbt
11
build.sbt
|
|
@ -1,4 +1,4 @@
|
|||
val scala3: String = "3.8.2"
|
||||
val scala3: String = "3.8.3"
|
||||
|
||||
ThisBuild / scalaVersion := scala3
|
||||
ThisBuild / versionScheme := Some("semver-spec")
|
||||
|
|
@ -35,10 +35,16 @@ val Deps = new {
|
|||
}
|
||||
|
||||
val Gs = new {
|
||||
val Std = new {
|
||||
private val Version: String = "0.1.3"
|
||||
|
||||
val Core: ModuleID = "gs" %% "gs-std-core-v0" % Version
|
||||
}
|
||||
|
||||
val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.4.1"
|
||||
}
|
||||
|
||||
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.2.4"
|
||||
val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.3.0"
|
||||
}
|
||||
|
||||
lazy val testSettings = Seq(
|
||||
|
|
@ -61,6 +67,7 @@ lazy val core = project
|
|||
.settings(sharedSettings)
|
||||
.settings(testSettings)
|
||||
.settings(name := s"${gsProjectName.value}-core-v${semVerMajor.value}")
|
||||
.settings(libraryDependencies ++= Seq(Deps.Gs.Std.Core))
|
||||
|
||||
lazy val cats = project
|
||||
.in(file("modules/cats"))
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import scala.collection.mutable.ListBuffer
|
|||
* the corresponding vector is a "to" [[Vertex]] -- there are edges _from_ some
|
||||
* vertex _to_ another vertex.
|
||||
*/
|
||||
final class Adjacency(val neighbors: Vector[Vector[Vertex]]):
|
||||
final class Adjacency private (val neighbors: Vector[Vector[Vertex]]):
|
||||
/** Get the vector of [[Vertex]] that receive a connection _from_ the input
|
||||
* [[Vertex]].
|
||||
*
|
||||
|
|
@ -103,7 +103,7 @@ object Adjacency:
|
|||
* @return
|
||||
* Some new empty adjacency.
|
||||
*/
|
||||
def empty(numberOfVertices: Size): Adjacency =
|
||||
def noEdges(numberOfVertices: Size): Adjacency =
|
||||
new Adjacency(Vector.fill(numberOfVertices.value)(Vector.empty))
|
||||
|
||||
given CanEqual[Adjacency, Adjacency] = CanEqual.derived
|
||||
|
|
@ -118,8 +118,7 @@ object Adjacency:
|
|||
*/
|
||||
final val Single: Adjacency = new Adjacency(Vector(Vector.empty))
|
||||
|
||||
/** Calculate an [[Adjacency]] from some collection of [[Edge]], where those
|
||||
* edges are assumed to be directed.
|
||||
/** Calculate an [[Adjacency]] from some collection of [[Edge]].
|
||||
*
|
||||
* @param numberOfVertices
|
||||
* The number of [[Vertex]] (`N`) in this graph.
|
||||
|
|
@ -128,14 +127,44 @@ object Adjacency:
|
|||
* @return
|
||||
* The calculated [[Adjacency]].
|
||||
*/
|
||||
def fromDirectedEdges(
|
||||
def fromEdges(
|
||||
numberOfVertices: Size,
|
||||
edges: Iterable[Edge]
|
||||
): Adjacency =
|
||||
val buffs = Vector.fill(numberOfVertices.value)(ListBuffer.empty[Vertex])
|
||||
val _ = edges.foreach { edge =>
|
||||
if edge.v1 >= numberOfVertices then
|
||||
throw GraphException.VertexExceedsGraphBounds(edge.v1, numberOfVertices)
|
||||
else if edge.v2 >= numberOfVertices then
|
||||
throw GraphException.VertexExceedsGraphBounds(edge.v2, numberOfVertices)
|
||||
else
|
||||
val _ = buffs(edge.from.ordinal).addOne(edge.to)
|
||||
}
|
||||
new Adjacency(buffs.map(_.distinct.toVector))
|
||||
|
||||
/** Calculate an [[Adjacency]] from some collection of [[Edge]].
|
||||
*
|
||||
* @param edges
|
||||
* The collection of [[Edge]] present in this graph.
|
||||
* @return
|
||||
* The calculated [[Adjacency]].
|
||||
*/
|
||||
def fromEdges(
|
||||
edges: Iterable[Edge]
|
||||
): Adjacency =
|
||||
val numberOfVertices = findMaximumVertex(edges).ordinal + 1
|
||||
val buffs = Vector.fill(numberOfVertices)(ListBuffer.empty[Vertex])
|
||||
val _ = edges.foreach { edge =>
|
||||
val _ = buffs(edge.from.ordinal).addOne(edge.to)
|
||||
}
|
||||
new Adjacency(buffs.map(_.distinct.toVector))
|
||||
|
||||
private def findMaximumVertex(edges: Iterable[Edge]): Vertex =
|
||||
var maximum = Vertex.Zero
|
||||
edges.foreach { edge =>
|
||||
if edge.max > maximum then maximum = edge.max
|
||||
else ()
|
||||
}
|
||||
maximum
|
||||
|
||||
end Adjacency
|
||||
|
|
|
|||
135
modules/core/src/main/scala/gs/graph/v0/ConnectedComponent.scala
Normal file
135
modules/core/src/main/scala/gs/graph/v0/ConnectedComponent.scala
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import gs.graph.v0.Vertex
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.collection.mutable.Queue
|
||||
|
||||
/** Represents a _ Connected Component_.
|
||||
*
|
||||
* TODO: ADD EDGES!
|
||||
*/
|
||||
final class ConnectedComponent private (val value: Set[Vertex]):
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: ConnectedComponent => 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 component.
|
||||
*/
|
||||
def size: Int = value.size
|
||||
|
||||
/** @return
|
||||
* True if this component contains a single vertex.
|
||||
*/
|
||||
def isSingle: Boolean = value.size == 1
|
||||
|
||||
/** @return
|
||||
* The set of vertices in the Connected Component.
|
||||
*/
|
||||
def vertices: Set[Vertex] = value
|
||||
|
||||
/** Assign an arbitrary vertex as the root vertex.
|
||||
*/
|
||||
lazy val root: Vertex = value.toList.head
|
||||
|
||||
end ConnectedComponent
|
||||
|
||||
object ConnectedComponent:
|
||||
|
||||
/** Instantiate a new Connected Component from the given collection of
|
||||
* [[Vertex]].
|
||||
*
|
||||
* This constructor ensures that each [[Vertex]] only occurs once.
|
||||
*
|
||||
* @param value
|
||||
* The input collection.
|
||||
* @return
|
||||
* The new Connected Component.
|
||||
*/
|
||||
def apply(value: Vertex*): ConnectedComponent =
|
||||
if value.isEmpty then
|
||||
throw new IllegalArgumentException(
|
||||
"Empty connected components are not allowed."
|
||||
)
|
||||
else new ConnectedComponent(value.toSet)
|
||||
|
||||
def of(value: Int*): ConnectedComponent =
|
||||
if value.isEmpty then
|
||||
throw new IllegalArgumentException(
|
||||
"Empty connected components are not allowed."
|
||||
)
|
||||
else new ConnectedComponent(value.map(Vertex.apply).toSet)
|
||||
|
||||
/** Instantiate a new Connected Component that contains a single [[Vertex]].
|
||||
*
|
||||
* @param value
|
||||
* The input vertex.
|
||||
* @return
|
||||
* The new Connected Component.
|
||||
*/
|
||||
def single(value: Vertex): ConnectedComponent =
|
||||
new ConnectedComponent(Set(value))
|
||||
|
||||
given CanEqual[ConnectedComponent, ConnectedComponent] = CanEqual.derived
|
||||
|
||||
def findAll(g: Graph): List[ConnectedComponent] =
|
||||
if g.isEmpty then Nil
|
||||
else
|
||||
val tracker = Tracker.initialize(g)
|
||||
val ccs = ListBuffer.empty[ConnectedComponent]
|
||||
(0 until g.numberOfVertices.value).foreach { ordinal =>
|
||||
val v = Vertex(ordinal)
|
||||
if !tracker.isVisited(v) then ccs.addOne(bfsFromVertex(g, v, tracker))
|
||||
else ()
|
||||
}
|
||||
ccs.toList
|
||||
|
||||
private def bfsFromVertex(
|
||||
g: Graph,
|
||||
v: Vertex,
|
||||
t: Tracker
|
||||
): ConnectedComponent =
|
||||
val q = Queue.empty[Vertex]
|
||||
val b = ListBuffer.empty[Vertex]
|
||||
q.enqueue(v)
|
||||
|
||||
while !q.isEmpty
|
||||
do
|
||||
val current = q.dequeue()
|
||||
t.visit(current)
|
||||
b.addOne(current)
|
||||
g.neighbors(current).foreach { neighbor =>
|
||||
if !t.isVisited(neighbor) then q.enqueue(neighbor)
|
||||
else ()
|
||||
}
|
||||
|
||||
new ConnectedComponent(b.toSet)
|
||||
|
||||
private class Tracker(val data: Array[Boolean]):
|
||||
def visit(v: Vertex): Unit = data(v.ordinal) = true
|
||||
|
||||
def isVisited(v: Vertex): Boolean = data(v.ordinal)
|
||||
|
||||
private object Tracker:
|
||||
|
||||
def initialize(g: Graph): Tracker =
|
||||
new Tracker(Array.fill(g.numberOfVertices.value)(false))
|
||||
|
||||
end ConnectedComponent
|
||||
|
|
@ -2,7 +2,7 @@ package gs.graph.v0
|
|||
|
||||
import java.util.Objects
|
||||
|
||||
/** Represents a relationship between two [[Vertex]].
|
||||
/** Represents a relationship between two distinct [[Vertex]].
|
||||
*
|
||||
* When used is a directed context, the edge goes _from_ `v1` _to_ `v2`.
|
||||
*
|
||||
|
|
@ -11,11 +11,13 @@ import java.util.Objects
|
|||
* @param v2
|
||||
* The second [[Vertex]].
|
||||
*/
|
||||
final class Edge(
|
||||
final class Edge private (
|
||||
val v1: Vertex,
|
||||
val v2: Vertex
|
||||
):
|
||||
|
||||
def max: Vertex = if v1 >= v2 then v1 else v2
|
||||
|
||||
/** When considering this edge as _directed_, this function returns the
|
||||
* [[Vertex]] that is the beginning of the connection.
|
||||
*
|
||||
|
|
@ -54,11 +56,63 @@ object Edge:
|
|||
|
||||
given CanEqual[Edge, Edge] = CanEqual.derived
|
||||
|
||||
/** Instantiate a new Edge.
|
||||
*
|
||||
* Throws an exception if the given vertices are the same.
|
||||
*
|
||||
* @param v1
|
||||
* The first [[Vertex]].
|
||||
* @param v2
|
||||
* The second [[Vertex]].
|
||||
* @return
|
||||
* The new edge.
|
||||
*/
|
||||
def apply(
|
||||
v1: Vertex,
|
||||
v2: Vertex
|
||||
): Edge = new Edge(v1, v2)
|
||||
): Edge =
|
||||
if v1 != v2 then new Edge(v1, v2)
|
||||
else
|
||||
throw new IllegalArgumentException(
|
||||
"Loop edges are not supported. Edges must refer to two distinct vertexes."
|
||||
)
|
||||
|
||||
def apply(vs: (Vertex, Vertex)): Edge = new Edge(vs._1, vs._2)
|
||||
/** Instantiate a new Edge.
|
||||
*
|
||||
* Throws an exception if the given vertices are the same.
|
||||
*
|
||||
* @param vs
|
||||
* The pair of [[Vertex]].
|
||||
* @return
|
||||
* The new edge.
|
||||
*/
|
||||
def apply(vs: (Vertex, Vertex)): Edge = Edge(vs._1, vs._2)
|
||||
|
||||
/** Instantiate a new Edge.
|
||||
*
|
||||
* Throws an exception if the given vertices are the same or are not valid
|
||||
* [[Vertex]] values.
|
||||
*
|
||||
* @param v1
|
||||
* The first [[Vertex]].
|
||||
* @param v2
|
||||
* The second [[Vertex]].
|
||||
* @return
|
||||
* The new edge.
|
||||
*/
|
||||
def apply(
|
||||
v1: Int,
|
||||
v2: Int
|
||||
): Edge = Edge(Vertex(v1), Vertex(v2))
|
||||
|
||||
/** Instantiate a list of Edge.
|
||||
*
|
||||
* @param edges
|
||||
* The list of Edge.
|
||||
* @return
|
||||
* The captured list of Edge.
|
||||
*/
|
||||
def list(edges: (Int, Int)*): List[Edge] =
|
||||
edges.map(e => apply(e._1, e._2)).toList
|
||||
|
||||
end Edge
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import gs.graph.v0.directed.Digraph
|
||||
import scala.collection.IndexedSeqView
|
||||
|
||||
/** Graph representation based on an adjacency list.
|
||||
*
|
||||
* See:
|
||||
|
|
@ -27,11 +30,22 @@ trait Graph:
|
|||
*/
|
||||
def numberOfVertices: Size
|
||||
|
||||
/** @return
|
||||
* View over all [[Vertex]] values in this graph.
|
||||
*/
|
||||
def view: IndexedSeqView[Vertex] =
|
||||
(0 until numberOfVertices.value).view.map(Vertex(_))
|
||||
|
||||
/** @return
|
||||
* True if this graph has no vertices, false otherwise.
|
||||
*/
|
||||
def isEmpty: Boolean = numberOfVertices == Size.Zero
|
||||
|
||||
/** @return
|
||||
* True if this graph has one vertex, false otherwise.
|
||||
*/
|
||||
def isSingle: Boolean = numberOfVertices == Size.One
|
||||
|
||||
/** @return
|
||||
* The roots that should be used for traversal operations.
|
||||
*/
|
||||
|
|
@ -58,3 +72,59 @@ trait Graph:
|
|||
case _ => false
|
||||
|
||||
end Graph
|
||||
|
||||
object Graph:
|
||||
|
||||
/** Construct a new [[UndirectedGraph]] from the given [[Edge]].
|
||||
*
|
||||
* @param numberOfVertices
|
||||
* The number of vertices in this [[Graph]].
|
||||
* @param edges
|
||||
* The [[Edge]] in this graph.
|
||||
* @return
|
||||
* The new [[UndirectedGraph]].
|
||||
*/
|
||||
def undirected(
|
||||
numberOfVertices: Size,
|
||||
edges: (Int, Int)*
|
||||
): UndirectedGraph =
|
||||
new UndirectedGraph(
|
||||
numberOfVertices,
|
||||
Adjacency.fromEdges(numberOfVertices, Edge.list(edges*))
|
||||
)
|
||||
|
||||
/** Construct a new [[UndirectedGraph]] from the given [[Edge]].
|
||||
*
|
||||
* @param edges
|
||||
* The [[Edge]] in this graph.
|
||||
* @return
|
||||
* The new [[UndirectedGraph]].
|
||||
*/
|
||||
def undirected(
|
||||
edges: (Int, Int)*
|
||||
): UndirectedGraph =
|
||||
val adj = Adjacency.fromEdges(Edge.list(edges*))
|
||||
new UndirectedGraph(
|
||||
adj.numberOfVertices,
|
||||
adj
|
||||
)
|
||||
|
||||
/** Construct a new [[Digraph]] from the given [[Edge]].
|
||||
*
|
||||
* @param numberOfVertices
|
||||
* The number of vertices in this [[Graph]].
|
||||
* @param edges
|
||||
* The [[Edge]] in this graph.
|
||||
* @return
|
||||
* The new [[Digraph]].
|
||||
*/
|
||||
def directed(
|
||||
numberOfVertices: Size,
|
||||
edges: (Int, Int)*
|
||||
): Digraph =
|
||||
Digraph.fromEdges(
|
||||
numberOfVertices,
|
||||
edges.map(pair => Edge(pair._1, pair._2))
|
||||
)
|
||||
|
||||
end Graph
|
||||
|
|
|
|||
|
|
@ -7,13 +7,19 @@ package gs.graph.v0
|
|||
*/
|
||||
sealed abstract class GraphDisposition(val name: String):
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(that: Any): Boolean =
|
||||
that match
|
||||
case other: GraphDisposition => name == other.name
|
||||
case _ => false
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def hashCode(): Int = name.hashCode()
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def toString(): String = name
|
||||
|
||||
object GraphDisposition:
|
||||
|
|
@ -28,4 +34,17 @@ object GraphDisposition:
|
|||
*/
|
||||
case object Undirected extends GraphDisposition("undirected")
|
||||
|
||||
/** Parse the given string as a [[GraphDisposition]].
|
||||
*
|
||||
* @param candidate
|
||||
* The string to validate.
|
||||
* @return
|
||||
* The parsed [[GraphDisposition]], or `None` if the input does not
|
||||
* represent a valid disposition.
|
||||
*/
|
||||
def parse(candidate: String): Option[GraphDisposition] =
|
||||
if candidate.equalsIgnoreCase(Directed.name) then Some(Directed)
|
||||
else if candidate.equalsIgnoreCase(Undirected.name) then Some(Undirected)
|
||||
else None
|
||||
|
||||
end GraphDisposition
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ object GraphTraversal:
|
|||
/** Depth-first search that executes a side-effecting function on each
|
||||
* [[Vertex]]. This function will operate on _any_ [[Graph]].
|
||||
*
|
||||
* This implementation selects the first [[Vertex]] as an arbitrary starting
|
||||
* point.
|
||||
*
|
||||
* @param graph
|
||||
* The input [[Graph]] on which to run DFS.
|
||||
* @param visit
|
||||
|
|
@ -27,25 +24,23 @@ object GraphTraversal:
|
|||
val s = Stack.empty[Vertex]
|
||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||
|
||||
graph.selectRoots().foreach { root =>
|
||||
val _ = s.push(root)
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
val _ = visit(v)
|
||||
discovered(v.ordinal) = true
|
||||
graph.neighbors(v).foreach(w => s.push(w))
|
||||
else ()
|
||||
()
|
||||
graph.view.foreach { root =>
|
||||
if !discovered(root.ordinal) then
|
||||
val _ = s.push(root)
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
val _ = visit(v)
|
||||
discovered(v.ordinal) = true
|
||||
val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
|
||||
else ()
|
||||
else ()
|
||||
}
|
||||
|
||||
/** Depth-first search that executes a function on each [[Vertex]] to produce
|
||||
* some output. This function will operate on _any_ [[Graph]].
|
||||
*
|
||||
* This implementation selects the first [[Vertex]] as an arbitrary starting
|
||||
* point.
|
||||
*
|
||||
* @param graph
|
||||
* The input [[Graph]] on which to run DFS.
|
||||
* @param visit
|
||||
|
|
@ -61,20 +56,35 @@ object GraphTraversal:
|
|||
val s = Stack.empty[Vertex]
|
||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||
|
||||
graph.selectRoots().foreach { root =>
|
||||
val _ = s.push(root)
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
val _ = output.addOne(visit(v))
|
||||
discovered(v.ordinal) = true
|
||||
graph.neighbors(v).foreach(w => s.push(w))
|
||||
else ()
|
||||
graph.view.foreach { root =>
|
||||
if !discovered(root.ordinal) then
|
||||
val _ = s.push(root)
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
val _ = output.addOne(visit(v))
|
||||
discovered(v.ordinal) = true
|
||||
val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
|
||||
else ()
|
||||
else ()
|
||||
}
|
||||
|
||||
output.toList
|
||||
|
||||
/** Depth-first search that executes a function on each [[Vertex]] in the
|
||||
* context of some accumulator value to produce some accumulated output. This
|
||||
* function will operate on _any_ [[Graph]].
|
||||
*
|
||||
* @param graph
|
||||
* The input [[Graph]] on which to run DFS.
|
||||
* @param initial
|
||||
* The initial value.
|
||||
* @param f
|
||||
* The function to apply to each vertex in the context of an accumulator.
|
||||
* @return
|
||||
* The accumulated value.
|
||||
*/
|
||||
def dfsFold[Acc](
|
||||
graph: Graph,
|
||||
initial: Acc,
|
||||
|
|
@ -84,16 +94,18 @@ object GraphTraversal:
|
|||
val s = Stack.empty[Vertex]
|
||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||
|
||||
graph.selectRoots().foreach { root =>
|
||||
val _ = s.push(root)
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
acc = f(acc, v)
|
||||
discovered(v.ordinal) = true
|
||||
graph.neighbors(v).foreach(w => s.push(w))
|
||||
else ()
|
||||
graph.view.foreach { root =>
|
||||
if !discovered(root.ordinal) then
|
||||
val _ = s.push(root)
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
acc = f(acc, v)
|
||||
discovered(v.ordinal) = true
|
||||
val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
|
||||
else ()
|
||||
else ()
|
||||
}
|
||||
|
||||
acc
|
||||
|
|
@ -101,9 +113,6 @@ object GraphTraversal:
|
|||
/** Depth-first search that executes a side-effecting function on each
|
||||
* [[Vertex]], accepting the data stored for that [[Vertex]] as input.
|
||||
*
|
||||
* This implementation performs DFS for _each root_ in the input
|
||||
* [[AnyGraphWithData]].
|
||||
*
|
||||
* @param graph
|
||||
* The input [[Graph]] on which to run DFS.
|
||||
* @param visit
|
||||
|
|
@ -116,16 +125,18 @@ object GraphTraversal:
|
|||
val s = Stack.empty[Vertex]
|
||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||
|
||||
graph.selectRoots().foreach { root =>
|
||||
val _ = s.push(root)
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
val _ = visit(v, graph.data(v.ordinal))
|
||||
discovered(v.ordinal) = true
|
||||
graph.neighbors(v).foreach(w => s.push(w))
|
||||
else ()
|
||||
graph.view.foreach { root =>
|
||||
if !discovered(root.ordinal) then
|
||||
val _ = s.push(root)
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
val _ = visit(v, graph.data(v.ordinal))
|
||||
discovered(v.ordinal) = true
|
||||
val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
|
||||
else ()
|
||||
else ()
|
||||
}
|
||||
|
||||
()
|
||||
|
|
@ -133,9 +144,6 @@ object GraphTraversal:
|
|||
/** Depth-first search that executes a function on each [[Vertex]] to produce
|
||||
* some output, accepting the data stored for that [[Vertex]] as input.
|
||||
*
|
||||
* This implementation performs DFS for _each root_ in the input
|
||||
* [[AnyGraphWithData]].
|
||||
*
|
||||
* @param graph
|
||||
* The input [[Graph]] on which to run DFS.
|
||||
* @param visit
|
||||
|
|
@ -174,16 +182,18 @@ object GraphTraversal:
|
|||
val s = Stack.empty[Vertex]
|
||||
val discovered = Array.fill(graph.numberOfVertices.value)(false)
|
||||
|
||||
graph.selectRoots().foreach { root =>
|
||||
val _ = s.push(root)
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
acc = f(acc, graph.data(v.ordinal))
|
||||
discovered(v.ordinal) = true
|
||||
graph.neighbors(v).foreach(w => s.push(w))
|
||||
else ()
|
||||
graph.view.foreach { root =>
|
||||
if !discovered(root.ordinal) then
|
||||
val _ = s.push(root)
|
||||
while !s.isEmpty
|
||||
do
|
||||
val v = s.pop()
|
||||
if !discovered(v.ordinal) then
|
||||
acc = f(acc, graph.data(v.ordinal))
|
||||
discovered(v.ordinal) = true
|
||||
val _ = graph.neighbors(v).reverseIterator.foreach(s.push)
|
||||
else ()
|
||||
else ()
|
||||
}
|
||||
|
||||
acc
|
||||
|
|
@ -194,19 +204,23 @@ object GraphTraversal:
|
|||
): Unit =
|
||||
val q = Queue.empty[Vertex]
|
||||
val visited = Array.fill(graph.numberOfVertices.value)(false)
|
||||
val _ = graph.selectRoots().foreach(q.enqueue)
|
||||
|
||||
while !q.isEmpty
|
||||
do
|
||||
val v = q.dequeue()
|
||||
if !visited(v.ordinal) then
|
||||
val _ = visit(v)
|
||||
visited(v.ordinal) = true
|
||||
graph.neighbors(v).foreach { neighbor =>
|
||||
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
||||
graph.view.foreach { root =>
|
||||
if !visited(root.ordinal) then
|
||||
val _ = q.enqueue(root)
|
||||
while !q.isEmpty
|
||||
do
|
||||
val v = q.dequeue()
|
||||
if !visited(v.ordinal) then
|
||||
val _ = visit(v)
|
||||
visited(v.ordinal) = true
|
||||
graph.neighbors(v).foreach { neighbor =>
|
||||
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
||||
else ()
|
||||
}
|
||||
else ()
|
||||
}
|
||||
else ()
|
||||
}
|
||||
|
||||
def bfs[Out](
|
||||
graph: Graph,
|
||||
|
|
@ -215,19 +229,23 @@ object GraphTraversal:
|
|||
val output = ListBuffer.empty[Out]
|
||||
val q = Queue.empty[Vertex]
|
||||
val visited = Array.fill(graph.numberOfVertices.value)(false)
|
||||
val _ = graph.selectRoots().foreach(q.enqueue)
|
||||
|
||||
while !q.isEmpty
|
||||
do
|
||||
val v = q.dequeue()
|
||||
if !visited(v.ordinal) then
|
||||
val _ = output.addOne(visit(v))
|
||||
visited(v.ordinal) = true
|
||||
graph.neighbors(v).foreach { neighbor =>
|
||||
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
||||
graph.view.foreach { root =>
|
||||
if !visited(root.ordinal) then
|
||||
val _ = q.enqueue(root)
|
||||
while !q.isEmpty
|
||||
do
|
||||
val v = q.dequeue()
|
||||
if !visited(v.ordinal) then
|
||||
val _ = output.addOne(visit(v))
|
||||
visited(v.ordinal) = true
|
||||
graph.neighbors(v).foreach { neighbor =>
|
||||
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
||||
else ()
|
||||
}
|
||||
else ()
|
||||
}
|
||||
else ()
|
||||
}
|
||||
|
||||
output.toList
|
||||
|
||||
|
|
@ -239,19 +257,23 @@ object GraphTraversal:
|
|||
var acc = initial
|
||||
val q = Queue.empty[Vertex]
|
||||
val visited = Array.fill(graph.numberOfVertices.value)(false)
|
||||
val _ = graph.selectRoots().foreach(q.enqueue)
|
||||
|
||||
while !q.isEmpty
|
||||
do
|
||||
val v = q.dequeue()
|
||||
if !visited(v.ordinal) then
|
||||
acc = f(acc, v)
|
||||
visited(v.ordinal) = true
|
||||
graph.neighbors(v).foreach { neighbor =>
|
||||
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
||||
graph.view.foreach { root =>
|
||||
if !visited(root.ordinal) then
|
||||
val _ = q.enqueue(root)
|
||||
while !q.isEmpty
|
||||
do
|
||||
val v = q.dequeue()
|
||||
if !visited(v.ordinal) then
|
||||
acc = f(acc, v)
|
||||
visited(v.ordinal) = true
|
||||
graph.neighbors(v).foreach { neighbor =>
|
||||
if !visited(neighbor.ordinal) then q.enqueue(neighbor)
|
||||
else ()
|
||||
}
|
||||
else ()
|
||||
}
|
||||
else ()
|
||||
}
|
||||
|
||||
acc
|
||||
|
||||
|
|
|
|||
|
|
@ -83,17 +83,6 @@ object Size:
|
|||
if candidate >= 0 then new Size(candidate)
|
||||
else throw new IllegalArgumentException("Size values must be 0 or greater.")
|
||||
|
||||
given CanEqual[Size, Size] = CanEqual.derived
|
||||
|
||||
given Ordering[Size] with
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
def compare(
|
||||
x: Size,
|
||||
y: Size
|
||||
): Int = x.value - y.value
|
||||
|
||||
/** Instantiate the size of some array.
|
||||
*
|
||||
* @param arr
|
||||
|
|
@ -112,4 +101,24 @@ object Size:
|
|||
*/
|
||||
def fromVector(vec: Vector[?]): Size = new Size(vec.length)
|
||||
|
||||
/** Instantiate the size of any iterable.
|
||||
*
|
||||
* @param iter
|
||||
* The iterable.
|
||||
* @return
|
||||
* Size representing the total length of the iterable.
|
||||
*/
|
||||
def of(iter: Iterable[?]): Size = new Size(iter.size)
|
||||
|
||||
given CanEqual[Size, Size] = CanEqual.derived
|
||||
|
||||
given Ordering[Size] with
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
def compare(
|
||||
x: Size,
|
||||
y: Size
|
||||
): Int = x.value - y.value
|
||||
|
||||
end Size
|
||||
|
|
|
|||
|
|
@ -17,8 +17,22 @@ class UndirectedGraph(
|
|||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def selectRoots(): Vector[Vertex] =
|
||||
if numberOfVertices == Size.Zero then Vector.empty else Vector(Vertex.Zero)
|
||||
override def selectRoots(): Vector[Vertex] = roots
|
||||
|
||||
/** The lazily calculated roots of this graph.
|
||||
*
|
||||
* In an undirected graph, the roots are calculated by selecting an arbitrary
|
||||
* vertex from each connected component in the graph.
|
||||
*/
|
||||
lazy val roots: Vector[Vertex] =
|
||||
calculateRoots()
|
||||
|
||||
private def calculateRoots(): Vector[Vertex] =
|
||||
if numberOfVertices == Size.Zero then Vector.empty
|
||||
else oneFromEachConnectedComponent().toVector
|
||||
|
||||
private def oneFromEachConnectedComponent(): List[Vertex] =
|
||||
ConnectedComponent.findAll(this).map(_.root)
|
||||
|
||||
end UndirectedGraph
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ package gs.graph.v0
|
|||
*/
|
||||
final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
|
||||
|
||||
/** @return
|
||||
* The value (ordinal) of the Vertex.
|
||||
*/
|
||||
def value: Int = ordinal
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def compare(that: Vertex): Int =
|
||||
|
|
@ -37,6 +42,16 @@ final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
|
|||
*/
|
||||
infix def <(value: Int): Boolean = ordinal < value
|
||||
|
||||
/** Is the ordinal of this vertex less than or equal to some integer value?
|
||||
*
|
||||
* @param value
|
||||
* The integer value.
|
||||
* @return
|
||||
* True if the ordinal is less than or equal to the integer value. False
|
||||
* otherwise.
|
||||
*/
|
||||
infix def <=(value: Int): Boolean = ordinal <= value
|
||||
|
||||
/** Is the ordinal of this vertex greater than some integer value?
|
||||
*
|
||||
* @param value
|
||||
|
|
@ -46,6 +61,16 @@ final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
|
|||
*/
|
||||
infix def >(value: Int): Boolean = ordinal > value
|
||||
|
||||
/** Is the ordinal of this vertex greater than or equal to some integer value?
|
||||
*
|
||||
* @param value
|
||||
* The integer value.
|
||||
* @return
|
||||
* True if the ordinal is greater than or equal to the integer value. False
|
||||
* otherwise.
|
||||
*/
|
||||
infix def >=(value: Int): Boolean = ordinal >= value
|
||||
|
||||
/** Is the ordinal of this vertex less than some [[Size]] value?
|
||||
*
|
||||
* @param value
|
||||
|
|
@ -55,6 +80,16 @@ final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
|
|||
*/
|
||||
infix def <(size: Size): Boolean = ordinal < size.value
|
||||
|
||||
/** Is the ordinal of this vertex less than or equal to some [[Size]] value?
|
||||
*
|
||||
* @param value
|
||||
* The [[Size]] value.
|
||||
* @return
|
||||
* True if the ordinal is less than or equal to the [[Size]] value. False
|
||||
* otherwise.
|
||||
*/
|
||||
infix def <=(size: Size): Boolean = ordinal <= size.value
|
||||
|
||||
/** Is the ordinal of this vertex greater than some [[Size]] value?
|
||||
*
|
||||
* @param value
|
||||
|
|
@ -64,6 +99,17 @@ final class Vertex private (val ordinal: Int) extends Ordered[Vertex]:
|
|||
*/
|
||||
infix def >(size: Size): Boolean = ordinal > size.value
|
||||
|
||||
/** Is the ordinal of this vertex greater than or equal to some [[Size]]
|
||||
* value?
|
||||
*
|
||||
* @param value
|
||||
* The [[Size]] value.
|
||||
* @return
|
||||
* True if the ordinal is greater than or equal to the [[Size]] value.
|
||||
* False otherwise.
|
||||
*/
|
||||
infix def >=(size: Size): Boolean = ordinal >= size.value
|
||||
|
||||
object Vertex:
|
||||
|
||||
/** The fixed value 0 expressed as a [[Vertex]].
|
||||
|
|
@ -84,6 +130,24 @@ object Vertex:
|
|||
else
|
||||
throw new IllegalArgumentException("Vertex values must be 0 or greater.")
|
||||
|
||||
/** Instantiate an immutable sequence of vertices.
|
||||
*
|
||||
* Throws an `IllegalArgumentException` if any input value is less than 0.
|
||||
*
|
||||
* @param values
|
||||
* The integer values to validate.
|
||||
* @return
|
||||
* The new vertices.
|
||||
*/
|
||||
def of(values: Int*): Vector[Vertex] =
|
||||
values.map(apply).toVector
|
||||
|
||||
def list(values: Int*): List[Vertex] =
|
||||
values.map(apply).toList
|
||||
|
||||
def set(values: Int*): Set[Vertex] =
|
||||
values.map(apply).toSet
|
||||
|
||||
given CanEqual[Vertex, Vertex] = CanEqual.derived
|
||||
|
||||
given Ordering[Vertex] with
|
||||
|
|
|
|||
|
|
@ -18,6 +18,16 @@ class Dag protected (
|
|||
|
||||
object Dag:
|
||||
|
||||
/** The empty DAG.
|
||||
*/
|
||||
final val Empty: Dag =
|
||||
new Dag(Size.Zero, Adjacency.Empty, Vector.empty)
|
||||
|
||||
/** DAG with a single vertex.
|
||||
*/
|
||||
final val Single: Dag =
|
||||
new Dag(Size.One, Adjacency.Single, Vector(Vertex.Zero))
|
||||
|
||||
given CanEqual[Dag, Dag] = CanEqual.derived
|
||||
given CanEqual[Dag, Digraph] = CanEqual.derived
|
||||
given CanEqual[Digraph, Dag] = CanEqual.derived
|
||||
|
|
|
|||
|
|
@ -36,9 +36,12 @@ class Digraph(
|
|||
case other: Digraph =>
|
||||
numberOfVertices == other.numberOfVertices
|
||||
&& adjacency == other.adjacency
|
||||
&& roots.sameElements(other.roots)
|
||||
case _ => false
|
||||
|
||||
/** `True` if this directed graph has a cycle, `false` otherwise.
|
||||
*/
|
||||
lazy val hasCycle: Boolean = Digraph.hasCycle(this)
|
||||
|
||||
end Digraph
|
||||
|
||||
object Digraph:
|
||||
|
|
@ -63,7 +66,7 @@ object Digraph:
|
|||
): Digraph =
|
||||
new Digraph(
|
||||
numberOfVertices = numberOfVertices,
|
||||
adjacency = Adjacency.fromDirectedEdges(numberOfVertices, edges),
|
||||
adjacency = Adjacency.fromEdges(numberOfVertices, edges),
|
||||
roots = findRootsForDirectedEdges(numberOfVertices, edges)
|
||||
)
|
||||
|
||||
|
|
@ -121,9 +124,10 @@ object Digraph:
|
|||
*/
|
||||
def hasCycle(digraph: Digraph): Boolean =
|
||||
digraph match
|
||||
case _ if digraph.isEmpty => false
|
||||
case _: Dag => false
|
||||
case _ => new CycleDetector(digraph).hasCycle
|
||||
case _ if digraph.isEmpty => false
|
||||
case _ if digraph.isSingle => false
|
||||
case _: Dag => false
|
||||
case _ => new CycleDetector(digraph).hasCycle
|
||||
|
||||
// Internal class for finding cycles.
|
||||
// This implementation comes (roughly) from Algorithms (Fourth Edition) by
|
||||
|
|
|
|||
|
|
@ -1,147 +0,0 @@
|
|||
package gs.graph.v0.directed
|
||||
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.Vertex
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.collection.mutable.Stack
|
||||
|
||||
/** Represents a _Strongly Connected Component_ (SCC).
|
||||
*/
|
||||
opaque type SCC = Vector[Vertex]
|
||||
|
||||
object SCC:
|
||||
|
||||
/** Instantiate a new SCC from the given collection of [[Vertex]].
|
||||
*
|
||||
* This constructor ensures that each [[Vertex]] only occurs once.
|
||||
*
|
||||
* @param value
|
||||
* The input collection.
|
||||
* @return
|
||||
* The new SCC.
|
||||
*/
|
||||
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
|
||||
|
||||
/** Implementation of Tarjan's Algorithm for finding all strongly connected
|
||||
* components for some directed graph.
|
||||
*
|
||||
* @param g
|
||||
* The directed graph to analyze.
|
||||
* @return
|
||||
* The complete list of [[SCC]] for the input graph.
|
||||
*/
|
||||
def findAll(g: Graph): List[SCC] =
|
||||
val V = g.numberOfVertices.value
|
||||
|
||||
// Vertex numbers.
|
||||
val num = Array.fill(V)(-1)
|
||||
|
||||
// Array holding the minimum reachable vertex numbers. `lowest(v)` is the
|
||||
// minimum number of a vertex reachable from `v`.
|
||||
val lowest = Array.fill(V)(-1)
|
||||
|
||||
// For each vertex, indicates whether that vertex has been visited.
|
||||
val visited = Array.fill(V)(false)
|
||||
|
||||
// For each vertex, indicates whether that vertex has been processed.
|
||||
val processed = Array.fill(V)(false)
|
||||
|
||||
// Stack that holds the working set of vertices. S holds all vertices
|
||||
// reachable from the starting vertex. When an SCC is found, the stack is
|
||||
// unwound to get each element of that SCC.
|
||||
val s = Stack.empty[Vertex]
|
||||
|
||||
// Counter -- provides sequential numbers to the vertices.
|
||||
val counter = new Counter(0)
|
||||
|
||||
// List of output SCCs.
|
||||
val sccs = ListBuffer.empty[SCC]
|
||||
|
||||
(0 until V).foreach { v =>
|
||||
if !visited(v) then
|
||||
tarjanDfs(g, v, num, lowest, visited, processed, s, counter, sccs)
|
||||
()
|
||||
else ()
|
||||
}
|
||||
|
||||
sccs.toList
|
||||
|
||||
private def tarjanDfs(
|
||||
g: Graph,
|
||||
v: Int,
|
||||
num: Array[Int],
|
||||
lowest: Array[Int],
|
||||
visited: Array[Boolean],
|
||||
processed: Array[Boolean],
|
||||
s: Stack[Vertex],
|
||||
counter: Counter,
|
||||
output: ListBuffer[SCC]
|
||||
): Unit =
|
||||
// Convenience integer representation for array access.
|
||||
val V = Vertex(v)
|
||||
|
||||
num(v) = counter.get()
|
||||
lowest(v) = num(v)
|
||||
val _ = counter.increment()
|
||||
visited(v) = true
|
||||
s.push(V)
|
||||
|
||||
// Traverse the neighbors of the current vertex.
|
||||
g.neighbors(V).foreach { neighbor =>
|
||||
// Convenience integer representation for array access.
|
||||
val n = neighbor.ordinal
|
||||
|
||||
if !visited(n) then
|
||||
tarjanDfs(g, n, num, lowest, visited, processed, s, counter, output)
|
||||
lowest(v) = Math.min(lowest(v), lowest(n))
|
||||
else if !processed(n) then lowest(v) = Math.min(lowest(v), num(n))
|
||||
else ()
|
||||
}
|
||||
|
||||
// The current vertex is now processed.
|
||||
processed(v) = true
|
||||
|
||||
if lowest(v) == num(v) then
|
||||
val scc = ListBuffer.empty[Vertex]
|
||||
var sccVertex = s.pop()
|
||||
|
||||
while sccVertex != V
|
||||
do
|
||||
scc.addOne(sccVertex)
|
||||
sccVertex = s.pop()
|
||||
|
||||
scc.addOne(sccVertex)
|
||||
output.addOne(SCC(scc.toVector))
|
||||
else ()
|
||||
|
||||
final private class Counter(var i: Int = 0):
|
||||
|
||||
def increment(): Unit =
|
||||
i += 1
|
||||
|
||||
def get(): Int = i
|
||||
|
||||
end SCC
|
||||
|
|
@ -61,7 +61,7 @@ object SingleRootDigraph:
|
|||
if root < numberOfVertices then
|
||||
new SingleRootDigraph(
|
||||
numberOfVertices,
|
||||
Adjacency.fromDirectedEdges(numberOfVertices, edges),
|
||||
Adjacency.fromEdges(numberOfVertices, edges),
|
||||
root
|
||||
)
|
||||
else throw GraphException.RootOutOfBounds(root, numberOfVertices)
|
||||
|
|
@ -86,7 +86,7 @@ object SingleRootDigraph:
|
|||
Some(
|
||||
new SingleRootDigraph(
|
||||
numberOfVertices,
|
||||
Adjacency.fromDirectedEdges(numberOfVertices, edges),
|
||||
Adjacency.fromEdges(numberOfVertices, edges),
|
||||
roots(0)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
package gs.graph.v0.directed
|
||||
|
||||
import gs.graph.v0.Vertex
|
||||
import scala.collection.mutable.ListBuffer
|
||||
import scala.collection.mutable.Stack
|
||||
|
||||
/** Represents a _Strongly Connected Component_ (SCC).
|
||||
*
|
||||
* Each SCC is guaranteed to be non-empty.
|
||||
*
|
||||
* This class is used with [[Digraph]]. See [[gs.graph.v0.ConnectedComponent]]
|
||||
* for general connected components.
|
||||
*
|
||||
* TODO: ADD EDGES
|
||||
*/
|
||||
final class StronglyConnectedComponent private (val value: Set[Vertex]):
|
||||
|
||||
/** @inheritDocs
|
||||
*/
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match
|
||||
case other: StronglyConnectedComponent => 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 StronglyConnectedComponent
|
||||
|
||||
object StronglyConnectedComponent:
|
||||
|
||||
/** Instantiate a new SCC from the given collection of [[Vertex]].
|
||||
*
|
||||
* This constructor ensures that each [[Vertex]] only occurs once.
|
||||
*
|
||||
* @param value
|
||||
* The input collection.
|
||||
* @return
|
||||
* The new SCC.
|
||||
*/
|
||||
def apply(value: Vertex*): StronglyConnectedComponent =
|
||||
if value.isEmpty then
|
||||
throw new IllegalArgumentException(
|
||||
"Empty strongly connected components (SCC) are not allowed."
|
||||
)
|
||||
else new StronglyConnectedComponent(value.toSet)
|
||||
|
||||
def of(value: Int*): StronglyConnectedComponent =
|
||||
if value.isEmpty then
|
||||
throw new IllegalArgumentException(
|
||||
"Empty strongly connected components (SCC) are not allowed."
|
||||
)
|
||||
else new StronglyConnectedComponent(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): StronglyConnectedComponent =
|
||||
new StronglyConnectedComponent(Set(value))
|
||||
|
||||
given CanEqual[StronglyConnectedComponent, StronglyConnectedComponent] =
|
||||
CanEqual.derived
|
||||
|
||||
/** 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.
|
||||
* @return
|
||||
* The complete list of [[SCC]] for the input graph.
|
||||
*/
|
||||
def tarjan(g: Digraph): List[StronglyConnectedComponent] = findAll(g)
|
||||
|
||||
/** 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
|
||||
* The complete list of [[SCC]] for the input graph.
|
||||
*/
|
||||
def findAll(g: Digraph): List[StronglyConnectedComponent] =
|
||||
val V = g.numberOfVertices.value
|
||||
|
||||
// Vertex numbers.
|
||||
val num = Array.fill(V)(-1)
|
||||
|
||||
// Array holding the minimum reachable vertex numbers. `lowest(v)` is the
|
||||
// minimum number of a vertex reachable from `v`.
|
||||
val lowest = Array.fill(V)(-1)
|
||||
|
||||
// For each vertex, indicates whether that vertex has been visited.
|
||||
val visited = Array.fill(V)(false)
|
||||
|
||||
// For each vertex, indicates whether that vertex has been processed.
|
||||
val processed = Array.fill(V)(false)
|
||||
|
||||
// Stack that holds the working set of vertices. S holds all vertices
|
||||
// reachable from the starting vertex. When an SCC is found, the stack is
|
||||
// unwound to get each element of that SCC.
|
||||
val s = Stack.empty[Vertex]
|
||||
|
||||
// Counter -- provides sequential numbers to the vertices.
|
||||
val counter = new Counter(0)
|
||||
|
||||
// List of output SCCs.
|
||||
val sccs = ListBuffer.empty[StronglyConnectedComponent]
|
||||
|
||||
(0 until V).foreach { v =>
|
||||
if !visited(v) then
|
||||
tarjanDfs(g, v, num, lowest, visited, processed, s, counter, sccs)
|
||||
()
|
||||
else ()
|
||||
}
|
||||
|
||||
sccs.toList
|
||||
|
||||
private def tarjanDfs(
|
||||
g: Digraph,
|
||||
v: Int,
|
||||
num: Array[Int],
|
||||
lowest: Array[Int],
|
||||
visited: Array[Boolean],
|
||||
processed: Array[Boolean],
|
||||
s: Stack[Vertex],
|
||||
counter: Counter,
|
||||
output: ListBuffer[StronglyConnectedComponent]
|
||||
): Unit =
|
||||
// Convenience integer representation for array access.
|
||||
val V = Vertex(v)
|
||||
|
||||
num(v) = counter.get()
|
||||
lowest(v) = num(v)
|
||||
val _ = counter.increment()
|
||||
visited(v) = true
|
||||
s.push(V)
|
||||
|
||||
// Traverse the neighbors of the current vertex.
|
||||
g.neighbors(V).foreach { neighbor =>
|
||||
// Convenience integer representation for array access.
|
||||
val n = neighbor.ordinal
|
||||
|
||||
if !visited(n) then
|
||||
tarjanDfs(g, n, num, lowest, visited, processed, s, counter, output)
|
||||
lowest(v) = Math.min(lowest(v), lowest(n))
|
||||
else if !processed(n) then lowest(v) = Math.min(lowest(v), num(n))
|
||||
else ()
|
||||
}
|
||||
|
||||
// The current vertex is now processed.
|
||||
processed(v) = true
|
||||
|
||||
if lowest(v) == num(v) then
|
||||
val scc = ListBuffer.empty[Vertex]
|
||||
var sccVertex = s.pop()
|
||||
|
||||
while sccVertex != V
|
||||
do
|
||||
scc.addOne(sccVertex)
|
||||
sccVertex = s.pop()
|
||||
|
||||
scc.addOne(sccVertex)
|
||||
if !scc.isEmpty then
|
||||
output.addOne(new StronglyConnectedComponent(scc.toSet))
|
||||
else println("ERROR")
|
||||
else ()
|
||||
|
||||
/** Mutable state wrapper around an integer.
|
||||
*
|
||||
* @param i
|
||||
* The integer value.
|
||||
*/
|
||||
final private class Counter(var i: Int):
|
||||
|
||||
def increment(): Unit =
|
||||
i += 1
|
||||
|
||||
def get(): Int = i
|
||||
|
||||
end Counter
|
||||
|
||||
end StronglyConnectedComponent
|
||||
|
|
@ -1,10 +1,26 @@
|
|||
package gs.graph.v0.syntax
|
||||
|
||||
import gs.graph.v0.Edge
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.GraphTraversal
|
||||
import gs.graph.v0.Vertex
|
||||
import gs.graph.v0.data.AnyGraphWithData
|
||||
|
||||
/** Construction helper for [[Vertex]] values.
|
||||
*
|
||||
* @param value
|
||||
* The value of the vertex.
|
||||
* @return
|
||||
* The new [[Vertex]] instance.
|
||||
*/
|
||||
def V(value: Int): Vertex = Vertex(value)
|
||||
|
||||
extension (value: (Int, Int))
|
||||
/** @return
|
||||
* The [[Edge]] represented by this tuple.
|
||||
*/
|
||||
def edge: Edge = Edge(value._1, value._2)
|
||||
|
||||
extension (graph: Graph)
|
||||
|
||||
/** Visit vertex node in this graph, using Depth-First Search (DFS) as the
|
||||
|
|
|
|||
|
|
@ -1,27 +1,104 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import gs.graph.v0.syntax.V
|
||||
import scala.util.Failure
|
||||
import scala.util.Try
|
||||
|
||||
class AdjacencyTests extends munit.FunSuite:
|
||||
|
||||
test("should provide incoming connections") {
|
||||
val N = Size(7)
|
||||
val vs = (0 until N.value).map(Vertex(_)).toArray
|
||||
val E = List(
|
||||
Edge(vs(0), vs(1)),
|
||||
Edge(vs(0), vs(2)),
|
||||
Edge(vs(0), vs(3)),
|
||||
Edge(vs(1), vs(4)),
|
||||
Edge(vs(2), vs(4)),
|
||||
Edge(vs(3), vs(4)),
|
||||
Edge(vs(3), vs(5)),
|
||||
Edge(vs(4), vs(6))
|
||||
)
|
||||
val A = Adjacency.fromDirectedEdges(N, E)
|
||||
private val size = Size(7)
|
||||
|
||||
assertEquals(A.incoming(vs(0)), Vector.empty)
|
||||
assertEquals(A.incoming(vs(1)), Vector(vs(0)))
|
||||
assertEquals(A.incoming(vs(2)), Vector(vs(0)))
|
||||
assertEquals(A.incoming(vs(3)), Vector(vs(0)))
|
||||
assertEquals(A.incoming(vs(4)), Vector(vs(1), vs(2), vs(3)))
|
||||
assertEquals(A.incoming(vs(5)), Vector(vs(3)))
|
||||
assertEquals(A.incoming(vs(6)), Vector(vs(4)))
|
||||
private val edges = Edge.list(
|
||||
0 -> 1,
|
||||
0 -> 2,
|
||||
0 -> 3,
|
||||
1 -> 4,
|
||||
2 -> 4,
|
||||
3 -> 4,
|
||||
3 -> 5,
|
||||
4 -> 6
|
||||
)
|
||||
|
||||
private val adj = Adjacency.fromEdges(size, edges)
|
||||
|
||||
test("should provide equality") {
|
||||
val a2 = Adjacency(adj.neighbors)
|
||||
val a3 = Adjacency.Single
|
||||
|
||||
assertEquals(adj, a2)
|
||||
assertNotEquals(adj, a3)
|
||||
assert(!adj.equals(null))
|
||||
assert(!adj.equals("anything"))
|
||||
}
|
||||
|
||||
test("should provide incoming connections") {
|
||||
assertEquals(adj.incoming(V(0)), Vector.empty)
|
||||
assertEquals(adj.incoming(V(1)), Vertex.of(0))
|
||||
assertEquals(adj.incoming(V(2)), Vertex.of(0))
|
||||
assertEquals(adj.incoming(V(3)), Vertex.of(0))
|
||||
assertEquals(adj.incoming(V(4)), Vertex.of(1, 2, 3))
|
||||
assertEquals(adj.incoming(V(5)), Vertex.of(3))
|
||||
assertEquals(adj.incoming(V(6)), Vertex.of(4))
|
||||
}
|
||||
|
||||
test("should show no incoming connections for a vertex not in the graph") {
|
||||
assertEquals(adj.incoming(Vertex(999)), Vector.empty)
|
||||
}
|
||||
|
||||
test("should express an adjacency as edges") {
|
||||
val es = adj.toEdges()
|
||||
assertEquals(es, edges.toVector)
|
||||
}
|
||||
|
||||
test("should track the number of vertices in the graph") {
|
||||
assertEquals(adj.numberOfVertices, size)
|
||||
}
|
||||
|
||||
test("should track the number of edges in the graph") {
|
||||
assertEquals(adj.numberOfEdges, Size.of(edges))
|
||||
}
|
||||
|
||||
test("should construct an adjacency with no edges") {
|
||||
val sz = Size(10)
|
||||
val a = Adjacency.noEdges(sz)
|
||||
assertEquals(a.numberOfVertices, sz)
|
||||
assertEquals(a.numberOfEdges, Size.Zero)
|
||||
}
|
||||
|
||||
test(
|
||||
"should guard against out of bound values when building from edges with an explicit bound (first vertex)"
|
||||
) {
|
||||
val n = Size(2)
|
||||
val es = Edge.list(3 -> 0)
|
||||
Try(Adjacency.fromEdges(n, es)) match
|
||||
case Failure(ex: GraphException.VertexExceedsGraphBounds) =>
|
||||
assertEquals(ex.vertex, Vertex(3))
|
||||
assertEquals(ex.bound, n)
|
||||
case _ =>
|
||||
fail(
|
||||
"Expected adjacency construction to fail with a GraphException."
|
||||
)
|
||||
}
|
||||
|
||||
test(
|
||||
"should guard against out of bound values when building from edges with an explicit bound (second vertex)"
|
||||
) {
|
||||
val n = Size(2)
|
||||
val es = Edge.list(0 -> 3)
|
||||
Try(Adjacency.fromEdges(n, es)) match
|
||||
case Failure(ex: GraphException.VertexExceedsGraphBounds) =>
|
||||
assertEquals(ex.vertex, Vertex(3))
|
||||
assertEquals(ex.bound, n)
|
||||
case _ =>
|
||||
fail(
|
||||
"Expected adjacency construction to fail with a GraphException."
|
||||
)
|
||||
}
|
||||
|
||||
test("should construct from edges, finding the largest known vertex") {
|
||||
val expectedSize = Size(10)
|
||||
val es = Edge.list(0 -> 1, 1 -> 2, 0 -> 9, 3 -> 1)
|
||||
val a = Adjacency.fromEdges(es)
|
||||
assertEquals(a.numberOfVertices, expectedSize)
|
||||
assertEquals(a.numberOfEdges, Size.of(es))
|
||||
}
|
||||
|
|
|
|||
200
modules/core/src/test/scala/gs/graph/v0/BfsTraversalTests.scala
Normal file
200
modules/core/src/test/scala/gs/graph/v0/BfsTraversalTests.scala
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import gs.graph.v0.BfsTraversalTests.Counter
|
||||
import gs.graph.v0.BfsTraversalTests.Tracker
|
||||
import gs.graph.v0.directed.Digraph
|
||||
import munit.*
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
class BfsTraversalTests extends FunSuite:
|
||||
|
||||
test("should perform BFS on an empty graph") {
|
||||
val c1 = new Counter
|
||||
val _ = GraphTraversal.bfs(graph = UndirectedGraph.Empty, visit = c1.visit)
|
||||
assertEquals(c1.count, 0)
|
||||
|
||||
val c2 = new Counter
|
||||
val _ = GraphTraversal.bfs(graph = Digraph.Empty, visit = c2.visit)
|
||||
assertEquals(c2.count, 0)
|
||||
}
|
||||
|
||||
test("should perform BFS on a graph of size 1") {
|
||||
val c1 = new Counter
|
||||
val _ = GraphTraversal.bfs(graph = UndirectedGraph.Single, visit = c1.visit)
|
||||
assertEquals(c1.count, 1)
|
||||
|
||||
val c2 = new Counter
|
||||
val _ = GraphTraversal.bfs(graph = Digraph.Single, visit = c2.visit)
|
||||
assertEquals(c2.count, 1)
|
||||
}
|
||||
|
||||
test("should perform BFS on a complex directed graph") {
|
||||
val n = Size(6)
|
||||
val g = Graph.directed(
|
||||
numberOfVertices = n,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 1,
|
||||
0 -> 3,
|
||||
3 -> 2,
|
||||
4 -> 5
|
||||
)
|
||||
val expectedVisitOrder = List(0, 1, 3, 2, 4, 5)
|
||||
val t = new Tracker
|
||||
val b = ListBuffer.empty[Int]
|
||||
val _ = GraphTraversal.bfs(g, t.visit)
|
||||
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
|
||||
val o2 = GraphTraversal.bfsFold(
|
||||
g,
|
||||
b,
|
||||
(
|
||||
acc,
|
||||
vx
|
||||
) => acc.addOne(vx.ordinal)
|
||||
)
|
||||
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||
assertEquals(o1, expectedVisitOrder)
|
||||
assertEquals(o2.toList, expectedVisitOrder)
|
||||
}
|
||||
|
||||
test("should perform BFS on a DAG") {
|
||||
val n = Size(4)
|
||||
val g = Graph.directed(
|
||||
numberOfVertices = n,
|
||||
edges = 0 -> 1,
|
||||
0 -> 2,
|
||||
1 -> 3,
|
||||
2 -> 3
|
||||
)
|
||||
val expectedVisitOrder = List(0, 1, 2, 3)
|
||||
val t = new Tracker
|
||||
val b = ListBuffer.empty[Int]
|
||||
val _ = GraphTraversal.bfs(g, t.visit)
|
||||
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
|
||||
val o2 = GraphTraversal.bfsFold(
|
||||
g,
|
||||
b,
|
||||
(
|
||||
acc,
|
||||
vx
|
||||
) => acc.addOne(vx.ordinal)
|
||||
)
|
||||
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||
assertEquals(o1, expectedVisitOrder)
|
||||
assertEquals(o2.toList, expectedVisitOrder)
|
||||
}
|
||||
|
||||
test("should perform BFS on a strongly connected graph") {
|
||||
val n = Size(3)
|
||||
val g = Graph.directed(
|
||||
numberOfVertices = n,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 0
|
||||
)
|
||||
val expectedVisitOrder = List(0, 1, 2)
|
||||
val t = new Tracker
|
||||
val b = ListBuffer.empty[Int]
|
||||
val _ = GraphTraversal.bfs(g, t.visit)
|
||||
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
|
||||
val o2 = GraphTraversal.bfsFold(
|
||||
g,
|
||||
b,
|
||||
(
|
||||
acc,
|
||||
vx
|
||||
) => acc.addOne(vx.ordinal)
|
||||
)
|
||||
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||
assertEquals(o1, expectedVisitOrder)
|
||||
assertEquals(o2.toList, expectedVisitOrder)
|
||||
}
|
||||
|
||||
test("should perform BFS on a line") {
|
||||
val n = Size(4)
|
||||
val g = Graph.directed(
|
||||
numberOfVertices = n,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 3
|
||||
)
|
||||
val expectedVisitOrder = List(0, 1, 2, 3)
|
||||
val t = new Tracker
|
||||
val b = ListBuffer.empty[Int]
|
||||
val _ = GraphTraversal.bfs(g, t.visit)
|
||||
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
|
||||
val o2 = GraphTraversal.bfsFold(
|
||||
g,
|
||||
b,
|
||||
(
|
||||
acc,
|
||||
vx
|
||||
) => acc.addOne(vx.ordinal)
|
||||
)
|
||||
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||
assertEquals(o1, expectedVisitOrder)
|
||||
assertEquals(o2.toList, expectedVisitOrder)
|
||||
}
|
||||
|
||||
test("should perform BFS on a larger complex digraph") {
|
||||
val n = Size(10)
|
||||
val g = Graph.directed(
|
||||
numberOfVertices = n,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 3,
|
||||
3 -> 6,
|
||||
4 -> 5,
|
||||
5 -> 6,
|
||||
6 -> 7,
|
||||
7 -> 8,
|
||||
8 -> 9,
|
||||
9 -> 6,
|
||||
6 -> 8
|
||||
)
|
||||
val expectedVisitOrder = List(0, 1, 2, 3, 6, 7, 8, 9, 4, 5)
|
||||
val t = new Tracker
|
||||
val b = ListBuffer.empty[Int]
|
||||
val _ = GraphTraversal.bfs(g, t.visit)
|
||||
val o1 = GraphTraversal.bfs(g, v => v.ordinal)
|
||||
val o2 = GraphTraversal.bfsFold(
|
||||
g,
|
||||
b,
|
||||
(
|
||||
acc,
|
||||
vx
|
||||
) => acc.addOne(vx.ordinal)
|
||||
)
|
||||
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||
assertEquals(o1, expectedVisitOrder)
|
||||
assertEquals(o2.toList, expectedVisitOrder)
|
||||
}
|
||||
|
||||
object BfsTraversalTests:
|
||||
|
||||
trait BasicVisitor:
|
||||
def visit(v: Vertex): Unit
|
||||
|
||||
class Counter extends BasicVisitor:
|
||||
|
||||
var count: Int = 0
|
||||
|
||||
def visit(v: Vertex): Unit =
|
||||
count = count + 1
|
||||
()
|
||||
|
||||
end Counter
|
||||
|
||||
class Tracker extends BasicVisitor:
|
||||
|
||||
private val output: ListBuffer[Int] = ListBuffer.empty
|
||||
|
||||
def snapshot(): List[Int] = output.toList
|
||||
|
||||
def visit(v: Vertex): Unit =
|
||||
val _ = output.addOne(v.ordinal)
|
||||
()
|
||||
|
||||
end Tracker
|
||||
|
||||
end BfsTraversalTests
|
||||
203
modules/core/src/test/scala/gs/graph/v0/DfsTraversalTests.scala
Normal file
203
modules/core/src/test/scala/gs/graph/v0/DfsTraversalTests.scala
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import gs.graph.v0.DfsTraversalTests.Counter
|
||||
import gs.graph.v0.DfsTraversalTests.Tracker
|
||||
import gs.graph.v0.directed.Digraph
|
||||
import munit.*
|
||||
import scala.collection.mutable.ListBuffer
|
||||
|
||||
class DfsTraversalTests extends FunSuite:
|
||||
|
||||
test("should perform DFS on an empty graph") {
|
||||
val c1 = new Counter
|
||||
val _ = GraphTraversal.dfs(graph = UndirectedGraph.Empty, visit = c1.visit)
|
||||
assertEquals(c1.count, 0)
|
||||
|
||||
val c2 = new Counter
|
||||
val _ = GraphTraversal.dfs(graph = Digraph.Empty, visit = c2.visit)
|
||||
assertEquals(c2.count, 0)
|
||||
}
|
||||
|
||||
test("should perform DFS on a graph of size 1") {
|
||||
val c1 = new Counter
|
||||
val _ = GraphTraversal.dfs(graph = UndirectedGraph.Single, visit = c1.visit)
|
||||
assertEquals(c1.count, 1)
|
||||
|
||||
val c2 = new Counter
|
||||
val _ = GraphTraversal.dfs(graph = Digraph.Single, visit = c2.visit)
|
||||
assertEquals(c2.count, 1)
|
||||
}
|
||||
|
||||
test("should perform DFS on a complex directed graph") {
|
||||
// This test case includes:
|
||||
// - Cycles
|
||||
// - Multi-Root (0 and 4 are the roots)
|
||||
val n = Size(6)
|
||||
val g = Graph.directed(
|
||||
numberOfVertices = n,
|
||||
edges = (0, 1),
|
||||
(1, 2),
|
||||
(2, 1),
|
||||
(0, 3),
|
||||
(3, 2),
|
||||
(4, 5)
|
||||
)
|
||||
val expectedVisitOrder = List(0, 1, 2, 3, 4, 5)
|
||||
val t = new Tracker
|
||||
val b = ListBuffer.empty[Int]
|
||||
val _ = GraphTraversal.dfs(g, t.visit)
|
||||
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
|
||||
val o2 = GraphTraversal.dfsFold(
|
||||
g,
|
||||
b,
|
||||
(
|
||||
acc,
|
||||
vx
|
||||
) => acc.addOne(vx.ordinal)
|
||||
)
|
||||
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||
assertEquals(o1, expectedVisitOrder)
|
||||
assertEquals(o2.toList, expectedVisitOrder)
|
||||
}
|
||||
|
||||
test("should perform DFS on a DAG") {
|
||||
val n = Size(4)
|
||||
val g = Graph.directed(
|
||||
numberOfVertices = n,
|
||||
edges = (0, 1),
|
||||
(0, 2),
|
||||
(1, 3),
|
||||
(2, 3)
|
||||
)
|
||||
val expectedVisitOrder = List(0, 1, 3, 2)
|
||||
val t = new Tracker
|
||||
val b = ListBuffer.empty[Int]
|
||||
val _ = GraphTraversal.dfs(g, t.visit)
|
||||
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
|
||||
val o2 = GraphTraversal.dfsFold(
|
||||
g,
|
||||
b,
|
||||
(
|
||||
acc,
|
||||
vx
|
||||
) => acc.addOne(vx.ordinal)
|
||||
)
|
||||
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||
assertEquals(o1, expectedVisitOrder)
|
||||
assertEquals(o2.toList, expectedVisitOrder)
|
||||
}
|
||||
|
||||
test("should perform DFS on a strongly connected graph") {
|
||||
val n = Size(3)
|
||||
val g = Graph.directed(
|
||||
numberOfVertices = n,
|
||||
edges = (0, 1),
|
||||
(1, 2),
|
||||
(2, 0)
|
||||
)
|
||||
val expectedVisitOrder = List(0, 1, 2)
|
||||
val t = new Tracker
|
||||
val b = ListBuffer.empty[Int]
|
||||
val _ = GraphTraversal.dfs(g, t.visit)
|
||||
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
|
||||
val o2 = GraphTraversal.dfsFold(
|
||||
g,
|
||||
b,
|
||||
(
|
||||
acc,
|
||||
vx
|
||||
) => acc.addOne(vx.ordinal)
|
||||
)
|
||||
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||
assertEquals(o1, expectedVisitOrder)
|
||||
assertEquals(o2.toList, expectedVisitOrder)
|
||||
}
|
||||
|
||||
test("should perform DFS on a line") {
|
||||
val n = Size(4)
|
||||
val g = Graph.directed(
|
||||
numberOfVertices = n,
|
||||
edges = (0, 1),
|
||||
(1, 2),
|
||||
(2, 3)
|
||||
)
|
||||
val expectedVisitOrder = List(0, 1, 2, 3)
|
||||
val t = new Tracker
|
||||
val b = ListBuffer.empty[Int]
|
||||
val _ = GraphTraversal.dfs(g, t.visit)
|
||||
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
|
||||
val o2 = GraphTraversal.dfsFold(
|
||||
g,
|
||||
b,
|
||||
(
|
||||
acc,
|
||||
vx
|
||||
) => acc.addOne(vx.ordinal)
|
||||
)
|
||||
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||
assertEquals(o1, expectedVisitOrder)
|
||||
assertEquals(o2.toList, expectedVisitOrder)
|
||||
}
|
||||
|
||||
test("should perform DFS on a larger complex digraph") {
|
||||
val n = Size(10)
|
||||
val g = Graph.directed(
|
||||
numberOfVertices = n,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 3,
|
||||
3 -> 6,
|
||||
4 -> 5,
|
||||
5 -> 6,
|
||||
6 -> 7,
|
||||
7 -> 8,
|
||||
8 -> 9,
|
||||
9 -> 6,
|
||||
6 -> 8
|
||||
)
|
||||
val expectedVisitOrder = List(0, 1, 2, 3, 6, 7, 8, 9, 4, 5)
|
||||
val t = new Tracker
|
||||
val b = ListBuffer.empty[Int]
|
||||
val _ = GraphTraversal.dfs(g, t.visit)
|
||||
val o1 = GraphTraversal.dfs(g, v => v.ordinal)
|
||||
val o2 = GraphTraversal.dfsFold(
|
||||
g,
|
||||
b,
|
||||
(
|
||||
acc,
|
||||
vx
|
||||
) => acc.addOne(vx.ordinal)
|
||||
)
|
||||
assertEquals(t.snapshot(), expectedVisitOrder)
|
||||
assertEquals(o1, expectedVisitOrder)
|
||||
assertEquals(o2.toList, expectedVisitOrder)
|
||||
}
|
||||
|
||||
object DfsTraversalTests:
|
||||
|
||||
trait BasicVisitor:
|
||||
def visit(v: Vertex): Unit
|
||||
|
||||
class Counter extends BasicVisitor:
|
||||
|
||||
var count: Int = 0
|
||||
|
||||
def visit(v: Vertex): Unit =
|
||||
count = count + 1
|
||||
()
|
||||
|
||||
end Counter
|
||||
|
||||
class Tracker extends BasicVisitor:
|
||||
|
||||
private val output: ListBuffer[Int] = ListBuffer.empty
|
||||
|
||||
def snapshot(): List[Int] = output.toList
|
||||
|
||||
def visit(v: Vertex): Unit =
|
||||
val _ = output.addOne(v.ordinal)
|
||||
()
|
||||
|
||||
end Tracker
|
||||
|
||||
end DfsTraversalTests
|
||||
36
modules/core/src/test/scala/gs/graph/v0/EdgeTests.scala
Normal file
36
modules/core/src/test/scala/gs/graph/v0/EdgeTests.scala
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import munit.*
|
||||
import scala.util.Failure
|
||||
import scala.util.Try
|
||||
|
||||
class EdgeTests extends FunSuite:
|
||||
|
||||
test("should represent an edge with two vertexes") {
|
||||
val v1 = Vertex.Zero
|
||||
val v2 = Vertex(1)
|
||||
val v3 = Vertex(3)
|
||||
val edge1 = Edge(v1, v2)
|
||||
val edge2 = Edge(v1 -> v2)
|
||||
val edge3 = Edge(v3, v1)
|
||||
assertEquals(edge1, edge2)
|
||||
assertNotEquals(edge1, edge3)
|
||||
assertEquals(edge1.toString(), s"($v1, $v2)")
|
||||
assertEquals(edge1.toString(), edge2.toString())
|
||||
assertEquals(edge1.hashCode(), edge2.hashCode())
|
||||
assertNotEquals(edge1.toString(), edge3.toString())
|
||||
assertNotEquals(edge1.hashCode(), edge3.hashCode())
|
||||
assertEquals(edge1.from, v1)
|
||||
assertEquals(edge1.to, v2)
|
||||
assertEquals(edge1.equals("unrelated-type"), false)
|
||||
}
|
||||
|
||||
test("should forbid loop edges") {
|
||||
Try(Edge(1, 1)) match
|
||||
case Failure(ex: IllegalArgumentException) =>
|
||||
assertEquals(
|
||||
ex.getMessage(),
|
||||
"Loop edges are not supported. Edges must refer to two distinct vertexes."
|
||||
)
|
||||
case _ => fail("Expected to fail with an IllegalArgumentException")
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import munit.*
|
||||
|
||||
class GraphDispositionTests extends FunSuite:
|
||||
|
||||
test("should parse and match all defined dispositions") {
|
||||
val d1 = GraphDisposition.Directed
|
||||
val d2 = GraphDisposition.Undirected
|
||||
assertEquals(d1, GraphDisposition.Directed)
|
||||
assertEquals(d2, GraphDisposition.Undirected)
|
||||
assertEquals(d1.equals(d2), false)
|
||||
assertEquals(d1.hashCode(), GraphDisposition.Directed.hashCode())
|
||||
assertEquals(d2.hashCode(), GraphDisposition.Undirected.hashCode())
|
||||
assertEquals(d1.toString(), GraphDisposition.Directed.name)
|
||||
assertEquals(d2.toString(), GraphDisposition.Undirected.name)
|
||||
assertEquals(d1.equals(null), false)
|
||||
assertEquals(d1.equals("foo"), false)
|
||||
assertEquals(d1.equals(1), false)
|
||||
|
||||
val p1 = GraphDisposition.parse(d1.name)
|
||||
val p2 = GraphDisposition.parse(d2.name)
|
||||
val p3 = GraphDisposition.parse("something else")
|
||||
|
||||
assertEquals(p3, None)
|
||||
assertEquals(p1, Some(d1))
|
||||
assertEquals(p2, Some(d2))
|
||||
}
|
||||
53
modules/core/src/test/scala/gs/graph/v0/SizeTests.scala
Normal file
53
modules/core/src/test/scala/gs/graph/v0/SizeTests.scala
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import munit.*
|
||||
import scala.util.Try
|
||||
|
||||
class SizeTests extends FunSuite:
|
||||
|
||||
test("should represent a vertex - some integer >= 0") {
|
||||
val s1 = Size.Zero
|
||||
val s2 = Size(0)
|
||||
val s3 = Size(3)
|
||||
assertEquals(s1, s2)
|
||||
assertNotEquals(s1, s3)
|
||||
assertEquals(s1.compare(s2), 0)
|
||||
assertEquals(s1.compare(s3), -3)
|
||||
assertEquals(s1.equals("unrelated-type"), false)
|
||||
assertEquals(s1.hashCode(), s2.hashCode())
|
||||
assertNotEquals(s1.hashCode(), s3.hashCode())
|
||||
assertEquals(s1 < s3, true)
|
||||
assertEquals(s1 < s2, false)
|
||||
assertEquals(s1 > s3, false)
|
||||
assertEquals(s3 > s1, true)
|
||||
assertEquals(s1 < 3, true)
|
||||
assertEquals(s1 < 0, false)
|
||||
assertEquals(s1 > 0, false)
|
||||
assertEquals(s3 > 0, true)
|
||||
assertEquals(s1 < Vertex(3), true)
|
||||
assertEquals(s1 < Vertex.Zero, false)
|
||||
assertEquals(s1 > Vertex.Zero, false)
|
||||
assertEquals(s3 > Vertex.Zero, true)
|
||||
assertEquals(s1.toString(), s2.toString())
|
||||
assertNotEquals(s1.toString(), s3.toString())
|
||||
assertEquals(Size.Zero.value, 0)
|
||||
assertEquals(Size.One.value, 1)
|
||||
}
|
||||
|
||||
test("should throw an exception if an invalid value is given") {
|
||||
Try(Size(-1)).toEither.left.toOption match
|
||||
case None => fail("Size instantiation should have failed.")
|
||||
case Some(cause) =>
|
||||
assertEquals(cause.getMessage(), "Size values must be 0 or greater.")
|
||||
}
|
||||
|
||||
test("should instantiate from arrays based on the array length") {
|
||||
val n1 = 3
|
||||
val n2 = 5
|
||||
val a1 = Array.fill(n1)(0)
|
||||
val a2 = Array.fill(n2)(0)
|
||||
val s1 = Size.fromArray(a1)
|
||||
val s2 = Size.fromArray(a2)
|
||||
assertEquals(s1.value, n1)
|
||||
assertEquals(s2.value, n2)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import munit.*
|
||||
|
||||
class UndirectedGraphTests extends FunSuite:
|
||||
|
||||
test("should support an empty graph") {
|
||||
val g = UndirectedGraph.Empty
|
||||
assertEquals(g.numberOfVertices, Size.Zero)
|
||||
assertEquals(g.adjacency, Adjacency.Empty)
|
||||
assertEquals(g.disposition, GraphDisposition.Undirected)
|
||||
assertEquals(g.selectRoots(), Vector.empty)
|
||||
}
|
||||
|
||||
test("should construct a graph based on # of edges and edge list") {
|
||||
val N = Size(3)
|
||||
val g = Graph.undirected(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
0 -> 2
|
||||
)
|
||||
|
||||
assertEquals(g.numberOfVertices, N)
|
||||
assertEquals(g.neighbors(Vertex(0)), Vector(Vertex(1), Vertex(2)))
|
||||
assertEquals(g.neighbors(Vertex(4)), Vector.empty)
|
||||
assertEquals(g.selectRoots(), Vector(Vertex.Zero))
|
||||
}
|
||||
|
||||
test("should construct a graph based on edge list") {
|
||||
val N = Size(3)
|
||||
val g = Graph.undirected(
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
0 -> 2
|
||||
)
|
||||
|
||||
assertEquals(g.numberOfVertices, N)
|
||||
assertEquals(g.neighbors(Vertex(0)), Vector(Vertex(1), Vertex(2)))
|
||||
assertEquals(g.neighbors(Vertex(4)), Vector.empty)
|
||||
assertEquals(g.selectRoots(), Vector(Vertex.Zero))
|
||||
}
|
||||
|
||||
test("should calculate roots for each connected component") {
|
||||
val N = Size(10)
|
||||
val g = Graph.undirected(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 3,
|
||||
4 -> 5,
|
||||
5 -> 6,
|
||||
4 -> 6,
|
||||
7 -> 8
|
||||
)
|
||||
val expectedRoots = Vertex.of(0, 4, 7, 9)
|
||||
|
||||
assertEquals(g.roots, expectedRoots)
|
||||
}
|
||||
82
modules/core/src/test/scala/gs/graph/v0/VertexTests.scala
Normal file
82
modules/core/src/test/scala/gs/graph/v0/VertexTests.scala
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package gs.graph.v0
|
||||
|
||||
import munit.*
|
||||
import scala.util.Try
|
||||
|
||||
class VertexTests extends FunSuite:
|
||||
|
||||
test("should represent a vertex - some integer >= 0") {
|
||||
val v1 = Vertex.Zero
|
||||
val v2 = Vertex(0)
|
||||
val v3 = Vertex(3)
|
||||
assertEquals(v1, v2)
|
||||
assertNotEquals(v1, v3)
|
||||
assertEquals(v1.compare(v2), 0)
|
||||
assertEquals(v1.compare(v3), -3)
|
||||
assertEquals(v1.equals("unrelated-type"), false)
|
||||
assertEquals(v1.hashCode(), v2.hashCode())
|
||||
assertNotEquals(v1.hashCode(), v3.hashCode())
|
||||
assertEquals(v1 < v3, true)
|
||||
assertEquals(v1 <= v3, true)
|
||||
assertEquals(v1 < v2, false)
|
||||
assertEquals(v1 <= v2, true)
|
||||
assertEquals(v1 > v3, false)
|
||||
assertEquals(v1 >= v3, false)
|
||||
assertEquals(v3 > v1, true)
|
||||
assertEquals(v3 >= v1, true)
|
||||
assertEquals(v2 >= v1, true)
|
||||
assertEquals(v2 > v1, false)
|
||||
assertEquals(v1 < 3, true)
|
||||
assertEquals(v1 <= 3, true)
|
||||
assertEquals(v1 < 0, false)
|
||||
assertEquals(v1 <= 0, true)
|
||||
assertEquals(v1 > 0, false)
|
||||
assertEquals(v1 >= 0, true)
|
||||
assertEquals(v3 > 0, true)
|
||||
assertEquals(v3 >= 0, true)
|
||||
assertEquals(v1 < Size(3), true)
|
||||
assertEquals(v1 <= Size(3), true)
|
||||
assertEquals(v1 < Size.Zero, false)
|
||||
assertEquals(v1 <= Size.Zero, true)
|
||||
assertEquals(v1 > Size.Zero, false)
|
||||
assertEquals(v1 >= Size.Zero, true)
|
||||
assertEquals(v3 > Size.Zero, true)
|
||||
assertEquals(v3 >= Size.Zero, true)
|
||||
assertEquals(v1.toString(), v2.toString())
|
||||
assertNotEquals(v1.toString(), v3.toString())
|
||||
assertEquals(v1.compare(v3), -3)
|
||||
assertEquals(v1.compare(v2), 0)
|
||||
assertEquals(v1.value, 0)
|
||||
assertEquals(v2.value, 0)
|
||||
assertEquals(v3.value, 3)
|
||||
}
|
||||
|
||||
test("should throw an exception if an invalid value is given") {
|
||||
Try(Vertex(-1)).toEither.left.toOption match
|
||||
case None => fail("Vertex instantiation should have failed.")
|
||||
case Some(cause) =>
|
||||
assertEquals(cause.getMessage(), "Vertex values must be 0 or greater.")
|
||||
}
|
||||
|
||||
test("should instantiate a list of vertices") {
|
||||
val vs = Vertex.list(0, 1, 2, 3)
|
||||
assertEquals(vs, List(0, 1, 2, 3).map(Vertex(_)))
|
||||
}
|
||||
|
||||
test("should instantiate a set of vertices") {
|
||||
val vs = Vertex.set(0, 1, 2, 3, 0, 1, 2, 3)
|
||||
assertEquals(vs, Set(0, 1, 2, 3).map(Vertex(_)))
|
||||
}
|
||||
|
||||
test("should support the ordering type class") {
|
||||
val o = Ordering[Vertex]
|
||||
val v1 = Vertex(0)
|
||||
val v2 = Vertex(0)
|
||||
val v3 = Vertex(1)
|
||||
val v4 = Vertex(2)
|
||||
assertEquals(o.compare(v1, v2), 0)
|
||||
assertEquals(o.compare(v1, v3), -1)
|
||||
assertEquals(o.compare(v1, v4), -2)
|
||||
assertEquals(o.compare(v4, v1), 2)
|
||||
assertEquals(o.compare(v4, v3), 1)
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package gs.graph.v0.directed
|
|||
import gs.graph.v0.Adjacency
|
||||
import gs.graph.v0.Edge
|
||||
import gs.graph.v0.Size
|
||||
import gs.graph.v0.Vertex
|
||||
import munit.*
|
||||
|
||||
class DagTests extends FunSuite:
|
||||
|
|
@ -21,42 +20,52 @@ class DagTests extends FunSuite:
|
|||
fail("Expected a graph with one vertex to be validated as a DAG.")
|
||||
}
|
||||
|
||||
test("should not have cycles by definition") {
|
||||
Dag.validate(Digraph.Single) match
|
||||
case Right(dag) =>
|
||||
assertEquals(Digraph.hasCycle(dag), false)
|
||||
assertEquals(dag.hasCycle, false)
|
||||
case _ =>
|
||||
fail("Expected a graph with one vertex to be validated as a DAG.")
|
||||
}
|
||||
|
||||
test("should validate a single-root graph") {
|
||||
val size = Size(8)
|
||||
val vs = (0 until size.value).map(Vertex(_))
|
||||
val digraph: Digraph = Digraph.fromAdjacency(
|
||||
Adjacency.fromDirectedEdges(
|
||||
Adjacency.fromEdges(
|
||||
numberOfVertices = size,
|
||||
edges = Seq(
|
||||
Edge(vs(0) -> vs(1)),
|
||||
Edge(vs(0) -> vs(2)),
|
||||
Edge(vs(0) -> vs(3)),
|
||||
Edge(vs(1) -> vs(4)),
|
||||
Edge(vs(2) -> vs(4)),
|
||||
Edge(vs(3) -> vs(5)),
|
||||
Edge(vs(4) -> vs(6)),
|
||||
Edge(vs(5) -> vs(6)),
|
||||
Edge(vs(6) -> vs(7))
|
||||
edges = Edge.list(
|
||||
0 -> 1,
|
||||
0 -> 2,
|
||||
0 -> 3,
|
||||
1 -> 4,
|
||||
2 -> 4,
|
||||
3 -> 5,
|
||||
4 -> 6,
|
||||
5 -> 6,
|
||||
6 -> 7
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Dag.validate(digraph) match
|
||||
case Right(dag) => assertEquals(dag, digraph)
|
||||
case Right(dag) =>
|
||||
assertEquals(dag, digraph)
|
||||
assertEquals(Digraph.hasCycle(dag), false)
|
||||
assertEquals(dag.hasCycle, false)
|
||||
case _ => fail("Expected a single-root graph to be validated as a DAG.")
|
||||
}
|
||||
|
||||
test("should NOT validate a single-root digraph with a cycle") {
|
||||
val size = Size(4)
|
||||
val vs = (0 until size.value).map(Vertex(_))
|
||||
val digraph: Digraph = Digraph.fromAdjacency(
|
||||
Adjacency.fromDirectedEdges(
|
||||
Adjacency.fromEdges(
|
||||
numberOfVertices = size,
|
||||
edges = Seq(
|
||||
Edge(vs(0) -> vs(1)),
|
||||
Edge(vs(1) -> vs(2)),
|
||||
Edge(vs(2) -> vs(3)),
|
||||
Edge(vs(3) -> vs(1))
|
||||
edges = Edge.list(
|
||||
0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 3,
|
||||
3 -> 1
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
@ -68,3 +77,8 @@ class DagTests extends FunSuite:
|
|||
)
|
||||
case _ => ()
|
||||
}
|
||||
|
||||
test("should provide empty and single graphs") {
|
||||
assertEquals(Dag.Empty, Digraph.Empty)
|
||||
assertEquals(Dag.Single, Digraph.Single)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
package gs.graph.v0.directed
|
||||
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.Size
|
||||
import gs.graph.v0.Vertex
|
||||
import munit.*
|
||||
|
||||
class DigraphTests extends FunSuite:
|
||||
|
||||
test("should provide an empty graph") {
|
||||
val dg = Digraph.Empty
|
||||
assertEquals(dg.numberOfVertices, Size.Zero)
|
||||
assertEquals(dg.isSingle, false)
|
||||
assertEquals(dg.roots, Vector.empty)
|
||||
assertEquals(dg.isEmpty, true)
|
||||
assertEquals(dg.neighbors(Vertex.Zero), Vector.empty)
|
||||
assertEquals(dg.hasCycle, false)
|
||||
}
|
||||
|
||||
test("should provide a single-node graph") {
|
||||
val dg = Digraph.Single
|
||||
assertEquals(dg.numberOfVertices, Size.One)
|
||||
assertEquals(dg.isSingle, true)
|
||||
assertEquals(dg.roots, Vertex.of(0))
|
||||
assertEquals(dg.isEmpty, false)
|
||||
assertEquals(dg.neighbors(Vertex.Zero), Vector.empty)
|
||||
assertEquals(dg.hasCycle, false)
|
||||
}
|
||||
|
||||
test("should not equal non-digraph types") {
|
||||
val dg = Digraph.Empty
|
||||
assertEquals(dg.equals(null), false)
|
||||
assertEquals(dg.equals("foo"), false)
|
||||
assertEquals(dg.equals(1), false)
|
||||
}
|
||||
|
||||
test("should not equal a digraph of a different size") {
|
||||
assertNotEquals(Digraph.Empty, Digraph.Single)
|
||||
}
|
||||
|
||||
test("should not equal a digraph with different edges") {
|
||||
val N = Size(3)
|
||||
val d1 = Graph.directed(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2
|
||||
)
|
||||
val d2 = Graph.directed(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
0 -> 2
|
||||
)
|
||||
assertNotEquals(d1, d2)
|
||||
}
|
||||
|
||||
test(
|
||||
"should detect cycles where a single component has a cycle back to the root"
|
||||
) {
|
||||
val N = Size(3)
|
||||
val dg = Graph.directed(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 0
|
||||
)
|
||||
assertEquals(dg.hasCycle, true)
|
||||
}
|
||||
|
||||
test("should detect cycles where one component does not have a cycle") {
|
||||
val N = Size(6)
|
||||
val dg = Graph.directed(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
3 -> 4,
|
||||
4 -> 5,
|
||||
5 -> 3
|
||||
)
|
||||
assertEquals(dg.hasCycle, true)
|
||||
}
|
||||
|
||||
test("should detect cycles within a subset of a component") {
|
||||
val N = Size(3)
|
||||
val dg = Graph.directed(
|
||||
numberOfVertices = N,
|
||||
edges = 0 -> 1,
|
||||
1 -> 2,
|
||||
2 -> 1
|
||||
)
|
||||
assertEquals(dg.hasCycle, true)
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package gs.graph.v0.directed
|
||||
|
||||
import gs.graph.v0.Adjacency
|
||||
import gs.graph.v0.Edge
|
||||
import gs.graph.v0.Graph
|
||||
import gs.graph.v0.Size
|
||||
import gs.graph.v0.Vertex
|
||||
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 = StronglyConnectedComponent(v0)
|
||||
val s2 = StronglyConnectedComponent(v0, v1, v2)
|
||||
val s3 = StronglyConnectedComponent(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 = Digraph.Empty
|
||||
val sccs = StronglyConnectedComponent.tarjan(g)
|
||||
val alt = StronglyConnectedComponent.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.directed(Size.One)
|
||||
val sccs = StronglyConnectedComponent.findAll(g)
|
||||
val expected = StronglyConnectedComponent.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 = StronglyConnectedComponent.findAll(g)
|
||||
assertEquals(g.roots, Vector(Vertex.Zero))
|
||||
assertEquals(
|
||||
sccs.toSet,
|
||||
Set(
|
||||
StronglyConnectedComponent.of(0),
|
||||
StronglyConnectedComponent.of(1),
|
||||
StronglyConnectedComponent.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 = StronglyConnectedComponent.findAll(g)
|
||||
assertEquals(g.roots, Vector.empty)
|
||||
assertEquals(
|
||||
sccs.toSet,
|
||||
Set(
|
||||
StronglyConnectedComponent.of(0, 1, 2),
|
||||
StronglyConnectedComponent.of(3, 4, 5),
|
||||
StronglyConnectedComponent.of(6, 7, 8)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
end TarjansTests
|
||||
|
|
@ -1 +1 @@
|
|||
sbt.version=1.12.8
|
||||
sbt.version=1.12.11
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue