Facebook Pixel

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:

  1. First DFS (dfs1): Calculates the sum of values for each subtree rooted at every node
  2. Second DFS (dfs2): Uses a Trie data structure to efficiently find the maximum XOR between any two non-overlapping subtrees by traversing the tree and ensuring non-overlapping property through the traversal order

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 from 0 to n - 1, connected by n - 1 edges, with node 0 as the root.

DFS

  • Yes: Since we're dealing with a tree structure and need to:

    1. Calculate subtree sums (which requires traversing from leaves up to root)
    2. Find non-overlapping subtrees (which requires understanding parent-child relationships)
    3. 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:

  1. First DFS calculates the sum for each subtree
  2. Second DFS finds the maximum XOR between non-overlapping subtrees using a Trie for optimization
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. An ancestor subtree that doesn't include node i
  2. 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 add s[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 number x in the Trie, bit by bit from most significant to least significant
  • search(x): For a given number x, finds the number in the Trie that produces maximum XOR with x by greedily choosing the opposite bit at each level when possible

Main Algorithm:

  1. Build the adjacency list: Convert the edge list into an adjacency list representation g for efficient tree traversal.

  2. 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 node i.

  3. 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 between s[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).

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 Evaluator

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

  1. At node 0:

    • Trie is empty, so search(15) returns 0
    • Recursively visit child 1
  2. At node 1:

    • Trie is still empty, so search(8) returns 0
    • Recursively visit children 3 and 4
  3. At node 3:

    • Trie is empty, so search(1) returns 0
    • Insert s[3] = 1 into Trie
  4. 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
  5. Back at node 1:

    • Insert s[1] = 8 into Trie
    • Trie now contains [1, 4, 8]
  6. 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
  7. 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:

  1. Building the graph: O(n) - iterating through all edges once
  2. First DFS (dfs1): O(n) - visiting each node exactly once to compute subtree sums
  3. Second DFS (dfs2): O(n * log(max_value)) where log(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

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:

  1. Graph adjacency list: O(n) - storing all edges
  2. Subtree sum array s: O(n) - storing sum for each node
  3. 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
  4. DFS recursion stack: O(h) where h is the height of the tree, worst case O(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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

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

Load More