2872. Maximum Number of K-Divisible Components
Problem Description
You are given an undirected tree with n
nodes labeled from 0
to n - 1
. The tree structure is defined by a 2D array edges
of length n - 1
, where each edges[i] = [ai, bi]
represents an edge connecting nodes ai
and bi
.
Each node has an associated value given in the array values
, where values[i]
is the value of node i
. You are also given an integer k
.
Your task is to find a way to split the tree into multiple connected components by removing some edges (possibly none). For the split to be valid, each resulting connected component must have a sum of node values that is divisible by k
.
The value of a connected component is defined as the sum of all node values within that component.
You need to return the maximum number of components you can create through any valid split.
For example, if you have a tree with nodes having values that sum to 12 and k = 3
, you might be able to split it into components where each component's sum is divisible by 3 (like components with sums 3, 3, and 6, giving you 3 components total).
The key insight is that when you remove an edge from the tree, you create two separate components. If one component has a sum divisible by k
, and the total tree sum is also divisible by k
, then the other component will automatically have a sum divisible by k
as well.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly describes an undirected tree with
n
nodes and edges connecting them. A tree is a special type of graph (connected and acyclic).
Is it a tree?
- Yes: The problem states "There is an undirected tree with
n
nodes" and we have exactlyn-1
edges, which confirms it's a tree structure.
DFS
- We arrive at DFS: Since we're dealing with a tree structure, the flowchart leads us directly to DFS as the appropriate algorithm.
Why DFS is the right choice:
- We need to traverse the tree and calculate subtree sums at each node
- For each subtree rooted at a node, we need to determine if its sum is divisible by
k
- If a subtree's sum is divisible by
k
, we can "cut" the edge above it to create a new component - DFS naturally processes subtrees bottom-up, allowing us to accumulate sums from leaves to root
- Each time we find a subtree with sum divisible by
k
, we can treat it as a separate component
Conclusion: The flowchart correctly identifies DFS as the optimal approach for this tree-based problem where we need to explore all nodes, compute subtree properties (sums), and make decisions about splitting the tree into components based on divisibility conditions.
Intuition
The key observation is that if we cut an edge in the tree, we create two separate components. For both components to have sums divisible by k
, the subtree "below" the cut must have a sum divisible by k
. Since the total sum is divisible by k
(given by the problem), if one part is divisible by k
, the remaining part will automatically be divisible by k
too.
Think of it like cutting a cake that weighs 12 pounds into pieces where each piece must weigh a multiple of 3 pounds. If you cut off a 3-pound piece, the remaining 9 pounds is also divisible by 3.
This leads us to a greedy insight: whenever we find a subtree whose sum is divisible by k
, we can immediately "cut it off" as a separate component. We don't need to keep it attached to its parent because:
- The subtree itself forms a valid component (sum divisible by
k
) - The remaining tree will still have a sum divisible by
k
Using DFS, we can traverse the tree from bottom to top (post-order traversal). At each node, we:
- Calculate the sum of the current subtree (node value + sum of all child subtrees)
- If this sum is divisible by
k
, we've found a valid component - increment our answer - Return the sum to the parent (this represents the "weight" of the edge to the parent)
The beauty of this approach is that when a subtree sum is divisible by k
, we conceptually "cut" it off by counting it as a component. From the parent's perspective, this subtree contributes 0 to the parent's sum (since sum % k = 0
), effectively simulating the removal of that edge.
This greedy strategy works because we're maximizing components - any valid cut we can make should be made, as it increases our component count without affecting the validity of the remaining tree.
Learn more about Tree and Depth-First Search patterns.
Solution Approach
The implementation uses DFS to traverse the tree and calculate subtree sums. Here's how the solution works:
Data Structure Setup:
- Build an adjacency list
g
to represent the tree, whereg[i]
contains all neighbors of nodei
- Since the tree is undirected, we add edges in both directions
DFS Function:
The core logic is in the dfs(i, fa)
function where:
i
is the current node being visitedfa
is the parent node (to avoid revisiting in an undirected tree)
Algorithm Steps:
-
Initialize the sum for the current subtree with the current node's value:
s = values[i]
-
Recursively visit all children: For each neighbor
j
of nodei
:- Skip if
j
is the parent (to avoid going back up the tree) - Recursively call
dfs(j, i)
to get the sum of the subtree rooted atj
- Add this subtree sum to our current sum:
s += dfs(j, i)
- Skip if
-
Check divisibility: After collecting all child subtree sums:
- If
s % k == 0
, we've found a valid component - Increment the answer counter:
ans += s % k == 0
- This effectively "cuts" this subtree from its parent
- If
-
Return the sum: Return
s
to the parent call- If
s % k == 0
, this subtree conceptually contributes 0 to its parent's sum calculation - This simulates cutting the edge between this node and its parent
- If
Starting the traversal:
- Call
dfs(0, -1)
starting from node 0 with -1 as the parent (indicating no parent) - The
ans
variable accumulates the total number of valid components found
Why this works:
When we find a subtree with sum divisible by k
, we count it as a separate component. The mathematical property that makes this work is that if the total sum is divisible by k
and we remove a portion divisible by k
, the remainder is also divisible by k
. This allows us to greedily cut off valid subtrees without worrying about the rest of the tree becoming invalid.
The time complexity is O(n)
as we visit each node exactly once, and the space complexity is O(n)
for the adjacency list and recursion stack.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate how the solution works.
Example Tree:
Node values: [6, 2, 3, 3, 2] Edges: [[0,1], [0,2], [2,3], [2,4]] k = 3 Tree structure: 0(6) / \ 1(2) 2(3) / \ 3(3) 4(2)
Step-by-step DFS traversal:
-
Start at node 0 with parent -1
- Initialize sum:
s = 6
- Visit children: nodes 1 and 2
- Initialize sum:
-
Visit node 1 (child of 0)
- Initialize sum:
s = 2
- No children to visit
- Check:
2 % 3 ≠0
, so don't increment answer - Return sum = 2 to parent (node 0)
- Initialize sum:
-
Visit node 2 (child of 0)
- Initialize sum:
s = 3
- Visit children: nodes 3 and 4
- Initialize sum:
-
Visit node 3 (child of 2)
- Initialize sum:
s = 3
- No children to visit
- Check:
3 % 3 = 0
✓ Found a valid component! - Increment answer:
ans = 1
- Return sum = 3 to parent (node 2)
- Initialize sum:
-
Visit node 4 (child of 2)
- Initialize sum:
s = 2
- No children to visit
- Check:
2 % 3 ≠0
, so don't increment answer - Return sum = 2 to parent (node 2)
- Initialize sum:
-
Back at node 2
- Current sum:
s = 3 + 3 + 2 = 8
- Check:
8 % 3 ≠0
, so don't increment answer - Return sum = 8 to parent (node 0)
- Current sum:
-
Back at node 0
- Current sum:
s = 6 + 2 + 8 = 16
- Check:
16 % 3 ≠0
, so don't increment answer - Return sum = 16
- Current sum:
Result: Maximum components = 1
In this example, we found only one valid split: we can cut off the subtree rooted at node 3 (which has sum 3, divisible by k=3). The remaining tree would have sum 13, which is not divisible by 3, so this entire example actually shows an invalid case where the total sum (16) is not divisible by k.
Let's try a valid example where total sum is divisible by k:
Better Example:
Node values: [3, 3, 3] Edges: [[0,1], [0,2]] k = 3 Tree: 0(3) / \ 1(3) 2(3)
DFS traversal:
- Visit node 1: sum = 3,
3 % 3 = 0
✓, ans = 1 - Visit node 2: sum = 3,
3 % 3 = 0
✓, ans = 2 - Back at node 0: sum = 3 + 0 + 0 = 3 (children contribute 0 since they were cut),
3 % 3 = 0
✓, ans = 3
Result: Maximum components = 3 (each node becomes its own component)
This demonstrates how when we find a subtree divisible by k, it effectively contributes 0 to its parent's sum, simulating the edge removal.
Solution Implementation
1class Solution:
2 def maxKDivisibleComponents(
3 self, n: int, edges: List[List[int]], values: List[int], k: int
4 ) -> int:
5 def dfs(node: int, parent: int) -> int:
6 """
7 Perform DFS to calculate subtree sums and count valid components.
8
9 Args:
10 node: Current node being visited
11 parent: Parent node to avoid revisiting
12
13 Returns:
14 Sum of values in the subtree rooted at current node
15 """
16 # Start with the value of current node
17 subtree_sum = values[node]
18
19 # Visit all adjacent nodes except the parent
20 for neighbor in adjacency_list[node]:
21 if neighbor != parent:
22 # Add the sum of child subtree to current subtree sum
23 subtree_sum += dfs(neighbor, node)
24
25 # If subtree sum is divisible by k, we can cut this as a separate component
26 if subtree_sum % k == 0:
27 nonlocal component_count
28 component_count += 1
29
30 return subtree_sum
31
32 # Build adjacency list representation of the tree
33 adjacency_list = [[] for _ in range(n)]
34 for node_a, node_b in edges:
35 adjacency_list[node_a].append(node_b)
36 adjacency_list[node_b].append(node_a)
37
38 # Initialize counter for valid components
39 component_count = 0
40
41 # Start DFS from node 0 with -1 as parent (indicating no parent)
42 dfs(0, -1)
43
44 return component_count
45
1class Solution {
2 private int componentCount;
3 private List<Integer>[] adjacencyList;
4 private int[] nodeValues;
5 private int divisor;
6
7 /**
8 * Finds the maximum number of k-divisible components that can be created
9 * by removing edges from the tree.
10 *
11 * @param n The number of nodes in the tree
12 * @param edges The edges of the tree (undirected)
13 * @param values The values associated with each node
14 * @param k The divisor for checking divisibility
15 * @return The maximum number of k-divisible components
16 */
17 public int maxKDivisibleComponents(int n, int[][] edges, int[] values, int k) {
18 // Initialize the adjacency list for the tree
19 adjacencyList = new List[n];
20 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
21
22 // Build the undirected graph representation
23 for (int[] edge : edges) {
24 int nodeA = edge[0];
25 int nodeB = edge[1];
26 adjacencyList[nodeA].add(nodeB);
27 adjacencyList[nodeB].add(nodeA);
28 }
29
30 // Store instance variables for use in DFS
31 this.nodeValues = values;
32 this.divisor = k;
33 this.componentCount = 0;
34
35 // Start DFS from node 0 with no parent (-1)
36 dfs(0, -1);
37
38 return componentCount;
39 }
40
41 /**
42 * Performs depth-first search to calculate subtree sums and count
43 * k-divisible components.
44 *
45 * @param currentNode The current node being processed
46 * @param parentNode The parent node to avoid revisiting
47 * @return The sum of values in the subtree rooted at currentNode
48 */
49 private long dfs(int currentNode, int parentNode) {
50 // Initialize sum with the current node's value
51 long subtreeSum = nodeValues[currentNode];
52
53 // Traverse all adjacent nodes (children in the tree)
54 for (int adjacentNode : adjacencyList[currentNode]) {
55 // Skip the parent node to avoid going back up the tree
56 if (adjacentNode != parentNode) {
57 // Add the sum of the child's subtree
58 subtreeSum += dfs(adjacentNode, currentNode);
59 }
60 }
61
62 // If the subtree sum is divisible by k, it can form a separate component
63 // This effectively "cuts" the edge between this subtree and its parent
64 if (subtreeSum % divisor == 0) {
65 componentCount++;
66 }
67
68 // Return the subtree sum for parent calculations
69 return subtreeSum;
70 }
71}
72
1class Solution {
2public:
3 int maxKDivisibleComponents(int n, vector<vector<int>>& edges, vector<int>& values, int k) {
4 int componentCount = 0;
5
6 // Build adjacency list representation of the tree
7 vector<vector<int>> adjacencyList(n);
8 for (const auto& edge : edges) {
9 int nodeA = edge[0];
10 int nodeB = edge[1];
11 adjacencyList[nodeA].push_back(nodeB);
12 adjacencyList[nodeB].push_back(nodeA);
13 }
14
15 // DFS function to traverse the tree and count valid components
16 // Returns the sum of values in the subtree rooted at current node
17 function<long long(int, int)> dfs = [&](int currentNode, int parentNode) -> long long {
18 // Start with the value of current node
19 long long subtreeSum = values[currentNode];
20
21 // Traverse all children (neighbors except parent)
22 for (int childNode : adjacencyList[currentNode]) {
23 if (childNode != parentNode) {
24 // Add the sum from child's subtree
25 subtreeSum += dfs(childNode, currentNode);
26 }
27 }
28
29 // If current subtree sum is divisible by k, it can form a separate component
30 if (subtreeSum % k == 0) {
31 componentCount++;
32 }
33
34 // Return the subtree sum for parent's calculation
35 return subtreeSum;
36 };
37
38 // Start DFS from node 0 with no parent (-1)
39 dfs(0, -1);
40
41 return componentCount;
42 }
43};
44
1/**
2 * Finds the maximum number of components in a tree where each component's sum is divisible by k
3 * @param n - Number of nodes in the tree
4 * @param edges - Array of edges connecting nodes [node1, node2]
5 * @param values - Array of values for each node
6 * @param k - The divisor for checking component sums
7 * @returns Maximum number of k-divisible components
8 */
9function maxKDivisibleComponents(
10 n: number,
11 edges: number[][],
12 values: number[],
13 k: number,
14): number {
15 // Build adjacency list representation of the tree
16 const adjacencyList: number[][] = Array.from({ length: n }, () => []);
17
18 // Add bidirectional edges to the adjacency list
19 for (const [nodeA, nodeB] of edges) {
20 adjacencyList[nodeA].push(nodeB);
21 adjacencyList[nodeB].push(nodeA);
22 }
23
24 // Counter for k-divisible components
25 let componentCount: number = 0;
26
27 /**
28 * Performs DFS traversal to calculate subtree sums and count k-divisible components
29 * @param currentNode - Current node being visited
30 * @param parentNode - Parent node to avoid revisiting
31 * @returns Sum of values in the subtree rooted at currentNode
32 */
33 const dfs = (currentNode: number, parentNode: number): number => {
34 // Initialize sum with current node's value
35 let subtreeSum: number = values[currentNode];
36
37 // Traverse all adjacent nodes except parent
38 for (const adjacentNode of adjacencyList[currentNode]) {
39 if (adjacentNode !== parentNode) {
40 // Add sum of child subtree
41 subtreeSum += dfs(adjacentNode, currentNode);
42 }
43 }
44
45 // If subtree sum is divisible by k, we can cut this as a separate component
46 if (subtreeSum % k === 0) {
47 componentCount++;
48 }
49
50 // Return the subtree sum for parent's calculation
51 return subtreeSum;
52 };
53
54 // Start DFS from node 0 with -1 as parent (indicating no parent)
55 dfs(0, -1);
56
57 return componentCount;
58}
59
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the tree. Each node is visited exactly once during the DFS traversal. For each node, we:
- Process its value (constant time)
- Iterate through its adjacent nodes
- Perform modulo operation and increment counter (constant time)
Since this is a tree with n
nodes and n-1
edges, the total number of edges traversed is 2(n-1)
(each edge is traversed twice, once from each direction). Therefore, the overall time complexity is O(n + 2(n-1)) = O(n)
.
Space Complexity: O(n)
The space complexity consists of:
- The adjacency list
g
which stores all edges:O(n)
space (storing2(n-1)
edge relationships) - The recursive call stack for DFS:
O(h)
whereh
is the height of the tree. In the worst case (skewed tree),h = n
, giving usO(n)
- Other variables (
ans
,s
, loop variables):O(1)
The dominant factor is the adjacency list and the potential recursive stack depth, resulting in an overall space complexity of O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Subtree Sum After Finding a Valid Component
The Pitfall: A common mistake is to reset the subtree sum to 0 when finding a component divisible by k, thinking this "cuts" the edge:
def dfs(node: int, parent: int) -> int:
subtree_sum = values[node]
for neighbor in adjacency_list[node]:
if neighbor != parent:
subtree_sum += dfs(neighbor, node)
if subtree_sum % k == 0:
nonlocal component_count
component_count += 1
return 0 # WRONG: This breaks the logic for parent calculations
return subtree_sum
Why It's Wrong:
Returning 0 when subtree_sum % k == 0
seems logical (to simulate cutting the edge), but it causes problems:
- When a parent node receives 0 from a child, it loses information about that entire subtree
- This can lead to incorrect calculations for ancestor nodes
- The parent might incorrectly think it can form another valid component
The Correct Approach:
The original solution correctly returns the actual subtree_sum
regardless of divisibility:
def dfs(node: int, parent: int) -> int:
subtree_sum = values[node]
for neighbor in adjacency_list[node]:
if neighbor != parent:
subtree_sum += dfs(neighbor, node)
if subtree_sum % k == 0:
nonlocal component_count
component_count += 1
return subtree_sum # Always return the actual sum
This works because:
- We count valid components when found but continue passing the actual sum up the tree
- The mathematical property ensures that if we remove components divisible by k from a total divisible by k, the remainder is also divisible by k
- Each valid subtree is counted exactly once
2. Forgetting to Check if Total Sum is Divisible by k
The Pitfall: Not verifying that the total sum of all node values is divisible by k before proceeding:
# Missing validation
def maxKDivisibleComponents(self, n, edges, values, k):
# Directly start DFS without checking total sum
# ...
Why It Matters: If the total sum isn't divisible by k, it's impossible to split the tree into valid components where each has a sum divisible by k.
The Solution: Add a preliminary check (though the given solution implicitly handles this):
def maxKDivisibleComponents(self, n, edges, values, k):
total_sum = sum(values)
if total_sum % k != 0:
return 0 # No valid split possible
# Continue with DFS...
3. Double-Counting Components in Undirected Trees
The Pitfall: Forgetting to track the parent node during DFS traversal, causing revisits:
def dfs(node: int) -> int: # Missing parent parameter
subtree_sum = values[node]
for neighbor in adjacency_list[node]:
# No parent check - will revisit nodes!
subtree_sum += dfs(neighbor)
Why It's Wrong: In an undirected tree, edges go both ways. Without tracking the parent, the DFS will:
- Visit node A from B
- Then try to visit B from A again
- Leading to infinite recursion or incorrect calculations
The Correct Approach: Always pass and check the parent:
def dfs(node: int, parent: int) -> int:
subtree_sum = values[node]
for neighbor in adjacency_list[node]:
if neighbor != parent: # Skip the parent
subtree_sum += dfs(neighbor, node)
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!