LeetCode 3373 - Maximize the Number of Target Nodes After Connecting Trees II
The problem gives us two separate undirected trees. The first tree contains n nodes and the second tree contains m nodes. A tree is an acyclic connected graph, so every pair of nodes has exactly one simple path between them.
Difficulty: 🔴 Hard
Topics: Tree, Depth-First Search, Breadth-First Search
Solution
Problem Understanding
The problem gives us two separate undirected trees. The first tree contains n nodes and the second tree contains m nodes. A tree is an acyclic connected graph, so every pair of nodes has exactly one simple path between them.
For any two nodes u and v, node u is considered a target node to v if the number of edges on the path between them is even. Since the distance from a node to itself is 0, and 0 is even, every node is always target to itself.
For every node i in the first tree, we are allowed to temporarily connect exactly one node from the first tree to exactly one node from the second tree using a new edge. After adding this edge, we want to maximize the number of nodes that are target to node i.
The important detail is that each query is independent. After computing the answer for one node i, the added edge is removed before processing the next node.
The input consists of:
edges1, describing the first treeedges2, describing the second tree
Each edge [a, b] means there is an undirected connection between nodes a and b.
The output should be an array answer where:
answer[i]is the maximum possible number of target nodes to nodeiafter optimally connecting the two trees.
The constraints are very large:
n, m <= 100000
This immediately tells us that any solution involving repeated BFS or DFS from every node will likely be too slow. An O(n^2) or O(m^2) solution is infeasible because 100000^2 operations are far beyond acceptable limits.
There are several important observations and edge cases:
- Trees are bipartite graphs, which means nodes can be split into two parity groups based on depth.
- Distance parity between two nodes depends entirely on whether they belong to the same bipartite color.
- The added edge always contributes exactly one extra edge, which flips parity between trees.
- The queries are independent, so we do not need to maintain any persistent structure after each connection.
- Extremely unbalanced trees, such as chains, must still run efficiently.
- Very small trees, such as size
2, must also work correctly.
Approaches
Brute Force Approach
A direct brute-force solution would process each node i independently.
For each node i in the first tree:
- Try every possible node
uin the first tree. - Try every possible node
vin the second tree. - Add edge
(u, v). - Run BFS or DFS from
iacross the combined graph. - Count how many nodes are at even distance from
i. - Keep the maximum result.
This works because it explicitly evaluates every possible connection and directly measures the number of target nodes.
However, this approach is far too slow.
If we run BFS for every pair (u, v):
- There are
O(n * m)possible connections. - Each BFS costs
O(n + m).
Total complexity becomes:
O(n * m * (n + m))
With sizes up to 100000, this is impossible to run within time limits.
Key Insight
The key observation is that parity of distance in a tree depends entirely on bipartite coloring.
If we root a tree arbitrarily and assign:
- color
0to even-depth nodes - color
1to odd-depth nodes
then:
- two nodes have even distance if and only if they have the same color
- two nodes have odd distance if and only if they have different colors
This transforms the problem from a shortest-path problem into a parity-counting problem.
Suppose node i in the first tree has color c.
Inside the first tree:
- every node with color
cis target toi - every node with opposite color is not
Now consider the second tree.
If we connect node u from the first tree to node v from the second tree, then any path from i to a node x in the second tree becomes:
distance(i, u) + 1 + distance(v, x)
The added edge contributes 1, which flips parity.
To make the total distance even:
distance(i, u) parity != distance(v, x) parity
Using bipartite coloring, this means:
- nodes in the second tree that are target to
iare exactly one color class of the second tree - we can choose which color class by selecting the connection appropriately
Therefore:
- contribution from first tree is fixed for node
i - contribution from second tree is simply the larger bipartite partition size
This gives a very efficient solution.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n * m * (n + m)) | O(n + m) | Tries every connection and runs traversal each time |
| Optimal | O(n + m) | O(n + m) | Uses bipartite parity properties of trees |
Algorithm Walkthrough
Step 1: Build adjacency lists
Convert both edge lists into adjacency lists.
This allows efficient traversal of each tree using DFS or BFS.
Step 2: Bipartite-color the first tree
Run DFS from node 0.
Assign:
- color
0to the root - alternate colors for neighbors
While traversing:
- count how many nodes belong to color
0 - count how many belong to color
1
For every node i, store its color.
This works because trees are bipartite.
Step 3: Compute target counts inside the first tree
If node i has:
- color
0, then all color0nodes are at even distance - color
1, then all color1nodes are at even distance
Therefore:
sameColorCount[i] =
count0 if color[i] == 0
count1 otherwise
This is the number of target nodes already reachable inside the first tree.
Step 4: Bipartite-color the second tree
Repeat the same DFS process for the second tree.
Again compute:
- number of color
0nodes - number of color
1nodes
Step 5: Compute the best possible contribution from the second tree
Because the added edge flips parity, we can make node i target exactly one bipartite partition of the second tree.
We are free to choose the connection optimally.
Therefore the maximum possible contribution from the second tree is:
bestSecond = max(count0Second, count1Second)
Step 6: Build the final answer
For every node i:
answer[i] = sameColorCount[i] + bestSecond
Return the resulting array.
Why it works
In a bipartite tree, parity of distance is determined entirely by node colors.
Nodes with the same color are always at even distance.
The added bridge edge contributes exactly one extra edge, which flips parity between the two trees.
By choosing the connection endpoints appropriately, we can decide which color partition of the second tree becomes even-distance reachable from node i.
Therefore the optimal strategy is always to select the larger partition of the second tree, while the contribution from the first tree is fixed.
This guarantees the algorithm always produces the maximum possible number of target nodes.
Python Solution
from typing import List
from collections import deque
class Solution:
def maxTargetNodes(self, edges1: List[List[int]], edges2: List[List[int]]) -> List[int]:
def build_graph(size: int, edges: List[List[int]]) -> List[List[int]]:
graph = [[] for _ in range(size)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
return graph
def color_tree(graph: List[List[int]]):
n = len(graph)
color = [-1] * n
counts = [0, 0]
queue = deque([0])
color[0] = 0
counts[0] = 1
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if color[neighbor] == -1:
color[neighbor] = color[node] ^ 1
counts[color[neighbor]] += 1
queue.append(neighbor)
return color, counts
n = len(edges1) + 1
m = len(edges2) + 1
graph1 = build_graph(n, edges1)
graph2 = build_graph(m, edges2)
color1, counts1 = color_tree(graph1)
_, counts2 = color_tree(graph2)
best_second = max(counts2)
answer = []
for node in range(n):
answer.append(counts1[color1[node]] + best_second)
return answer
The implementation begins by constructing adjacency lists for both trees. Since trees are sparse graphs with exactly n - 1 edges, adjacency lists are the most efficient representation.
The color_tree function performs BFS to assign bipartite colors. The root starts with color 0, and every neighboring node receives the opposite color using XOR with 1.
While coloring the tree, the function also counts how many nodes belong to each partition. This is important because nodes with the same color always have even distance between them.
For the first tree, the algorithm stores each node's color so it can determine how many target nodes already exist inside the tree.
For the second tree, only the partition sizes matter. Since we can choose the connection optimally, we simply take the larger partition.
Finally, each answer is computed by adding:
- nodes in the same partition as
iin the first tree - the best partition size from the second tree
Go Solution
package main
func maxTargetNodes(edges1 [][]int, edges2 [][]int) []int {
buildGraph := func(size int, edges [][]int) [][]int {
graph := make([][]int, size)
for _, edge := range edges {
u, v := edge[0], edge[1]
graph[u] = append(graph[u], v)
graph[v] = append(graph[v], u)
}
return graph
}
colorTree := func(graph [][]int) ([]int, [2]int) {
n := len(graph)
color := make([]int, n)
for i := range color {
color[i] = -1
}
var counts [2]int
queue := make([]int, 0)
queue = append(queue, 0)
color[0] = 0
counts[0] = 1
head := 0
for head < len(queue) {
node := queue[head]
head++
for _, neighbor := range graph[node] {
if color[neighbor] == -1 {
color[neighbor] = color[node] ^ 1
counts[color[neighbor]]++
queue = append(queue, neighbor)
}
}
}
return color, counts
}
n := len(edges1) + 1
m := len(edges2) + 1
graph1 := buildGraph(n, edges1)
graph2 := buildGraph(m, edges2)
color1, counts1 := colorTree(graph1)
_, counts2 := colorTree(graph2)
bestSecond := counts2[0]
if counts2[1] > bestSecond {
bestSecond = counts2[1]
}
answer := make([]int, n)
for i := 0; i < n; i++ {
answer[i] = counts1[color1[i]] + bestSecond
}
return answer
}
The Go implementation follows the same logic as the Python version.
Instead of Python lists and deque, Go uses slices and a manual queue index called head. This avoids inefficient slice shifting operations.
The color array is initialized with -1 to indicate unvisited nodes.
Go integers are sufficient because the maximum answer is at most n + m, which easily fits within 32-bit integer limits.
Worked Examples
Example 1
edges1 = [[0,1],[0,2],[2,3],[2,4]]
edges2 = [[0,1],[0,2],[0,3],[2,7],[1,4],[4,5],[4,6]]
First Tree Coloring
Starting BFS from node 0:
| Node | Color |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 1 |
| 3 | 0 |
| 4 | 0 |
Partition sizes:
| Color | Count |
|---|---|
| 0 | 3 |
| 1 | 2 |
Therefore:
| Node | Same-color nodes |
|---|---|
| 0 | 3 |
| 1 | 2 |
| 2 | 2 |
| 3 | 3 |
| 4 | 3 |
Second Tree Coloring
One valid coloring:
| Node | Color |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 0 |
| 5 | 1 |
| 6 | 1 |
| 7 | 0 |
Partition sizes:
| Color | Count |
|---|---|
| 0 | 3 |
| 1 | 5 |
Best contribution:
bestSecond = 5
Final Answers
| Node | First Tree Contribution | Second Tree Contribution | Total |
|---|---|---|---|
| 0 | 3 | 5 | 8 |
| 1 | 2 | 5 | 7 |
| 2 | 2 | 5 | 7 |
| 3 | 3 | 5 | 8 |
| 4 | 3 | 5 | 8 |
Final result:
[8,7,7,8,8]
Example 2
edges1 = [[0,1],[0,2],[0,3],[0,4]]
edges2 = [[0,1],[1,2],[2,3]]
First Tree Coloring
| Node | Color |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 1 |
Partition sizes:
| Color | Count |
|---|---|
| 0 | 1 |
| 1 | 4 |
Second Tree Coloring
| Node | Color |
|---|---|
| 0 | 0 |
| 1 | 1 |
| 2 | 0 |
| 3 | 1 |
Partition sizes:
| Color | Count |
|---|---|
| 0 | 2 |
| 1 | 2 |
Best contribution:
2
Final Answers
| Node | First Tree Contribution | Second Tree Contribution | Total |
|---|---|---|---|
| 0 | 1 | 2 | 3 |
| 1 | 4 | 2 | 6 |
| 2 | 4 | 2 | 6 |
| 3 | 4 | 2 | 6 |
| 4 | 4 | 2 | 6 |
Final result:
[3,6,6,6,6]
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n + m) | Each tree is traversed exactly once |
| Space | O(n + m) | Adjacency lists, color arrays, and BFS queue |
The algorithm performs a single BFS traversal on each tree.
Since a tree with k nodes has exactly k - 1 edges, traversing the entire graph costs O(k).
No nested traversals or repeated shortest-path computations occur, which allows the solution to scale efficiently to the maximum constraint size.
Test Cases
from typing import List
class Solution:
def maxTargetNodes(self, edges1: List[List[int]], edges2: List[List[int]]) -> List[int]:
from collections import deque
def build_graph(size, edges):
graph = [[] for _ in range(size)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
return graph
def color_tree(graph):
n = len(graph)
color = [-1] * n
counts = [0, 0]
queue = deque([0])
color[0] = 0
counts[0] = 1
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if color[neighbor] == -1:
color[neighbor] = color[node] ^ 1
counts[color[neighbor]] += 1
queue.append(neighbor)
return color, counts
n = len(edges1) + 1
m = len(edges2) + 1
graph1 = build_graph(n, edges1)
graph2 = build_graph(m, edges2)
color1, counts1 = color_tree(graph1)
_, counts2 = color_tree(graph2)
best_second = max(counts2)
return [
counts1[color1[i]] + best_second
for i in range(n)
]
sol = Solution()
# Example 1
assert sol.maxTargetNodes(
[[0,1],[0,2],[2,3],[2,4]],
[[0,1],[0,2],[0,3],[2,7],[1,4],[4,5],[4,6]]
) == [8,7,7,8,8]
# Example 2
assert sol.maxTargetNodes(
[[0,1],[0,2],[0,3],[0,4]],
[[0,1],[1,2],[2,3]]
) == [3,6,6,6,6]
# Minimum size trees
assert sol.maxTargetNodes(
[[0,1]],
[[0,1]]
) == [2,2]
# Chain trees
assert sol.maxTargetNodes(
[[0,1],[1,2],[2,3]],
[[0,1],[1,2]]
) == [4,3,4,3]
# Star-shaped first tree
assert sol.maxTargetNodes(
[[0,1],[0,2],[0,3],[0,4],[0,5]],
[[0,1]]
) == [2,6,6,6,6,6]
# Balanced binary tree
assert sol.maxTargetNodes(
[[0,1],[0,2],[1,3],[1,4],[2,5],[2,6]],
[[0,1],[1,2],[2,3]]
) == [6,5,5,6,6,6,6]
# Second tree heavily unbalanced
assert sol.maxTargetNodes(
[[0,1],[1,2]],
[[0,1],[1,2],[2,3],[3,4],[4,5]]
) == [6,5,6]
print("All tests passed.")
Test Summary
| Test | Why |
|---|---|
| Example 1 | Verifies the main sample from the problem |
| Example 2 | Verifies star-shaped structure behavior |
| Minimum size trees | Confirms correctness on smallest valid input |
| Chain trees | Tests alternating parity along long paths |
| Star-shaped first tree | Tests highly skewed bipartite partitions |
| Balanced binary tree | Tests more symmetric structures |
| Second tree heavily unbalanced | Verifies choosing the larger partition |
Edge Cases
One important edge case occurs when the tree has only two nodes. In this situation, each bipartite partition contains exactly one node. A buggy implementation might incorrectly assume larger partition sizes or mishandle indexing. The implementation handles this naturally because BFS coloring works correctly even on the smallest valid tree.
Another important case is a chain-shaped tree. In a long path, node parity alternates continuously. A naive implementation based on repeated shortest-path calculations could become quadratic. The optimized solution avoids this entirely because parity is determined only by coloring, not by explicitly computing all distances.
A third important edge case is a highly unbalanced bipartite partition, such as a star graph. In a star centered at node 0, one partition contains only the center while the other contains all leaves. The algorithm correctly chooses the larger partition from the second tree, maximizing the number of target nodes after connection.
A final subtle edge case involves independent queries. Since every query removes the added edge afterward, we must not permanently modify either tree. The implementation never physically inserts edges. Instead, it uses the parity observation to compute results mathematically, which automatically preserves independence between queries.