diff --git a/README.md b/README.md index 1d84d63cb..106418ddf 100644 --- a/README.md +++ b/README.md @@ -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** diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphism.java b/src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphism.java index 015b3ca60..29c6dea72 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphism.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphism.java @@ -1,23 +1,41 @@ /** - * 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 * - *
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> tree1, List
> tree2) {
if (tree1.isEmpty() || tree2.isEmpty()) {
throw new IllegalArgumentException("Empty tree input");
@@ -75,6 +96,10 @@ public static boolean treesAreIsomorphic(List
> tree1, List
findTreeCenters(List
> tree) {
int n = tree.size();
@@ -116,45 +141,45 @@ private static TreeNode rootTree(List
> 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
> 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
> createEmptyGraph(int n) {
List
> 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;
}
@@ -163,15 +188,23 @@ public static void addUndirectedEdge(List
> 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
> tree1 = createEmptyGraph(5);
addUndirectedEdge(tree1, 2, 0);
@@ -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
> tree = createEmptyGraph(10);
addUndirectedEdge(tree, 0, 2);
@@ -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));
}
}
diff --git a/src/test/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphismTest.java b/src/test/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphismTest.java
index da79f3170..c5f840141 100644
--- a/src/test/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphismTest.java
+++ b/src/test/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphismTest.java
@@ -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 {
@@ -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
> 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
> generateRandomTree(int n) {
List