Facebook Pixel

3372. Maximize the Number of Target Nodes After Connecting Trees I


Problem Description

There exist two undirected trees with n and m nodes, with distinct labels in ranges [0, n - 1] and [0, m - 1], respectively. You are given two 2D integer arrays edges1 and edges2 of lengths n - 1 and m - 1, respectively, where edges1[i] = [a_i, b_i] indicates that there is an edge between nodes a_i and b_i in the first tree and edges2[i] = [u_i, v_i] indicates that there is an edge between nodes u_i and v_i in the second tree. You are also given an integer k.

Node u is target to node v if the number of edges on the path from u to v is less than or equal to k. Note that a node is always target to itself.

Return an array of n integers answer, where answer[i] is the maximum possible number of nodes target to node i of the first tree if you have to connect one node from the first tree to another node in the second tree.

Note that queries are independent from each other. That is, for every query you will remove the added edge before proceeding to the next query.

Intuition

To solve this problem, we need to calculate the maximum number of nodes in two trees that can become reachable or "target" from a given node in the first tree, when allowed to connect it to the second tree, given certain constraints.

The key steps involve:

  • First, for any node i in the first tree, we need to determine how many nodes are within reach (or target) when moving up to a depth of k in that tree.
  • Simultaneously, calculate how many nodes in the second tree can be reached from any node when traversing up to a depth of k - 1. The subtraction of 1 here is because we are considering adding a connecting edge between one node from each tree.
  • Finally, we calculate the maximum possible sum of nodes that are reachable from each node i of the first tree and add the best possible connection from the second tree.

This leads to the calculation: for each node i in the first tree, sum up the counts of nodes targetable within its own tree, and add the maximum count of nodes from the second tree that were found to be within k - 1 distance. This ensures that we are considering the optimal sub-tree split and combining it, thereby maximizing the reachability for any node in the first tree when connected to the second tree.

Learn more about Tree, Depth-First Search and Breadth-First Search patterns.

Solution Approach

To solve the problem, we use an Enumeration combined with a Depth-First Search (DFS) strategy to identify and maximize the number of target nodes for each node in the first tree.

Steps:

  1. Graph Representation:

    • Build the adjacency list for each of the given trees from the edges1 and edges2 arrays. Each tree is represented as a graph where nodes are connected by the given edges.
    def build(edges: List[List[int]]) -> List[List[int]]:
        n = len(edges) + 1
        g = [[] for _ in range(n)]
        for a, b in edges:
            g[a].append(b)
            g[b].append(a)
        return g
  2. Determining Reachability:

    • Perform a DFS traversal to count how many nodes can be reached from a starting node within a given depth d.
    • Use DFS to explore all nodes: if a node is at a distance greater than d, do not proceed further in that branch.
    • The DFS function recursively counts nodes while avoiding revisiting the parent node, ensuring a concise count of reachable nodes.
    def dfs(g: List[List[int]], a: int, fa: int, d: int) -> int:
        if d < 0:
            return 0
        cnt = 1
        for b in g[a]:
            if b != fa:
                cnt += dfs(g, b, a, d - 1)
        return cnt
  3. Combining Results:

    • Compute the maximum reachable nodes (target) from any node in the second tree within a depth of k - 1 using the DFS approach. This precomputation allows quick addition for every node of the first tree.
    g2 = build(edges2)
    m = len(edges2) + 1
    t = max(dfs(g2, i, -1, k - 1) for i in range(m))
    • For each node i in the first tree, calculate the sum of target nodes within its tree and the optimal target nodes from the second tree. This sum gives the desired result for each node.
    g1 = build(edges1)
    n = len(edges1) + 1
    return [dfs(g1, i, -1, k) + t for i in range(n)]

By effectively combining DFS for local tree exploration and utilizing the pre-calculated maximum from the second tree, this approach ensures an optimal solution for each node in the first tree. The dynamic DFS and enumeration provide an efficient means to achieve the desired outputs for the problem.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a simple example to illustrate the solution approach.

Consider two trees:

  • Tree 1 with n = 4 nodes and edges edges1 = [[0, 1], [1, 2], [1, 3]].
  • Tree 2 with m = 3 nodes and edges edges2 = [[0, 1], [1, 2]].
  • We are given k = 2.

Tree Representations

  1. Tree 1:

    • It can be represented as:
      0
       \
        1
       / \
      2   3
    • The adjacency list representation of Tree 1 from edges1:
      0: [1]
      1: [0, 2, 3]
      2: [1]
      3: [1]
  2. Tree 2:

    • It can be represented as:
      0
       \
        1
         \
          2
    • The adjacency list representation of Tree 2 from edges2:
      0: [1]
      1: [0, 2]
      2: [1]

Steps to Find Target Nodes

  1. Build Adjacency Lists:

    • Convert edges1 and edges2 into their adjacency list representation as described above.
  2. Determine Reachability with DFS:

    • For each node in Tree 1, we use DFS to determine the number of nodes within a depth of k:

      • Node 0: Starting from 0 with k = 2, reachable nodes are [0, 1, 2, 3] (all nodes).
      • Node 1: Starting from 1, reachable nodes are [1, 0, 2, 3] (all nodes).
      • Node 2: Starting from 2, reachable nodes are [2, 1, 0, 3] (all nodes).
      • Node 3: Starting from 3, reachable nodes are [3, 1, 0, 2] (all nodes).
    • The counts for Tree 1, for each node (0 to 3), all nodes are reachable, therefore, the count is 4 each.

  3. Determine Maximum Reachability within Tree 2:

    • For Tree 2, compute the maximum reachable nodes from any node within a depth of k - 1 = 1:

      • Node 0: Reachable nodes are [0, 1].
      • Node 1: Reachable nodes are [1, 0, 2].
      • Node 2: Reachable nodes are [2, 1].
    • The maximum number of reachable nodes from the nodes in Tree 2 within k - 1 is 3 (found at node 1).

  4. Calculate Final Answers:

    • For each node in Tree 1, add the maximum reachable nodes of Tree 2 to their respective reachable node count:

      • Node 0: Reachable in Tree 1 = 4 + max(Tree 2) = 4 + 3 = 7
      • Node 1: Similarly, = 4 + 3 = 7
      • Node 2: Similarly, = 4 + 3 = 7
      • Node 3: Similarly, = 4 + 3 = 7

Thus, the result is [7, 7, 7, 7], indicating the maximum possible number of nodes targetable in each respective position of Tree 1 when optimally connected to Tree 2 spaces.

Solution Implementation

1from typing import List
2
3class Solution:
4    def maxTargetNodes(
5        self, edges1: List[List[int]], edges2: List[List[int]], k: int
6    ) -> List[int]:
7        def build(edges: List[List[int]]) -> List[List[int]]:
8            # Given an edge list, convert it into an adjacency list of the graph.
9            num_nodes = len(edges) + 1
10            graph = [[] for _ in range(num_nodes)]
11            for a, b in edges:
12                graph[a].append(b)
13                graph[b].append(a)
14            return graph
15
16        def dfs(graph: List[List[int]], node: int, parent: int, depth: int) -> int:
17            # Perform a Depth-First Search on the graph to count reachable nodes.
18            if depth < 0:
19                # If the depth is negative, no more nodes can be reached.
20                return 0
21            count = 1  # Count the current node.
22            for neighbor in graph[node]:
23                if neighbor != parent:
24                    # Recursively visit all children except the parent.
25                    count += dfs(graph, neighbor, node, depth - 1)
26            return count
27
28        # Build the second tree graph from edges2
29        graph2 = build(edges2)
30        num_nodes_graph2 = len(edges2) + 1
31
32        # Compute the maximum number of nodes reachable from any node within k-1 depth in `graph2`.
33        max_reachable_other = max(dfs(graph2, i, -1, k - 1) for i in range(num_nodes_graph2))
34
35        # Build the first tree graph from edges1
36        graph1 = build(edges1)
37        num_nodes_graph1 = len(edges1) + 1
38
39        # Calculate the total number of reachable nodes for each node in graph1 with depth k,
40        # adding the maximum from graph2.
41        return [
42            dfs(graph1, i, -1, k) + max_reachable_other
43            for i in range(num_nodes_graph1)
44        ]
45
1import java.util.ArrayList;
2import java.util.Arrays;
3import java.util.List;
4
5class Solution {
6    public int[] maxTargetNodes(int[][] edges1, int[][] edges2, int k) {
7        // Build adjacency list for graph represented by edges2
8        List<Integer>[] g2 = build(edges2);
9        int m = edges2.length + 1;
10        int t = 0;
11
12        // Calculate the maximum number of nodes reachable in graph2 within k-1 moves
13        for (int i = 0; i < m; ++i) {
14            t = Math.max(t, dfs(g2, i, -1, k - 1));
15        }
16
17        // Build adjacency list for graph represented by edges1
18        List<Integer>[] g1 = build(edges1);
19        int n = edges1.length + 1;
20        int[] answer = new int[n];
21        // Initialize answer array with the maximum nodes reachable from graph2
22        Arrays.fill(answer, t);
23
24        // Calculate the overall maximum nodes reachable for each node in graph1 with extra move
25        for (int i = 0; i < n; ++i) {
26            answer[i] += dfs(g1, i, -1, k);
27        }
28
29        return answer;
30    }
31
32    // Constructs an adjacency list from edge array
33    private List<Integer>[] build(int[][] edges) {
34        int numOfNodes = edges.length + 1;
35        List<Integer>[] graph = new List[numOfNodes];
36        Arrays.setAll(graph, i -> new ArrayList<>());
37
38        // Add edges to the adjacency list
39        for (var edge: edges) {
40            int a = edge[0], b = edge[1];
41            graph[a].add(b);
42            graph[b].add(a);
43        }
44
45        return graph;
46    }
47
48    // Depth First Search to count nodes reachable within d moves
49    private int dfs(List<Integer>[] graph, int currentNode, int parentNode, int remainingMoves) {
50        if (remainingMoves < 0) {
51            return 0;
52        }
53
54        int count = 1; // Count current node
55        for (int adjacentNode : graph[currentNode]) {
56            if (adjacentNode != parentNode) { // Avoid revisiting the parent node
57                count += dfs(graph, adjacentNode, currentNode, remainingMoves - 1);
58            }
59        }
60
61        return count;
62    }
63}
64
1class Solution {
2public:
3    // Method to compute the maximum number of target nodes for two graphs with defined edges
4    std::vector<int> maxTargetNodes(std::vector<std::vector<int>>& edges1, std::vector<std::vector<int>>& edges2, int k) {
5        // Build the graph for edges2
6        auto graph2 = build(edges2);
7        int size2 = edges2.size() + 1; // Number of nodes in the second graph
8        int maxTargets = 0;
9
10        // Find the maximum targets reachable within k-1 steps in edges2
11        for (int node = 0; node < size2; ++node) {
12            maxTargets = std::max(maxTargets, dfs(graph2, node, -1, k - 1));
13        }
14
15        // Build the graph for edges1
16        auto graph1 = build(edges1);
17        int size1 = edges1.size() + 1; // Number of nodes in the first graph
18
19        std::vector<int> result(size1, maxTargets);
20
21        // Calculate maximum targets reachable with k steps starting from any node in edges1
22        for (int node = 0; node < size1; ++node) {
23            result[node] += dfs(graph1, node, -1, k);
24        }
25
26        return result; // Return the result vector containing the max target nodes for each node in graph1
27    }
28
29private:
30    // Method to build the adjacency list representation of a graph from edge list
31    std::vector<std::vector<int>> build(const std::vector<std::vector<int>>& edges) {
32        int numNodes = edges.size() + 1; // Number of nodes in the graph
33        std::vector<std::vector<int>> graph(numNodes);
34      
35        // Add edges to the graph
36        for (const auto& edge : edges) {
37            int nodeA = edge[0], nodeB = edge[1];
38            graph[nodeA].push_back(nodeB);
39            graph[nodeB].push_back(nodeA);
40        }
41      
42        return graph;
43    }
44
45    // Depth First Search (DFS) method to count nodes reachable within d steps from a given node
46    int dfs(const std::vector<std::vector<int>>& graph, int currentNode, int parentNode, int stepsRemaining) {
47        // Base condition: No more steps left to take
48        if (stepsRemaining < 0) {
49            return 0;
50        }
51      
52        int count = 1; // Count the current node
53      
54        // Recursively visit all connected nodes (neighbors)
55        for (int neighbor : graph[currentNode]) {
56            if (neighbor != parentNode) {
57                count += dfs(graph, neighbor, currentNode, stepsRemaining - 1);
58            }
59        }
60      
61        return count; // Return the total count of reachable nodes
62    }
63};
64
1// Calculate the maximum number of target nodes for each node in the first graph
2function maxTargetNodes(edges1: number[][], edges2: number[][], k: number): number[] {
3    // Build the adjacency list for the second graph
4    const graph2 = build(edges2);
5
6    // Determine the number of nodes in graph2
7    const numNodesGraph2 = edges2.length + 1;
8
9    // Variable to keep track of the maximum nodes found
10    let maxNodesGraph2 = 0;
11
12    // Explore graph2 from each node and keep track of the maximum nodes reachable at depth k-1
13    for (let i = 0; i < numNodesGraph2; i++) {
14        maxNodesGraph2 = Math.max(maxNodesGraph2, dfs(graph2, i, -1, k - 1));
15    }
16
17    // Build the adjacency list for the first graph
18    const graph1 = build(edges1);
19
20    // Determine the number of nodes in graph1
21    const numNodesGraph1 = edges1.length + 1;
22
23    // Initialize the result array with baseline values from graph2
24    const results = Array(numNodesGraph1).fill(maxNodesGraph2);
25
26    // Explore graph1 from each node to adjust the maximum target nodes
27    for (let i = 0; i < numNodesGraph1; i++) {
28        results[i] += dfs(graph1, i, -1, k);
29    }
30
31    return results;
32}
33
34// Build the adjacency list representation of the graph
35function build(edges: number[][]): number[][] {
36    const numNodes = edges.length + 1;
37    const graph: number[][] = Array.from({ length: numNodes }, () => []);
38    for (const [a, b] of edges) {
39        graph[a].push(b);
40        graph[b].push(a);
41    }
42    return graph;
43}
44
45// Depth-first search to compute the number of nodes reachable within a given depth
46function dfs(graph: number[][], currentNode: number, parentNode: number, depth: number): number {
47    if (depth < 0) {
48        return 0;  // Base case: depth is negative, stop searching
49    }
50
51    let count = 1;  // Start with counting the current node
52    for (const neighbor of graph[currentNode]) {
53        if (neighbor !== parentNode) {  // Avoid revisiting the parent node
54            count += dfs(graph, neighbor, currentNode, depth - 1);
55        }
56    }
57    return count;
58}
59

Time and Space Complexity

The build function constructs an adjacency list for the graph, which takes O(n + m) time because it iterates through each list of edges once. Here, n and m represent the number of nodes in the tree generated from edges1 and edges2, respectively. The adjacency list creation also requires O(n + m) space to store the graph.

The dfs function recursively traverses the graph to count nodes within a distance d. For each node, this can take O(n) time in the worst case, where each call to dfs may visit all nodes. If run for each node, this results in a complexity of O(n^2) for the first graph and O(m^2) for the second graph.

The overall time complexity thus consists of O(n^2) for iterating over nodes of the first graph and O(m^2) for the nodes of the second graph. Combining, the total time complexity is O(n^2 + m^2).

The space complexity includes the space for the adjacency list, which is O(n + m), and the stack space for DFS, which is O(h) in the worst case, where h is the height of the tree, O(n) in the worst case for both trees due to recursion stack space being proportional to the depth of the deep recursion. Thus, the primary space complexity term remains O(n + m).

Learn more about how to find time and space complexity quickly.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

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


Load More