Feature/centroid decomposition (#7086)

* feat: Add Centroid Decomposition for trees (#7054)

- Implement CentroidDecomposition with O(N log N) construction
- Add CentroidTree class with parent tracking and query methods
- Include buildFromEdges helper for easy tree construction
- Add comprehensive test suite with 20+ test cases
- Cover edge cases, validation, and various tree structures

Closes #7054

* feat: Add Centroid Decomposition for trees (#7054)

- Implement CentroidDecomposition with O(N log N) construction
- Add CentroidTree class with parent tracking and query methods
- Include buildFromEdges helper for easy tree construction
- Add comprehensive test suite with 20+ test cases
- Cover edge cases, validation, and various tree structures

Closes #7054

* fix: Remove trailing whitespace from CentroidDecompositionTest

* fix: Remove trailing whitespace and add newlines at end of files

* fix: Format code to comply with clang-format and checkstyle requirements

* style: Fix lambda formatting in test assertions

- Change single-line lambdas to multi-line format
- Align with repository code style guidelines

* style: Apply clang-format to match repository style guide

- Format code according to .clang-format configuration
- Use single-line lambdas as allowed by AllowShortLambdasOnASingleLine: All
- Apply 4-space indentation
- Ensure proper line endings
This commit is contained in:
KANAKALA SAI KIRAN
2025-11-22 15:14:34 +05:30
committed by GitHub
parent e6c576c50a
commit fa8ea8e614
2 changed files with 453 additions and 0 deletions

View File

@@ -0,0 +1,217 @@
package com.thealgorithms.datastructures.trees;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Centroid Decomposition is a divide-and-conquer technique for trees.
* It recursively partitions a tree by finding centroids - nodes whose removal
* creates balanced subtrees (each with at most N/2 nodes).
*
* <p>
* Time Complexity: O(N log N) for construction
* Space Complexity: O(N)
*
* <p>
* Applications:
* - Distance queries on trees
* - Path counting problems
* - Nearest neighbor searches
*
* @see <a href="https://en.wikipedia.org/wiki/Centroid_decomposition">Centroid Decomposition</a>
* @see <a href="https://codeforces.com/blog/entry/81661">Centroid Decomposition Tutorial</a>
* @author lens161
*/
public final class CentroidDecomposition {
private CentroidDecomposition() {
}
/**
* Represents the centroid tree structure.
*/
public static final class CentroidTree {
private final int n;
private final List<List<Integer>> adj;
private final int[] parent;
private final int[] subtreeSize;
private final boolean[] removed;
private int root;
/**
* Constructs a centroid tree from an adjacency list.
*
* @param adj adjacency list representation of the tree (0-indexed)
* @throws IllegalArgumentException if tree is empty or null
*/
public CentroidTree(List<List<Integer>> adj) {
if (adj == null || adj.isEmpty()) {
throw new IllegalArgumentException("Tree cannot be empty or null");
}
this.n = adj.size();
this.adj = adj;
this.parent = new int[n];
this.subtreeSize = new int[n];
this.removed = new boolean[n];
Arrays.fill(parent, -1);
// Build centroid tree starting from node 0
this.root = decompose(0, -1);
}
/**
* Recursively builds the centroid tree.
*
* @param u current node
* @param p parent in centroid tree
* @return centroid of current component
*/
private int decompose(int u, int p) {
int size = getSubtreeSize(u, -1);
int centroid = findCentroid(u, -1, size);
removed[centroid] = true;
parent[centroid] = p;
// Recursively decompose each subtree
for (int v : adj.get(centroid)) {
if (!removed[v]) {
decompose(v, centroid);
}
}
return centroid;
}
/**
* Calculates subtree size from node u.
*
* @param u current node
* @param p parent node (-1 for root)
* @return size of subtree rooted at u
*/
private int getSubtreeSize(int u, int p) {
subtreeSize[u] = 1;
for (int v : adj.get(u)) {
if (v != p && !removed[v]) {
subtreeSize[u] += getSubtreeSize(v, u);
}
}
return subtreeSize[u];
}
/**
* Finds the centroid of a subtree.
* A centroid is a node whose removal creates components with size &lt;= totalSize/2.
*
* @param u current node
* @param p parent node
* @param totalSize total size of current component
* @return centroid node
*/
private int findCentroid(int u, int p, int totalSize) {
for (int v : adj.get(u)) {
if (v != p && !removed[v] && subtreeSize[v] > totalSize / 2) {
return findCentroid(v, u, totalSize);
}
}
return u;
}
/**
* Gets the parent of a node in the centroid tree.
*
* @param node the node
* @return parent node in centroid tree, or -1 if root
*/
public int getParent(int node) {
if (node < 0 || node >= n) {
throw new IllegalArgumentException("Invalid node: " + node);
}
return parent[node];
}
/**
* Gets the root of the centroid tree.
*
* @return root node
*/
public int getRoot() {
return root;
}
/**
* Gets the number of nodes in the tree.
*
* @return number of nodes
*/
public int size() {
return n;
}
/**
* Returns the centroid tree structure as a string.
* Format: node -&gt; parent (or ROOT for root node)
*
* @return string representation
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Centroid Tree:\n");
for (int i = 0; i < n; i++) {
sb.append("Node ").append(i).append(" -> ");
if (parent[i] == -1) {
sb.append("ROOT");
} else {
sb.append("Parent ").append(parent[i]);
}
sb.append("\n");
}
return sb.toString();
}
}
/**
* Creates a centroid tree from an edge list.
*
* @param n number of nodes (0-indexed: 0 to n-1)
* @param edges list of edges where each edge is [u, v]
* @return CentroidTree object
* @throws IllegalArgumentException if n &lt;= 0 or edges is invalid
*/
public static CentroidTree buildFromEdges(int n, int[][] edges) {
if (n <= 0) {
throw new IllegalArgumentException("Number of nodes must be positive");
}
if (edges == null) {
throw new IllegalArgumentException("Edges cannot be null");
}
if (edges.length != n - 1) {
throw new IllegalArgumentException("Tree must have exactly n-1 edges");
}
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) {
adj.add(new ArrayList<>());
}
for (int[] edge : edges) {
if (edge.length != 2) {
throw new IllegalArgumentException("Each edge must have exactly 2 nodes");
}
int u = edge[0];
int v = edge[1];
if (u < 0 || u >= n || v < 0 || v >= n) {
throw new IllegalArgumentException("Invalid node in edge: [" + u + ", " + v + "]");
}
adj.get(u).add(v);
adj.get(v).add(u);
}
return new CentroidTree(adj);
}
}

View File

@@ -0,0 +1,236 @@
package com.thealgorithms.datastructures.trees;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
/**
* Test cases for CentroidDecomposition
*
* @author lens161
*/
class CentroidDecompositionTest {
@Test
void testSingleNode() {
// Tree with just one node
int[][] edges = {};
CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(1, edges);
assertEquals(1, tree.size());
assertEquals(0, tree.getRoot());
assertEquals(-1, tree.getParent(0));
}
@Test
void testTwoNodes() {
// Simple tree: 0 - 1
int[][] edges = {{0, 1}};
CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(2, edges);
assertEquals(2, tree.size());
int root = tree.getRoot();
assertTrue(root == 0 || root == 1, "Root should be either node 0 or 1");
// One node should be root, other should have the root as parent
int nonRoot = (root == 0) ? 1 : 0;
assertEquals(-1, tree.getParent(root));
assertEquals(root, tree.getParent(nonRoot));
}
@Test
void testLinearTree() {
// Linear tree: 0 - 1 - 2 - 3 - 4
int[][] edges = {{0, 1}, {1, 2}, {2, 3}, {3, 4}};
CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(5, edges);
assertEquals(5, tree.size());
// For a linear tree of 5 nodes, the centroid should be the middle node (node 2)
assertEquals(2, tree.getRoot());
assertEquals(-1, tree.getParent(2));
}
@Test
void testBalancedBinaryTree() {
// Balanced binary tree:
// 0
// / \
// 1 2
// / \
// 3 4
int[][] edges = {{0, 1}, {0, 2}, {1, 3}, {1, 4}};
CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(5, edges);
assertEquals(5, tree.size());
// Root should be 0 or 1 (both are valid centroids)
int root = tree.getRoot();
assertTrue(root == 0 || root == 1);
assertEquals(-1, tree.getParent(root));
// All nodes should have a parent in centroid tree except root
for (int i = 0; i < 5; i++) {
if (i != root) {
assertTrue(tree.getParent(i) >= 0 && tree.getParent(i) < 5);
}
}
}
@Test
void testStarTree() {
// Star tree: center node 0 connected to 1, 2, 3, 4
int[][] edges = {{0, 1}, {0, 2}, {0, 3}, {0, 4}};
CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(5, edges);
assertEquals(5, tree.size());
// Center node (0) should be the root
assertEquals(0, tree.getRoot());
// All other nodes should have 0 as parent
for (int i = 1; i < 5; i++) {
assertEquals(0, tree.getParent(i));
}
}
@Test
void testCompleteTree() {
// Complete binary tree of 7 nodes:
// 0
// / \
// 1 2
// / \ / \
// 3 4 5 6
int[][] edges = {{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}, {2, 6}};
CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(7, edges);
assertEquals(7, tree.size());
assertEquals(0, tree.getRoot()); // Root should be the center
// Verify all nodes are reachable in centroid tree
boolean[] visited = new boolean[7];
visited[0] = true;
for (int i = 1; i < 7; i++) {
int parent = tree.getParent(i);
assertTrue(parent >= 0 && parent < 7);
assertTrue(visited[parent], "Parent should be processed before child");
visited[i] = true;
}
}
@Test
void testLargerTree() {
// Tree with 10 nodes
int[][] edges = {{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}, {2, 6}, {3, 7}, {4, 8}, {5, 9}};
CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(10, edges);
assertEquals(10, tree.size());
int root = tree.getRoot();
assertTrue(root >= 0 && root < 10);
assertEquals(-1, tree.getParent(root));
// Verify centroid tree structure is valid
for (int i = 0; i < 10; i++) {
if (i != root) {
assertTrue(tree.getParent(i) >= -1 && tree.getParent(i) < 10);
}
}
}
@Test
void testPathGraph() {
// Path graph with 8 nodes: 0-1-2-3-4-5-6-7
int[][] edges = {{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 6}, {6, 7}};
CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(8, edges);
assertEquals(8, tree.size());
// For path of 8 nodes, centroid should be around middle
int root = tree.getRoot();
assertTrue(root >= 2 && root <= 5, "Root should be near the middle of path");
}
@Test
void testInvalidEmptyTree() {
assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(0, new int[][] {}); });
}
@Test
void testInvalidNegativeNodes() {
assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(-1, new int[][] {}); });
}
@Test
void testInvalidNullEdges() {
assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(5, null); });
}
@Test
void testInvalidEdgeCount() {
// Tree with n nodes must have n-1 edges
int[][] edges = {{0, 1}, {1, 2}}; // 2 edges for 5 nodes (should be 4)
assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(5, edges); });
}
@Test
void testInvalidEdgeFormat() {
int[][] edges = {{0, 1, 2}}; // Edge with 3 elements instead of 2
assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(3, edges); });
}
@Test
void testInvalidNodeInEdge() {
int[][] edges = {{0, 5}}; // Node 5 doesn't exist in tree of size 3
assertThrows(IllegalArgumentException.class, () -> { CentroidDecomposition.buildFromEdges(3, edges); });
}
@Test
void testInvalidNodeQuery() {
int[][] edges = {{0, 1}, {1, 2}};
CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(3, edges);
assertThrows(IllegalArgumentException.class, () -> { tree.getParent(-1); });
assertThrows(IllegalArgumentException.class, () -> { tree.getParent(5); });
}
@Test
void testToString() {
int[][] edges = {{0, 1}, {1, 2}};
CentroidDecomposition.CentroidTree tree = CentroidDecomposition.buildFromEdges(3, edges);
String result = tree.toString();
assertNotNull(result);
assertTrue(result.contains("Centroid Tree"));
assertTrue(result.contains("Node"));
assertTrue(result.contains("ROOT"));
}
@Test
void testAdjacencyListConstructor() {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < 3; i++) {
adj.add(new ArrayList<>());
}
adj.get(0).add(1);
adj.get(1).add(0);
adj.get(1).add(2);
adj.get(2).add(1);
CentroidDecomposition.CentroidTree tree = new CentroidDecomposition.CentroidTree(adj);
assertEquals(3, tree.size());
assertEquals(1, tree.getRoot());
}
@Test
void testNullAdjacencyList() {
assertThrows(IllegalArgumentException.class, () -> { new CentroidDecomposition.CentroidTree(null); });
}
@Test
void testEmptyAdjacencyList() {
assertThrows(IllegalArgumentException.class, () -> { new CentroidDecomposition.CentroidTree(new ArrayList<>()); });
}
}