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> 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 edges = graph.get(at); - for (Integer to : edges) { + for (int to : graph.get(at)) { if (to == parent) continue; if (!visited[to]) { dfs(root, to, at); low[at] = min(low[at], low[to]); + // If no vertex in the subtree rooted at 'to' can reach above 'at', + // then removing 'at' would disconnect 'to's subtree. if (ids[at] <= low[to]) { isArticulationPoint[at] = true; } } else { + // Back edge: update low-link to the earliest reachable ancestor. low[at] = min(low[at], ids[to]); } } @@ -74,25 +100,35 @@ private void dfs(int root, int at, int parent) { /* Graph helpers */ - // Initialize a graph with 'n' nodes. public static 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 * - *

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 children; + private final int id; + private final TreeNode parent; + private final List children; // Useful constructor for root node. public TreeNode(int id) { @@ -25,7 +30,7 @@ public TreeNode(int id) { public TreeNode(int id, TreeNode parent) { this.id = id; this.parent = parent; - children = new LinkedList<>(); + this.children = new ArrayList<>(); } public void addChildren(TreeNode... nodes) { @@ -66,43 +71,63 @@ public int hashCode() { } } + /** + * Roots the undirected tree at the given node and returns the root TreeNode. + */ public static TreeNode rootTree(List> 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 * - *

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 children; + private final int id; + private final TreeNode parent; + private final List children; - // Useful constructor for root node. public TreeNode(int id) { this(id, /* parent= */ null); } @@ -25,7 +43,7 @@ public TreeNode(int id) { public TreeNode(int id, TreeNode parent) { this.id = id; this.parent = parent; - children = new LinkedList<>(); + this.children = new ArrayList<>(); } public void addChildren(TreeNode... nodes) { @@ -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> 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 labels = new LinkedList<>(); + List 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> 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 nodes = new ArrayList<>(); nodes.add(0);