From 51528c4cbee822a4b3574747fc38fdc539748e60 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 22 Jun 2019 22:17:56 -0400 Subject: [PATCH] Add Lowest Common Ancestor algorithm --- .../cacao/util/LowestCommonAncestor.kt | 56 ++++++++++++++ .../net/shadowfacts/cacao/util/LCATest.kt | 73 +++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 src/main/kotlin/net/shadowfacts/cacao/util/LowestCommonAncestor.kt create mode 100644 src/test/kotlin/net/shadowfacts/cacao/util/LCATest.kt diff --git a/src/main/kotlin/net/shadowfacts/cacao/util/LowestCommonAncestor.kt b/src/main/kotlin/net/shadowfacts/cacao/util/LowestCommonAncestor.kt new file mode 100644 index 0000000..6ee8951 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/cacao/util/LowestCommonAncestor.kt @@ -0,0 +1,56 @@ +package net.shadowfacts.cacao.util + +import java.util.* +import kotlin.NoSuchElementException + +/** + * A linear time algorithm for finding the lowest common ancestor of two nodes in a graph. + * Based on https://stackoverflow.com/a/6342546/4731558 + * + * Works be finding the path from each node back to the root node. + * The LCA will then be the node after which the paths diverge. + * + * @author shadowfacts + */ +object LowestCommonAncestor { + + fun find(node1: Node, node2: Node, parent: Node.() -> Node?): Node? { + @Suppress("NAME_SHADOWING") var node1: Node? = node1 + @Suppress("NAME_SHADOWING") var node2: Node? = node2 + + val parent1 = LinkedList() + while (node1 != null) { + parent1.push(node1) + node1 = node1.parent() + } + + val parent2 = LinkedList() + while (node2 != null) { + parent2.push(node2) + node2 = node2.parent() + } + + // paths don't converge on the same root element + if (parent1.first != parent2.first) { + return null + } + + var oldNode: Node? = null + while (node1 == node2 && parent1.isNotEmpty() && parent2.isNotEmpty()) { + oldNode = node1 + node1 = parent1.popOrNull() + node2 = parent2.popOrNull() + } + return if (node1 == node2) node1!! + else oldNode!! + } + +} + +private fun LinkedList.popOrNull(): T? { + return try { + pop() + } catch (e: NoSuchElementException) { + null + } +} \ No newline at end of file diff --git a/src/test/kotlin/net/shadowfacts/cacao/util/LCATest.kt b/src/test/kotlin/net/shadowfacts/cacao/util/LCATest.kt new file mode 100644 index 0000000..bb261fb --- /dev/null +++ b/src/test/kotlin/net/shadowfacts/cacao/util/LCATest.kt @@ -0,0 +1,73 @@ +package net.shadowfacts.cacao.util + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test + +/** + * @author shadowfacts + */ +class LCATest { + + class Node(val name: String, val parent: Node?) + + @Test + fun testDirectParent() { + val parent = Node("parent", null) + val child = Node("child", parent) + assertEquals(parent, LowestCommonAncestor.find(parent, child, Node::parent)) + assertEquals(parent, LowestCommonAncestor.find(child, parent, Node::parent)) + } + + @Test + fun testSiblings() { + val root = Node("root", null) + val a = Node("a", root) + val b = Node("b", root) + assertEquals(root, LowestCommonAncestor.find(a, b, Node::parent)) + } + + @Test + fun testBetweenSubtrees() { + // ┌────┐ + // │root│ + // └────┘ + // ╱ ╲ + // ╱ ╲ + // ┌─┐ ┌─┐ + // │A│ │B│ + // └─┘ └─┘ + // ╱ ╲ ╱ ╲ + // ╱ ╲ ╱ ╲ + // ┌─┐ ┌─┐┌─┐ ┌─┐ + // │C│ │D││E│ │F│ + // └─┘ └─┘└─┘ └─┘ + val root = Node("root", null) + + val a = Node("a", root) + val c = Node("c", a) + val d = Node("d", a) + + val b = Node("b", root) + val e = Node("e", b) + val f = Node("f", b) + + assertEquals(a, LowestCommonAncestor.find(c, d, Node::parent)) + assertEquals(root, LowestCommonAncestor.find(c, b, Node::parent)) + assertEquals(root, LowestCommonAncestor.find(d, e, Node::parent)) + assertEquals(root, LowestCommonAncestor.find(c, root, Node::parent)) + } + + @Test + fun testBetweenDisjointTrees() { + val a = Node("a", null) + val b = Node("b", a) + + val c = Node("c", null) + val d = Node("d", c) + + assertNull(LowestCommonAncestor.find(a, d, Node::parent)) + assertNull(LowestCommonAncestor.find(b, c, Node::parent)) + } + +} \ No newline at end of file