Skip to content

Commit 8cd42bc

Browse files
williamfisetclaude
andauthored
Refactor TreeIsomorphism with docs, diagrams, and README update (williamfiset#1288)
* Refactor TreeIsomorphism: add docs, diagrams, and update README complexity Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add test coverage for TreeIsomorphism encode() and TreeNode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 90a66df commit 8cd42bc

File tree

3 files changed

+203
-37
lines changed

3 files changed

+203
-37
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ $ java -cp classes com.williamfiset.algorithms.search.BinarySearch
182182
### Tree algorithms
183183

184184
- [: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)**
185-
- [:movie_camera:](https://www.youtube.com/watch?v=OCKvEMF0Xac) [Identifying isomorphic trees](src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphism.java) **- O(?)**
185+
- [: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))**
186186
- [: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)**
187187
- [Tree diameter](src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeDiameter.java) **- O(V+E)**
188188
- [: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**

src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphism.java

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,49 @@
11
/**
2-
* Determines if two unrooted trees are isomorphic. This algorithm can easily be modified to support
3-
* checking if two rooted trees are isomorphic.
2+
* Tree Isomorphism — Canonical Encoding
43
*
5-
* <p>Tested code against: https://uva.onlinejudge.org/external/124/p12489.pdf
4+
* Determines if two unrooted trees are isomorphic (structurally identical
5+
* regardless of labeling). The algorithm works in three steps:
6+
*
7+
* 1. Find the center(s) of each tree by iteratively pruning leaf nodes.
8+
* A tree has 1 or 2 centers.
9+
* 2. Root both trees at their center(s) and compute a canonical string
10+
* encoding via DFS. Each subtree is encoded as "(children...)" with
11+
* children sorted lexicographically so that isomorphic subtrees
12+
* produce identical strings.
13+
* 3. Compare the encodings. If tree2 has two centers, try both — if
14+
* either matches tree1's encoding, the trees are isomorphic.
15+
*
16+
* Can easily be adapted for rooted tree isomorphism by skipping step 1
17+
* and encoding directly from the given roots.
18+
*
19+
* Tested against: https://uva.onlinejudge.org/external/124/p12489.pdf
20+
*
21+
* Time: O(V * log(V)) — dominated by sorting child encodings at each node
22+
* Space: O(V)
623
*
724
* @author William Fiset, william.alexandre.fiset@gmail.com
825
*/
926
package com.williamfiset.algorithms.graphtheory.treealgorithms;
1027

11-
import java.util.*;
28+
import java.util.ArrayList;
29+
import java.util.Collections;
30+
import java.util.List;
1231

1332
public class TreeIsomorphism {
1433

1534
public static class TreeNode {
16-
private int id;
17-
private TreeNode parent;
18-
private List<TreeNode> children;
35+
private final int id;
36+
private final TreeNode parent;
37+
private final List<TreeNode> children;
1938

20-
// Useful constructor for root node.
2139
public TreeNode(int id) {
2240
this(id, /* parent= */ null);
2341
}
2442

2543
public TreeNode(int id, TreeNode parent) {
2644
this.id = id;
2745
this.parent = parent;
28-
children = new LinkedList<>();
46+
this.children = new ArrayList<>();
2947
}
3048

3149
public void addChildren(TreeNode... nodes) {
@@ -52,7 +70,10 @@ public String toString() {
5270
}
5371
}
5472

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

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

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

119-
// Do dfs to construct rooted tree.
144+
/** Recursively builds the rooted tree via DFS, skipping the edge back to parent. */
120145
private static TreeNode buildTree(List<List<Integer>> graph, TreeNode node) {
121146
for (int neighbor : graph.get(node.id())) {
122-
// Ignore adding an edge pointing back to parent.
123147
if (node.parent() != null && neighbor == node.parent().id()) {
124148
continue;
125149
}
126-
127150
TreeNode child = new TreeNode(neighbor, node);
128151
node.addChildren(child);
129-
130152
buildTree(graph, child);
131153
}
132154
return node;
133155
}
134156

135-
// Constructs the canonical form representation of a tree as a string.
157+
/**
158+
* Constructs a canonical string encoding of the subtree rooted at the given node.
159+
* Children encodings are sorted lexicographically so that isomorphic subtrees
160+
* always produce the same string. Example: "((()())())" for a small tree.
161+
*/
136162
public static String encode(TreeNode node) {
137163
if (node == null) {
138164
return "";
139165
}
140-
List<String> labels = new LinkedList<>();
166+
List<String> labels = new ArrayList<>();
141167
for (TreeNode child : node.children()) {
142168
labels.add(encode(child));
143169
}
144170
Collections.sort(labels);
145-
StringBuilder sb = new StringBuilder();
171+
StringBuilder sb = new StringBuilder("(");
146172
for (String label : labels) {
147173
sb.append(label);
148174
}
149-
return "(" + sb.toString() + ")";
175+
return sb.append(")").toString();
150176
}
151177

152-
/* Graph/Tree creation helper methods. */
178+
/* Graph helpers */
153179

154-
// Create a graph as a adjacency list with 'n' nodes.
155180
public static List<List<Integer>> createEmptyGraph(int n) {
156181
List<List<Integer>> graph = new ArrayList<>(n);
157-
for (int i = 0; i < n; i++) graph.add(new LinkedList<>());
182+
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
158183
return graph;
159184
}
160185

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

166-
/* Example usage */
191+
// ==================== Main ====================
167192

168193
public static void main(String[] args) {
169194
simpleIsomorphismTest();
170195
testEncodingTreeFromSlides();
171196
}
172197

173-
// Test if two tree are isomorphic, meaning they are structurally equivalent
174-
// but are labeled differently.
198+
// tree1 (rooted at center 2): tree2 (rooted at center 1):
199+
//
200+
// 2 1
201+
// / | \ / | \
202+
// 0 1 3 0 3 2
203+
// | |
204+
// 4 4
205+
//
206+
// Both are isomorphic — same structure, different labels.
207+
//
175208
private static void simpleIsomorphismTest() {
176209
List<List<Integer>> tree1 = createEmptyGraph(5);
177210
addUndirectedEdge(tree1, 2, 0);
@@ -185,11 +218,22 @@ private static void simpleIsomorphismTest() {
185218
addUndirectedEdge(tree2, 1, 3);
186219
addUndirectedEdge(tree2, 1, 2);
187220

188-
if (!treesAreIsomorphic(tree1, tree2)) {
189-
System.out.println("Oops, these tree should be isomorphic!");
190-
}
221+
// true
222+
System.out.println("Isomorphic: " + treesAreIsomorphic(tree1, tree2));
191223
}
192224

225+
// Rooted at node 0:
226+
//
227+
// 0
228+
// / | \
229+
// 2 1 3
230+
// / \ / \ \
231+
// 6 7 4 5 8
232+
// |
233+
// 9
234+
//
235+
// Canonical encoding: (((())())(()())(()))
236+
//
193237
private static void testEncodingTreeFromSlides() {
194238
List<List<Integer>> tree = createEmptyGraph(10);
195239
addUndirectedEdge(tree, 0, 2);
@@ -204,8 +248,7 @@ private static void testEncodingTreeFromSlides() {
204248

205249
TreeNode root0 = rootTree(tree, 0);
206250

207-
if (!encode(root0).equals("(((())())(()())(()))")) {
208-
System.out.println("Tree encoding is wrong: " + encode(root0));
209-
}
251+
// (((())())(()())(()))
252+
System.out.println("Encoding: " + encode(root0));
210253
}
211254
}

src/test/java/com/williamfiset/algorithms/graphtheory/treealgorithms/TreeIsomorphismTest.java

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
// To run this test in isolation from root folder:
2-
//
3-
// $ bazel test //src/test/java/com/williamfiset/algorithms/graphtheory/treealgorithms:TreeIsomorphismTest
4-
51
package com.williamfiset.algorithms.graphtheory.treealgorithms;
62

73
import static com.google.common.truth.Truth.assertThat;
84
import static com.williamfiset.algorithms.graphtheory.treealgorithms.TreeIsomorphism.addUndirectedEdge;
95
import static com.williamfiset.algorithms.graphtheory.treealgorithms.TreeIsomorphism.createEmptyGraph;
6+
import static com.williamfiset.algorithms.graphtheory.treealgorithms.TreeIsomorphism.encode;
107
import static com.williamfiset.algorithms.graphtheory.treealgorithms.TreeIsomorphism.treesAreIsomorphic;
118
import static org.junit.Assert.assertThrows;
129

13-
import java.util.*;
14-
import org.junit.jupiter.api.*;
10+
import com.williamfiset.algorithms.graphtheory.treealgorithms.TreeIsomorphism.TreeNode;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import org.junit.jupiter.api.Test;
1514

1615
public class TreeIsomorphismTest {
1716

@@ -149,6 +148,130 @@ public void testIsomorphismEquivilanceAgainstOtherImpl() {
149148
}
150149
}
151150

151+
// ==================== Encoding tests ====================
152+
153+
@Test
154+
public void testEncodeNullNode() {
155+
assertThat(encode(null)).isEqualTo("");
156+
}
157+
158+
@Test
159+
public void testEncodeLeafNode() {
160+
TreeNode leaf = new TreeNode(0);
161+
assertThat(encode(leaf)).isEqualTo("()");
162+
}
163+
164+
@Test
165+
public void testEncodeLinearTree() {
166+
// 0 -> 1 -> 2
167+
TreeNode root = new TreeNode(0);
168+
TreeNode child = new TreeNode(1, root);
169+
TreeNode grandchild = new TreeNode(2, child);
170+
root.addChildren(child);
171+
child.addChildren(grandchild);
172+
173+
assertThat(encode(root)).isEqualTo("((()))");
174+
}
175+
176+
@Test
177+
public void testEncodeStarTree() {
178+
// 0 with children 1, 2, 3
179+
TreeNode root = new TreeNode(0);
180+
root.addChildren(new TreeNode(1, root), new TreeNode(2, root), new TreeNode(3, root));
181+
182+
assertThat(encode(root)).isEqualTo("(()()())");
183+
}
184+
185+
@Test
186+
public void testEncodeFromSlides() {
187+
// 0
188+
// / | \
189+
// 2 1 3
190+
// / \ / \ \
191+
// 6 7 4 5 8
192+
// |
193+
// 9
194+
List<List<Integer>> tree = createEmptyGraph(10);
195+
addUndirectedEdge(tree, 0, 2);
196+
addUndirectedEdge(tree, 0, 1);
197+
addUndirectedEdge(tree, 0, 3);
198+
addUndirectedEdge(tree, 2, 6);
199+
addUndirectedEdge(tree, 2, 7);
200+
addUndirectedEdge(tree, 1, 4);
201+
addUndirectedEdge(tree, 1, 5);
202+
addUndirectedEdge(tree, 5, 9);
203+
addUndirectedEdge(tree, 3, 8);
204+
205+
// Root at node 0 and use treesAreIsomorphic's internal rootTree via encode
206+
// We build manually to test encode directly
207+
TreeNode n0 = new TreeNode(0);
208+
TreeNode n1 = new TreeNode(1, n0);
209+
TreeNode n2 = new TreeNode(2, n0);
210+
TreeNode n3 = new TreeNode(3, n0);
211+
TreeNode n4 = new TreeNode(4, n1);
212+
TreeNode n5 = new TreeNode(5, n1);
213+
TreeNode n6 = new TreeNode(6, n2);
214+
TreeNode n7 = new TreeNode(7, n2);
215+
TreeNode n8 = new TreeNode(8, n3);
216+
TreeNode n9 = new TreeNode(9, n5);
217+
218+
n0.addChildren(n2, n1, n3);
219+
n2.addChildren(n6, n7);
220+
n1.addChildren(n4, n5);
221+
n5.addChildren(n9);
222+
n3.addChildren(n8);
223+
224+
assertThat(encode(n0)).isEqualTo("(((())())(()())(()))");
225+
}
226+
227+
@Test
228+
public void testIsomorphicEncodingsMatch() {
229+
// Two isomorphic subtrees with different labels should produce the same encoding.
230+
// Tree A: root -> (child1, child2 -> grandchild)
231+
TreeNode rootA = new TreeNode(0);
232+
TreeNode a1 = new TreeNode(1, rootA);
233+
TreeNode a2 = new TreeNode(2, rootA);
234+
TreeNode a3 = new TreeNode(3, a2);
235+
rootA.addChildren(a1, a2);
236+
a2.addChildren(a3);
237+
238+
// Tree B: root -> (child5 -> grandchild, child6)
239+
TreeNode rootB = new TreeNode(10);
240+
TreeNode b1 = new TreeNode(5, rootB);
241+
TreeNode b2 = new TreeNode(6, rootB);
242+
TreeNode b3 = new TreeNode(7, b1);
243+
rootB.addChildren(b1, b2);
244+
b1.addChildren(b3);
245+
246+
assertThat(encode(rootA)).isEqualTo(encode(rootB));
247+
}
248+
249+
// ==================== TreeNode tests ====================
250+
251+
@Test
252+
public void testTreeNodeParent() {
253+
TreeNode root = new TreeNode(0);
254+
TreeNode child = new TreeNode(1, root);
255+
assertThat(root.parent()).isNull();
256+
assertThat(child.parent()).isEqualTo(root);
257+
}
258+
259+
@Test
260+
public void testTreeNodeChildren() {
261+
TreeNode root = new TreeNode(0);
262+
TreeNode c1 = new TreeNode(1, root);
263+
TreeNode c2 = new TreeNode(2, root);
264+
root.addChildren(c1, c2);
265+
assertThat(root.children()).containsExactly(c1, c2).inOrder();
266+
}
267+
268+
@Test
269+
public void testTreeNodeToString() {
270+
assertThat(new TreeNode(42).toString()).isEqualTo("42");
271+
}
272+
273+
// ==================== Helpers ====================
274+
152275
public static List<List<Integer>> generateRandomTree(int n) {
153276
List<Integer> nodes = new ArrayList<>();
154277
nodes.add(0);

0 commit comments

Comments
 (0)