Skip to content
This repository has been archived by the owner on Jan 1, 2023. It is now read-only.

Commit

Permalink
More graph improvements.
Browse files Browse the repository at this point in the history
  • Loading branch information
gchallen committed Oct 30, 2021
1 parent 0ccf993 commit 4bdc1b2
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 44 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

group = "com.github.cs125-illinois"
version = "2021.10.1"
version = "2021.10.2"

plugins {
kotlin("jvm") version "1.5.31"
Expand Down
132 changes: 99 additions & 33 deletions src/main/java/cs1/graphs/UnweightedGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ private fun <T> GraphNode<T>.find(visited: MutableSet<GraphNode<T>>) {
}

@Suppress("NestedBlockDepth")
fun <T> Map<Node<T>, Set<Node<T>>>.toGraphNodes(random: Random) =
fun <T> Map<Node<T>, Set<Node<T>>>.toGraphNodes(random: Random, checkUndirected: Boolean) =
keys.associateWith { GraphNode(it.value, random.nextInt()) }.let { mapping ->
map { (key, values) ->
check(mapping[key] != null) { "Missing mapping for node in graph creation" }
Expand All @@ -52,8 +52,10 @@ fun <T> Map<Node<T>, Set<Node<T>>>.toGraphNodes(random: Random) =
check(keys.first().find() == mapping.values.toSet()) { "Graph is not connected" }
keys.forEach { node ->
check(node !in node.neighbors) { "Graph contains a self-edge" }
for (neighbor in node.neighbors) {
check(node in neighbor.neighbors) { "Graph is not undirected" }
if (checkUndirected) {
for (neighbor in node.neighbors) {
check(node in neighbor.neighbors) { "Graph is not undirected" }
}
}
}
}
Expand Down Expand Up @@ -83,45 +85,84 @@ fun Map<GraphNode<*>, Set<GraphNode<*>>>.toNodes() = keys.associateWith { Node(i
}

@Suppress("unused")
class UnweightedGraph<T> private constructor(
val edges: Map<GraphNode<T>, Set<GraphNode<T>>>,
@Suppress("unused") private val unused: Boolean
) {
constructor(edges: Map<Node<T>, Set<Node<T>>>, random: Random = Random()) : this(edges.toGraphNodes(random), true)
constructor(graph: UnweightedGraph<T>) : this(graph.edges.copyGraphNodes(), true)
class UnweightedGraph<T> private constructor(edges: Map<GraphNode<T>, Set<GraphNode<T>>>) {
constructor(edges: Map<Node<T>, Set<Node<T>>>, random: Random = Random(), isUndirected: Boolean) :
this(edges.toGraphNodes(random, isUndirected))

constructor(graph: UnweightedGraph<T>) :
this(graph.edges.copyGraphNodes())

private val _edges = edges
val edges = edges
get() {
if (edgesLocked) {
throw IllegalAccessException()
}
return field
}
private var edgesLocked = false
fun lockEdges(): UnweightedGraph<T> {
edgesLocked = true
return this
}

@Suppress("UNCHECKED_CAST")
override fun equals(other: Any?) = when (other) {
!is UnweightedGraph<*> -> false
else -> (edges as Map<GraphNode<*>, Set<GraphNode<*>>>).toNodes() ==
(other.edges as Map<GraphNode<*>, Set<GraphNode<*>>>).toNodes()
else -> (_edges as Map<GraphNode<*>, Set<GraphNode<*>>>).toNodes() ==
(other._edges as Map<GraphNode<*>, Set<GraphNode<*>>>).toNodes()
}

override fun hashCode() = Objects.hash(edges)
override fun hashCode() = Objects.hash(_edges)

val node = edges.keys.minByOrNull { it.nonce }!!
val nodes = edges.keys
val nodes: Set<GraphNode<T>>
get() {
if (nodesLocked) {
throw IllegalAccessException()
}
return _edges.keys
}
private var nodesLocked = false
fun lockNodes(): UnweightedGraph<T> {
nodesLocked = true
return this
}

@Suppress("TooManyFunctions")
companion object {
@JvmStatic
fun <T> singleNodeGraph(value: T, random: Random = Random()) =
UnweightedGraph(mapOf(Node(value, 0) to setOf()), random)
UnweightedGraph(mapOf(Node(value, 0) to setOf()), random, true)

@JvmStatic
fun <T> twoNodeGraph(first: T, second: T, random: Random = Random()): UnweightedGraph<T> {
fun <T> twoNodeUndirectedGraph(first: T, second: T, random: Random = Random()): UnweightedGraph<T> {
val mapping = mapOf(0 to Node(first, 0), 1 to Node(second, 1))
return UnweightedGraph(
mapOf(
mapping[0]!! to setOf(mapping[1]!!),
mapping[1]!! to setOf(mapping[0]!!)
),
random
random,
true
)
}

@JvmStatic
fun <T> circleGraph(list: List<T>, random: Random = Random()): UnweightedGraph<T> {
fun <T> twoNodeDirectedGraph(first: T, second: T, random: Random = Random()): UnweightedGraph<T> {
val mapping = mapOf(0 to Node(first, 0), 1 to Node(second, 1))
return UnweightedGraph(
mapOf(
mapping[0]!! to setOf(mapping[1]!!),
mapping[1]!! to setOf()
),
random,
false
)
}

@JvmStatic
fun <T> circleUndirectedGraph(list: List<T>, random: Random = Random()): UnweightedGraph<T> {
require(list.size >= 2) { "List has fewer than two elements" }
val mapping = list.mapIndexed { i, it -> i to Node(it, i) }.toMap()
val edges = mapping.values.associateWith { mutableSetOf<Node<T>>() }
Expand All @@ -131,7 +172,19 @@ class UnweightedGraph<T> private constructor(
}
edges[mapping[0]]!! += mapping[list.size - 1]!!
edges[mapping[list.size - 1]]!! += mapping[0]!!
return UnweightedGraph(edges, random)
return UnweightedGraph(edges, random, true)
}

@JvmStatic
fun <T> circleDirectedGraph(list: List<T>, random: Random = Random()): UnweightedGraph<T> {
require(list.size >= 2) { "List has fewer than two elements" }
val mapping = list.mapIndexed { i, it -> i to Node(it, i) }.toMap()
val edges = mapping.values.associateWith { mutableSetOf<Node<T>>() }
for (i in 0 until (list.size - 1)) {
edges[mapping[i]]!! += mapping[i + 1]!!
}
edges[mapping[list.size - 1]]!! += mapping[0]!!
return UnweightedGraph(edges, random, false)
}

@JvmStatic
Expand All @@ -145,11 +198,11 @@ class UnweightedGraph<T> private constructor(
edges[mapping[j]]!! += mapping[i]!!
}
}
return UnweightedGraph(edges, random)
return UnweightedGraph(edges, random, true)
}

@JvmStatic
fun <T> randomGraph(list: List<T>, random: Random = Random()): UnweightedGraph<T> {
fun <T> randomUndirectedGraph(list: List<T>, random: Random = Random()): UnweightedGraph<T> {
require(list.size >= 2) { "List has fewer than two elements" }
val mapping = list.mapIndexed { i, it -> i to Node(it, i) }.toMap()
val edges = mapping.values.associateWith { mutableSetOf<Node<T>>() }
Expand All @@ -163,23 +216,36 @@ class UnweightedGraph<T> private constructor(
edges[mapping[j]]!! += mapping[i]!!
}
}
return UnweightedGraph(edges, random)
return UnweightedGraph(edges, random, true)
}

@JvmStatic
fun <T> randomDirectedGraph(list: List<T>, random: Random = Random()): UnweightedGraph<T> {
require(list.size >= 2) { "List has fewer than two elements" }
val mapping = list.mapIndexed { i, it -> i to Node(it, i) }.toMap()
val edges = mapping.values.associateWith { mutableSetOf<Node<T>>() }
for (i in list.indices) {
for (j in (list.indices - i).shuffled(random).take(random.nextInt(list.size) + 1)) {
edges[mapping[i]]!! += mapping[j]!!
}
}
return UnweightedGraph(edges, random, false)
}

@JvmStatic
fun randomIntegerGraph(random: Random, size: Int, maxInteger: Int): UnweightedGraph<Int> {
fun randomUndirectedIntegerGraph(random: Random, size: Int, maxInteger: Int): UnweightedGraph<Int> {
require(size > 0) { "size must be positive: $size" }
return randomGraph(List(size) { random.nextInt(maxInteger) - (maxInteger / 2) }, random)
return randomUndirectedGraph(List(size) { random.nextInt(maxInteger) - (maxInteger / 2) }, random)
}

@JvmStatic
fun randomIntegerGraph(size: Int, maxInteger: Int): UnweightedGraph<Int> {
return randomIntegerGraph(Random(), size, maxInteger)
fun randomUndirectedIntegerGraph(size: Int, maxInteger: Int): UnweightedGraph<Int> {
return randomUndirectedIntegerGraph(Random(), size, maxInteger)
}

@JvmStatic
fun randomIntegerGraph(size: Int): UnweightedGraph<Int> {
return randomIntegerGraph(Random(), size, 128)
fun randomUndirectedIntegerGraph(size: Int): UnweightedGraph<Int> {
return randomUndirectedIntegerGraph(Random(), size, 128)
}

private fun Random.nextInt(min: Int, max: Int) = let {
Expand All @@ -193,19 +259,19 @@ class UnweightedGraph<T> private constructor(
String(CharArray(random.nextInt(1, maxLength)) { CHARACTERS[random.nextInt(CHARACTERS.length)] })

@JvmStatic
fun randomStringGraph(random: Random, size: Int, maxLength: Int): UnweightedGraph<String> {
fun randomUndirectedStringGraph(random: Random, size: Int, maxLength: Int): UnweightedGraph<String> {
require(size > 0) { "size must be positive: $size" }
return randomGraph(List(size) { randomAlphanumericString(random, maxLength) }, random)
return randomUndirectedGraph(List(size) { randomAlphanumericString(random, maxLength) }, random)
}

@JvmStatic
fun randomStringGraph(size: Int, maxLength: Int): UnweightedGraph<String> {
return randomStringGraph(Random(), size, maxLength)
fun randomUndirectedStringGraph(size: Int, maxLength: Int): UnweightedGraph<String> {
return randomUndirectedStringGraph(Random(), size, maxLength)
}

@JvmStatic
fun randomStringGraph(size: Int): UnweightedGraph<String> {
return randomStringGraph(Random(), size, 32)
fun randomUndirectedStringGraph(size: Int): UnweightedGraph<String> {
return randomUndirectedStringGraph(Random(), size, 32)
}
}
}
4 changes: 4 additions & 0 deletions src/test/java/com/example/Example.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public static <T> int size(UnweightedGraph<T> graph) {
return graph.getEdges().size();
}

public static <T> int sizeWithNodes(UnweightedGraph<T> graph) {
return graph.getNodes().size();
}

public static <T> int size(GraphNode<T> node) {
Set<GraphNode<T>> nodes = new HashSet<>();
traverse(node, nodes);
Expand Down
71 changes: 61 additions & 10 deletions src/test/kotlin/TestUnweightedGraph.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import com.example.Example
import cs1.graphs.UnweightedGraph
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
Expand All @@ -15,8 +16,8 @@ class TestUnweightedGraph : StringSpec({
}
}
}
"it should create a two-node graph" {
UnweightedGraph.twoNodeGraph(8, 16).also { graph ->
"it should create a two-node undirected graph" {
UnweightedGraph.twoNodeUndirectedGraph(8, 16).also { graph ->
Example.size(graph) shouldBe 2
Example.sum(graph) shouldBe 24
graph.edges.keys.forEach { node ->
Expand All @@ -25,8 +26,28 @@ class TestUnweightedGraph : StringSpec({
}
}
}
"it should create a circular graph" {
UnweightedGraph.circleGraph((0..31).toList()).also { graph ->
"it should create a two-node directed graph" {
UnweightedGraph.twoNodeDirectedGraph(8, 16).also { graph ->
Example.size(graph) shouldBe 2
Example.sum(graph) shouldBe 24
graph.edges.keys.forEach { node ->
Example.size(node) shouldBe if (node.value == 8) { 2 } else { 1 }
Example.sum(node) shouldBe if (node.value == 8) { 24 } else { 16 }
}
}
}
"it should create a circular undirected graph" {
UnweightedGraph.circleUndirectedGraph((0..31).toList()).also { graph ->
Example.size(graph) shouldBe 32
Example.sum(graph) shouldBe (0..31).sum()
graph.edges.keys.forEach { node ->
Example.size(node) shouldBe 32
Example.sum(node) shouldBe (0..31).sum()
}
}
}
"it should create a circular directed graph" {
UnweightedGraph.circleDirectedGraph((0..31).toList()).also { graph ->
Example.size(graph) shouldBe 32
Example.sum(graph) shouldBe (0..31).sum()
graph.edges.keys.forEach { node ->
Expand All @@ -35,7 +56,7 @@ class TestUnweightedGraph : StringSpec({
}
}
}
"it should create a fully-connected graph" {
"it should create a fully-connected undirected graph" {
UnweightedGraph.fullyConnectedGraph((32..63).toList()).also { graph ->
Example.size(graph) shouldBe 32
Example.sum(graph) shouldBe (32..63).sum()
Expand All @@ -45,8 +66,8 @@ class TestUnweightedGraph : StringSpec({
}
}
}
"it should create a random graph" {
UnweightedGraph.randomGraph((32..63).toList()).also { graph ->
"it should create a random undirected graph" {
UnweightedGraph.randomUndirectedGraph((32..63).toList()).also { graph ->
Example.size(graph) shouldBe 32
Example.sum(graph) shouldBe (32..63).sum()
graph.edges.keys.forEach { node ->
Expand All @@ -56,18 +77,32 @@ class TestUnweightedGraph : StringSpec({
}
val graphs = mutableSetOf<UnweightedGraph<*>>()
repeat(1024) {
graphs += UnweightedGraph.randomGraph((32..63).toList())
graphs += UnweightedGraph.randomUndirectedGraph((32..63).toList())
}
graphs.size shouldBe 1024
}
"it should create a random directed graph" {
UnweightedGraph.randomDirectedGraph((32..63).toList()).also { graph ->
Example.size(graph) shouldBe 32
Example.sum(graph) shouldBe (32..63).sum()
graph.edges.keys.find { node ->
Example.size(node) == 32 && Example.sum(node) == (32..63).sum()
} shouldNotBe null
}
val graphs = mutableSetOf<UnweightedGraph<*>>()
repeat(1024) {
graphs += UnweightedGraph.randomDirectedGraph((32..63).toList())
}
graphs.size shouldBe 1024
}
"it should create a random string graph" {
UnweightedGraph.randomStringGraph(1024)
UnweightedGraph.randomUndirectedStringGraph(1024)
}
"it should create equal graphs" {
UnweightedGraph.fullyConnectedGraph((32..63).toList()).also { graph ->
graph shouldBe UnweightedGraph.fullyConnectedGraph((32..63).toList())
graph shouldNotBe UnweightedGraph.fullyConnectedGraph((31..62).toList())
graph shouldNotBe UnweightedGraph.circleGraph((32..63).toList())
graph shouldNotBe UnweightedGraph.circleUndirectedGraph((32..63).toList())
}
}
"it should copy graphs" {
Expand All @@ -81,4 +116,20 @@ class TestUnweightedGraph : StringSpec({
}
}
}
"it should lock nodes" {
val graph = UnweightedGraph.randomUndirectedIntegerGraph(32)
Example.size(graph.node) shouldBe Example.sizeWithNodes(graph)
graph.lockNodes()
shouldThrow<IllegalAccessException> {
Example.sizeWithNodes(graph)
}
}
"it should lock edges" {
val graph = UnweightedGraph.randomUndirectedIntegerGraph(32)
Example.size(graph.node) shouldBe Example.size(graph)
graph.lockEdges()
shouldThrow<IllegalAccessException> {
Example.size(graph)
}
}
})

0 comments on commit 4bdc1b2

Please sign in to comment.