mirror of
https://github.com/TheAlgorithms/Java.git
synced 2025-12-19 07:00:35 +08:00
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:
committed by
GitHub
parent
e6c576c50a
commit
fa8ea8e614
@@ -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 <= 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 -> 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 <= 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);
|
||||
}
|
||||
}
|
||||
@@ -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<>()); });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user