Facebook Pixel

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 node j in the second tree
  • Once connected, count how many total nodes from both trees are within distance k from node i
  • 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 node i in tree 1, OR
  • Within k-1 edges from node j in tree 2 (since it takes 1 edge to go from i to j)

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 and edges2.

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 for n nodes in the first tree and m-1 edges for m 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:

  1. Count nodes within a specific distance k from a given node in tree 1
  2. Count nodes within distance k-1 from any node in tree 2
  3. 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:

  1. First, to find the maximum number of nodes reachable within distance k-1 from any node in tree 2
  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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 node j

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:

  1. First, find the maximum number of nodes reachable within distance k-1 from any node in tree 2. Let's call this t.
  2. For each node i in tree 1, count the nodes within distance k from it in tree 1.
  3. The answer for node i is the sum of its local count plus t.

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 Evaluator

Example 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:

  1. Building the adjacency lists for both graphs takes O(n + m) time, where n = len(edges1) + 1 and m = len(edges2) + 1.

  2. For graph g2, we call dfs(g2, i, -1, k - 1) for each of the m nodes to find the maximum. Each DFS call explores nodes up to distance k-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 all m nodes, taking O(m) time. Therefore, running DFS from all m nodes takes O(m²) time.

  3. For graph g1, we call dfs(g1, i, -1, k) for each of the n nodes. Each DFS call explores nodes up to distance k from the starting node. Similar to above, in the worst case, each DFS call takes O(n) time, and running it for all n nodes results in O(n²) time.

  4. The overall time complexity is O(n + m) + O(m²) + O(n²) = O(n² + m²).

Space Complexity: O(n + m)

The space complexity analysis:

  1. The adjacency list g1 requires O(n) space to store all edges and nodes for the first tree.

  2. The adjacency list g2 requires O(m) space to store all edges and nodes for the second tree.

  3. The DFS recursion stack depth is at most O(min(n, k+1)) for g1 and O(min(m, k)) for g2, which is bounded by O(n) and O(m) respectively.

  4. The result array takes O(n) space.

  5. 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More