(minor) WIP continuing to fix, refine, refactor, test
This commit is contained in:
parent
95e9ec5e08
commit
117bff5242
15 changed files with 467 additions and 74 deletions
|
|
@ -35,6 +35,12 @@ val Deps = new {
|
||||||
}
|
}
|
||||||
|
|
||||||
val Gs = 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 Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.4.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,6 +67,7 @@ lazy val core = project
|
||||||
.settings(sharedSettings)
|
.settings(sharedSettings)
|
||||||
.settings(testSettings)
|
.settings(testSettings)
|
||||||
.settings(name := s"${gsProjectName.value}-core-v${semVerMajor.value}")
|
.settings(name := s"${gsProjectName.value}-core-v${semVerMajor.value}")
|
||||||
|
.settings(libraryDependencies ++= Seq(Deps.Gs.Std.Core))
|
||||||
|
|
||||||
lazy val cats = project
|
lazy val cats = project
|
||||||
.in(file("modules/cats"))
|
.in(file("modules/cats"))
|
||||||
|
|
|
||||||
|
|
@ -133,10 +133,10 @@ object Adjacency:
|
||||||
): Adjacency =
|
): Adjacency =
|
||||||
val buffs = Vector.fill(numberOfVertices.value)(ListBuffer.empty[Vertex])
|
val buffs = Vector.fill(numberOfVertices.value)(ListBuffer.empty[Vertex])
|
||||||
val _ = edges.foreach { edge =>
|
val _ = edges.foreach { edge =>
|
||||||
if edge.v1 >= numberOfVertices || edge.v2 >= numberOfVertices then
|
if edge.v1 >= numberOfVertices then
|
||||||
throw new IllegalArgumentException(
|
throw GraphException.VertexExceedsGraphBounds(edge.v1, numberOfVertices)
|
||||||
s"Edge (${edge.v1}, ${edge.v2}) is out of bounds. Maximum vertex value is ${numberOfVertices.value - 1}"
|
else if edge.v2 >= numberOfVertices then
|
||||||
)
|
throw GraphException.VertexExceedsGraphBounds(edge.v2, numberOfVertices)
|
||||||
else
|
else
|
||||||
val _ = buffs(edge.from.ordinal).addOne(edge.to)
|
val _ = buffs(edge.from.ordinal).addOne(edge.to)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
package gs.graph.v0
|
package gs.graph.v0
|
||||||
|
|
||||||
|
import gs.graph.v0.directed.Digraph
|
||||||
|
|
||||||
/** Graph representation based on an adjacency list.
|
/** Graph representation based on an adjacency list.
|
||||||
*
|
*
|
||||||
* See:
|
* See:
|
||||||
|
|
@ -32,6 +34,11 @@ trait Graph:
|
||||||
*/
|
*/
|
||||||
def isEmpty: Boolean = numberOfVertices == Size.Zero
|
def isEmpty: Boolean = numberOfVertices == Size.Zero
|
||||||
|
|
||||||
|
/** @return
|
||||||
|
* True if this graph has one vertex, false otherwise.
|
||||||
|
*/
|
||||||
|
def isSingle: Boolean = numberOfVertices == Size.One
|
||||||
|
|
||||||
/** @return
|
/** @return
|
||||||
* The roots that should be used for traversal operations.
|
* The roots that should be used for traversal operations.
|
||||||
*/
|
*/
|
||||||
|
|
@ -95,4 +102,22 @@ object Graph:
|
||||||
adj
|
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
|
end Graph
|
||||||
|
|
|
||||||
|
|
@ -19,22 +19,20 @@ class UndirectedGraph(
|
||||||
*/
|
*/
|
||||||
override def selectRoots(): Vector[Vertex] = roots
|
override def selectRoots(): Vector[Vertex] = roots
|
||||||
|
|
||||||
/** The roots of an undirected graph are identified by arbitrarily selecting
|
/** The lazily calculated roots of this graph.
|
||||||
* the first listed vertex from each strongly connected component in the
|
*
|
||||||
* 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] =
|
lazy val roots: Vector[Vertex] =
|
||||||
calculateRoots()
|
calculateRoots()
|
||||||
|
|
||||||
private def calculateRoots(): Vector[Vertex] =
|
private def calculateRoots(): Vector[Vertex] =
|
||||||
if numberOfVertices == Size.Zero then Vector.empty
|
if numberOfVertices == Size.Zero then Vector.empty
|
||||||
else oneRootFromEachSCC(calculateSCCs()).toVector
|
else oneFromEachConnectedComponent().toVector
|
||||||
|
|
||||||
private def calculateSCCs(): List[SCC] =
|
private def oneFromEachConnectedComponent(): List[Vertex] =
|
||||||
SCC.findAll(this)
|
ConnectedComponent.findAll(this).map(_.root)
|
||||||
|
|
||||||
private def oneRootFromEachSCC(sccs: List[SCC]): List[Vertex] =
|
|
||||||
sccs.map(_.root)
|
|
||||||
|
|
||||||
end UndirectedGraph
|
end UndirectedGraph
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,24 @@ object Vertex:
|
||||||
else
|
else
|
||||||
throw new IllegalArgumentException("Vertex values must be 0 or greater.")
|
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 CanEqual[Vertex, Vertex] = CanEqual.derived
|
||||||
|
|
||||||
given Ordering[Vertex] with
|
given Ordering[Vertex] with
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,16 @@ class Dag protected (
|
||||||
|
|
||||||
object Dag:
|
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, Dag] = CanEqual.derived
|
||||||
given CanEqual[Dag, Digraph] = CanEqual.derived
|
given CanEqual[Dag, Digraph] = CanEqual.derived
|
||||||
given CanEqual[Digraph, Dag] = CanEqual.derived
|
given CanEqual[Digraph, Dag] = CanEqual.derived
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,12 @@ class Digraph(
|
||||||
case other: Digraph =>
|
case other: Digraph =>
|
||||||
numberOfVertices == other.numberOfVertices
|
numberOfVertices == other.numberOfVertices
|
||||||
&& adjacency == other.adjacency
|
&& adjacency == other.adjacency
|
||||||
&& roots.sameElements(other.roots)
|
|
||||||
case _ => false
|
case _ => false
|
||||||
|
|
||||||
|
/** `True` if this directed graph has a cycle, `false` otherwise.
|
||||||
|
*/
|
||||||
|
lazy val hasCycle: Boolean = Digraph.hasCycle(this)
|
||||||
|
|
||||||
end Digraph
|
end Digraph
|
||||||
|
|
||||||
object Digraph:
|
object Digraph:
|
||||||
|
|
@ -122,6 +125,7 @@ object Digraph:
|
||||||
def hasCycle(digraph: Digraph): Boolean =
|
def hasCycle(digraph: Digraph): Boolean =
|
||||||
digraph match
|
digraph match
|
||||||
case _ if digraph.isEmpty => false
|
case _ if digraph.isEmpty => false
|
||||||
|
case _ if digraph.isSingle => false
|
||||||
case _: Dag => false
|
case _: Dag => false
|
||||||
case _ => new CycleDetector(digraph).hasCycle
|
case _ => new CycleDetector(digraph).hasCycle
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package gs.graph.v0
|
package gs.graph.v0.directed
|
||||||
|
|
||||||
import gs.graph.v0.Graph
|
|
||||||
import gs.graph.v0.Vertex
|
import gs.graph.v0.Vertex
|
||||||
import scala.collection.mutable.ListBuffer
|
import scala.collection.mutable.ListBuffer
|
||||||
import scala.collection.mutable.Stack
|
import scala.collection.mutable.Stack
|
||||||
|
|
@ -8,14 +7,19 @@ 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.
|
* 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 SCC private (val value: Set[Vertex]):
|
final class StronglyConnectedComponent private (val value: Set[Vertex]):
|
||||||
|
|
||||||
/** @inheritDocs
|
/** @inheritDocs
|
||||||
*/
|
*/
|
||||||
override def equals(obj: Any): Boolean =
|
override def equals(obj: Any): Boolean =
|
||||||
obj match
|
obj match
|
||||||
case other: SCC => value.equals(other.value)
|
case other: StronglyConnectedComponent => value.equals(other.value)
|
||||||
case _ => false
|
case _ => false
|
||||||
|
|
||||||
/** @inheritDocs
|
/** @inheritDocs
|
||||||
|
|
@ -50,9 +54,9 @@ final class SCC private (val value: Set[Vertex]):
|
||||||
*/
|
*/
|
||||||
lazy val root: Vertex = value.toList.head
|
lazy val root: Vertex = value.toList.head
|
||||||
|
|
||||||
end SCC
|
end StronglyConnectedComponent
|
||||||
|
|
||||||
object SCC:
|
object StronglyConnectedComponent:
|
||||||
|
|
||||||
/** Instantiate a new SCC from the given collection of [[Vertex]].
|
/** Instantiate a new SCC from the given collection of [[Vertex]].
|
||||||
*
|
*
|
||||||
|
|
@ -63,19 +67,19 @@ object SCC:
|
||||||
* @return
|
* @return
|
||||||
* The new SCC.
|
* The new SCC.
|
||||||
*/
|
*/
|
||||||
def apply(value: Vertex*): SCC =
|
def apply(value: Vertex*): StronglyConnectedComponent =
|
||||||
if value.isEmpty then
|
if value.isEmpty then
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Empty strongly connected components (SCC) are not allowed."
|
"Empty strongly connected components (SCC) are not allowed."
|
||||||
)
|
)
|
||||||
else new SCC(value.toSet)
|
else new StronglyConnectedComponent(value.toSet)
|
||||||
|
|
||||||
def of(value: Int*): SCC =
|
def of(value: Int*): StronglyConnectedComponent =
|
||||||
if value.isEmpty then
|
if value.isEmpty then
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Empty strongly connected components (SCC) are not allowed."
|
"Empty strongly connected components (SCC) are not allowed."
|
||||||
)
|
)
|
||||||
else new SCC(value.map(Vertex.apply).toSet)
|
else new StronglyConnectedComponent(value.map(Vertex.apply).toSet)
|
||||||
|
|
||||||
/** Instantiate a new SCC that contains a single [[Vertex]].
|
/** Instantiate a new SCC that contains a single [[Vertex]].
|
||||||
*
|
*
|
||||||
|
|
@ -84,9 +88,11 @@ object SCC:
|
||||||
* @return
|
* @return
|
||||||
* The new SCC.
|
* The new SCC.
|
||||||
*/
|
*/
|
||||||
def single(value: Vertex): SCC = new SCC(Set(value))
|
def single(value: Vertex): StronglyConnectedComponent =
|
||||||
|
new StronglyConnectedComponent(Set(value))
|
||||||
|
|
||||||
given CanEqual[SCC, SCC] = CanEqual.derived
|
given CanEqual[StronglyConnectedComponent, StronglyConnectedComponent] =
|
||||||
|
CanEqual.derived
|
||||||
|
|
||||||
/** Alias for `findAll`.
|
/** Alias for `findAll`.
|
||||||
*
|
*
|
||||||
|
|
@ -101,7 +107,7 @@ object SCC:
|
||||||
* @return
|
* @return
|
||||||
* The complete list of [[SCC]] for the input graph.
|
* The complete list of [[SCC]] for the input graph.
|
||||||
*/
|
*/
|
||||||
def tarjan(g: Graph): List[SCC] = findAll(g)
|
def tarjan(g: Digraph): List[StronglyConnectedComponent] = findAll(g)
|
||||||
|
|
||||||
/** 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.
|
||||||
|
|
@ -114,7 +120,7 @@ object SCC:
|
||||||
* @return
|
* @return
|
||||||
* The complete list of [[SCC]] for the input graph.
|
* The complete list of [[SCC]] for the input graph.
|
||||||
*/
|
*/
|
||||||
def findAll(g: Graph): List[SCC] =
|
def findAll(g: Digraph): List[StronglyConnectedComponent] =
|
||||||
val V = g.numberOfVertices.value
|
val V = g.numberOfVertices.value
|
||||||
|
|
||||||
// Vertex numbers.
|
// Vertex numbers.
|
||||||
|
|
@ -139,7 +145,7 @@ object SCC:
|
||||||
val counter = new Counter(0)
|
val counter = new Counter(0)
|
||||||
|
|
||||||
// List of output SCCs.
|
// List of output SCCs.
|
||||||
val sccs = ListBuffer.empty[SCC]
|
val sccs = ListBuffer.empty[StronglyConnectedComponent]
|
||||||
|
|
||||||
(0 until V).foreach { v =>
|
(0 until V).foreach { v =>
|
||||||
if !visited(v) then
|
if !visited(v) then
|
||||||
|
|
@ -151,7 +157,7 @@ object SCC:
|
||||||
sccs.toList
|
sccs.toList
|
||||||
|
|
||||||
private def tarjanDfs(
|
private def tarjanDfs(
|
||||||
g: Graph,
|
g: Digraph,
|
||||||
v: Int,
|
v: Int,
|
||||||
num: Array[Int],
|
num: Array[Int],
|
||||||
lowest: Array[Int],
|
lowest: Array[Int],
|
||||||
|
|
@ -159,7 +165,7 @@ object SCC:
|
||||||
processed: Array[Boolean],
|
processed: Array[Boolean],
|
||||||
s: Stack[Vertex],
|
s: Stack[Vertex],
|
||||||
counter: Counter,
|
counter: Counter,
|
||||||
output: ListBuffer[SCC]
|
output: ListBuffer[StronglyConnectedComponent]
|
||||||
): Unit =
|
): Unit =
|
||||||
// Convenience integer representation for array access.
|
// Convenience integer representation for array access.
|
||||||
val V = Vertex(v)
|
val V = Vertex(v)
|
||||||
|
|
@ -195,7 +201,8 @@ object SCC:
|
||||||
sccVertex = s.pop()
|
sccVertex = s.pop()
|
||||||
|
|
||||||
scc.addOne(sccVertex)
|
scc.addOne(sccVertex)
|
||||||
if !scc.isEmpty then output.addOne(new SCC(scc.toSet))
|
if !scc.isEmpty then
|
||||||
|
output.addOne(new StronglyConnectedComponent(scc.toSet))
|
||||||
else println("ERROR")
|
else println("ERROR")
|
||||||
else ()
|
else ()
|
||||||
|
|
||||||
|
|
@ -213,4 +220,4 @@ object SCC:
|
||||||
|
|
||||||
end Counter
|
end Counter
|
||||||
|
|
||||||
end SCC
|
end StronglyConnectedComponent
|
||||||
|
|
@ -1,10 +1,26 @@
|
||||||
package gs.graph.v0.syntax
|
package gs.graph.v0.syntax
|
||||||
|
|
||||||
|
import gs.graph.v0.Edge
|
||||||
import gs.graph.v0.Graph
|
import gs.graph.v0.Graph
|
||||||
import gs.graph.v0.GraphTraversal
|
import gs.graph.v0.GraphTraversal
|
||||||
import gs.graph.v0.Vertex
|
import gs.graph.v0.Vertex
|
||||||
import gs.graph.v0.data.AnyGraphWithData
|
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)
|
extension (graph: Graph)
|
||||||
|
|
||||||
/** Visit vertex node in this graph, using Depth-First Search (DFS) as the
|
/** Visit vertex node in this graph, using Depth-First Search (DFS) as the
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
package gs.graph.v0
|
package gs.graph.v0
|
||||||
|
|
||||||
|
import gs.graph.v0.syntax.V
|
||||||
import scala.util.Failure
|
import scala.util.Failure
|
||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
|
||||||
class AdjacencyTests extends munit.FunSuite:
|
class AdjacencyTests extends munit.FunSuite:
|
||||||
|
|
||||||
private val size = Size(7)
|
private val size = Size(7)
|
||||||
private val vs = (0 until size.value).map(Vertex(_)).toArray
|
|
||||||
|
|
||||||
private val edges = Edge.list(
|
private val edges = Edge.list(
|
||||||
0 -> 1,
|
0 -> 1,
|
||||||
|
|
@ -32,13 +32,13 @@ class AdjacencyTests extends munit.FunSuite:
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should provide incoming connections") {
|
test("should provide incoming connections") {
|
||||||
assertEquals(adj.incoming(vs(0)), Vector.empty)
|
assertEquals(adj.incoming(V(0)), Vector.empty)
|
||||||
assertEquals(adj.incoming(vs(1)), Vector(vs(0)))
|
assertEquals(adj.incoming(V(1)), Vertex.of(0))
|
||||||
assertEquals(adj.incoming(vs(2)), Vector(vs(0)))
|
assertEquals(adj.incoming(V(2)), Vertex.of(0))
|
||||||
assertEquals(adj.incoming(vs(3)), Vector(vs(0)))
|
assertEquals(adj.incoming(V(3)), Vertex.of(0))
|
||||||
assertEquals(adj.incoming(vs(4)), Vector(vs(1), vs(2), vs(3)))
|
assertEquals(adj.incoming(V(4)), Vertex.of(1, 2, 3))
|
||||||
assertEquals(adj.incoming(vs(5)), Vector(vs(3)))
|
assertEquals(adj.incoming(V(5)), Vertex.of(3))
|
||||||
assertEquals(adj.incoming(vs(6)), Vector(vs(4)))
|
assertEquals(adj.incoming(V(6)), Vertex.of(4))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should show no incoming connections for a vertex not in the graph") {
|
test("should show no incoming connections for a vertex not in the graph") {
|
||||||
|
|
@ -66,19 +66,32 @@ class AdjacencyTests extends munit.FunSuite:
|
||||||
}
|
}
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"should guard against out of bound values when building from edges with an explicit bound"
|
"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 n = Size(2)
|
||||||
val es = Edge.list(0 -> 3)
|
val es = Edge.list(0 -> 3)
|
||||||
Try(Adjacency.fromEdges(n, es)) match
|
Try(Adjacency.fromEdges(n, es)) match
|
||||||
case Failure(ex: IllegalArgumentException) =>
|
case Failure(ex: GraphException.VertexExceedsGraphBounds) =>
|
||||||
assertEquals(
|
assertEquals(ex.vertex, Vertex(3))
|
||||||
ex.getMessage(),
|
assertEquals(ex.bound, n)
|
||||||
"Edge (0, 3) is out of bounds. Maximum vertex value is 1"
|
|
||||||
)
|
|
||||||
case _ =>
|
case _ =>
|
||||||
fail(
|
fail(
|
||||||
"Expected adjacency construction to fail with an IllegalArgumentException."
|
"Expected adjacency construction to fail with a GraphException."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,48 @@ class UndirectedGraphTests extends FunSuite:
|
||||||
assertEquals(g.selectRoots(), Vector.empty)
|
assertEquals(g.selectRoots(), Vector.empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
test(
|
test("should construct a graph based on # of edges and edge list") {
|
||||||
"should calculate roots based on an arbitrary vertex from each connected component"
|
val N = Size(3)
|
||||||
) {
|
val g = Graph.undirected(
|
||||||
// TODO: implement - current logic is wrong.
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,15 @@ class DagTests extends FunSuite:
|
||||||
fail("Expected a graph with one vertex to be validated as a DAG.")
|
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") {
|
test("should validate a single-root graph") {
|
||||||
val size = Size(8)
|
val size = Size(8)
|
||||||
val digraph: Digraph = Digraph.fromAdjacency(
|
val digraph: Digraph = Digraph.fromAdjacency(
|
||||||
|
|
@ -40,7 +49,10 @@ class DagTests extends FunSuite:
|
||||||
)
|
)
|
||||||
|
|
||||||
Dag.validate(digraph) match
|
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.")
|
case _ => fail("Expected a single-root graph to be validated as a DAG.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,3 +77,8 @@ class DagTests extends FunSuite:
|
||||||
)
|
)
|
||||||
case _ => ()
|
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)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package gs.graph.v0
|
package gs.graph.v0.directed
|
||||||
|
|
||||||
import gs.graph.v0.directed.Digraph
|
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.*
|
import munit.*
|
||||||
|
|
||||||
class TarjansTests extends FunSuite:
|
class TarjansTests extends FunSuite:
|
||||||
|
|
@ -9,9 +13,9 @@ class TarjansTests extends FunSuite:
|
||||||
val v0 = Vertex.Zero
|
val v0 = Vertex.Zero
|
||||||
val v1 = Vertex(1)
|
val v1 = Vertex(1)
|
||||||
val v2 = Vertex(2)
|
val v2 = Vertex(2)
|
||||||
val s1 = SCC(v0)
|
val s1 = StronglyConnectedComponent(v0)
|
||||||
val s2 = SCC(v0, v1, v2)
|
val s2 = StronglyConnectedComponent(v0, v1, v2)
|
||||||
val s3 = SCC(v0)
|
val s3 = StronglyConnectedComponent(v0)
|
||||||
|
|
||||||
assertEquals(s1, s3)
|
assertEquals(s1, s3)
|
||||||
assertNotEquals(s1, s2)
|
assertNotEquals(s1, s2)
|
||||||
|
|
@ -23,9 +27,9 @@ class TarjansTests extends FunSuite:
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should produce an empty list of SCCs for an empty graph") {
|
test("should produce an empty list of SCCs for an empty graph") {
|
||||||
val g = UndirectedGraph.Empty
|
val g = Digraph.Empty
|
||||||
val sccs = SCC.tarjan(g)
|
val sccs = StronglyConnectedComponent.tarjan(g)
|
||||||
val alt = SCC.findAll(g)
|
val alt = StronglyConnectedComponent.findAll(g)
|
||||||
assertEquals(sccs, Nil)
|
assertEquals(sccs, Nil)
|
||||||
assertEquals(alt, Nil)
|
assertEquals(alt, Nil)
|
||||||
}
|
}
|
||||||
|
|
@ -33,9 +37,9 @@ class TarjansTests extends FunSuite:
|
||||||
test(
|
test(
|
||||||
"should produce a list of once SCC with one vertex for a graph with one vertex"
|
"should produce a list of once SCC with one vertex for a graph with one vertex"
|
||||||
) {
|
) {
|
||||||
val g = Graph.undirected(Size.One)
|
val g = Graph.directed(Size.One)
|
||||||
val sccs = SCC.findAll(g)
|
val sccs = StronglyConnectedComponent.findAll(g)
|
||||||
val expected = SCC.single(Vertex.Zero)
|
val expected = StronglyConnectedComponent.single(Vertex.Zero)
|
||||||
assertEquals(sccs, List(expected))
|
assertEquals(sccs, List(expected))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,9 +59,16 @@ class TarjansTests extends FunSuite:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val sccs = SCC.findAll(g)
|
val sccs = StronglyConnectedComponent.findAll(g)
|
||||||
assertEquals(g.roots, Vector(Vertex.Zero))
|
assertEquals(g.roots, Vector(Vertex.Zero))
|
||||||
assertEquals(sccs.toSet, Set(SCC.of(0), SCC.of(1), SCC.of(2)))
|
assertEquals(
|
||||||
|
sccs.toSet,
|
||||||
|
Set(
|
||||||
|
StronglyConnectedComponent.of(0),
|
||||||
|
StronglyConnectedComponent.of(1),
|
||||||
|
StronglyConnectedComponent.of(2)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should handle a graph with multiple SCCs") {
|
test("should handle a graph with multiple SCCs") {
|
||||||
|
|
@ -87,14 +98,14 @@ class TarjansTests extends FunSuite:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val sccs = SCC.findAll(g)
|
val sccs = StronglyConnectedComponent.findAll(g)
|
||||||
assertEquals(g.roots, Vector.empty)
|
assertEquals(g.roots, Vector.empty)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
sccs.toSet,
|
sccs.toSet,
|
||||||
Set(
|
Set(
|
||||||
SCC.of(0, 1, 2),
|
StronglyConnectedComponent.of(0, 1, 2),
|
||||||
SCC.of(3, 4, 5),
|
StronglyConnectedComponent.of(3, 4, 5),
|
||||||
SCC.of(6, 7, 8)
|
StronglyConnectedComponent.of(6, 7, 8)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue