Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ $ java -cp classes com.williamfiset.algorithms.search.BinarySearch
### Tree algorithms

- [:movie_camera:](https://www.youtube.com/watch?v=2FFq2_je7Lg) [Rooting an undirected tree](src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/RootingTree.java) **- O(V+E)**
- [:movie_camera:](https://www.youtube.com/watch?v=OCKvEMF0Xac) [Identifying isomorphic trees](src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphism.java) **- O(?)**
- [:movie_camera:](https://www.youtube.com/watch?v=OCKvEMF0Xac) [Identifying isomorphic trees](src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphism.java) **- O(V*log(V))**
- [:movie_camera:](https://www.youtube.com/watch?v=nzF_9bjDzdc) [Tree center(s)](src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeCenter.java) **- O(V+E)**
- [Tree diameter](src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeDiameter.java) **- O(V+E)**
- [:movie_camera:](https://www.youtube.com/watch?v=sD1IoalFomA) [Lowest Common Ancestor (LCA, Euler tour)](src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/LowestCommonAncestorEulerTour.java) **- O(1) queries, O(nlogn) preprocessing**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,49 @@
/**
* Determines if two unrooted trees are isomorphic. This algorithm can easily be modified to support
* checking if two rooted trees are isomorphic.
* Tree Isomorphism — Canonical Encoding
*
* <p>Tested code against: https://uva.onlinejudge.org/external/124/p12489.pdf
* Determines if two unrooted trees are isomorphic (structurally identical
* regardless of labeling). The algorithm works in three steps:
*
* 1. Find the center(s) of each tree by iteratively pruning leaf nodes.
* A tree has 1 or 2 centers.
* 2. Root both trees at their center(s) and compute a canonical string
* encoding via DFS. Each subtree is encoded as "(children...)" with
* children sorted lexicographically so that isomorphic subtrees
* produce identical strings.
* 3. Compare the encodings. If tree2 has two centers, try both — if
* either matches tree1's encoding, the trees are isomorphic.
*
* Can easily be adapted for rooted tree isomorphism by skipping step 1
* and encoding directly from the given roots.
*
* Tested against: https://uva.onlinejudge.org/external/124/p12489.pdf
*
* Time: O(V * log(V)) — dominated by sorting child encodings at each node
* Space: O(V)
*
* @author William Fiset, william.alexandre.fiset@gmail.com
*/
package com.williamfiset.algorithms.graphtheory.treealgorithms;

import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TreeIsomorphism {

public static class TreeNode {
private int id;
private TreeNode parent;
private List<TreeNode> children;
private final int id;
private final TreeNode parent;
private final List<TreeNode> children;

// Useful constructor for root node.
public TreeNode(int id) {
this(id, /* parent= */ null);
}

public TreeNode(int id, TreeNode parent) {
this.id = id;
this.parent = parent;
children = new LinkedList<>();
this.children = new ArrayList<>();
}

public void addChildren(TreeNode... nodes) {
Expand All @@ -52,7 +70,10 @@ public String toString() {
}
}

// Determines if two unrooted trees are isomorphic
/**
* Returns true if the two unrooted trees are isomorphic.
* Roots each tree at its center(s) and compares canonical encodings.
*/
public static boolean treesAreIsomorphic(List<List<Integer>> tree1, List<List<Integer>> tree2) {
if (tree1.isEmpty() || tree2.isEmpty()) {
throw new IllegalArgumentException("Empty tree input");
Expand All @@ -75,6 +96,10 @@ public static boolean treesAreIsomorphic(List<List<Integer>> tree1, List<List<In
return false;
}

/**
* Finds the center node(s) of the tree by iteratively removing leaf nodes.
* A tree has either 1 center (odd diameter) or 2 centers (even diameter).
*/
private static List<Integer> findTreeCenters(List<List<Integer>> tree) {
int n = tree.size();

Expand Down Expand Up @@ -116,45 +141,45 @@ private static TreeNode rootTree(List<List<Integer>> graph, int rootId) {
return buildTree(graph, root);
}

// Do dfs to construct rooted tree.
/** Recursively builds the rooted tree via DFS, skipping the edge back to parent. */
private static TreeNode buildTree(List<List<Integer>> graph, TreeNode node) {
for (int neighbor : graph.get(node.id())) {
// Ignore adding an edge pointing back to parent.
if (node.parent() != null && neighbor == node.parent().id()) {
continue;
}

TreeNode child = new TreeNode(neighbor, node);
node.addChildren(child);

buildTree(graph, child);
}
return node;
}

// Constructs the canonical form representation of a tree as a string.
/**
* Constructs a canonical string encoding of the subtree rooted at the given node.
* Children encodings are sorted lexicographically so that isomorphic subtrees
* always produce the same string. Example: "((()())())" for a small tree.
*/
public static String encode(TreeNode node) {
if (node == null) {
return "";
}
List<String> labels = new LinkedList<>();
List<String> labels = new ArrayList<>();
for (TreeNode child : node.children()) {
labels.add(encode(child));
}
Collections.sort(labels);
StringBuilder sb = new StringBuilder();
StringBuilder sb = new StringBuilder("(");
for (String label : labels) {
sb.append(label);
}
return "(" + sb.toString() + ")";
return sb.append(")").toString();
}

/* Graph/Tree creation helper methods. */
/* Graph helpers */

// Create a graph as a adjacency list with 'n' nodes.
public static List<List<Integer>> createEmptyGraph(int n) {
List<List<Integer>> graph = new ArrayList<>(n);
for (int i = 0; i < n; i++) graph.add(new LinkedList<>());
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
return graph;
}

Expand All @@ -163,15 +188,23 @@ public static void addUndirectedEdge(List<List<Integer>> graph, int from, int to
graph.get(to).add(from);
}

/* Example usage */
// ==================== Main ====================

public static void main(String[] args) {
simpleIsomorphismTest();
testEncodingTreeFromSlides();
}

// Test if two tree are isomorphic, meaning they are structurally equivalent
// but are labeled differently.
// tree1 (rooted at center 2): tree2 (rooted at center 1):
//
// 2 1
// / | \ / | \
// 0 1 3 0 3 2
// | |
// 4 4
//
// Both are isomorphic — same structure, different labels.
//
private static void simpleIsomorphismTest() {
List<List<Integer>> tree1 = createEmptyGraph(5);
addUndirectedEdge(tree1, 2, 0);
Expand All @@ -185,11 +218,22 @@ private static void simpleIsomorphismTest() {
addUndirectedEdge(tree2, 1, 3);
addUndirectedEdge(tree2, 1, 2);

if (!treesAreIsomorphic(tree1, tree2)) {
System.out.println("Oops, these tree should be isomorphic!");
}
// true
System.out.println("Isomorphic: " + treesAreIsomorphic(tree1, tree2));
}

// Rooted at node 0:
//
// 0
// / | \
// 2 1 3
// / \ / \ \
// 6 7 4 5 8
// |
// 9
//
// Canonical encoding: (((())())(()())(()))
//
private static void testEncodingTreeFromSlides() {
List<List<Integer>> tree = createEmptyGraph(10);
addUndirectedEdge(tree, 0, 2);
Expand All @@ -204,8 +248,7 @@ private static void testEncodingTreeFromSlides() {

TreeNode root0 = rootTree(tree, 0);

if (!encode(root0).equals("(((())())(()())(()))")) {
System.out.println("Tree encoding is wrong: " + encode(root0));
}
// (((())())(()())(()))
System.out.println("Encoding: " + encode(root0));
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
// To run this test in isolation from root folder:
//
// $ bazel test //src/test/java/com/williamfiset/algorithms/graphtheory/treealgorithms:TreeIsomorphismTest

package com.williamfiset.algorithms.graphtheory.treealgorithms;

import static com.google.common.truth.Truth.assertThat;
import static com.williamfiset.algorithms.graphtheory.treealgorithms.TreeIsomorphism.addUndirectedEdge;
import static com.williamfiset.algorithms.graphtheory.treealgorithms.TreeIsomorphism.createEmptyGraph;
import static com.williamfiset.algorithms.graphtheory.treealgorithms.TreeIsomorphism.encode;
import static com.williamfiset.algorithms.graphtheory.treealgorithms.TreeIsomorphism.treesAreIsomorphic;
import static org.junit.Assert.assertThrows;

import java.util.*;
import org.junit.jupiter.api.*;
import com.williamfiset.algorithms.graphtheory.treealgorithms.TreeIsomorphism.TreeNode;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;

public class TreeIsomorphismTest {

Expand Down Expand Up @@ -149,6 +148,130 @@ public void testIsomorphismEquivilanceAgainstOtherImpl() {
}
}

// ==================== Encoding tests ====================

@Test
public void testEncodeNullNode() {
assertThat(encode(null)).isEqualTo("");
}

@Test
public void testEncodeLeafNode() {
TreeNode leaf = new TreeNode(0);
assertThat(encode(leaf)).isEqualTo("()");
}

@Test
public void testEncodeLinearTree() {
// 0 -> 1 -> 2
TreeNode root = new TreeNode(0);
TreeNode child = new TreeNode(1, root);
TreeNode grandchild = new TreeNode(2, child);
root.addChildren(child);
child.addChildren(grandchild);

assertThat(encode(root)).isEqualTo("((()))");
}

@Test
public void testEncodeStarTree() {
// 0 with children 1, 2, 3
TreeNode root = new TreeNode(0);
root.addChildren(new TreeNode(1, root), new TreeNode(2, root), new TreeNode(3, root));

assertThat(encode(root)).isEqualTo("(()()())");
}

@Test
public void testEncodeFromSlides() {
// 0
// / | \
// 2 1 3
// / \ / \ \
// 6 7 4 5 8
// |
// 9
List<List<Integer>> tree = createEmptyGraph(10);
addUndirectedEdge(tree, 0, 2);
addUndirectedEdge(tree, 0, 1);
addUndirectedEdge(tree, 0, 3);
addUndirectedEdge(tree, 2, 6);
addUndirectedEdge(tree, 2, 7);
addUndirectedEdge(tree, 1, 4);
addUndirectedEdge(tree, 1, 5);
addUndirectedEdge(tree, 5, 9);
addUndirectedEdge(tree, 3, 8);

// Root at node 0 and use treesAreIsomorphic's internal rootTree via encode
// We build manually to test encode directly
TreeNode n0 = new TreeNode(0);
TreeNode n1 = new TreeNode(1, n0);
TreeNode n2 = new TreeNode(2, n0);
TreeNode n3 = new TreeNode(3, n0);
TreeNode n4 = new TreeNode(4, n1);
TreeNode n5 = new TreeNode(5, n1);
TreeNode n6 = new TreeNode(6, n2);
TreeNode n7 = new TreeNode(7, n2);
TreeNode n8 = new TreeNode(8, n3);
TreeNode n9 = new TreeNode(9, n5);

n0.addChildren(n2, n1, n3);
n2.addChildren(n6, n7);
n1.addChildren(n4, n5);
n5.addChildren(n9);
n3.addChildren(n8);

assertThat(encode(n0)).isEqualTo("(((())())(()())(()))");
}

@Test
public void testIsomorphicEncodingsMatch() {
// Two isomorphic subtrees with different labels should produce the same encoding.
// Tree A: root -> (child1, child2 -> grandchild)
TreeNode rootA = new TreeNode(0);
TreeNode a1 = new TreeNode(1, rootA);
TreeNode a2 = new TreeNode(2, rootA);
TreeNode a3 = new TreeNode(3, a2);
rootA.addChildren(a1, a2);
a2.addChildren(a3);

// Tree B: root -> (child5 -> grandchild, child6)
TreeNode rootB = new TreeNode(10);
TreeNode b1 = new TreeNode(5, rootB);
TreeNode b2 = new TreeNode(6, rootB);
TreeNode b3 = new TreeNode(7, b1);
rootB.addChildren(b1, b2);
b1.addChildren(b3);

assertThat(encode(rootA)).isEqualTo(encode(rootB));
}

// ==================== TreeNode tests ====================

@Test
public void testTreeNodeParent() {
TreeNode root = new TreeNode(0);
TreeNode child = new TreeNode(1, root);
assertThat(root.parent()).isNull();
assertThat(child.parent()).isEqualTo(root);
}

@Test
public void testTreeNodeChildren() {
TreeNode root = new TreeNode(0);
TreeNode c1 = new TreeNode(1, root);
TreeNode c2 = new TreeNode(2, root);
root.addChildren(c1, c2);
assertThat(root.children()).containsExactly(c1, c2).inOrder();
}

@Test
public void testTreeNodeToString() {
assertThat(new TreeNode(42).toString()).isEqualTo("42");
}

// ==================== Helpers ====================

public static List<List<Integer>> generateRandomTree(int n) {
List<Integer> nodes = new ArrayList<>();
nodes.add(0);
Expand Down
Loading