3372. Maximize the Number of Target Nodes After Connecting Trees I
Problem Description
You are given two undirected trees with n
and m
nodes respectively. The nodes in the first tree are labeled from [0, n-1]
and nodes in the second tree are labeled from [0, m-1]
. Each tree is represented by its edge list: edges1
for the first tree and edges2
for the second tree.
The key concept is that a node u
is considered target to node v
if the distance (number of edges) between them is at most k
. Every node is always target to itself.
Your task is to find, for each node i
in the first tree, the maximum number of nodes that can be target to it when you connect node i
to exactly one node in the second tree.
The process works as follows:
- For each node
i
in the first tree, you can choose to connect it to any nodej
in the second tree - Once connected, count how many total nodes from both trees are within distance
k
from nodei
- Find the maximum possible count by choosing the optimal node
j
to connect to - Each query is independent - the edge added for one query is removed before the next
The answer should be an array where answer[i]
represents the maximum number of target nodes for node i
in the first tree.
For example, if you connect node i
from tree 1 to node j
from tree 2, then any node that is:
- Within
k
edges from nodei
in tree 1, OR - Within
k-1
edges from nodej
in tree 2 (since it takes 1 edge to go fromi
toj
)
would be considered target to node i
.
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 involves two undirected trees, which are special types of graphs. We have nodes and edges explicitly defined in
edges1
andedges2
.
Is it a tree?
- Yes: The problem explicitly states we're working with two undirected trees. A tree is a connected graph with no cycles, and we have
n-1
edges forn
nodes in the first tree andm-1
edges form
nodes in the second tree.
DFS
- Yes: Since we're dealing with trees and need to explore nodes within a certain distance (depth
k
), DFS is the appropriate choice.
Conclusion: The flowchart leads us directly to using DFS for this problem.
Why DFS is the Right Choice
The problem requires us to:
- Count nodes within a specific distance
k
from a given node in tree 1 - Count nodes within distance
k-1
from any node in tree 2 - Find the maximum possible sum when connecting a node from tree 1 to the optimal node in tree 2
DFS is perfect for this because:
- It naturally explores paths in trees while tracking depth/distance
- We can control the exploration depth by passing a depth parameter that decrements with each recursive call
- When the depth reaches 0 or negative, we stop exploring further
- It efficiently counts all reachable nodes within the specified distance constraint
The solution uses DFS twice:
- First, to find the maximum number of nodes reachable within distance
k-1
from any node in tree 2 - Second, for each node in tree 1, to find nodes reachable within distance
k
This approach aligns perfectly with the DFS pattern identified by the flowchart for tree-based problems.
Intuition
Let's think about what happens when we connect a node i
from tree 1 to a node j
from tree 2. After this connection, node i
can reach:
- All nodes in tree 1 that are within distance
k
from itself - All nodes in tree 2 that are within distance
k
from itself through the bridge edge to nodej
Since it takes 1 edge to go from node i
to node j
, any node in tree 2 that is within distance k-1
from node j
will be within distance k
from node i
.
Here's the key insight: The contribution from tree 1 is fixed for each node i
- it's simply the count of nodes within distance k
from node i
in tree 1. What varies is the contribution from tree 2, which depends on which node j
we choose to connect to.
To maximize the total count for node i
, we want to connect it to the node j
in tree 2 that has the maximum number of nodes within distance k-1
from it. But here's another crucial observation: for a given node i
, we always want to connect to the same optimal node in tree 2, regardless of which node i
we're considering.
Why? Because the "best" node in tree 2 (the one with maximum nodes within distance k-1
) doesn't change based on which node in tree 1 we're connecting from. It's an independent property of tree 2.
This leads to our solution strategy:
- First, find the maximum number of nodes reachable within distance
k-1
from any node in tree 2. Let's call thist
. - For each node
i
in tree 1, count the nodes within distancek
from it in tree 1. - The answer for node
i
is the sum of its local count plust
.
This decomposition works because the two contributions (from tree 1 and tree 2) are independent - they don't interfere with each other since we're only adding a single bridge edge between the trees.
Learn more about Tree, Depth-First Search and Breadth-First Search patterns.
Solution Approach
The implementation follows the intuition we developed, using DFS to count reachable nodes within a given distance. Let's walk through the key components:
1. Building the Adjacency List
The build
function converts the edge list into an adjacency list representation:
def build(edges: List[List[int]]) -> List[List[int]]:
n = len(edges) + 1 # n nodes means n-1 edges in a [tree](/problems/tree_intro)
g = [[] for _ in range(n)]
for a, b in edges:
g[a].append(b)
g[b].append(a)
return g
This creates a bidirectional graph where g[i]
contains all neighbors of node i
.
2. DFS to Count Nodes Within Distance
The dfs
function counts nodes reachable from node a
within distance d
:
def dfs(g: List[List[int]], a: int, fa: int, d: int) -> int:
if d < 0:
return 0
cnt = 1 # Count current node
for b in g[a]:
if b != fa: # Avoid going back to parent
cnt += dfs(g, b, a, d - 1)
return cnt
Key aspects:
a
is the current node,fa
is the parent (to avoid cycles in tree traversal)d
is the remaining distance we can travel- Base case: when
d < 0
, we've exceeded the distance limit - We count the current node (
cnt = 1
) and recursively count nodes from all children - For each child, we decrease the distance by 1
3. Finding Maximum Contribution from Tree 2
g2 = build(edges2)
m = len(edges2) + 1
t = max(dfs(g2, i, -1, k - 1) for i in range(m))
We try every node in tree 2 as a potential connection point and find which one gives us the maximum nodes within distance k-1
. We use k-1
because connecting from tree 1 to tree 2 takes 1 edge.
4. Computing Answer for Each Node in Tree 1
g1 = build(edges1)
n = len(edges1) + 1
return [dfs(g1, i, -1, k) + t for i in range(n)]
For each node i
in tree 1:
- Count nodes within distance
k
in tree 1:dfs(g1, i, -1, k)
- Add the maximum contribution from tree 2:
+ t
The time complexity is O(n² + m²)
where n
and m
are the sizes of the two trees, as we perform DFS from every node in both trees. The space complexity is O(n + m)
for storing the adjacency lists 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 small example to illustrate the solution approach.
Given:
- Tree 1 has edges:
[[0,1], [0,2]]
(nodes: 0, 1, 2) - Tree 2 has edges:
[[0,1], [1,2], [2,3]]
(nodes: 0, 1, 2, 3) k = 2
Tree 1: Tree 2: 0 0---1---2---3 / \ 1 2
Step 1: Find maximum contribution from Tree 2
We need to find which node in Tree 2 has the most nodes within distance k-1 = 1
:
- From node 0: Can reach {0, 1} → count = 2
- From node 1: Can reach {0, 1, 2} → count = 3
- From node 2: Can reach {1, 2, 3} → count = 3
- From node 3: Can reach {2, 3} → count = 2
Maximum contribution t = 3
(from either node 1 or node 2)
Step 2: Calculate answer for each node in Tree 1
For node 0 in Tree 1:
- Nodes within distance 2 in Tree 1: {0, 1, 2} → count = 3
- Total = 3 + 3 = 6
- (This happens when we connect node 0 to node 1 or 2 in Tree 2)
For node 1 in Tree 1:
- Nodes within distance 2 in Tree 1: {0, 1, 2} → count = 3
- Total = 3 + 3 = 6
- (This happens when we connect node 1 to node 1 or 2 in Tree 2)
For node 2 in Tree 1:
- Nodes within distance 2 in Tree 1: {0, 1, 2} → count = 3
- Total = 3 + 3 = 6
- (This happens when we connect node 2 to node 1 or 2 in Tree 2)
Answer: [6, 6, 6]
The key insight is that we always connect to the optimal node in Tree 2 (node 1 or 2 in this case) regardless of which node in Tree 1 we're querying. Each node in Tree 1 can reach all 3 nodes in its own tree within distance 2, plus 3 additional nodes from Tree 2 through the optimal connection.
Solution Implementation
1class Solution:
2 def maxTargetNodes(
3 self, edges1: List[List[int]], edges2: List[List[int]], k: int
4 ) -> List[int]:
5 """
6 Find the maximum number of target nodes for each node in tree1.
7 A target node is reachable within distance k from tree1 and k-1 from tree2.
8
9 Args:
10 edges1: Edge list for tree1
11 edges2: Edge list for tree2
12 k: Maximum distance to consider
13
14 Returns:
15 List where result[i] is the max target nodes when starting from node i in tree1
16 """
17
18 def build_adjacency_list(edges: List[List[int]]) -> List[List[int]]:
19 """
20 Build adjacency list representation of a tree from edge list.
21
22 Args:
23 edges: List of edges where each edge is [node_a, node_b]
24
25 Returns:
26 Adjacency list where graph[i] contains all neighbors of node i
27 """
28 num_nodes = len(edges) + 1
29 graph = [[] for _ in range(num_nodes)]
30
31 for node_a, node_b in edges:
32 graph[node_a].append(node_b)
33 graph[node_b].append(node_a)
34
35 return graph
36
37 def count_nodes_within_distance(
38 graph: List[List[int]],
39 start_node: int,
40 parent: int,
41 remaining_distance: int
42 ) -> int:
43 """
44 Count all nodes reachable from start_node within given distance using DFS.
45
46 Args:
47 graph: Adjacency list representation of the tree
48 start_node: Current node in DFS traversal
49 parent: Parent node to avoid revisiting (-1 if root)
50 remaining_distance: Remaining distance we can travel
51
52 Returns:
53 Number of nodes reachable within the distance limit
54 """
55 # Base case: no more distance to travel
56 if remaining_distance < 0:
57 return 0
58
59 # Count current node
60 node_count = 1
61
62 # Recursively count nodes from all neighbors except parent
63 for neighbor in graph[start_node]:
64 if neighbor != parent:
65 node_count += count_nodes_within_distance(
66 graph, neighbor, start_node, remaining_distance - 1
67 )
68
69 return node_count
70
71 # Build adjacency list for tree2
72 graph2 = build_adjacency_list(edges2)
73 num_nodes_tree2 = len(edges2) + 1
74
75 # Find maximum nodes reachable within distance k-1 from any node in tree2
76 max_nodes_from_tree2 = max(
77 count_nodes_within_distance(graph2, node, -1, k - 1)
78 for node in range(num_nodes_tree2)
79 )
80
81 # Build adjacency list for tree1
82 graph1 = build_adjacency_list(edges1)
83 num_nodes_tree1 = len(edges1) + 1
84
85 # For each node in tree1, count nodes within distance k and add max from tree2
86 result = [
87 count_nodes_within_distance(graph1, node, -1, k) + max_nodes_from_tree2
88 for node in range(num_nodes_tree1)
89 ]
90
91 return result
92
1class Solution {
2 /**
3 * Finds the maximum number of target nodes reachable within k distance
4 * from each node in graph1, considering the maximum from graph2
5 * @param edges1 Edge list for the first tree
6 * @param edges2 Edge list for the second tree
7 * @param k Maximum distance to consider
8 * @return Array where ans[i] is the maximum target nodes for node i in graph1
9 */
10 public int[] maxTargetNodes(int[][] edges1, int[][] edges2, int k) {
11 // Build adjacency list for graph2
12 List<Integer>[] graph2 = buildGraph(edges2);
13 int graph2Size = edges2.length + 1;
14
15 // Find maximum reachable nodes in graph2 within distance k-1
16 int maxNodesInGraph2 = 0;
17 for (int node = 0; node < graph2Size; node++) {
18 int reachableNodes = countReachableNodes(graph2, node, -1, k - 1);
19 maxNodesInGraph2 = Math.max(maxNodesInGraph2, reachableNodes);
20 }
21
22 // Build adjacency list for graph1
23 List<Integer>[] graph1 = buildGraph(edges1);
24 int graph1Size = edges1.length + 1;
25
26 // Calculate result for each node in graph1
27 int[] result = new int[graph1Size];
28 Arrays.fill(result, maxNodesInGraph2);
29
30 for (int node = 0; node < graph1Size; node++) {
31 // Add reachable nodes from current node in graph1 within distance k
32 result[node] += countReachableNodes(graph1, node, -1, k);
33 }
34
35 return result;
36 }
37
38 /**
39 * Builds an adjacency list representation of the tree from edges
40 * @param edges Array of edges where each edge is [nodeA, nodeB]
41 * @return Adjacency list representation of the graph
42 */
43 private List<Integer>[] buildGraph(int[][] edges) {
44 int numberOfNodes = edges.length + 1;
45 List<Integer>[] adjacencyList = new List[numberOfNodes];
46
47 // Initialize each node's adjacency list
48 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
49
50 // Add bidirectional edges to the adjacency list
51 for (int[] edge : edges) {
52 int nodeA = edge[0];
53 int nodeB = edge[1];
54 adjacencyList[nodeA].add(nodeB);
55 adjacencyList[nodeB].add(nodeA);
56 }
57
58 return adjacencyList;
59 }
60
61 /**
62 * Counts the number of nodes reachable from a starting node within a given distance
63 * @param graph The adjacency list representation of the graph
64 * @param currentNode Current node being visited
65 * @param parentNode Parent node to avoid revisiting (use -1 for root)
66 * @param remainingDistance Remaining distance that can be traversed
67 * @return Number of nodes reachable within the specified distance
68 */
69 private int countReachableNodes(List<Integer>[] graph, int currentNode,
70 int parentNode, int remainingDistance) {
71 // Base case: no more distance to traverse
72 if (remainingDistance < 0) {
73 return 0;
74 }
75
76 // Count current node
77 int nodeCount = 1;
78
79 // Recursively count nodes reachable from neighbors
80 for (int neighbor : graph[currentNode]) {
81 // Skip parent node to avoid cycles in tree traversal
82 if (neighbor != parentNode) {
83 nodeCount += countReachableNodes(graph, neighbor, currentNode,
84 remainingDistance - 1);
85 }
86 }
87
88 return nodeCount;
89 }
90}
91
1class Solution {
2public:
3 vector<int> maxTargetNodes(vector<vector<int>>& edges1, vector<vector<int>>& edges2, int k) {
4 // Build adjacency list for tree2
5 auto graph2 = buildGraph(edges2);
6 int tree2Size = edges2.size() + 1;
7
8 // Find the maximum number of nodes reachable within distance k-1 in tree2
9 // We use k-1 because we need to account for the edge connecting the two trees
10 int maxNodesInTree2 = 0;
11 for (int node = 0; node < tree2Size; ++node) {
12 maxNodesInTree2 = max(maxNodesInTree2, countReachableNodes(graph2, node, -1, k - 1));
13 }
14
15 // Build adjacency list for tree1
16 auto graph1 = buildGraph(edges1);
17 int tree1Size = edges1.size() + 1;
18
19 // Calculate result for each node in tree1
20 vector<int> result(tree1Size, maxNodesInTree2);
21 for (int node = 0; node < tree1Size; ++node) {
22 // Add nodes reachable within distance k from current node in tree1
23 result[node] += countReachableNodes(graph1, node, -1, k);
24 }
25
26 return result;
27 }
28
29private:
30 // Build adjacency list representation of the tree from edges
31 vector<vector<int>> buildGraph(const vector<vector<int>>& edges) {
32 int nodeCount = edges.size() + 1; // Tree with n nodes has n-1 edges
33 vector<vector<int>> adjacencyList(nodeCount);
34
35 for (const auto& edge : edges) {
36 int nodeA = edge[0];
37 int nodeB = edge[1];
38 // Add bidirectional edges
39 adjacencyList[nodeA].push_back(nodeB);
40 adjacencyList[nodeB].push_back(nodeA);
41 }
42
43 return adjacencyList;
44 }
45
46 // Count nodes reachable from 'currentNode' within distance 'remainingDistance'
47 // using DFS traversal, avoiding revisiting 'parentNode'
48 int countReachableNodes(const vector<vector<int>>& graph, int currentNode,
49 int parentNode, int remainingDistance) {
50 // Base case: if no more distance allowed, stop traversal
51 if (remainingDistance < 0) {
52 return 0;
53 }
54
55 // Count current node
56 int nodeCount = 1;
57
58 // Traverse all neighbors except the parent
59 for (int neighbor : graph[currentNode]) {
60 if (neighbor != parentNode) {
61 // Recursively count nodes from neighbor with decreased distance
62 nodeCount += countReachableNodes(graph, neighbor, currentNode,
63 remainingDistance - 1);
64 }
65 }
66
67 return nodeCount;
68 }
69};
70
1/**
2 * Calculates the maximum number of target nodes for each node in the first graph
3 * @param edges1 - Edge list for the first graph
4 * @param edges2 - Edge list for the second graph
5 * @param k - Maximum distance to consider
6 * @returns Array where each element represents the maximum target nodes for corresponding node in graph1
7 */
8function maxTargetNodes(edges1: number[][], edges2: number[][], k: number): number[] {
9 // Build adjacency list for the second graph
10 const graph2: number[][] = build(edges2);
11 const graph2Size: number = edges2.length + 1;
12
13 // Find the maximum number of reachable nodes within distance k-1 in graph2
14 let maxReachableInGraph2: number = 0;
15 for (let nodeIndex: number = 0; nodeIndex < graph2Size; nodeIndex++) {
16 const reachableCount: number = dfs(graph2, nodeIndex, -1, k - 1);
17 maxReachableInGraph2 = Math.max(maxReachableInGraph2, reachableCount);
18 }
19
20 // Build adjacency list for the first graph
21 const graph1: number[][] = build(edges1);
22 const graph1Size: number = edges1.length + 1;
23
24 // Initialize result array with the maximum reachable nodes from graph2
25 const result: number[] = Array(graph1Size).fill(maxReachableInGraph2);
26
27 // For each node in graph1, add the count of nodes reachable within distance k
28 for (let nodeIndex: number = 0; nodeIndex < graph1Size; nodeIndex++) {
29 const reachableInGraph1: number = dfs(graph1, nodeIndex, -1, k);
30 result[nodeIndex] += reachableInGraph1;
31 }
32
33 return result;
34}
35
36/**
37 * Builds an adjacency list representation of the graph from edge list
38 * @param edges - Array of edges where each edge is [nodeA, nodeB]
39 * @returns Adjacency list representation of the graph
40 */
41function build(edges: number[][]): number[][] {
42 const nodeCount: number = edges.length + 1;
43 const adjacencyList: number[][] = Array.from({ length: nodeCount }, () => []);
44
45 // Create bidirectional edges in the adjacency list
46 for (const [nodeA, nodeB] of edges) {
47 adjacencyList[nodeA].push(nodeB);
48 adjacencyList[nodeB].push(nodeA);
49 }
50
51 return adjacencyList;
52}
53
54/**
55 * Performs depth-first search to count reachable nodes within given distance
56 * @param graph - Adjacency list representation of the graph
57 * @param currentNode - Current node being visited
58 * @param parentNode - Parent node to avoid revisiting (use -1 for root)
59 * @param remainingDistance - Remaining distance that can be traversed
60 * @returns Number of nodes reachable within the given distance
61 */
62function dfs(graph: number[][], currentNode: number, parentNode: number, remainingDistance: number): number {
63 // Base case: if no more distance can be traversed
64 if (remainingDistance < 0) {
65 return 0;
66 }
67
68 // Count current node
69 let nodeCount: number = 1;
70
71 // Recursively visit all neighbors except the parent
72 for (const neighbor of graph[currentNode]) {
73 if (neighbor !== parentNode) {
74 nodeCount += dfs(graph, neighbor, currentNode, remainingDistance - 1);
75 }
76 }
77
78 return nodeCount;
79}
80
Time and Space Complexity
Time Complexity: O(n² + m²)
The time complexity analysis breaks down as follows:
-
Building the adjacency lists for both graphs takes
O(n + m)
time, wheren = len(edges1) + 1
andm = len(edges2) + 1
. -
For graph
g2
, we calldfs(g2, i, -1, k - 1)
for each of them
nodes to find the maximum. Each DFS call explores nodes up to distancek-1
from the starting node. In the worst case (when the tree is a line/path), a single DFS call from one end can visit allm
nodes, takingO(m)
time. Therefore, running DFS from allm
nodes takesO(m²)
time. -
For graph
g1
, we calldfs(g1, i, -1, k)
for each of then
nodes. Each DFS call explores nodes up to distancek
from the starting node. Similar to above, in the worst case, each DFS call takesO(n)
time, and running it for alln
nodes results inO(n²)
time. -
The overall time complexity is
O(n + m) + O(m²) + O(n²) = O(n² + m²)
.
Space Complexity: O(n + m)
The space complexity analysis:
-
The adjacency list
g1
requiresO(n)
space to store all edges and nodes for the first tree. -
The adjacency list
g2
requiresO(m)
space to store all edges and nodes for the second tree. -
The DFS recursion stack depth is at most
O(min(n, k+1))
forg1
andO(min(m, k))
forg2
, which is bounded byO(n)
andO(m)
respectively. -
The result array takes
O(n)
space. -
The overall space complexity is
O(n + m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Distance Calculation When Connecting Trees
Pitfall: A common mistake is using the same distance k
for both trees when counting reachable nodes. When connecting node i
from tree1 to node j
from tree2, the edge between them consumes 1 unit of distance. Therefore, nodes in tree2 should be counted within distance k-1
from node j
, not k
.
Incorrect approach:
# Wrong: Using k for both trees
max_from_tree2 = max(count_nodes_within_distance(graph2, node, -1, k)
for node in range(num_nodes_tree2))
Correct approach:
# Correct: Using k-1 for tree2 since connecting edge uses 1 distance
max_from_tree2 = max(count_nodes_within_distance(graph2, node, -1, k - 1)
for node in range(num_nodes_tree2))
2. Edge Case: When k = 0
Pitfall: Not handling the case when k = 0
properly. When k = 0
, only the node itself is reachable, and connecting to tree2 won't add any additional nodes since the connecting edge itself requires distance 1.
Solution: The current implementation handles this correctly because when k = 0
, the DFS with k - 1 = -1
for tree2 will return 0, and only the node itself from tree1 will be counted.
3. Assuming Fixed Tree Sizes
Pitfall: Hardcoding tree sizes or making assumptions about node labels. Some might assume nodes are always labeled from 0 to n-1 consecutively.
Incorrect approach:
# Wrong: Assuming fixed size or non-standard labeling
n = 100 # Hardcoded size
graph = [[] for _ in range(n)]
Correct approach:
# Correct: Deriving size from edges (n nodes = n-1 edges in a tree)
num_nodes = len(edges) + 1
graph = [[] for _ in range(num_nodes)]
4. Not Preventing Revisiting in Tree Traversal
Pitfall: In the DFS traversal, forgetting to track the parent node can cause infinite recursion since trees are undirected graphs stored with bidirectional edges.
Incorrect approach:
def dfs(graph, node, distance):
if distance < 0:
return 0
count = 1
for neighbor in graph[node]:
# Wrong: No parent tracking, will revisit nodes
count += dfs(graph, neighbor, distance - 1)
return count
Correct approach:
def dfs(graph, node, parent, distance):
if distance < 0:
return 0
count = 1
for neighbor in graph[node]:
if neighbor != parent: # Correct: Skip parent to avoid cycles
count += dfs(graph, neighbor, node, distance - 1)
return count
5. Optimization Misconception
Pitfall: Trying to optimize by computing distances between all pairs of nodes once and reusing them. While this seems efficient, it actually increases complexity without benefit since we only need distances from specific starting nodes.
Why the current approach is better: The solution efficiently computes only what's needed:
- For tree2: We find the best starting point by trying all nodes
- For tree1: We compute distances from each node independently
- Total complexity remains O(n² + m²) which is optimal for this problem
Which of the following shows the order of node visit in a Breadth-first Search?
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
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!