2479. Maximum XOR of Two Non-Overlapping Subtrees 🔒
Problem Description
You are given an undirected tree with n
nodes labeled from 0
to n - 1
. The tree structure is defined by an array edges
of length n - 1
, where each edges[i] = [ai, bi]
represents an edge between nodes ai
and bi
. Node 0
is the root of the tree.
Each node has a value associated with it, given in the array values
where values[i]
represents the value of node i
.
Your task is to select two non-overlapping subtrees from this tree. The score of your selection is calculated as the bitwise XOR of the sum of values in each subtree. You need to find the maximum possible score.
Key definitions:
- A subtree of a node consists of that node and all its descendants
- Two subtrees are non-overlapping if they don't share any common nodes
- If it's impossible to find two non-overlapping subtrees, return
0
For example, if you select two non-overlapping subtrees where:
- Subtree 1 has nodes with values summing to
sum1
- Subtree 2 has nodes with values summing to
sum2
Then your score would be sum1 XOR sum2
, and you want to maximize this value.
The solution uses a two-pass DFS approach:
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 explicitly mentions an undirected tree with
n
nodes and edges connecting them. A tree is a special type of graph.
Is it a tree?
- Yes: The problem clearly states we have an undirected tree with
n
nodes labeled from0
ton - 1
, connected byn - 1
edges, with node0
as the root.
DFS
-
Yes: Since we're dealing with a tree structure and need to:
- Calculate subtree sums (which requires traversing from leaves up to root)
- Find non-overlapping subtrees (which requires understanding parent-child relationships)
- Process each subtree rooted at different nodes
DFS is the natural choice for tree traversal problems involving subtree calculations.
Conclusion: The flowchart leads us directly to using DFS (Depth-First Search) for this problem.
The DFS pattern is particularly suitable here because:
- We need to compute values for each subtree (sum of all node values in that subtree)
- We need to ensure subtrees don't overlap, which DFS naturally handles through its traversal order
- The problem requires exploring all possible subtree combinations, which DFS can systematically achieve by visiting nodes in a specific order
The solution implements two DFS passes:
- First DFS calculates the sum for each subtree
- Second DFS finds the maximum XOR between non-overlapping subtrees using a Trie for optimization
Intuition
The key insight is recognizing that for two subtrees to be non-overlapping in a tree, one subtree must be completely contained within a branch that doesn't include the other subtree. This means if we pick any subtree rooted at node i
, any non-overlapping subtree must be either:
- An ancestor subtree that doesn't include node
i
- A subtree in a different branch of the tree
To maximize the XOR of two sums, we need to find pairs of sums whose binary representations differ as much as possible in the most significant bits. XOR produces 1
when bits differ and 0
when they're the same, so maximum XOR occurs when the binary representations are most different.
The challenge is efficiently finding the best pair among all possible non-overlapping subtree combinations. Here's how we arrive at the solution:
Step 1: Calculate all subtree sums
We first need to know the sum of values for every possible subtree. Using DFS from the root, we can calculate s[i]
= sum of all values in the subtree rooted at node i
. This is done bottom-up: a node's subtree sum equals its own value plus the subtree sums of all its children.
Step 2: Ensure non-overlapping property
The clever part is realizing that if we traverse the tree in post-order (visiting children before parents), we can maintain a set of "available" subtree sums that don't overlap with the current subtree. When we're at node i
:
- All subtrees in previously visited branches are non-overlapping with subtree
i
- After processing node
i
and its subtree, we adds[i]
to our available set
Step 3: Optimize XOR search with Trie Finding the maximum XOR with a given number from a set is a classic problem solved using a binary Trie. For each bit position (starting from most significant), we try to go the opposite direction in the Trie to maximize the XOR value. The Trie stores binary representations of subtree sums we've seen so far.
By combining these insights - calculating subtree sums via DFS, ensuring non-overlap through traversal order, and optimizing XOR search with a Trie - we arrive at an elegant solution that finds the maximum XOR of two non-overlapping subtrees efficiently.
Learn more about Tree, Depth-First Search, Graph and Trie patterns.
Solution Approach
The solution implements a two-pass DFS strategy combined with a Trie data structure for efficient XOR maximization.
Trie Implementation for Maximum XOR:
The [Trie](/problems/trie_intro)
class is designed to store binary representations of numbers and find the maximum XOR:
insert(x)
: Stores the 48-bit binary representation of numberx
in the Trie, bit by bit from most significant to least significantsearch(x)
: For a given numberx
, finds the number in the Trie that produces maximum XOR withx
by greedily choosing the opposite bit at each level when possible
Main Algorithm:
-
Build the adjacency list: Convert the edge list into an adjacency list representation
g
for efficient tree traversal. -
First DFS (
dfs1
) - Calculate Subtree Sums:dfs1(node, parent): sum = values[node] for each child of node (except parent): sum += dfs1(child, node) s[node] = sum return sum
This recursively calculates and stores in
s[i]
the sum of all values in the subtree rooted at nodei
. -
Second DFS (
dfs2
) - Find Maximum XOR:dfs2(node, parent): ans = max(ans, [tree](/problems/tree_intro).search(s[node])) for each child of node (except parent): dfs2(child, node) tree.insert(s[node])
The key insight here is the traversal order:
- When at node
i
, we first check the maximum XOR betweens[i]
and all previously inserted subtree sums in the Trie - We then recursively process all children
- Finally, we insert
s[i]
into the Trie
This order ensures that when we check XOR for subtree at node
i
, the Trie only contains sums from non-overlapping subtrees (those from different branches already processed). - When at node
Why This Works:
The post-order insertion (processing children before inserting parent) guarantees that:
- When processing a node, all subtrees in its descendants haven't been added to the Trie yet
- All subtrees from sibling branches and their ancestors are already in the Trie
- These are exactly the subtrees that don't overlap with the current subtree
The algorithm efficiently finds the maximum XOR value among all pairs of non-overlapping subtrees in O(n * log(max_sum))
time, where the log factor comes from the Trie operations on 48-bit numbers.
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.
Consider a tree with 5 nodes and the following structure:
0 (value: 2) / \ 1 2 (3) (5) / \ 3 4 (1) (4)
Given:
edges = [[0,1], [0,2], [1,3], [1,4]]
values = [2, 3, 5, 1, 4]
Step 1: Build adjacency list
g[0] = [1, 2] g[1] = [0, 3, 4] g[2] = [0] g[3] = [1] g[4] = [1]
Step 2: First DFS - Calculate subtree sums
Starting from root 0, we calculate bottom-up:
dfs1(3, 1)
: s[3] = 1 (leaf node)dfs1(4, 1)
: s[4] = 4 (leaf node)dfs1(1, 0)
: s[1] = 3 + 1 + 4 = 8 (node 1 + subtrees 3 and 4)dfs1(2, 0)
: s[2] = 5 (leaf node)dfs1(0, -1)
: s[0] = 2 + 8 + 5 = 15 (entire tree)
Result: s = [15, 8, 5, 1, 4]
Step 3: Second DFS - Find maximum XOR
Starting from root 0, we traverse and maintain a Trie:
-
At node 0:
- Trie is empty, so
search(15)
returns 0 - Recursively visit child 1
- Trie is empty, so
-
At node 1:
- Trie is still empty, so
search(8)
returns 0 - Recursively visit children 3 and 4
- Trie is still empty, so
-
At node 3:
- Trie is empty, so
search(1)
returns 0 - Insert s[3] = 1 into Trie
- Trie is empty, so
-
At node 4:
- Trie contains [1], so
search(4)
finds XOR with 1 = 4 XOR 1 = 5 - Current max = 5
- Insert s[4] = 4 into Trie
- Trie contains [1], so
-
Back at node 1:
- Insert s[1] = 8 into Trie
- Trie now contains [1, 4, 8]
-
At node 2:
- Trie contains [1, 4, 8]
search(5)
checks XOR with each:- 5 XOR 1 = 4
- 5 XOR 4 = 1
- 5 XOR 8 = 13 ← Maximum!
- Update max = 13
- Insert s[2] = 5 into Trie
-
Back at node 0:
- Insert s[0] = 15 into Trie
Result: Maximum XOR = 13
This comes from selecting:
- Subtree rooted at node 1 (sum = 8)
- Subtree rooted at node 2 (sum = 5)
- 8 XOR 5 = 13
Note how the traversal order ensures these subtrees don't overlap - node 2's subtree sum was only compared with sums from node 1's branch after node 1's entire subtree was processed.
Solution Implementation
1from typing import List
2from collections import defaultdict
3
4
5class Trie:
6 """Binary Trie data structure for finding maximum XOR."""
7
8 def __init__(self):
9 # Each node has at most 2 children (for bits 0 and 1)
10 self.children = [None] * 2
11
12 def insert(self, num: int) -> None:
13 """Insert a number into the trie by its binary representation."""
14 current_node = self
15 # Process 48 bits from most significant to least significant
16 for bit_position in range(47, -1, -1):
17 # Extract the bit at current position
18 bit_value = (num >> bit_position) & 1
19
20 # Create new node if path doesn't exist
21 if current_node.children[bit_value] is None:
22 current_node.children[bit_value] = Trie()
23
24 # Move to the child node
25 current_node = current_node.children[bit_value]
26
27 def search(self, num: int) -> int:
28 """Find the maximum XOR value for the given number against all inserted numbers."""
29 current_node = self
30 max_xor_result = 0
31
32 # Process 48 bits from most significant to least significant
33 for bit_position in range(47, -1, -1):
34 # Extract the bit at current position
35 bit_value = (num >> bit_position) & 1
36
37 # Early termination if node doesn't exist
38 if current_node is None:
39 return max_xor_result
40
41 # Try to go opposite direction for maximum XOR
42 opposite_bit = bit_value ^ 1
43
44 if current_node.children[opposite_bit]:
45 # If opposite bit path exists, take it and set result bit to 1
46 max_xor_result = (max_xor_result << 1) | 1
47 current_node = current_node.children[opposite_bit]
48 else:
49 # Otherwise, follow the same bit path and set result bit to 0
50 max_xor_result <<= 1
51 current_node = current_node.children[bit_value]
52
53 return max_xor_result
54
55
56class Solution:
57 def maxXor(self, n: int, edges: List[List[int]], values: List[int]) -> int:
58 """
59 Find the maximum XOR value between subtree sums in a tree.
60
61 Args:
62 n: Number of nodes in the tree
63 edges: List of edges representing the tree structure
64 values: List of values for each node
65
66 Returns:
67 Maximum XOR value between any two subtree sums
68 """
69
70 def calculate_subtree_sums(node: int, parent: int) -> int:
71 """
72 DFS to calculate sum of values in subtree rooted at node.
73
74 Args:
75 node: Current node being processed
76 parent: Parent of current node (-1 for root)
77
78 Returns:
79 Sum of all values in the subtree rooted at node
80 """
81 # Start with current node's value
82 subtree_sum = values[node]
83
84 # Add sums from all child subtrees
85 for neighbor in adjacency_list[node]:
86 if neighbor != parent:
87 subtree_sum += calculate_subtree_sums(neighbor, node)
88
89 # Store the subtree sum for this node
90 subtree_sums[node] = subtree_sum
91 return subtree_sum
92
93 def find_max_xor(node: int, parent: int) -> None:
94 """
95 DFS to find maximum XOR by comparing current subtree sum with previously seen sums.
96
97 Args:
98 node: Current node being processed
99 parent: Parent of current node (-1 for root)
100 """
101 nonlocal max_xor_value
102
103 # Find maximum XOR with current subtree sum against all previously inserted sums
104 max_xor_value = max(max_xor_value, trie.search(subtree_sums[node]))
105
106 # Process all children first (post-order traversal)
107 for neighbor in adjacency_list[node]:
108 if neighbor != parent:
109 find_max_xor(neighbor, node)
110
111 # Insert current subtree sum after processing children
112 trie.insert(subtree_sums[node])
113
114 # Build adjacency list representation of the tree
115 adjacency_list = defaultdict(list)
116 for node_a, node_b in edges:
117 adjacency_list[node_a].append(node_b)
118 adjacency_list[node_b].append(node_a)
119
120 # Initialize array to store subtree sums
121 subtree_sums = [0] * n
122
123 # First DFS: Calculate all subtree sums
124 calculate_subtree_sums(0, -1)
125
126 # Initialize maximum XOR value and Trie
127 max_xor_value = 0
128 trie = Trie()
129
130 # Second DFS: Find maximum XOR value
131 find_max_xor(0, -1)
132
133 return max_xor_value
134
1/**
2 * Binary Trie data structure for efficient XOR operations
3 * Stores binary representations of numbers for maximum XOR queries
4 */
5class Trie {
6 // Array to store left (0) and right (1) children
7 Trie[] children = new Trie[2];
8
9 /**
10 * Inserts a number into the trie by its binary representation
11 * @param x The number to insert
12 */
13 void insert(long x) {
14 Trie currentNode = this;
15 // Traverse from most significant bit (bit 47) to least significant bit
16 for (int bitPosition = 47; bitPosition >= 0; --bitPosition) {
17 // Extract the bit at current position
18 int bitValue = (int) (x >> bitPosition) & 1;
19 // Create new node if path doesn't exist
20 if (currentNode.children[bitValue] == null) {
21 currentNode.children[bitValue] = new Trie();
22 }
23 // Move to the child node
24 currentNode = currentNode.children[bitValue];
25 }
26 }
27
28 /**
29 * Searches for the maximum XOR value with the given number
30 * @param x The number to find maximum XOR with
31 * @return The maximum XOR value possible
32 */
33 long search(long x) {
34 Trie currentNode = this;
35 long result = 0;
36 // Traverse from most significant bit to least significant bit
37 for (int bitPosition = 47; bitPosition >= 0; --bitPosition) {
38 // Extract the bit at current position
39 int bitValue = (int) (x >> bitPosition) & 1;
40 // Safety check for null node
41 if (currentNode == null) {
42 return result;
43 }
44 // Try to go opposite direction for maximum XOR
45 if (currentNode.children[bitValue ^ 1] != null) {
46 // Set corresponding bit in result to 1 (XOR will be 1)
47 result = result << 1 | 1;
48 currentNode = currentNode.children[bitValue ^ 1];
49 } else {
50 // Have to go same direction (XOR will be 0)
51 result <<= 1;
52 currentNode = currentNode.children[bitValue];
53 }
54 }
55 return result;
56 }
57}
58
59/**
60 * Solution class for finding maximum XOR of subtree sums in a tree
61 */
62class Solution {
63 // Adjacency list representation of the tree
64 private List<Integer>[] adjacencyList;
65 // Node values array
66 private int[] nodeValues;
67 // Subtree sum for each node
68 private long[] subtreeSums;
69 // Trie for XOR operations
70 private Trie trie;
71 // Maximum XOR result
72 private long maximumXor;
73
74 /**
75 * Finds the maximum XOR value between any two subtree sums
76 * @param n Number of nodes in the tree
77 * @param edges Array of edges connecting nodes
78 * @param values Array of values for each node
79 * @return Maximum XOR value between any two subtree sums
80 */
81 public long maxXor(int n, int[][] edges, int[] values) {
82 // Initialize data structures
83 adjacencyList = new List[n];
84 subtreeSums = new long[n];
85 nodeValues = values;
86
87 // Create adjacency list for each node
88 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
89
90 // Build bidirectional edges
91 for (var edge : edges) {
92 int nodeA = edge[0];
93 int nodeB = edge[1];
94 adjacencyList[nodeA].add(nodeB);
95 adjacencyList[nodeB].add(nodeA);
96 }
97
98 // First DFS: Calculate subtree sums for all nodes
99 calculateSubtreeSums(0, -1);
100
101 // Initialize trie for XOR operations
102 trie = new Trie();
103
104 // Second DFS: Find maximum XOR using trie
105 findMaximumXor(0, -1);
106
107 return maximumXor;
108 }
109
110 /**
111 * Second DFS to find maximum XOR value
112 * Processes nodes in DFS order, checking XOR with previously visited nodes
113 * @param currentNode Current node being processed
114 * @param parent Parent of current node (-1 for root)
115 */
116 private void findMaximumXor(int currentNode, int parent) {
117 // Check maximum XOR with all previously inserted subtree sums
118 maximumXor = Math.max(maximumXor, trie.search(subtreeSums[currentNode]));
119
120 // Recursively process all children
121 for (int child : adjacencyList[currentNode]) {
122 if (child != parent) {
123 findMaximumXor(child, currentNode);
124 }
125 }
126
127 // Insert current node's subtree sum after processing its subtree
128 trie.insert(subtreeSums[currentNode]);
129 }
130
131 /**
132 * First DFS to calculate subtree sum for each node
133 * @param currentNode Current node being processed
134 * @param parent Parent of current node (-1 for root)
135 * @return Subtree sum rooted at current node
136 */
137 private long calculateSubtreeSums(int currentNode, int parent) {
138 // Start with current node's value
139 long currentSubtreeSum = nodeValues[currentNode];
140
141 // Add subtree sums of all children
142 for (int child : adjacencyList[currentNode]) {
143 if (child != parent) {
144 currentSubtreeSum += calculateSubtreeSums(child, currentNode);
145 }
146 }
147
148 // Store the subtree sum for current node
149 subtreeSums[currentNode] = currentSubtreeSum;
150
151 return currentSubtreeSum;
152 }
153}
154
1using ll = long long;
2
3class Trie {
4public:
5 vector<Trie*> children;
6
7 Trie() : children(2, nullptr) {}
8
9 // Insert a number into the trie by its binary representation
10 void insert(ll num) {
11 Trie* currentNode = this;
12
13 // Process 48 bits from most significant to least significant
14 for (int bitPos = 47; bitPos >= 0; --bitPos) {
15 int bit = (num >> bitPos) & 1;
16
17 // Create new node if path doesn't exist
18 if (!currentNode->children[bit]) {
19 currentNode->children[bit] = new Trie();
20 }
21 currentNode = currentNode->children[bit];
22 }
23 }
24
25 // Find maximum XOR value with given number among all inserted numbers
26 ll search(ll num) {
27 Trie* currentNode = this;
28 ll maxXorValue = 0;
29
30 // Process each bit from most significant to least significant
31 for (int bitPos = 47; bitPos >= 0; --bitPos) {
32 if (!currentNode) {
33 return maxXorValue;
34 }
35
36 int currentBit = (num >> bitPos) & 1;
37
38 // Try to go opposite direction for maximum XOR
39 if (currentNode->children[currentBit ^ 1]) {
40 maxXorValue = (maxXorValue << 1) | 1; // Set bit to 1 in result
41 currentNode = currentNode->children[currentBit ^ 1];
42 } else {
43 maxXorValue <<= 1; // Set bit to 0 in result
44 currentNode = currentNode->children[currentBit];
45 }
46 }
47
48 return maxXorValue;
49 }
50};
51
52class Solution {
53public:
54 long long maxXor(int n, vector<vector<int>>& edges, vector<int>& values) {
55 // Build adjacency list representation of the tree
56 vector<vector<int>> adjacencyList(n);
57 for (auto& edge : edges) {
58 int nodeA = edge[0];
59 int nodeB = edge[1];
60 adjacencyList[nodeA].emplace_back(nodeB);
61 adjacencyList[nodeB].emplace_back(nodeA);
62 }
63
64 // Array to store subtree sums for each node
65 vector<ll> subtreeSum(n);
66
67 // First DFS: Calculate subtree sum for each node
68 function<ll(int, int)> calculateSubtreeSum = [&](int currentNode, int parent) -> ll {
69 ll sum = values[currentNode];
70
71 for (int neighbor : adjacencyList[currentNode]) {
72 if (neighbor != parent) {
73 sum += calculateSubtreeSum(neighbor, currentNode);
74 }
75 }
76
77 subtreeSum[currentNode] = sum;
78 return sum;
79 };
80
81 // Calculate all subtree sums starting from root node 0
82 calculateSubtreeSum(0, -1);
83
84 // Initialize trie for finding maximum XOR
85 Trie trie;
86 ll maxXorResult = 0;
87
88 // Second DFS: Find maximum XOR between subtree sums
89 function<void(int, int)> findMaxXor = [&](int currentNode, int parent) {
90 // Find maximum XOR with current node's subtree sum and all previously visited nodes
91 maxXorResult = max(maxXorResult, trie.search(subtreeSum[currentNode]));
92
93 // Visit all children first (post-order traversal)
94 for (int neighbor : adjacencyList[currentNode]) {
95 if (neighbor != parent) {
96 findMaxXor(neighbor, currentNode);
97 }
98 }
99
100 // Insert current node's subtree sum after visiting children
101 trie.insert(subtreeSum[currentNode]);
102 };
103
104 // Start the second DFS from root
105 findMaxXor(0, -1);
106
107 return maxXorResult;
108 }
109};
110
1type ll = bigint;
2
3class TrieNode {
4 children: (TrieNode | null)[];
5
6 constructor() {
7 this.children = [null, null];
8 }
9}
10
11class Trie {
12 root: TrieNode;
13
14 constructor() {
15 this.root = new TrieNode();
16 }
17
18 // Insert a number into the trie by its binary representation
19 insert(num: ll): void {
20 let currentNode = this.root;
21
22 // Process 48 bits from most significant to least significant
23 for (let bitPos = 47; bitPos >= 0; bitPos--) {
24 const bit = Number((num >> BigInt(bitPos)) & 1n);
25
26 // Create new node if path doesn't exist
27 if (!currentNode.children[bit]) {
28 currentNode.children[bit] = new TrieNode();
29 }
30 currentNode = currentNode.children[bit]!;
31 }
32 }
33
34 // Find maximum XOR value with given number among all inserted numbers
35 search(num: ll): ll {
36 let currentNode: TrieNode | null = this.root;
37 let maxXorValue = 0n;
38
39 // Process each bit from most significant to least significant
40 for (let bitPos = 47; bitPos >= 0; bitPos--) {
41 if (!currentNode) {
42 return maxXorValue;
43 }
44
45 const currentBit = Number((num >> BigInt(bitPos)) & 1n);
46
47 // Try to go opposite direction for maximum XOR
48 if (currentNode.children[currentBit ^ 1]) {
49 maxXorValue = (maxXorValue << 1n) | 1n; // Set bit to 1 in result
50 currentNode = currentNode.children[currentBit ^ 1];
51 } else {
52 maxXorValue <<= 1n; // Set bit to 0 in result
53 currentNode = currentNode.children[currentBit];
54 }
55 }
56
57 return maxXorValue;
58 }
59}
60
61function maxXor(n: number, edges: number[][], values: number[]): number {
62 // Build adjacency list representation of the tree
63 const adjacencyList: number[][] = Array(n).fill(null).map(() => []);
64 for (const edge of edges) {
65 const nodeA = edge[0];
66 const nodeB = edge[1];
67 adjacencyList[nodeA].push(nodeB);
68 adjacencyList[nodeB].push(nodeA);
69 }
70
71 // Array to store subtree sums for each node
72 const subtreeSum: ll[] = Array(n);
73
74 // First DFS: Calculate subtree sum for each node
75 const calculateSubtreeSum = (currentNode: number, parent: number): ll => {
76 let sum = BigInt(values[currentNode]);
77
78 for (const neighbor of adjacencyList[currentNode]) {
79 if (neighbor !== parent) {
80 sum += calculateSubtreeSum(neighbor, currentNode);
81 }
82 }
83
84 subtreeSum[currentNode] = sum;
85 return sum;
86 };
87
88 // Calculate all subtree sums starting from root node 0
89 calculateSubtreeSum(0, -1);
90
91 // Initialize trie for finding maximum XOR
92 const trie = new Trie();
93 let maxXorResult = 0n;
94
95 // Second DFS: Find maximum XOR between subtree sums
96 const findMaxXor = (currentNode: number, parent: number): void => {
97 // Find maximum XOR with current node's subtree sum and all previously visited nodes
98 maxXorResult = maxXorResult > trie.search(subtreeSum[currentNode]) ?
99 maxXorResult : trie.search(subtreeSum[currentNode]);
100
101 // Visit all children first (post-order traversal)
102 for (const neighbor of adjacencyList[currentNode]) {
103 if (neighbor !== parent) {
104 findMaxXor(neighbor, currentNode);
105 }
106 }
107
108 // Insert current node's subtree sum after visiting children
109 trie.insert(subtreeSum[currentNode]);
110 };
111
112 // Start the second DFS from root
113 findMaxXor(0, -1);
114
115 return Number(maxXorResult);
116}
117
Time and Space Complexity
Time Complexity: O(n * log(max_value))
The algorithm consists of three main parts:
- Building the graph:
O(n)
- iterating through all edges once - First DFS (dfs1):
O(n)
- visiting each node exactly once to compute subtree sums - Second DFS (dfs2):
O(n * log(max_value))
wherelog(max_value) = 48
bits- Each node is visited once:
O(n)
- For each node, we perform:
- Trie search operation:
O(48)
- iterating through 48 bits - Trie insert operation:
O(48)
- iterating through 48 bits
- Trie search operation:
- Each node is visited once:
Since the bit operations are performed for a fixed 48-bit representation, the overall time complexity is O(n * 48) = O(n)
when considering 48 as a constant.
Space Complexity: O(n * log(max_value))
The space usage includes:
- Graph adjacency list:
O(n)
- storing all edges - Subtree sum array
s
:O(n)
- storing sum for each node - Trie structure:
O(n * 48)
in worst case- Each inserted number can create up to 48 new nodes
- With n insertions, maximum nodes =
n * 48
- DFS recursion stack:
O(h)
where h is the height of the tree, worst caseO(n)
for a skewed tree
The dominant factor is the Trie structure, giving us O(n * 48) = O(n)
space complexity when treating 48 as a constant.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Bit Width in Trie Implementation
One of the most common pitfalls is using an insufficient number of bits in the Trie implementation. The problem doesn't specify constraints on node values, but the sum of all values in a subtree can be quite large.
The Issue: If you use too few bits (e.g., 32 bits), large subtree sums will overflow or be incorrectly represented, leading to wrong XOR calculations.
# INCORRECT: Using only 32 bits
def insert(self, num: int) -> None:
current_node = self
for bit_position in range(31, -1, -1): # Only 32 bits!
bit_value = (num >> bit_position) & 1
# ... rest of code
The Solution: Use a sufficient number of bits to handle the maximum possible subtree sum. The provided solution uses 48 bits, which can handle sums up to 2^48 - 1 (approximately 281 trillion).
# CORRECT: Using 48 bits for larger sums
def insert(self, num: int) -> None:
current_node = self
for bit_position in range(47, -1, -1): # 48 bits
bit_value = (num >> bit_position) & 1
# ... rest of code
2. Wrong Traversal Order in Second DFS
Another critical pitfall is inserting the current node's subtree sum into the Trie before processing its children, which would allow overlapping subtrees to be compared.
The Issue:
# INCORRECT: Inserting before processing children
def find_max_xor(node: int, parent: int) -> None:
nonlocal max_xor_value
# Wrong! Inserting before processing children
trie.insert(subtree_sums[node])
max_xor_value = max(max_xor_value, trie.search(subtree_sums[node]))
for neighbor in adjacency_list[node]:
if neighbor != parent:
find_max_xor(neighbor, node)
This would allow a parent node's subtree sum to be compared with its descendant's subtree sum, violating the non-overlapping requirement.
The Solution: Always insert the current node's subtree sum AFTER processing all its children:
# CORRECT: Post-order insertion
def find_max_xor(node: int, parent: int) -> None:
nonlocal max_xor_value
# First, search for maximum XOR with existing non-overlapping subtrees
max_xor_value = max(max_xor_value, trie.search(subtree_sums[node]))
# Then process all children
for neighbor in adjacency_list[node]:
if neighbor != parent:
find_max_xor(neighbor, node)
# Finally, insert current subtree sum after children are processed
trie.insert(subtree_sums[node])
3. Forgetting to Handle Single-Node Trees
When n=1 (only one node), there's only one subtree possible, so finding two non-overlapping subtrees is impossible.
The Issue: The algorithm might try to XOR the single subtree sum with nothing (or 0), giving an incorrect result.
The Solution: The current implementation handles this correctly because:
- When there's only one node, the Trie is initially empty
trie.search()
returns 0 when searching in an empty Trie- The final answer is 0, which is correct as per the problem statement
However, for clarity, you could add an explicit check:
def maxXor(self, n: int, edges: List[List[int]], values: List[int]) -> int:
# Edge case: single node tree
if n == 1:
return 0
# ... rest of the implementation
4. Not Handling Negative Values Correctly
If the problem allows negative node values, the Trie implementation needs modification to handle negative numbers properly.
The Issue: Negative numbers in two's complement representation have different bit patterns that could cause issues with the current Trie implementation.
The Solution: If negative values are possible, offset all values by a constant to make them non-negative, or modify the Trie to handle signed integers:
# If negative values are possible, offset to make all positive
MIN_VALUE = min(values)
if MIN_VALUE < 0:
offset = -MIN_VALUE
adjusted_values = [v + offset for v in values]
# Use adjusted_values in calculations
# Note: XOR result remains the same since both sums are offset equally
Which technique can we use to find the middle of a linked list?
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 graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Don’t Miss This!