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 ofk
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 of1
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:
-
Graph Representation:
- Build the adjacency list for each of the given trees from the
edges1
andedges2
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
- Build the adjacency list for each of the given trees from the
-
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
- Perform a DFS traversal to count how many nodes can be reached from a starting node within a given depth
-
Combining Results:
- Compute the maximum reachable nodes (
target
) from any node in the second tree within a depth ofk - 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 oftarget
nodes within its tree and the optimaltarget
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)]
- Compute the maximum reachable nodes (
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 EvaluatorExample Walkthrough
Let's walk through a simple example to illustrate the solution approach.
Consider two trees:
- Tree 1 with
n = 4
nodes and edgesedges1 = [[0, 1], [1, 2], [1, 3]]
. - Tree 2 with
m = 3
nodes and edgesedges2 = [[0, 1], [1, 2]]
. - We are given
k = 2
.
Tree Representations
-
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]
- It can be represented as:
-
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]
- It can be represented as:
Steps to Find Target Nodes
-
Build Adjacency Lists:
- Convert
edges1
andedges2
into their adjacency list representation as described above.
- Convert
-
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).
- Node 0: Starting from 0 with
-
The counts for Tree 1, for each node (0 to 3), all nodes are reachable, therefore, the count is 4 each.
-
-
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).
-
-
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.
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
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 algomonster s3 us east 2 amazonaws com 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
https algomonster s3 us east 2 amazonaws com 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
Want a Structured Path to Master System Design Too? Don’t Miss This!