Isomorphic Trees - AHU Algorithm
Algorithms #algorithms #trees #graphsIsomorphic trees are trees that have the same shape
or structure
, but may differ
in the labels
of their nodes. In other words, two trees are isomorphic if one
can be transformed into the other by renaming the nodes.
For example, consider the two binary trees below:
Although the labels of the nodes in the two trees are different, the shape or
structure of the trees is the same. We can transform the tree (A)
into the tree (B)
by renaming its nodes as follows:
A -> X
B -> Z
C -> Y
D -> I
E -> J
Similarly, two graphs are isomorphic if they have the same connectivity pattern, even if their vertices and edges are labeled differently.
AHU Algorithm
The AHU algorithm is a well-known algorithm for testing isomorphism of rooted trees.
The algorithm was developed by Alfred Aho
, John Hopcroft
, and Jeffrey Ullman
, and is
named after their initials.
The AHU algorithm works by comparing the canonical form of two trees to determine if they are isomorphic.
The canonical form of a tree is a unique labeling
of the nodes of the tree such
that isomorphic trees have the same canonical form. The labeling is obtained by
assigning a rank to each node in the tree based on its subtree structure.
The rank of a node is a string that represents the ranks of its children,
ordered lexicographically
(lexicographic order is the ordering of words or
strings based on the alphabetical order of their component letters).
The canonical form of a tree is obtained by labeling the
nodes in post-order traversal
, using the ranks of their children to compute
their own rank.
To use the AHU algorithm to test the isomorphism of two trees T1 and T2:
- Compute the canonical form of T1 and T2.
- Compare the canonical forms of T1 and T2 to determine if they are equal. If they are equal, then T1 and T2 are isomorphic. If they are not equal, then T1 and T2 are not isomorphic.
The AHU algorithm has a time complexity of O(n log n)
, where n is the number of nodes
in the trees. This makes it one of the most efficient algorithms for testing the
isomorphism of trees.
Note that the AHU algorithm
only works for rooted trees
, where each node has at most one parent. It cannot be used for testing the isomorphism of general graphs.
Steps
- Find center(s) of both trees. If there are multiple centers, tree needs to be rooted at all centers
- Root the tree at the center node(s)
- Run AHU algorithm, and collect ranks i.e. perform post order traversal on tree, assign ranks and sort them lexicographically
- If both tree has same ranks for any centers, trees are isomorphic
The following articles provide more information on finding tree centers and rooting trees
Example
- Following script check if the above given trees
(A & B)
are isomorphic or not. Each node is assigned a tuple()
upon visiting until a sorted string rank is calculated for whole tree
fun getExampleATree(): Map<String, List<String>> {
return mutableMapOf<String, List<String>>().apply {
put("1", listOf("2", "3"))
put("2", listOf("4", "1"))
put("3", listOf("5"))
put("4", listOf("2"))
put("5", listOf("3"))
}
}
fun getExampleBTree(): Map<String, List<String>> {
return mutableMapOf<String, List<String>>().apply {
put("a", listOf("b", "c"))
put("b", listOf("d", "a"))
put("c", listOf("a"))
put("d", listOf("e"))
put("e", listOf("d"))
}
}
//sampleStart
fun main() {
// click + to see full script
val adjacencyMapA = getExampleATree()
val adjacencyMapB = getExampleBTree()
val centersA = findCenter(adjacencyMapA)
val centersB = findCenter(adjacencyMapB)
println("center(s) of Tree (A) -> $centersA")
println("center(s) of Tree (B) -> $centersB")
val treeARanks = mutableListOf<String>()
val treeBRanks = mutableListOf<String>()
// if there are multiple centers, tree is rooted for each center
// and rank is calculated
centersA.forEach { center ->
val root = TreeNode(value = center, parent = null,
children = mutableListOf())
rootTreeAtNode(root, adjacencyMapA)
treeARanks.add(getTreeRank(root))
}
centersB.forEach { center ->
val root = TreeNode(value = center, parent = null,
children = mutableListOf())
rootTreeAtNode(root, adjacencyMapB)
treeBRanks.add(getTreeRank(root))
}
treeARanks.forEach {
if (treeBRanks.contains(it)) {
println("trees are isomorphic")
}
}
}
fun getTreeRank(tree: TreeNode): String {
fun postOrderTraversal(node: TreeNode): String {
// assign rank to this node and append ranks for all
// child nodes to its rank
val ranks = StringBuilder().append("()")
node.children.forEach {
ranks.append(postOrderTraversal(it))
}
return ranks.toString()
}
// sort the rank lexicographically
val treeRank = postOrderTraversal(tree).toCharArray().also { it.sort() }
return treeRank.concatToString()
}
fun findCenter(adjacencyMap: Map<String, List<String>>): List<String> {
val degree = mutableMapOf<String, Int>()
var leaves = mutableListOf<String>()
adjacencyMap.keys.forEach { node ->
degree[node] = degree.getOrDefault(node, 0).plus(
adjacencyMap.getValue(node).size
)
val isLeafNode = degree[node] == 0 || degree[node] == 1
if (isLeafNode) {
leaves.add(node)
}
}
val noOfNodes = adjacencyMap.keys.size
var currentLeaveCount = leaves.size
while (currentLeaveCount < noOfNodes) {
val newLeaves = mutableListOf<String>()
leaves.forEach { leafNode ->
adjacencyMap[leafNode]?.forEach { adjacentNode ->
degree[adjacentNode] = degree.getValue(adjacentNode) - 1
// add adjacent node to leave if degree became 0 after update
if (degree[adjacentNode] == 1) {
newLeaves.add(adjacentNode)
}
}
degree[leafNode] = 0
}
currentLeaveCount += newLeaves.size
leaves = newLeaves
}
return leaves
}
class TreeNode(
val value: String,
val parent: TreeNode? = null,
val children: MutableList<TreeNode> = mutableListOf(),
)
fun rootTreeAtNode(node: TreeNode, adjacencyMap: Map<String, List<String>>) {
adjacencyMap.getValue(node.value).forEach { adjacentNode ->
if (node.parent?.value != adjacentNode) {
val childNode = TreeNode(value = adjacentNode, parent = node)
node.children.add(childNode)
rootTreeAtNode(childNode, adjacencyMap)
}
}
}
//sampleEnd