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/ArticulationPointsAdjacencyList.java b/src/main/java/com/williamfiset/algorithms/graphtheory/ArticulationPointsAdjacencyList.java index bd8cc51de..d2a710b24 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/ArticulationPointsAdjacencyList.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/ArticulationPointsAdjacencyList.java @@ -1,9 +1,29 @@ /** - * Finds all articulation points on an undirected graph. + * Articulation Points (Cut Vertices) — Adjacency List * - *
Tested against HackerEarth online judge at:
+ * An articulation point is a vertex whose removal disconnects the graph
+ * (or increases the number of connected components). This implementation
+ * uses Tarjan's DFS-based algorithm with low-link values.
+ *
+ * For each DFS tree rooted at node r, a non-root node u is an articulation
+ * point if it has a child v such that no vertex in the subtree rooted at v
+ * has a back edge to an ancestor of u:
+ *
+ * ids[u] <= low[v]
+ *
+ * The root node r is an articulation point if it has more than one child
+ * in the DFS tree.
+ *
+ * Works on disconnected graphs by running DFS from every unvisited node.
+ *
+ * See also: {@link BridgesAdjacencyList} for finding bridge edges.
+ *
+ * Tested against HackerEarth online judge at:
* https://www.hackerearth.com/practice/algorithms/graphs/articulation-points-and-bridges/tutorial
*
+ * Time: O(V + E)
+ * Space: O(V)
+ *
* @author William Fiset, william.alexandre.fiset@gmail.com
*/
package com.williamfiset.algorithms.graphtheory;
@@ -15,11 +35,12 @@
public class ArticulationPointsAdjacencyList {
- private int n, id, rootNodeOutgoingEdgeCount;
+ private final int n;
+ private final List Time Complexity: O(V+E)
+ * Given an undirected tree as an adjacency list, this algorithm converts it
+ * into a rooted tree by performing a DFS from a chosen root node. Each node
+ * in the resulting tree stores its parent and children.
+ *
+ * Time: O(V + E)
+ * 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.List;
public class RootingTree {
public static class TreeNode {
- private int id;
- private TreeNode parent;
- private List 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> graph;
private boolean solved;
+ private int id, rootNodeOutgoingEdgeCount;
private int[] low, ids;
private boolean[] visited, isArticulationPoint;
- private List
> graph;
public ArticulationPointsAdjacencyList(List
> graph, int n) {
if (graph == null || n <= 0 || graph.size() != n) throw new IllegalArgumentException();
@@ -27,21 +48,25 @@ public ArticulationPointsAdjacencyList(List
> graph, int n) {
this.n = n;
}
- // Returns the indexes for all articulation points in the graph even if the
- // graph is not fully connected.
+ /**
+ * Returns a boolean array where index i is true if node i is an articulation point.
+ * Works even if the graph is not fully connected.
+ */
public boolean[] findArticulationPoints() {
if (solved) return isArticulationPoint;
id = 0;
- low = new int[n]; // Low link values
- ids = new int[n]; // Nodes ids
+ low = new int[n];
+ ids = new int[n];
visited = new boolean[n];
isArticulationPoint = new boolean[n];
+ // Run DFS from each unvisited node to handle disconnected components.
for (int i = 0; i < n; i++) {
if (!visited[i]) {
rootNodeOutgoingEdgeCount = 0;
dfs(i, i, -1);
+ // Root is an articulation point only if it has 2+ children in the DFS tree.
isArticulationPoint[i] = (rootNodeOutgoingEdgeCount > 1);
}
}
@@ -51,22 +76,23 @@ public boolean[] findArticulationPoints() {
}
private void dfs(int root, int at, int parent) {
-
if (parent == root) rootNodeOutgoingEdgeCount++;
visited[at] = true;
low[at] = ids[at] = id++;
- List
> createGraph(int n) {
List
> graph = new ArrayList<>(n);
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
return graph;
}
- // Add an undirected edge to a graph.
public static void addEdge(List
> graph, int from, int to) {
graph.get(from).add(to);
graph.get(to).add(from);
}
- /* Example usage: */
+ // ==================== Main ====================
public static void main(String[] args) {
+ testExample1();
testExample2();
}
+ //
+ // 0 --- 1
+ // | /
+ // 2 -------- 3 --- 4
+ // |
+ // 5 --- 6
+ // | |
+ // 8 --- 7
+ //
+ // Articulation points: 2, 3, 5
+ //
private static void testExample1() {
int n = 9;
List
> graph = createGraph(n);
@@ -111,7 +147,6 @@ private static void testExample1() {
ArticulationPointsAdjacencyList solver = new ArticulationPointsAdjacencyList(graph, n);
boolean[] isArticulationPoint = solver.findArticulationPoints();
- // Prints:
// Node 2 is an articulation
// Node 3 is an articulation
// Node 5 is an articulation
@@ -119,8 +154,11 @@ private static void testExample1() {
if (isArticulationPoint[i]) System.out.printf("Node %d is an articulation\n", i);
}
- // Tests a graph with 3 nodes in a line: A - B - C
- // Only node 'B' should be an articulation point.
+ //
+ // 0 --- 1 --- 2
+ //
+ // Articulation point: 1
+ //
private static void testExample2() {
int n = 3;
List
> graph = createGraph(n);
@@ -131,7 +169,6 @@ private static void testExample2() {
ArticulationPointsAdjacencyList solver = new ArticulationPointsAdjacencyList(graph, n);
boolean[] isArticulationPoint = solver.findArticulationPoints();
- // Prints:
// Node 1 is an articulation
for (int i = 0; i < n; i++)
if (isArticulationPoint[i]) System.out.printf("Node %d is an articulation\n", i);
diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/RootingTree.java b/src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/RootingTree.java
index 38b776046..b7c6dd4c2 100644
--- a/src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/RootingTree.java
+++ b/src/main/java/com/williamfiset/algorithms/graphtheory/treealgorithms/RootingTree.java
@@ -1,21 +1,26 @@
/**
- * Often when working with trees we are given them as a graph with undirected edges, however
- * sometimes a better representation is a rooted tree.
+ * Rooting an Undirected Tree
*
- *
> graph, int rootId) {
TreeNode root = new TreeNode(rootId);
return buildTree(graph, root);
}
- // Do dfs to construct rooted tree.
+ /**
+ * Recursively builds the rooted tree via DFS. Skips the edge back to the
+ * parent to avoid cycles.
+ */
private static TreeNode buildTree(List
> graph, TreeNode node) {
for (int childId : graph.get(node.id())) {
- // Ignore adding an edge pointing back to parent.
+ // Ignore the edge pointing back to parent.
if (node.parent() != null && childId == node.parent().id()) {
continue;
}
-
TreeNode child = new TreeNode(childId, node);
node.addChildren(child);
-
buildTree(graph, child);
}
return node;
}
- /** ********** TESTING ********* */
+ /* Graph helpers */
- // Create a graph as a adjacency list
- private static List
> createGraph(int n) {
+ public static List
> createGraph(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;
}
- private static void addUndirectedEdge(List
> graph, int from, int to) {
+ public static void addUndirectedEdge(List
> graph, int from, int to) {
graph.get(from).add(to);
graph.get(to).add(from);
}
+ // ==================== Main ====================
+
public static void main(String[] args) {
+ // Undirected tree:
+ //
+ // 0 - 1 - 2 - 3 - 4
+ // | |
+ // 6 5
+ // / \
+ // 7 8
+ //
+ // Rooted at 6:
+ //
+ // 6
+ // 2 7 8
+ // 1 3
+ // 0 4 5
+
List
> graph = createGraph(9);
addUndirectedEdge(graph, 0, 1);
addUndirectedEdge(graph, 2, 1);
@@ -113,12 +138,6 @@ public static void main(String[] args) {
addUndirectedEdge(graph, 6, 7);
addUndirectedEdge(graph, 6, 8);
- // Rooted at 6 the tree should look like:
- // 6
- // 2 7 8
- // 1 3
- // 0 4 5
-
TreeNode root = rootTree(graph, 6);
// Layer 0: [6]
@@ -136,7 +155,8 @@ public static void main(String[] args) {
+ ", "
+ root.children.get(0).children.get(1).children);
- // Rooted at 3 the tree should look like:
+ // Rooted at 3:
+ //
// 3
// 2 4 5
// 6 1
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
*
- *
> 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