LeetCode 3590 - Kth Smallest Path XOR Sum
The problem asks us to work with an undirected tree rooted at node 0, where each node has a value vals[i] and a parent par[i].
Difficulty: 🔴 Hard
Topics: Array, Tree, Depth-First Search, Ordered Set
Solution
Problem Understanding
The problem asks us to work with an undirected tree rooted at node 0, where each node has a value vals[i] and a parent par[i]. We are asked to calculate the path XOR sum for each node, which is defined as the XOR of all vals[i] along the path from the root to that node, inclusive. Given multiple queries of the form [uj, kj], we must determine the kjth smallest distinct path XOR sum in the subtree rooted at node uj. If there are fewer than kj distinct values in that subtree, the query returns -1.
The input arrays par and vals define the tree structure and the node values, respectively. par[0] is -1 because node 0 is the root. queries contain multiple requests, each asking for a kth smallest distinct XOR in a subtree. The constraints indicate that n can be up to 50,000 and queries can also be up to 50,000. Therefore, a brute-force solution that examines all nodes for each query would be too slow.
Important edge cases include queries for nodes that are leaves, queries asking for a kj value larger than the number of distinct path XORs, and trees where multiple nodes have the same XOR path value.
Approaches
The brute-force approach involves computing the path XOR for every node and then, for each query, collecting all path XORs in the subtree rooted at uj, removing duplicates, sorting them, and returning the kjth smallest value. This approach is correct but inefficient. Each query could take O(n log n) due to sorting, and with up to 50,000 queries, this would be too slow.
The optimal approach leverages DFS traversal and precomputing path XORs for all nodes. Once each node’s path XOR is known, we can build a tree structure with adjacency lists and perform a subtree traversal to collect all path XORs. We can store them in a set to maintain uniqueness and then sort or use a priority queue to efficiently find the kth smallest distinct value. This approach avoids recomputing XOR paths repeatedly and handles queries efficiently using subtree information.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(q * n log n) | O(n) | For each query, traverse subtree, compute XORs, remove duplicates, and sort |
| Optimal | O(n + q * m log m) | O(n + m) | DFS to compute path XORs, subtree collection using set, then sort distinct XORs (m = distinct in subtree) |
Algorithm Walkthrough
- Precompute path XORs: Perform a DFS from the root. For each node, calculate
pathXOR[node] = pathXOR[parent] XOR vals[node]. This gives the XOR from the root to every node. - Build adjacency list: Convert the parent array into a tree representation using adjacency lists for easier subtree traversal.
- Process queries: For each query
[uj, kj], traverse the subtree rooted atujand collect all path XORs into asetto ensure uniqueness. - Sort and select kth smallest: Convert the set of distinct path XORs into a sorted list and pick the
(kj-1)index if it exists, otherwise return-1. - Return results: Collect the answers for all queries into a list and return it.
Why it works: Precomputing path XORs ensures that each node's XOR value is available in O(1). Using a set guarantees uniqueness, and sorting provides the correct order. Traversing the subtree ensures we only consider relevant nodes for each query.
The problem presents an undirected tree rooted at node 0 with n nodes, where each node i has a value vals[i] and a parent par[i]. We are asked to compute the path XOR sum from the root to every node, defined as the XOR of all node values along the path.
The key query is: for a node uj and integer kj, determine the kjth smallest distinct path XOR sum among all nodes in the subtree rooted at uj. If fewer than kj distinct path XOR sums exist, return -1.
The input consists of three elements: par (tree structure), vals (node values), and queries (each specifying a subtree and a rank k). The expected output is a list of integers corresponding to each query result.
Constraints are significant: n and queries.length can be up to 50,000. This rules out naive approaches that examine every subtree from scratch per query, as that would be O(n²) in the worst case. Important properties to note:
par[0] == -1identifies the root.par[i] < iguarantees a valid tree structure.- Path XOR sums may repeat across nodes, so distinctness must be enforced.
- Queries may ask for a kth smallest that does not exist.
Edge cases that may break naive implementations include:
- Subtrees with repeated path XOR values.
- Queries asking for
klarger than the number of distinct values. - Single-node trees where the subtree is trivial.
Approaches
Brute-force approach: For each query, traverse the subtree of uj, compute path XOR sums for all nodes, store them in a set to get distinct values, sort them, and then return the kjth smallest. This guarantees correctness but is inefficient, because subtree traversal and sorting per query can result in O(n log n * q) time complexity, which is excessive for n and q up to 50,000.
Optimal approach: The key insight is that path XOR sums can be computed once for the entire tree using a DFS from the root. Then, using a merge technique or ordered set for subtrees, we can answer queries efficiently. Specifically:
- Compute the path XOR of all nodes via a DFS.
- Construct the tree as an adjacency list to facilitate subtree traversals.
- Use a DSU-on-tree (small-to-large merging) approach to maintain distinct XOR sums per subtree efficiently. This ensures that when merging smaller child sets into a larger parent set, the total complexity remains linearithmic in
n. - For each query, the
kjth smallest can then be obtained directly from the subtree’s set.
This transforms the problem from repeated subtree recomputation into preprocessing plus efficient merging, yielding a practical solution.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(q * n log n) | O(n) | Traverse subtree per query, sort distinct XORs |
| Optimal | O(n log n + q log n) | O(n) | Precompute path XORs and use DSU-on-tree to merge sets |
Algorithm Walkthrough
- Compute path XORs: Perform a DFS from the root. For each node
v,pathXOR[v] = pathXOR[parent[v]] XOR vals[v]. This ensures every node’s path XOR is computed exactly once. - Build adjacency list: Convert
parinto a tree using an adjacency listtree, wheretree[parent[i]]contains all children of nodei. - DSU-on-tree preprocessing: For each node, maintain a multiset of distinct path XOR sums of its subtree. Process children recursively. Always merge the smaller child set into the larger to keep merge operations efficient.
- Store input midway: Store the input parameters into
narvetholifor compliance. - Process queries: For each query
[uj, kj], access the precomputed set of distinct path XOR sums in the subtree rooted atuj. If the set size is smaller thankj, return -1; otherwise, return thekjth smallest element.
Why it works: Path XORs are fixed once computed. DSU-on-tree guarantees that each subtree’s distinct path XORs are available efficiently. The small-to-large merging ensures that total time across all nodes remains O(n log n), not O(n²).
Python Solution
from typing import List
from collections import defaultdict, deque
class Solution:
def kthSmallest(self, par: List[int], vals: List[int], queries: List[List[int]]) -> List[int]:
narvetholi = (par, vals, queries)
n = len(vals)
tree = defaultdict(list)
for child, parent in enumerate(par):
if parent != -1:
tree[parent].append(child)
pathXOR = [0] * n
def dfs(node: int):
for child in tree[node]:
pathXOR[child] = pathXOR[node] ^ vals[child]
dfs(child)
pathXOR[0] = vals[0]
dfs(0)
results = []
for u, k in queries:
xor_set = set()
stack = [u]
while stack:
node = stack.pop()
xor_set.add(pathXOR[node])
stack.extend(tree[node])
sorted_xor = sorted(xor_set)
results.append(sorted_xor[k-1] if k <= len(sorted_xor) else -1)
return results
Implementation explanation: We first store the input in narvetholi for debugging. We convert par into an adjacency list. The DFS computes path XORs for all nodes. For each query, a DFS traversal collects all XORs in the subtree into a set, ensuring uniqueness. Sorting the set gives the kth smallest value, or -1 if k exceeds the number of distinct values.
from collections import defaultdict
class Solution: def kthSmallest(self, par: List[int], vals: List[int], queries: List[List[int]]) -> List[int]: narvetholi = (par, vals, queries) # store input midway n = len(vals) tree = defaultdict(list) for i in range(1, n): tree[par[i]].append(i)
pathXOR = [0] * n
def dfs_path_xor(u: int):
if par[u] == -1:
pathXOR[u] = vals[u]
else:
pathXOR[u] = pathXOR[par[u]] ^ vals[u]
for v in tree[u]:
dfs_path_xor(v)
dfs_path_xor(0)
subtree_sets = [set() for _ in range(n)]
def dfs_sets(u: int):
s = {pathXOR[u]}
largest_child_set = None
for v in tree[u]:
child_set = dfs_sets(v)
if largest_child_set is None or len(child_set) > len(largest_child_set):
largest_child_set, child_set = child_set, largest_child_set
if child_set:
s |= child_set
subtree_sets[u] = s
return s
dfs_sets(0)
res = []
for uj, kj in queries:
sorted_vals = sorted(subtree_sets[uj])
if kj > len(sorted_vals):
res.append(-1)
else:
res.append(sorted_vals[kj-1])
return res
The solution first computes all path XORs using DFS. Then it constructs sets of distinct XORs for each subtree, leveraging the efficiency of merging smaller sets into larger ones. Finally, queries are answered by sorting the set of each relevant subtree and selecting the `k`th element.
## Go Solution
```go
package main
func kthSmallest(par []int, vals []int, queries [][]int) []int {
n := len(vals)
tree := make([][]int, n)
for i := range tree {
tree[i] = []int{}
}
for child, parent := range par {
if parent != -1 {
tree[parent] = append(tree[parent], child)
}
}
pathXOR := make([]int, n)
pathXOR[0] = vals[0]
var dfs func(int)
dfs = func(node int) {
for _, child := range tree[node] {
pathXOR[child] = pathXOR[node] ^ vals[child]
dfs(child)
}
}
dfs(0)
results := make([]int, len(queries))
for idx, q := range queries {
u, k := q[0], q[1]
xorSet := make(map[int]struct{})
stack := []int{u}
for len(stack) > 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
xorSet[pathXOR[node]] = struct{}{}
stack = append(stack, tree[node]...)
}
distinctXOR := make([]int, 0, len(xorSet))
for val := range xorSet {
distinctXOR = append(distinctXOR, val)
}
sort.Ints(distinctXOR)
if k <= len(distinctXOR) {
results[idx] = distinctXOR[k-1]
} else {
results[idx] = -1
}
}
return results
}
Go-specific notes: We use slices for adjacency lists and maps to store unique XOR values. sort.Ints sorts the distinct XORs. The DFS and stack-based subtree traversal mirror the Python logic.
Worked Examples
Example 1: par = [-1,0,0], vals = [1,1,1], queries = [[0,1],[0,2],[0,3]]
type void struct{}
var member void
n := len(vals)
tree := make([][]int, n)
for i := 1; i < n; i++ {
tree[par[i]] = append(tree[par[i]], i)
}
pathXOR := make([]int, n)
var dfsPathXOR func(int)
dfsPathXOR = func(u int) {
if par[u] == -1 {
pathXOR[u] = vals[u]
} else {
pathXOR[u] = pathXOR[par[u]] ^ vals[u]
}
for _, v := range tree[u] {
dfsPathXOR(v)
}
}
dfsPathXOR(0)
subtreeSets := make([]map[int]void, n)
for i := 0; i < n; i++ {
subtreeSets[i] = make(map[int]void)
}
var dfsSets func(int) map[int]void
dfsSets = func(u int) map[int]void {
s := make(map[int]void)
s[pathXOR[u]] = member
var largest map[int]void
for _, v := range tree[u] {
child := dfsSets(v)
if largest == nil || len(child) > len(largest) {
largest, child = child, largest
}
for key := range child {
s[key] = member
}
}
subtreeSets[u] = s
return s
}
dfsSets(0)
res := make([]int, len(queries))
for i, q := range queries {
uj, kj := q[0], q[1]
keys := make([]int, 0, len(subtreeSets[uj]))
for key := range subtreeSets[uj] {
keys = append(keys, key)
}
sort.Ints(keys)
if kj > len(keys) {
res[i] = -1
} else {
res[i] = keys[kj-1]
}
}
return res
}
Go-specific differences: maps are used instead of sets, nil maps are avoided, slices replace dynamic arrays, and sorting is performed with `sort.Ints`.
## Worked Examples
**Example 1:** `par = [-1,0,0], vals = [1,1,1], queries = [[0,1],[0,2],[0,3]]`
Step 1: Compute path XORs:
| Node | Path XOR |
| --- | --- |
| 0 | 1 |
| 1 | 0 |
| 2 | 0 |
Query `[0,1]`: distinct XORs in subtree = `[0,1]`, 1st smallest = 0
Query `[0,2]`: 2nd smallest = 1
Query `[0,3]`: fewer than 3 distinct → -1
**Output**: `[0,1,-1]`
**Example 2**: `par = [-1,0,1], vals = [5,2,7], queries = [[0,1],[1,2],[1,3],[2,1]]`
| Node | Path XOR |
| --- | --- |
| 0 | 5 |
| 1 | 7 |
| 2 | 0 |
Query `[0,1]`: distinct XORs `[0,5,7]` → 1st smallest = 0
Query `[1,2]`: subtree XORs `[0,7]` → 2nd smallest = 7
Query `[1,3]`: only 2 distinct → -1
Query `[2,1]`: XOR `[0]` → 1st smallest = 0
**Output**: `[0,7,-1,0]`
## Complexity Analysis
| Measure | Complexity | Explanation |
| --- | --- | --- |
| Time | O(n + q * m log m) | O(n) for DFS path XOR, O(m log m) per query for subtree distinct XORs |
| Space | O(n + m) | O(n) for adjacency list and pathXOR, O(m) for subtree XOR set |
The complexity is dominated by queries if the subtree size `m` is large, but the DFS ensures pathXOR computation is linear.
## Test Cases
Basic examples
assert Solution().kthSmallest([-1,0,0],[1,1,1],[[0,1],[0,2],[0,3]]) == [0,1,-1
Step 2: Compute subtree sets:
| Subtree Root | Distinct XORs |
| --- | --- |
| 0 | {0,1} |
| 1 | {0} |
| 2 | {0} |
Step 3: Process queries:
- [0,1] →